From 4aea350f2a0fac9cb4ff463f3cff41a229aef4e1 Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Fri, 9 Oct 2026 14:49:19 -0400 Subject: [PATCH] Packaging: the installer's Repair puts back a damaged effectcraft-cli.exe (#466) Cause: Repair used REINSTALLMODE=ecmus. effectcraft-cli.exe has no version resource, so Windows Installer treats it as unversioned: a changed copy with a later write time counts as a user edit and is kept, and "c" only checks files that carry an MSI checksum, which these don't. Fix: Repair uses "amus", which puts back every file whatever its version or date. Tests: test-msi.ps1's stand-in CLI is unversioned like the real one; the test damages it in place and repairs with the REINSTALLMODE read from installer-ui.wxs (no longer msiexec /fa after deleting it), then compares contents. It fails with ecmus (the damaged file stays) and passes now. --- packaging/windows/installer-ui.wxs | 5 ++++- packaging/windows/test-msi.ps1 | 22 +++++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packaging/windows/installer-ui.wxs b/packaging/windows/installer-ui.wxs index 9fd1d689..14989932 100644 --- a/packaging/windows/installer-ui.wxs +++ b/packaging/windows/installer-ui.wxs @@ -90,7 +90,10 @@ - + + diff --git a/packaging/windows/test-msi.ps1 b/packaging/windows/test-msi.ps1 index e9d4b4b6..0bed8c43 100644 --- a/packaging/windows/test-msi.ps1 +++ b/packaging/windows/test-msi.ps1 @@ -9,7 +9,7 @@ 2. upgrades to 0.0.2 without naming a folder: it must stay in that folder (found from the installed effectcraft.exe, as there is no Path value yet), 3. upgrades to 0.0.3 the same way (found from the Path value 0.0.2 wrote), - 4. repairs a deleted file there, + 4. repairs a damaged (unversioned) CLI there with the wizard's Repair mode, 5. uninstalls: files and the App Paths registration go, 6. installs 0.0.3 with no folder: it must land in Program Files, then uninstalls. The wizard itself needs a person (or the snapshot steps in the PR); this covers what msiexec /qn @@ -54,9 +54,11 @@ function Assert-InstalledIn([string] $Dir, [switch] $NoPathValue) { Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue $Bin = Join-Path $Work 'bin' New-Item -ItemType Directory -Force -Path $Bin | Out-Null -# Any GUI and console executables will do: the MSI only copies them. +# The MSI only copies these. The app is versioned (it embeds VERSIONINFO); the real CLI has no +# version resource, so its stand-in is an unversioned file too: Windows Installer keeps a changed +# unversioned file on repair unless the repair mode forces it. Copy-Item (Join-Path $env:SystemRoot 'System32\notepad.exe') (Join-Path $Bin 'effectcraft.exe') -Copy-Item (Join-Path $env:SystemRoot 'System32\whoami.exe') (Join-Path $Bin 'effectcraft-cli.exe') +[IO.File]::WriteAllText((Join-Path $Bin 'effectcraft-cli.exe'), 'effectcraft-cli stand-in') # 0.0.1 stands in for 0.6.0 and earlier: the same package without the App Paths "Path" value. $Wxs = Join-Path $PSScriptRoot 'effectcraft.wxs' @@ -97,9 +99,19 @@ $arp = @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstal ForEach-Object { if ($_.GetValue('DisplayName') -eq 'EffectCraft') { $_.GetValue('DisplayVersion') } }) if ($arp.Count -ne 1 -or $arp[0] -ne '0.0.3') { throw "expected one EffectCraft 0.0.3 in Apps, found: $($arp -join ', ')" } -Remove-Item -LiteralPath (Join-Path $Custom 'effectcraft-cli.exe') -Invoke-Msiexec 'repair' @('/fa', "`"$($Msi['0.0.3'])`"") +# Repair the way the wizard's Repair button does: damage the CLI in place (a later write time, as a +# user edit or corruption would have), repair with the wizard's REINSTALLMODE, compare contents. +$mode = [regex]::Match((Get-Content -Raw (Join-Path $PSScriptRoot 'installer-ui.wxs')), 'Event="ReinstallMode" Value="([^"]+)"').Groups[1].Value +if (-not $mode) { throw 'no ReinstallMode in installer-ui.wxs' } +$cli = Join-Path $Custom 'effectcraft-cli.exe' +Start-Sleep -Seconds 2 +[IO.File]::WriteAllText($cli, 'damaged') +Invoke-Msiexec "repair ($mode)" @('/i', "`"$($Msi['0.0.3'])`"", 'REINSTALL=ALL', "REINSTALLMODE=$mode") Assert-InstalledIn $Custom +if ((Get-FileHash -LiteralPath $cli).Hash -ne (Get-FileHash -LiteralPath (Join-Path $Bin 'effectcraft-cli.exe')).Hash) { + throw "repair ($mode) left the damaged effectcraft-cli.exe in place" +} +Write-Output "ok repair ($mode) restored effectcraft-cli.exe" Invoke-Msiexec 'uninstall the custom install' @('/x', "`"$($Msi['0.0.3'])`"") if (Test-Path -LiteralPath (Join-Path $Custom 'effectcraft.exe')) { throw 'uninstall left effectcraft.exe behind' }