From 2172d085c656bbf15df9d9a1f8917e36dd0b6fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8D=E5=80=BE=E6=A2=A6=E5=8D=8EV?= <185041488+CQMHV@users.noreply.github.com> Date: Tue, 6 Oct 2026 06:13:51 +0800 Subject: [PATCH] Fix Windows MSI advertised shortcut icons (#238) (#238) --- .github/workflows/packaging-lint.yml | 1 + docs/releasing.md | 4 +++ packaging/windows/check-icons.ps1 | 46 ++++++++++++++++++++++++++++ packaging/windows/package.ps1 | 4 +++ packaging/windows/photocraft.wxs | 8 +++-- 5 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 packaging/windows/check-icons.ps1 diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 7b0b619fd7..a12bbd91f7 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -44,6 +44,7 @@ jobs: if ($errors) { $errors | ForEach-Object { Write-Output "::error file=$($f.Name)::$_" }; exit 1 } Write-Output "ok $($f.Name)" } + ./packaging/windows/check-icons.ps1 - name: XML, desktop entry, AppStream, Flatpak manifest run: | xmllint --noout packaging/windows/photocraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* diff --git a/docs/releasing.md b/docs/releasing.md index 69f632fedf..269fea24db 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -103,6 +103,10 @@ goes in `CARGO_TARGET__RUSTFLAGS`, so host build scripts aren't affected "Open with" for PSD/PSB and image files. It also registers App Paths (Win+R `photocraft`). The MSI version is the numeric `X.Y.Z`, because MSI has no pre-release field. Same-version upgrades are allowed so that release candidates replace each other. +- Shortcut icon identifiers keep the executable's `.exe` extension: MSI uses the identifier + as the cached icon filename, and an extensionless filename can render as a blank document + icon. `packaging/windows/check-icons.ps1` checks the references and extensions in CI; + `package.ps1` also validates the built MSI with ICE50 before signing it. - **Signing:** `packaging/windows/sign.ps1` signs both `.exe` files and then the `.msi` with `signtool`, using SHA-256 and an RFC 3161 timestamp. It uses whichever material is present: 1. a `.pfx` file (`WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD`), timestamped by diff --git a/packaging/windows/check-icons.ps1 b/packaging/windows/check-icons.ps1 new file mode 100644 index 0000000000..66b06a2d8d --- /dev/null +++ b/packaging/windows/check-icons.ps1 @@ -0,0 +1,46 @@ +<# +.SYNOPSIS + Check MSI icon references without building or installing PhotoCraft. + +.DESCRIPTION + ICE50 requires advertised shortcut icon identifiers to have the target file's extension. + Windows Installer uses those identifiers as cached filenames; extensionless identifiers + can display a generic document icon even when the ICO data and application are valid. +#> +param( + [string] $WxsPath = (Join-Path $PSScriptRoot 'photocraft.wxs') +) +$ErrorActionPreference = 'Stop' +[xml] $Wxs = Get-Content -LiteralPath $WxsPath -Raw +$Ns = [Xml.XmlNamespaceManager]::new($Wxs.NameTable) +$Ns.AddNamespace('w', 'http://wixtoolset.org/schemas/v4/wxs') +$Icons = @{} +foreach ($icon in $Wxs.SelectNodes('//w:Icon', $Ns)) { + $Icons[$icon.GetAttribute('Id')] = $icon +} + +foreach ($shortcut in $Wxs.SelectNodes('//w:File/w:Shortcut[@Advertise="yes"]', $Ns)) { + $id = $shortcut.GetAttribute('Id') + $iconId = $shortcut.GetAttribute('Icon') + if (-not $Icons.ContainsKey($iconId)) { + throw "Advertised shortcut '$id' references missing icon '$iconId'." + } + $iconExtension = [IO.Path]::GetExtension($iconId) + if ($iconExtension -notin '.exe', '.ico') { + throw "Advertised shortcut '$id' icon '$iconId' must have an .exe or .ico extension (ICE50)." + } + $file = $shortcut.ParentNode + $fileName = if ($file.HasAttribute('Name')) { $file.GetAttribute('Name') } else { $file.GetAttribute('Source') } + # Source paths use Windows separators even when this check runs on Linux in packaging-lint. + $fileName = ($fileName -split '[\\/]')[-1] + $targetExtension = [IO.Path]::GetExtension($fileName) + if ($iconExtension -ne $targetExtension) { + throw "Advertised shortcut '$id' icon '$iconId' must match target '$fileName' extension '$targetExtension' (ICE50)." + } +} + +$arpIcon = $Wxs.SelectSingleNode('//w:Property[@Id="ARPPRODUCTICON"]', $Ns) +if ($arpIcon -and -not $Icons.ContainsKey($arpIcon.GetAttribute('Value'))) { + throw "ARPPRODUCTICON references missing icon '$($arpIcon.GetAttribute('Value'))'." +} +Write-Output 'Windows shortcut icon references ok' diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index 8cb76f1127..edfcf13ed2 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -74,11 +74,15 @@ Copy-Item (Join-Path $Bin 'photocraft.exe'), (Join-Path $Bin 'photocraft-cli.exe # ---- MSI --------------------------------------------------------------------------------------- $Msi = Join-Path $Dist "photocraft-$Version-windows-$Arch.msi" +& (Join-Path $PSScriptRoot 'check-icons.ps1') Invoke-Native 'wix build' { wix build (Join-Path $PSScriptRoot 'photocraft.wxs') -arch $Arch ` -d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\photocraft.ico')" ` -o $Msi } +Invoke-Native 'MSI shortcut icon validation (ICE50)' { + wix msi validate $Msi -ice ICE50 -intermediateFolder (Join-Path $Stage 'msi-validation') +} # wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets. Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb')) & (Join-Path $PSScriptRoot 'sign.ps1') $Msi diff --git a/packaging/windows/photocraft.wxs b/packaging/windows/photocraft.wxs index 8d97f9bc96..7ad692b42e 100644 --- a/packaging/windows/photocraft.wxs +++ b/packaging/windows/photocraft.wxs @@ -24,8 +24,10 @@ DowngradeErrorMessage="A newer version of PhotoCraft is already installed." /> - - + + + @@ -46,7 +48,7 @@ + Icon="PhotocraftIcon.exe" IconIndex="0" />