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.
This commit is contained in:
tylerjenningsw committed 2026-10-09 14:49:19 -04:00
1 parent 462913a483
commit 4aea350f2a
2 files changed
+21 -6

No files matched your search

+4 -1
View File
@@ -90,7 +90,10 @@
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}[ProductName] is already installed" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="24" NoPrefix="yes" Text="Repair puts back missing or damaged files in its folder. Remove uninstalls it; your projects are not touched." />
<Control Id="Repair" Type="PushButton" X="20" Y="80" Width="80" Height="20" Default="yes" ElevationShield="yes" Text="&amp;Repair">
<Publish Event="ReinstallMode" Value="ecmus" Order="1" />
<!-- "a": put back every file, whatever its version or date. With "e"/"o" a changed
unversioned file (effectcraft-cli.exe has no version resource) counts as a user edit
and is kept, and "c" only checks files that carry an MSI checksum. -->
<Publish Event="ReinstallMode" Value="amus" Order="1" />
<Publish Event="Reinstall" Value="ALL" Order="2" />
<Publish Event="EndDialog" Value="Return" Order="3" />
</Control>
+17 -5
View File
@@ -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' }