diff --git a/CHANGELOG.md b/CHANGELOG.md index 040a8ae..62a3a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +## [0.8.1] - 2026-09-27 + +### Fixed + +- Windows setup keeps a single desktop shortcut across upgrades. An icon that is already there stays when the checkbox is off, and a checked box no longer leaves a second `.lnk` or a `(2)` copy. + ## [0.8.0] - 2026-09-26 ### Added diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d7bae65..a8bfa53 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ latest enable enable - 0.8.0 + 0.8.1 MaksIT.ClusterConsole MaksIT.ClusterConsole diff --git a/utils/plugins/Desktop/DesktopPackSupport.psm1 b/utils/plugins/Desktop/DesktopPackSupport.psm1 index 7277fde..4f7bf9d 100644 --- a/utils/plugins/Desktop/DesktopPackSupport.psm1 +++ b/utils/plugins/Desktop/DesktopPackSupport.psm1 @@ -323,8 +323,33 @@ function New-WixPackageXml { $desktopProp = $xml.CreateElement('Property', $ns) $null = $desktopProp.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT') $null = $desktopProp.SetAttribute('Value', '0') + $null = $desktopProp.SetAttribute('Secure', 'yes') $null = $package.AppendChild($desktopProp) + # Present when a previous install wrote the desktop shortcut marker. + # Burn also searches HKCU as the user and passes DESKTOPSHORTCUTFOUND, + # because a per-machine execute sequence runs as SYSTEM. + $foundProp = $xml.CreateElement('Property', $ns) + $null = $foundProp.SetAttribute('Id', 'DESKTOPSHORTCUTFOUND') + $null = $foundProp.SetAttribute('Secure', 'yes') + $foundSearch = $xml.CreateElement('RegistrySearch', $ns) + $null = $foundSearch.SetAttribute('Id', 'DesktopShortcutFoundSearch') + $null = $foundSearch.SetAttribute('Root', 'HKCU') + $null = $foundSearch.SetAttribute('Key', "Software\$Manufacturer\$AppName") + $null = $foundSearch.SetAttribute('Name', 'desktop') + $null = $foundSearch.SetAttribute('Type', 'raw') + $null = $foundProp.AppendChild($foundSearch) + $null = $package.AppendChild($foundProp) + + # Checkbox off on reinstall must not drop an icon that is already there. + # Major upgrade removes the previous shortcut; this installs that one again. + $keepShortcut = $xml.CreateElement('SetProperty', $ns) + $null = $keepShortcut.SetAttribute('Id', 'INSTALLDESKTOPSHORTCUT') + $null = $keepShortcut.SetAttribute('Value', '1') + $null = $keepShortcut.SetAttribute('Before', 'CostFinalize') + $null = $keepShortcut.SetAttribute('Condition', 'DESKTOPSHORTCUTFOUND = "1"') + $null = $package.AppendChild($keepShortcut) + $productFolder = Get-DesktopInstallFolderName ` -AppName $AppName ` -Manufacturer $Manufacturer ` @@ -449,7 +474,8 @@ function New-WixPackageXml { $desktop = $xml.CreateElement('Component', $ns) $null = $desktop.SetAttribute('Id', 'DesktopShortcut') $null = $desktop.SetAttribute('Directory', 'DesktopFolder') - $null = $desktop.SetAttribute('Guid', '*') + $shortcutGuid = Get-DesktopShortcutComponentGuid -Manufacturer $Manufacturer -AppName $AppName + $null = $desktop.SetAttribute('Guid', $shortcutGuid.ToString('D')) $null = $desktop.SetAttribute('Condition', 'INSTALLDESKTOPSHORTCUT = 1') $desktopShortcut = $xml.CreateElement('Shortcut', $ns) $null = $desktopShortcut.SetAttribute('Id', 'AppDesktopShortcut') @@ -461,6 +487,26 @@ function New-WixPackageXml { } $null = $desktop.AppendChild($desktopShortcut) + # Same file name is replaced. A second ".lnk" (and the "(2)" copy) is removed + # before CreateShortcuts so a checked box does not leave two desktop icons. + $cleanupNames = [System.Collections.Generic.List[string]]::new() + $cleanupNames.Add($productFolder) + if (-not $cleanupNames.Contains($AppName)) { + $cleanupNames.Add($AppName) + } + + $cleanupIndex = 0 + foreach ($shortcutName in $cleanupNames) { + foreach ($suffix in @('', ' (2)')) { + $cleanupIndex++ + $removeLnk = $xml.CreateElement('RemoveFile', $ns) + $null = $removeLnk.SetAttribute('Id', "DesktopShortcutCleanup$cleanupIndex") + $null = $removeLnk.SetAttribute('Name', "$shortcutName$suffix.lnk") + $null = $removeLnk.SetAttribute('On', 'install') + $null = $desktop.AppendChild($removeLnk) + } + } + $desktopReg = $xml.CreateElement('RegistryValue', $ns) $null = $desktopReg.SetAttribute('Root', 'HKCU') $null = $desktopReg.SetAttribute('Key', "Software\$Manufacturer\$AppName") @@ -874,6 +920,27 @@ function Assert-FlatpakAppId { } } +function Get-DesktopShortcutComponentGuid { + param( + [Parameter(Mandatory = $true)] + [string]$Manufacturer, + + [Parameter(Mandatory = $true)] + [string]$AppName + ) + + $identity = "maksit-desktop-shortcut|$Manufacturer|$AppName" + $md5 = [System.Security.Cryptography.MD5]::Create() + try { + $hash = $md5.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($identity)) + } + finally { + $md5.Dispose() + } + + return [guid]::new($hash) +} + function Get-DerivedBundleUpgradeCode { param( [Parameter(Mandatory = $true)] @@ -1196,15 +1263,18 @@ function New-WixBundleXml { $folderRoot + $Manufacturer + '\' + $productFolder } $escapedFolder = [System.Security.SecurityElement]::Escape($folderPath) + $escapedShortcutKey = [System.Security.SecurityElement]::Escape("Software\$Manufacturer\$AppName") # WiX v7 Bundle has no Scope attribute (WIX0004). MSI Package/@Scope plus # InstallFolder tokens decide per-machine vs per-user; Burn infers bundle scope. return @" - + + + @@ -1212,6 +1282,7 @@ function New-WixBundleXml { + diff --git a/utils/plugins/Desktop/WindowsInstaller.psm1 b/utils/plugins/Desktop/WindowsInstaller.psm1 index ce27788..8a4a083 100644 --- a/utils/plugins/Desktop/WindowsInstaller.psm1 +++ b/utils/plugins/Desktop/WindowsInstaller.psm1 @@ -13,7 +13,8 @@ per-machine installs go to `C:\Program Files`, not Program Files (x86). Requires the WiX CLI (`dotnet tool install -g wix`). WiX v7: accept the OSMF EULA (`wix eula accept wix7` or `-acceptEula wix7`) and - `wix extension add -g WixToolset.BootstrapperApplications.wixext`. + `wix extension add -g WixToolset.BootstrapperApplications.wixext` and + `wix extension add -g WixToolset.Util.wixext`. Reopen the shell so `%USERPROFILE%\.dotnet\tools` is on PATH. #> @@ -40,7 +41,7 @@ function Test-WixMissingException { } function Get-WixMissingInstallMessage { - return "WiX CLI ('wix') is not on PATH. Install with: dotnet tool install -g wix. For WiX v7 also run: wix eula accept wix7; wix extension add -g WixToolset.BootstrapperApplications.wixext. Reopen the terminal so %USERPROFILE%\.dotnet\tools is on PATH." + return "WiX CLI ('wix') is not on PATH. Install with: dotnet tool install -g wix. For WiX v7 also run: wix eula accept wix7; wix extension add -g WixToolset.BootstrapperApplications.wixext; wix extension add -g WixToolset.Util.wixext. Reopen the terminal so %USERPROFILE%\.dotnet\tools is on PATH." } function Get-WixCliVersionText { @@ -280,7 +281,7 @@ function Invoke-Plugin { Write-Log -Level "STEP" -Message "Building Windows installer exe ($wixArch)..." try { - Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($bundleWxsPath, '-ext', $bundleExt, '-o', $exePath)) | Out-Null + Invoke-ExternalCommand -Name wix -ArgumentList (@('build') + $eulaArgs + $archArgs + @($bundleWxsPath, '-ext', $bundleExt, '-ext', 'WixToolset.Util.wixext', '-o', $exePath)) | Out-Null } catch { if (Test-WixMissingException -ErrorRecord $_) {