From 9e0fcc13809e35e9b2918d6641f722349de1fe7c Mon Sep 17 00:00:00 2001 From: DWAA1660 Date: Wed, 7 Oct 2026 19:38:02 -0600 Subject: [PATCH] #143: Windows MSI desktop shortcut checkbox, Start Menu fix and install-complete dialog (#183) * #143: Windows MSI desktop shortcut checkbox and install-complete dialog Add native installer dialogs (installer-ui.wxs): a welcome screen with a "Create a desktop shortcut" checkbox (ticked by default), repair/remove, progress, and "Setup completed successfully" / failed / cancelled screens with Finish. Unattended /qn and /qb installs never wait on a dialog and can skip the desktop shortcut with INSTALLDESKTOPSHORTCUT=0. Start Menu and desktop shortcuts are now plain links to pdfcraft.exe in their own components (HKLM key paths, removed on uninstall) instead of advertised MSI shortcuts. test-msi.ps1 checks the compiled MSI's shortcut, checkbox and dialog tables during packaging; the ARM64 install smoke test checks both shortcut targets, their removal on uninstall, and the INSTALLDESKTOPSHORTCUT=0 opt-out. Co-authored-by: ChatGPT Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JqiBTPWGWbhQDYNFcxFte6 * #143: run test-msi out of process before signing; add files-in-use dialog test-msi.ps1 held the MSI open through a Windows Installer COM handle in package.ps1's own process, so signtool could fail on signed release builds. Run it in a child process and release the COM objects. The hand-authored UI had no MsiRMFilesInUse dialog, so an upgrade or repair while PdfCraft is running could not offer to close it. Add one (OK closes the programs with MSIRMSHUTDOWN=2, Cancel stops setup) and check for it in test-msi.ps1. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JqiBTPWGWbhQDYNFcxFte6 --------- Co-authored-by: ChatGPT Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- .github/workflows/packaging-lint.yml | 2 +- .github/workflows/windows-arm64.yml | 27 +++++- ROADMAP.md | 1 + docs/releasing.md | 18 +++- packaging/windows/installer-ui.wxs | 120 +++++++++++++++++++++++++++ packaging/windows/package.ps1 | 4 + packaging/windows/pdfcraft.wxs | 39 +++++++-- packaging/windows/test-msi.ps1 | 75 +++++++++++++++++ parity/acrobat-features.toml | 2 +- 9 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 packaging/windows/installer-ui.wxs create mode 100644 packaging/windows/test-msi.ps1 diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml index 39adb8d5..40376cf5 100644 --- a/.github/workflows/packaging-lint.yml +++ b/.github/workflows/packaging-lint.yml @@ -46,7 +46,7 @@ jobs: } - name: XML, desktop entry, AppStream, Flatpak manifest run: | - xmllint --noout packaging/windows/pdfcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + xmllint --noout packaging/windows/*.wxs packaging/macos/*.plist* packaging/linux/*.xml* # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon (#57). if grep -oE ' $target" + } $dir = Join-Path $env:ProgramFiles 'PdfCraft' foreach ($exe in 'pdfcraft.exe', 'pdfcraft-cli.exe') { $path = Join-Path $dir $exe @@ -110,7 +123,19 @@ jobs: $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } if (Test-Path (Join-Path $dir 'pdfcraft.exe')) { throw 'uninstall left pdfcraft.exe behind' } + foreach ($shortcut in $shortcuts) { + if (Test-Path -LiteralPath $shortcut) { throw "uninstall left $shortcut behind" } + } Write-Output 'ok uninstalled' + # Unattended installs can opt out of the desktop shortcut (#143); the Start Menu one stays. + $p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', 'INSTALLDESKTOPSHORTCUT=0', '/l*v', 'install-nodesktop.log' -Wait -PassThru + if ($p.ExitCode -ne 0) { Get-Content install-nodesktop.log -Tail 60; throw "msiexec /i INSTALLDESKTOPSHORTCUT=0 exited $($p.ExitCode)" } + if (-not (Test-Path -LiteralPath $shortcuts[0])) { throw "INSTALLDESKTOPSHORTCUT=0 also skipped $($shortcuts[0])" } + if (Test-Path -LiteralPath $shortcuts[1]) { throw "INSTALLDESKTOPSHORTCUT=0 still created $($shortcuts[1])" } + $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru + if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } + if (Test-Path -LiteralPath $shortcuts[0]) { throw "uninstall left $($shortcuts[0]) behind" } + Write-Output 'ok INSTALLDESKTOPSHORTCUT=0: Start Menu shortcut only' test: name: test (native ARM64) diff --git a/ROADMAP.md b/ROADMAP.md index a7214e00..c9fdfdf8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -127,6 +127,7 @@ M0 → M1 → M2 → M3 → M4 must happen in order. After M4, M5–M12 can run Newest first. One line per session: the date, what moved, and the new overall percentage. +- **2026-10-07 (community, #143):** Windows MSI gets a full-UI welcome dialog with a "Create a desktop shortcut" checkbox (on by default; `INSTALLDESKTOPSHORTCUT=0` for unattended installs), progress, files in use, repair/remove, and a "Setup completed successfully" screen with Finish (plus distinct failed/cancelled screens). Start Menu and desktop shortcuts are now plain links to `pdfcraft.exe` instead of advertised MSI shortcuts. Packaging checks the compiled MSI's shortcut, checkbox and dialog tables; the ARM64 smoke test checks both links' targets, their removal on uninstall, and the opt-out. Installer polish only; feature counts unchanged. ≈ 30–35%. - **2026-10-07 (session 16):** PrintCraft is renamed PdfCraft: repo `storytold/pdfcraft`, crates and binaries `pdfcraft*`, bundle id `ai.storyteller.pdfcraft`. On first launch the app moves the old PrintCraft settings and crash-recovery folders to the new name, so upgrades keep recent files, preferences and unsaved work. No feature change. ≈ 30–35%. - **2026-10-07 (community, #163):** Fill & Sign shows saved signature/initials previews with add, change and remove controls in the quick picker and left panel. The editor loads the saved typed text or drawn strokes; Apply replaces future placements and Cancel preserves the saved value. Existing annotations stay unchanged. Long typed names shrink to the placement width and include glyph overhangs in preview/PDF bounds. Regression coverage includes persistence, removal/recreation, long names and the opt-in UI control channel. Parity counts and effort estimate unchanged (50.2% shipped, ≈ 30–35% effort). diff --git a/docs/releasing.md b/docs/releasing.md index 7277047e..89ee2d46 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -93,10 +93,20 @@ neither the MSI nor the portable zip needs the Visual C++ redistributable. - Before packaging, it reads both executables' PE headers: the machine type must match `-Arch`, and `pdfcraft.exe` must be a GUI-subsystem program (no console window, #57) while `pdfcraft-cli.exe` stays a console program. -- `pdfcraft.wxs` (WiX v5) installs per machine into Program Files with a Start Menu shortcut and an - App Paths entry. The MSI version is the numeric `X.Y.Z` (MSI has no pre-release field), and - same-version upgrades are allowed so release candidates replace each other. Icon ids end in `.ico` - or `.exe` (Windows Installer requires it); packaging-lint checks this. +- `pdfcraft.wxs` (WiX v5) installs per machine into Program Files with a Start Menu shortcut, a + desktop shortcut (on by default) and an App Paths entry. Both shortcuts are plain links to + `pdfcraft.exe`, not advertised MSI shortcuts (#107, #143). The MSI version is the numeric `X.Y.Z` + (MSI has no pre-release field), and same-version upgrades are allowed so release candidates replace + each other. Icon ids end in `.ico` or `.exe` (Windows Installer requires it); packaging-lint checks + this. +- `installer-ui.wxs` supplies native welcome, maintenance, progress, files-in-use and outcome + dialogs. Full UI confirms success with Finish; failures and cancellations have distinct messages. + The welcome dialog has a "Create a desktop shortcut" checkbox, ticked by default. `/qn` and `/qb` + stay unattended and create the desktop shortcut unless `INSTALLDESKTOPSHORTCUT=0` is passed. +- `test-msi.ps1 ` checks the compiled shortcut and dialog tables. Packaging runs it in a + child process before signing, so the MSI isn't held open when signtool runs. The ARM64 install + smoke test checks that both all-users shortcuts point at the installed `pdfcraft.exe` and are + removed on uninstall, and that `INSTALLDESKTOPSHORTCUT=0` skips the desktop one. - The portable zip holds both executables, the README, the licences, and the OFL licence of each embedded craft-fonts family. - **Signing:** `packaging/windows/sign.ps1` signs both executables and the MSI with `signtool` diff --git a/packaging/windows/installer-ui.wxs b/packaging/windows/installer-ui.wxs new file mode 100644 index 00000000..23de6624 --- /dev/null +++ b/packaging/windows/installer-ui.wxs @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1 index 98e84ab1..f6fc6a47 100644 --- a/packaging/windows/package.ps1 +++ b/packaging/windows/package.ps1 @@ -94,9 +94,13 @@ Copy-Item (Join-Path $Bin 'pdfcraft.exe'), (Join-Path $Bin 'pdfcraft-cli.exe') $ $Msi = Join-Path $Dist "pdfcraft-$Version-windows-$Arch.msi" Invoke-Native 'wix build' { wix build (Join-Path $PSScriptRoot 'pdfcraft.wxs') -arch $Arch ` + (Join-Path $PSScriptRoot 'installer-ui.wxs') ` -d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\pdfcraft.ico')" ` -o $Msi } +# Inspect the built MSI, not just the XML, before signing/publishing it. In a child process, so +# its Windows Installer database handle is gone before signtool opens the MSI. +Invoke-Native 'test-msi' { & (Get-Process -Id $PID).Path -NoProfile -File (Join-Path $PSScriptRoot 'test-msi.ps1') $Msi } # 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/pdfcraft.wxs b/packaging/windows/pdfcraft.wxs index cc895cc8..4396e86c 100644 --- a/packaging/windows/pdfcraft.wxs +++ b/packaging/windows/pdfcraft.wxs @@ -2,11 +2,12 @@ + + + + @@ -45,11 +52,7 @@ - - - + @@ -73,6 +76,24 @@ + + + + + + + + + + + diff --git a/packaging/windows/test-msi.ps1 b/packaging/windows/test-msi.ps1 new file mode 100644 index 00000000..60aa535b --- /dev/null +++ b/packaging/windows/test-msi.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS + Check the compiled MSI's shortcuts, desktop-shortcut checkbox and full-UI outcome wiring (#143), without installing it. +.EXAMPLE + pwsh packaging/windows/test-msi.ps1 dist/release/pdfcraft-0.2.1-windows-x64.msi +#> +param([Parameter(Mandatory)] [string] $Path) +$ErrorActionPreference = 'Stop' +$Installer = New-Object -ComObject WindowsInstaller.Installer +$Database = $Installer.OpenDatabase((Resolve-Path -LiteralPath $Path).Path, 0) + +function Read-Row([string] $Sql, [int] $Columns) { + $view = $Database.OpenView($Sql) + try { + [void] $view.Execute() + $record = $view.Fetch() + if (-not $record) { throw "MSI row missing: $Sql" } + $values = @(for ($i = 1; $i -le $Columns; $i++) { $record.StringData($i) }) + return ,$values + } finally { [void] $view.Close() } +} + +function Assert-Equal($Actual, $Expected, [string] $What) { + if ($Actual -cne $Expected) { throw "$What`: expected '$Expected', got '$Actual'" } +} + +# Plain (non-advertised) shortcuts to pdfcraft.exe, each in its own component; the desktop one is +# gated by INSTALLDESKTOPSHORTCUT, which defaults to 1 and is secure so the UI choice reaches the +# elevated install. +foreach ($entry in @(@('StartMenuShortcut', 'ProgramMenuFolder', 'PdfcraftStartMenuShortcut', ''), + @('DesktopShortcut', 'DesktopFolder', 'PdfcraftDesktopShortcut', 'INSTALLDESKTOPSHORTCUT = 1'))) { + $row = Read-Row ('SELECT `Directory_`, `Target`, `Icon_`, `WkDir`, `Component_` FROM `Shortcut` WHERE `Shortcut` = ''' + $entry[0] + '''') 5 + Assert-Equal $row[0] $entry[1] "$($entry[0]) directory" + Assert-Equal $row[1] '[#PdfcraftExe]' "$($entry[0]) target" + Assert-Equal $row[2] 'PdfcraftIcon.ico' "$($entry[0]) icon" + Assert-Equal $row[3] 'INSTALLFOLDER' "$($entry[0]) working directory" + Assert-Equal $row[4] $entry[2] "$($entry[0]) component" + $component = Read-Row ('SELECT `Directory_`, `Condition` FROM `Component` WHERE `Component` = ''' + $entry[2] + '''') 2 + Assert-Equal $component[0] $entry[1] "$($entry[2]) directory" + Assert-Equal $component[1] $entry[3] "$($entry[2]) condition" +} +$default = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''INSTALLDESKTOPSHORTCUT''' 1 +Assert-Equal $default[0] '1' 'Desktop shortcut default' +$secure = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''SecureCustomProperties''' 1 +if (($secure[0] -split ';') -notcontains 'INSTALLDESKTOPSHORTCUT') { throw "INSTALLDESKTOPSHORTCUT is not secure: '$($secure[0])'" } +$checkbox = Read-Row 'SELECT `Type`, `Property`, `Text` FROM `Control` WHERE `Dialog_` = ''InstallWelcome'' AND `Control` = ''DesktopShortcut''' 3 +Assert-Equal $checkbox[0] 'CheckBox' 'Welcome desktop-shortcut control' +Assert-Equal $checkbox[1] 'INSTALLDESKTOPSHORTCUT' 'Welcome checkbox property' +if ($checkbox[2] -notmatch 'desktop shortcut') { throw "Welcome checkbox label: '$($checkbox[2])'" } +$checked = Read-Row 'SELECT `Value` FROM `CheckBox` WHERE `Property` = ''INSTALLDESKTOPSHORTCUT''' 1 +Assert-Equal $checked[0] '1' 'Welcome checkbox value' +$app = Read-Row 'SELECT `KeyPath` FROM `Component` WHERE `Component` = ''PdfcraftApp''' 1 +Assert-Equal $app[0] 'PdfcraftExe' 'Shortcut executable key path' +$scope = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''ALLUSERS''' 1 +Assert-Equal $scope[0] '1' 'Per-machine shortcut scope' + +# Negative sequences are Windows Installer's success/user-exit/failure paths. Only full UI +# shows these dialogs: an unattended /qn or /qb install must never wait for a Finish click. +foreach ($exit in @(@('InstallComplete', '-1'), @('InstallCancelled', '-2'), @('InstallFailed', '-3'))) { + $row = Read-Row ('SELECT `Condition`, `Sequence` FROM `InstallUISequence` WHERE `Action` = ''' + $exit[0] + '''') 2 + Assert-Equal $row[0] 'UILevel = 5' "$($exit[0]) UI level" + Assert-Equal $row[1] $exit[1] "$($exit[0]) exit path" + $finish = Read-Row ('SELECT `Type`, `Text` FROM `Control` WHERE `Dialog_` = ''' + $exit[0] + ''' AND `Control` = ''Finish''') 2 + Assert-Equal $finish[0] 'PushButton' "$($exit[0]) Finish control" + Assert-Equal $finish[1] '&Finish' "$($exit[0]) Finish label" + $endDialog = Read-Row ('SELECT `Argument` FROM `ControlEvent` WHERE `Dialog_` = ''' + $exit[0] + ''' AND `Control_` = ''Finish'' AND `Event` = ''EndDialog''') 1 + Assert-Equal $endDialog[0] 'Return' "$($exit[0]) Finish event" +} +$title = Read-Row 'SELECT `Text` FROM `Control` WHERE `Dialog_` = ''InstallComplete'' AND `Control` = ''Title''' 1 +if ($title[0] -notmatch 'completed successfully') { throw 'Success dialog does not confirm completion' } +$rm = Read-Row 'SELECT `Dialog` FROM `Dialog` WHERE `Dialog` = ''MsiRMFilesInUse''' 1 +Assert-Equal $rm[0] 'MsiRMFilesInUse' 'Files-in-use dialog' +[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Database) +[void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($Installer) +Write-Output 'ok MSI: Start Menu shortcut, optional desktop shortcut (default on, checkbox), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog' diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 3e3eeb2c..cef5d3ec 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -9963,7 +9963,7 @@ status = "planned" commands = [] tools = [] evidence = [] -notes = "" +notes = "Windows MSI creates a Start menu shortcut and an optional (default on) desktop shortcut and confirms full-UI completion with Finish (#143); packaging/windows/test-msi.ps1 checks the compiled MSI and windows-arm64.yml checks installation/removal. Signed installers across platforms remain planned." [[feature]] id = "misc.web-deploy"