#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 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JqiBTPWGWbhQDYNFcxFte6

---------

Co-authored-by: ChatGPT <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-07 21:38:02 -04:00
1 parent d7b93b56b5
commit 9e0fcc1380
9 files changed
+272 -16

No files matched your search

+1 -1
View File
@@ -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 '<Icon Id="[^"]*"' packaging/windows/pdfcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error file=packaging/windows/pdfcraft.wxs::Icon Id must end in .ico or .exe"; exit 1
+26 -1
View File
@@ -68,13 +68,26 @@ jobs:
with:
name: pdfcraft-windows-arm64
path: dist
- name: Install the MSI, check it, run it, uninstall it
- name: Install the MSI, check it, run it, uninstall it, reinstall without the desktop shortcut
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName
$p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', 'install.log' -Wait -PassThru
if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 60; throw "msiexec /i exited $($p.ExitCode)" }
$shortcuts = @(
(Join-Path ([Environment]::GetFolderPath('CommonPrograms')) 'PdfCraft.lnk'),
(Join-Path ([Environment]::GetFolderPath('CommonDesktopDirectory')) 'PdfCraft.lnk')
)
# Plain shortcuts to the program (#143), not advertised ones, so TargetPath is the exe.
$exePath = Join-Path $env:ProgramFiles 'PdfCraft\pdfcraft.exe'
$wsh = New-Object -ComObject WScript.Shell
foreach ($shortcut in $shortcuts) {
if (-not (Test-Path -LiteralPath $shortcut)) { throw "installer did not create $shortcut" }
$target = $wsh.CreateShortcut($shortcut).TargetPath
if ($target -ne $exePath) { throw "$shortcut points to '$target', not $exePath" }
Write-Output "ok $shortcut -> $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)
+1
View File
@@ -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).
+14 -4
View File
@@ -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 <file.msi>` 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`
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Original, text-only native MSI dialogs. No third-party UI artwork or custom-action DLLs.
Full UI gets welcome (with the desktop-shortcut checkbox)/progress/outcome screens; /qn and /qb stay unattended. -->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<UI Id="PdfcraftInstallerUI">
<TextStyle Id="InstallerNormal" FaceName="Segoe UI" Size="9" />
<TextStyle Id="InstallerTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
<Property Id="DefaultUIFont" Value="InstallerNormal" />
<Dialog Id="InstallWelcome" Width="370" Height="180" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Install [ProductName]" />
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="40"
Text="Setup will install [ProductName] for all users and add it to the Start menu. Click Install to continue." />
<Control Id="DesktopShortcut" Type="CheckBox" X="20" Y="105" Width="330" Height="17"
Property="INSTALLDESKTOPSHORTCUT" CheckBoxValue="1" Text="Create a &amp;desktop shortcut" />
<Control Id="Install" Type="PushButton" X="195" Y="145" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&amp;Install">
<Publish Event="EndDialog" Value="Return" />
</Control>
<Control Id="Cancel" Type="PushButton" X="280" Y="145" Width="70" Height="23" Cancel="yes" Text="Cancel">
<Publish Event="EndDialog" Value="Exit" />
</Control>
</Dialog>
<Dialog Id="InstallMaintenance" Width="370" Height="180" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Maintain [ProductName]" />
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
Text="[ProductName] is already installed. Choose Repair to restore missing files and shortcuts, or Remove to uninstall it." />
<Control Id="Repair" Type="PushButton" X="110" Y="145" Width="75" Height="23" Default="yes" ElevationShield="yes" Text="&amp;Repair">
<Publish Property="REINSTALL" Value="ALL" Order="1" />
<Publish Property="REINSTALLMODE" Value="amus" Order="2" />
<Publish Event="EndDialog" Value="Return" Order="3" />
</Control>
<Control Id="Remove" Type="PushButton" X="195" Y="145" Width="75" Height="23" ElevationShield="yes" Text="R&amp;emove">
<Publish Property="REMOVE" Value="ALL" Order="1" />
<Publish Event="EndDialog" Value="Return" Order="2" />
</Control>
<Control Id="Cancel" Type="PushButton" X="280" Y="145" Width="70" Height="23" Cancel="yes" Text="Cancel">
<Publish Event="EndDialog" Value="Exit" />
</Control>
</Dialog>
<Dialog Id="InstallProgress" Width="370" Height="180" Title="[ProductName] Setup" Modeless="yes">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setting up [ProductName]" />
<Control Id="Status" Type="Text" X="20" Y="60" Width="330" Height="40" Text="Please wait while setup completes.">
<Subscribe Event="ActionText" Attribute="Text" />
</Control>
<Control Id="Progress" Type="ProgressBar" X="20" Y="110" Width="330" Height="15" ProgressBlocks="yes">
<Subscribe Event="SetProgress" Attribute="Progress" />
</Control>
<Control Id="Cancel" Type="PushButton" X="280" Y="145" Width="70" Height="23" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="ConfirmCancel" />
</Control>
</Dialog>
<Dialog Id="ConfirmCancel" Width="300" Height="110" Title="Cancel setup">
<Control Id="Description" Type="Text" X="20" Y="20" Width="260" Height="35" Text="Cancel [ProductName] setup?" />
<Control Id="Yes" Type="PushButton" X="130" Y="70" Width="70" Height="23" Text="&amp;Yes">
<Publish Event="EndDialog" Value="Exit" />
</Control>
<Control Id="No" Type="PushButton" X="210" Y="70" Width="70" Height="23" Default="yes" Cancel="yes" Text="&amp;No">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<Dialog Id="InstallComplete" Width="370" Height="180" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup completed successfully" />
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
Text="[ProductName] setup has completed successfully. Click Finish to close this window." />
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<Dialog Id="InstallFailed" Width="370" Height="180" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup failed" />
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
Text="[ProductName] setup could not be completed. Resolve the reported error and run setup again." />
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<Dialog Id="InstallCancelled" Width="370" Height="180" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="20" Width="330" Height="25" Text="{\InstallerTitle}Setup cancelled" />
<Control Id="Description" Type="Text" X="20" Y="55" Width="330" Height="70"
Text="[ProductName] setup was cancelled. Run setup again when you are ready." />
<Control Id="Finish" Type="PushButton" X="280" Y="145" Width="70" Height="23" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<!-- Shown by Windows Installer (Restart Manager) when an upgrade or repair must replace files
that a running PdfCraft holds open. OK closes those programs (MSIRMSHUTDOWN=2) and
continues; Cancel ends setup. -->
<Dialog Id="MsiRMFilesInUse" Width="370" Height="200" Title="[ProductName] Setup" KeepModeless="yes">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="25" Text="{\InstallerTitle}Files in use" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="30"
Text="These programs are using files that setup needs to update. Click OK to close them and continue, or Cancel to stop setup." />
<Control Id="List" Type="ListBox" X="20" Y="80" Width="330" Height="55" Property="FileInUseProcess" Sunken="yes" TabSkip="yes" />
<Control Id="OK" Type="PushButton" X="195" Y="165" Width="75" Height="23" Default="yes" Text="OK">
<Publish Property="MSIRMSHUTDOWN" Value="2" Order="1" />
<Publish Event="EndDialog" Value="Return" Order="2" />
</Control>
<Control Id="Cancel" Type="PushButton" X="280" Y="165" Width="70" Height="23" Cancel="yes" Text="Cancel">
<Publish Event="EndDialog" Value="Exit" />
</Control>
</Dialog>
<InstallUISequence>
<Show Dialog="InstallWelcome" Before="InstallMaintenance" Condition="UILevel = 5 AND NOT Installed" />
<Show Dialog="InstallMaintenance" Before="InstallProgress" Condition="UILevel = 5 AND Installed AND NOT REMOVE AND NOT REINSTALL" />
<Show Dialog="InstallProgress" Before="ExecuteAction" Condition="UILevel = 5" />
<Show Dialog="InstallComplete" OnExit="success" Condition="UILevel = 5" />
<Show Dialog="InstallFailed" OnExit="error" Condition="UILevel = 5" />
<Show Dialog="InstallCancelled" OnExit="cancel" Condition="UILevel = 5" />
</InstallUISequence>
</UI>
</Fragment>
</Wix>
+4
View File
@@ -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
+30 -9
View File
@@ -2,11 +2,12 @@
<!--
PdfCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
wix build pdfcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
wix build pdfcraft.wxs installer-ui.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
Per-machine install into Program Files, a Start Menu shortcut, an App Paths entry, and PdfCraft
registered as a PDF handler (offered under "Open with" and in Default apps, never forced as the
default). Files opened with PdfCraft arrive on its command line.
Per-machine install into Program Files, a Start Menu shortcut, a desktop shortcut (on by default,
optional), an App Paths entry, and PdfCraft registered as a PDF handler (offered under "Open with"
and in Default apps, never forced as the default). Files opened with PdfCraft arrive on its
command line.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Package
@@ -31,11 +32,17 @@
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/pdfcraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/pdfcraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<!-- Desktop shortcut (#143): on by default, a checkbox in the full-UI welcome dialog, and
INSTALLDESKTOPSHORTCUT=0 on the msiexec command line for unattended installs. Secure, so the
choice made in the (unelevated) UI reaches the elevated install. -->
<Property Id="INSTALLDESKTOPSHORTCUT" Value="1" Secure="yes" />
<UIRef Id="PdfcraftInstallerUI" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="PdfCraft" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<StandardDirectory Id="DesktopFolder" />
<Feature Id="Main" Title="PdfCraft" Level="1">
<ComponentGroupRef Id="PdfcraftFiles" />
@@ -45,11 +52,7 @@
<Fragment>
<ComponentGroup Id="PdfcraftFiles" Directory="INSTALLFOLDER">
<Component Id="PdfcraftApp" Guid="E724C1C6-091F-4E21-A685-80D089FDBE21">
<File Id="PdfcraftExe" Source="$(var.BinDir)\pdfcraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="PdfCraft"
Description="PdfCraft PDF workbench" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="PdfcraftIcon.ico" IconIndex="0" />
</File>
<File Id="PdfcraftExe" Source="$(var.BinDir)\pdfcraft.exe" KeyPath="yes" />
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\pdfcraft.exe" Value="[#PdfcraftExe]" Type="string" />
@@ -73,6 +76,24 @@
<RegistryValue Root="HKLM" Key="Software\RegisteredApplications" Name="PdfCraft" Value="Software\PdfCraft\Capabilities" Type="string" />
</Component>
<!-- Shortcuts (#143) are plain links to pdfcraft.exe, not advertised MSI shortcuts: advertised
ones point at a Windows Installer descriptor rather than the program, which Windows can't
use as a file handler (#107), and 0.2.1's advertised one went missing from the Start menu
(#143). Each has its own component with an HKLM key path, so Windows Installer removes it
on uninstall; in this per-machine package both folders are the all-users ones. -->
<Component Id="PdfcraftStartMenuShortcut" Directory="ProgramMenuFolder" Guid="DC028192-C233-4F99-9A77-ED458C60DF26">
<Shortcut Id="StartMenuShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench"
Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Shortcuts" Name="StartMenu" Type="integer" Value="1" KeyPath="yes" />
</Component>
<Component Id="PdfcraftDesktopShortcut" Directory="DesktopFolder" Guid="F6948627-32AB-4D78-8865-0FDF417CE442"
Condition="INSTALLDESKTOPSHORTCUT = 1">
<Shortcut Id="DesktopShortcut" Name="PdfCraft" Description="PdfCraft PDF workbench"
Target="[#PdfcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="PdfcraftIcon.ico" IconIndex="0" />
<RegistryValue Root="HKLM" Key="Software\PdfCraft\Shortcuts" Name="Desktop" Type="integer" Value="1" KeyPath="yes" />
</Component>
<Component Id="PdfcraftCli">
<File Id="PdfcraftCliExe" Source="$(var.BinDir)\pdfcraft-cli.exe" KeyPath="yes" />
</Component>
+75
View File
@@ -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'
+1 -1
View File
@@ -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"