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

+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'