Windows MSI: reject per-user overrides and retain progress text (#305) (#310)

* #305: harden Windows MSI deployment and progress UI

Co-authored-by: Codex <[email protected]>

* Keep the MSI manufacturer as Learning Machines LLC

The company name matches the Linux packages; changing it is the owner's call.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* #305: keep compiled-MSI checks compatible with PowerShell COM

Validated the compiled x64 MSI and negative scope, publisher and progress mutations.

Co-authored-by: Codex <[email protected]>

* ROADMAP: the publisher name is unchanged

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Codex <[email protected]>
Co-authored-by: Brandon Thomas <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-08 13:40:30 -04:00
1 parent 7458f7d47d
commit d801407a9f
6 files changed
+75 -6

No files matched your search

+7
View File
@@ -73,6 +73,13 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'
$msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName
# A command-line override must not put files in a user profile while registering
# their handlers machine-wide (#305). This runs on a disposable CI runner.
$p = Start-Process msiexec.exe -WindowStyle Hidden -ArgumentList '/i', "`"$msi`"", '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1', '/l*v', 'per-user.log' -Wait -PassThru
if ($p.ExitCode -ne 1603) { Get-Content per-user.log -Tail 60; throw "per-user install should fail with 1603, got $($p.ExitCode)" }
if (-not (Select-String -Path per-user.log -SimpleMatch 'PdfCraft must be installed for all users.')) {
Get-Content per-user.log -Tail 60; throw 'per-user install did not fail on the scope guard'
}
$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 = @(
+9
View File
@@ -418,6 +418,15 @@ The honest assessment by area, what's lacking and where we're going are in **[RO
Installers and executables are code-signed.
The MSI installs for all users and requires administrator privileges. For unattended deployment
without a desktop shortcut, run from an elevated terminal:
```powershell
msiexec /i "pdfcraft-<ver>-windows-x64.msi" /qn /norestart INSTALLDESKTOPSHORTCUT=0
```
Use the MSI for your architecture. Per-user installation overrides are not supported.
### macOS
| Build | File | Notes |
+2 -1
View File
@@ -131,6 +131,8 @@ Newest first. One line per session: the date, what moved, and the new overall pe
- **2026-10-09 (M14, Simplified Chinese About):** The About tabs, contributor controls and statistics, and model table now have Simplified Chinese translations, along with multiline link-privacy and cut-and-stack guidance. Coverage checks include direct `i18n::t` calls, multiline literals and dynamic credit labels; UI control checks exercise the translated About tabs and tables. Chinese font delivery remains incomplete; overall ≈ 30–35% (unchanged).
- **2026-10-09 (M14, #305):** Windows MSI rejects per-user installation overrides while allowing removal of legacy per-user installs and keeps its progress message visible. README documents unattended deployment without a desktop shortcut. Compiled x64 MSI checks pass; ARM64 install regressions added for CI. Overall estimate unchanged (about 30–35%).
- **2026-10-08 (M14, French):** Français (`fr`) is a complete interface catalog (about 1,880 entries), selected automatically for `fr`, `fr_FR`, `fr_CA` and other French locales, and from Preferences. Coverage tests check commands, All tools labels and every `tl!` literal; history labels keep captured names. Latin UI faces cover the catalog. Localization remains partial for Czech and Brazilian Portuguese; overall ≈ 30–35% (unchanged).
- **2026-10-08 (M14, right-to-left file names):** A tab with an Arabic or Hebrew file name showed replacement boxes. The interface now takes an `Arab` face from craft-fonts when the build input has one, and on desktop falls back to one font installed on the machine for characters no embedded face has (`PDFCRAFT_SYSTEM_FONTS=0` turns that off; README screenshots do). File names and paths in the tab strip, the recent-files list and Properties are put in display order, since egui joins the letters but does not reorder runs. Still missing: multi-line text (bookmarks, comments, search results), text input and a mirrored layout, so `misc.rtl-ui` stays planned; the craft-fonts revision pinned for releases predates its Arabic face, so release builds rely on the installed font until the pin moves. Overall estimate unchanged (about 30–35%).
@@ -236,4 +238,3 @@ Newest first. One line per session: the date, what moved, and the new overall pe
- Robustness sweep (963 of 983 files open, 0 crashes), text layer, find and select, tiles, web build, polish.
- Overall ≈ 3–4%.
- **2026-09-30 (session 1):** planning complete; viewer vertical slice.
+2 -3
View File
@@ -42,9 +42,8 @@
<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>
<!-- No ActionText table is authored: subscribing would blank this message (#305). -->
<Control Id="Status" Type="Text" X="20" Y="60" Width="330" Height="40" Text="Please wait while setup completes." />
<Control Id="Progress" Type="ProgressBar" X="20" Y="110" Width="330" Height="15" ProgressBlocks="yes">
<Subscribe Event="SetProgress" Attribute="Progress" />
</Control>
+4
View File
@@ -19,6 +19,10 @@
Compressed="yes"
Language="1033">
<SummaryInformation Description="PdfCraft PDF workbench" Manufacturer="Learning Machines LLC" />
<!-- HKLM registrations must never point into a per-user installation (#305). Allow removal
of an existing installation even if an older MSI let it install per-user. -->
<Launch Condition="ALLUSERS = 1 OR (Installed AND REMOVE = &quot;ALL&quot;)"
Message="PdfCraft must be installed for all users. Run setup with administrator privileges and ALLUSERS=1; per-user installation is not supported." />
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
<MajorUpgrade AllowSameVersionUpgrades="yes"
+51 -2
View File
@@ -1,6 +1,6 @@
<#
.SYNOPSIS
Check the compiled MSI's shortcuts, desktop-shortcut checkbox and full-UI outcome wiring (#143), without installing it.
Check the compiled MSI's install scope, publisher, shortcuts and native UI, without installing it.
.EXAMPLE
pwsh packaging/windows/test-msi.ps1 dist/release/pdfcraft-0.2.1-windows-x64.msi
#>
@@ -24,6 +24,55 @@ function Assert-Equal($Actual, $Expected, [string] $What) {
if ($Actual -cne $Expected) { throw "$What`: expected '$Expected', got '$Actual'" }
}
function Assert-NoRow([string] $Sql, [string] $What) {
$view = $Database.OpenView($Sql)
try {
[void] $view.Execute()
if ($view.Fetch()) { throw "Unexpected MSI row ($What): $Sql" }
} finally { [void] $view.Close() }
}
# Test the compiled condition with Windows Installer's evaluator, in a restricted session that
# cannot change machine state. Normal installs/repairs work, per-user overrides fail, and removal
# of an older incorrectly scoped installation remains possible (#305).
$scopeMessage = 'PdfCraft must be installed for all users. Run setup with administrator privileges and ALLUSERS=1; per-user installation is not supported.'
$scopeCondition = Read-Row ('SELECT `Condition` FROM `LaunchCondition` WHERE `Description` = ''' + $scopeMessage + '''') 1
$Installer.UILevel = 2
$session = $null
try {
$session = $Installer.OpenPackage((Resolve-Path -LiteralPath $Path).Path, 1)
foreach ($case in @(
@('machine install', '1', '', '', '', 1),
@('machine repair', '1', '', '1', '', 1),
@('forced per-user install', '2', '1', '', '', 0),
@('empty ALLUSERS', '', '', '', '', 0),
@('resolved per-user install', '', '1', '', '', 0),
@('forced per-user repair', '2', '1', '1', '', 0),
@('machine uninstall', '1', '', '1', 'ALL', 1),
@('legacy per-user uninstall', '', '1', '1', 'ALL', 1))) {
$session.Property('ALLUSERS') = $case[1]
$session.Property('MSIINSTALLPERUSER') = $case[2]
$session.Property('Installed') = $case[3]
$session.Property('REMOVE') = $case[4]
Assert-Equal ($session.EvaluateCondition($scopeCondition[0])) $case[5] $case[0]
}
} finally {
if ($session) { [void] [Runtime.InteropServices.Marshal]::FinalReleaseComObject($session) }
}
foreach ($sequence in @('InstallUISequence', 'InstallExecuteSequence')) {
$launch = Read-Row ('SELECT `Condition`, `Sequence` FROM `' + $sequence + '` WHERE `Action` = ''LaunchConditions''') 2
Assert-Equal $launch[0] '' "$sequence launch conditions are unconditional"
$cost = Read-Row ('SELECT `Sequence` FROM `' + $sequence + '` WHERE `Action` = ''CostInitialize''') 1
if ([int] $launch[1] -le 0 -or [int] $launch[1] -ge [int] $cost[0]) {
throw "$sequence must reject per-user overrides before costing"
}
}
$manufacturer = Read-Row 'SELECT `Value` FROM `Property` WHERE `Property` = ''Manufacturer''' 1
Assert-Equal $manufacturer[0] 'Learning Machines LLC' 'MSI manufacturer'
$status = Read-Row 'SELECT `Text` FROM `Control` WHERE `Dialog_` = ''InstallProgress'' AND `Control` = ''Status''' 1
Assert-Equal $status[0] 'Please wait while setup completes.' 'Persistent progress message'
Assert-NoRow 'SELECT `Event` FROM `EventMapping` WHERE `Dialog_` = ''InstallProgress'' AND `Control_` = ''Status''' 'progress text subscription'
# 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.
@@ -86,4 +135,4 @@ $rm = Read-Row 'SELECT `Dialog` FROM `Dialog` WHERE `Dialog` = ''MsiRMFilesInUse
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'
Write-Output 'ok MSI: per-machine scope guard, publisher, persistent progress text, Start Menu shortcut, optional desktop shortcut (default on, checkbox), icon/key path, full-UI success/cancel/error and Finish controls, files-in-use dialog'