From d801407a9f956281d612dee3d2d72c76d43ef399 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: Fri, 9 Oct 2026 01:40:30 +0800 Subject: [PATCH] Windows MSI: reject per-user overrides and retain progress text (#305) (#310) * #305: harden Windows MSI deployment and progress UI Co-authored-by: Codex * 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 * #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 * ROADMAP: the publisher name is unchanged Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Codex Co-authored-by: Brandon Thomas Co-authored-by: Claude Opus 5.5 --- .github/workflows/windows-arm64.yml | 7 ++++ README.md | 9 +++++ ROADMAP.md | 3 +- packaging/windows/installer-ui.wxs | 5 ++- packaging/windows/pdfcraft.wxs | 4 +++ packaging/windows/test-msi.ps1 | 53 +++++++++++++++++++++++++++-- 6 files changed, 75 insertions(+), 6 deletions(-) diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml index b16eda06..2ddcf82a 100644 --- a/.github/workflows/windows-arm64.yml +++ b/.github/workflows/windows-arm64.yml @@ -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 = @( diff --git a/README.md b/README.md index 53be56b2..e08d5551 100644 --- a/README.md +++ b/README.md @@ -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--windows-x64.msi" /qn /norestart INSTALLDESKTOPSHORTCUT=0 +``` + +Use the MSI for your architecture. Per-user installation overrides are not supported. + ### macOS | Build | File | Notes | diff --git a/ROADMAP.md b/ROADMAP.md index 2872582a..ecd96d48 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. - diff --git a/packaging/windows/installer-ui.wxs b/packaging/windows/installer-ui.wxs index 23de6624..da965874 100644 --- a/packaging/windows/installer-ui.wxs +++ b/packaging/windows/installer-ui.wxs @@ -42,9 +42,8 @@ - - - + + diff --git a/packaging/windows/pdfcraft.wxs b/packaging/windows/pdfcraft.wxs index 2cec443a..cd0480fa 100644 --- a/packaging/windows/pdfcraft.wxs +++ b/packaging/windows/pdfcraft.wxs @@ -19,6 +19,10 @@ Compressed="yes" Language="1033"> + + @@ -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'