mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
The App Paths directory search doesn't check that lightcraft.exe exists, and when the old installers' unquoted path is gone, Windows Installer falls back to the text before the first space: a deleted C:\Program Files\LightCraft\lightcraft.exe resolved to C:\, so an upgrade would install into the drive root. Gate the fallback on a second search of the same value with a FileSearch for lightcraft.exe. The install-folder test gains legacy-folder-deleted (failed before this fix) and legacy-exe-deleted cases, and removes its empty parent registry key. Co-Authored-By: Claude Opus 5.5 <[email protected]>
128 lines
6.5 KiB
PowerShell
128 lines
6.5 KiB
PowerShell
<#
|
|
Exercise the install-folder searches and actions from lightcraft.wxs with real MSI AppSearch.
|
|
Requires Windows and WiX 5.0.2. No application binaries, administrator rights, or real
|
|
LightCraft installation are needed: a per-user probe writes only to a unique test registry key.
|
|
Run: pwsh packaging/windows/test-install-folder.ps1 [-Arch x86|x64|arm64] [-Wix path/to/wix.exe]
|
|
#>
|
|
param(
|
|
[ValidateSet('x86', 'x64', 'arm64')][string] $Arch = 'x64',
|
|
[string] $Wix = 'wix'
|
|
)
|
|
$ErrorActionPreference = 'Stop'
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
$id = [Guid]::NewGuid().ToString('N')
|
|
$root = Join-Path ([IO.Path]::GetTempPath()) "lightcraft-install-folder-$id"
|
|
$testsKey = 'Software\LightCraftInstallerTests'
|
|
$key = "$testsKey\$id"
|
|
$product = [Guid]::NewGuid().ToString('B')
|
|
$upgrade = [Guid]::NewGuid().ToString('B')
|
|
$view = if ($Arch -eq 'x86') { [Microsoft.Win32.RegistryView]::Registry32 } else { [Microsoft.Win32.RegistryView]::Registry64 }
|
|
$registry = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::CurrentUser, $view)
|
|
$installed = $false
|
|
$passed = $false
|
|
New-Item -ItemType Directory -Path $root | Out-Null
|
|
|
|
function Invoke-Msi([string[]] $Arguments, [string] $Log, [switch] $AllowAbsent) {
|
|
$p = Start-Process msiexec.exe -ArgumentList ($Arguments + @('/qn', '/norestart', '/l*v', "`"$Log`"")) -Wait -PassThru
|
|
if ($p.ExitCode -ne 0 -and -not ($AllowAbsent -and $p.ExitCode -eq 1605)) {
|
|
Get-Content $Log -Tail 40
|
|
throw "msiexec exited $($p.ExitCode)"
|
|
}
|
|
}
|
|
|
|
function Set-TestValue([string] $Subkey, [string] $Name, [string] $Value) {
|
|
$k = $registry.CreateSubKey("$key\$Subkey")
|
|
try { $k.SetValue($Name, $Value, [Microsoft.Win32.RegistryValueKind]::String) } finally { $k.Dispose() }
|
|
}
|
|
|
|
try {
|
|
[xml] $source = Get-Content (Join-Path $PSScriptRoot 'lightcraft.wxs') -Raw
|
|
$ns = [Xml.XmlNamespaceManager]::new($source.NameTable)
|
|
$ns.AddNamespace('w', 'http://wixtoolset.org/schemas/v4/wxs')
|
|
# Copy the real search/action authoring, changing only registry roots/keys for isolation.
|
|
# Do not reimplement folder resolution here: removing the fallback must fail the legacy case.
|
|
$nodes = $source.SelectNodes('/w:Wix/w:Package/w:Property[w:RegistrySearch] | /w:Wix/w:Package/w:SetProperty[@Id="INSTALLFOLDER"]', $ns)
|
|
$authoring = foreach ($node in $nodes) {
|
|
$copy = $node.CloneNode($true)
|
|
foreach ($search in $copy.SelectNodes('.//w:RegistrySearch', $ns)) {
|
|
$search.SetAttribute('Root', 'HKCU')
|
|
$search.SetAttribute('Key', "$key\$($search.GetAttribute('Key'))")
|
|
}
|
|
$copy.OuterXml
|
|
}
|
|
$directory = $source.SelectSingleNode('/w:Wix/w:Package/w:StandardDirectory[w:Directory[@Id="INSTALLFOLDER"]]', $ns).OuterXml
|
|
$probe = @"
|
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
|
<Package Name="LightCraft folder search test $id" Manufacturer="LightCraft tests"
|
|
Version="1.0.0" ProductCode="$product" UpgradeCode="$upgrade" Scope="perUser">
|
|
$($authoring -join "`n")
|
|
$directory
|
|
<Feature Id="Main"><ComponentRef Id="Result" /></Feature>
|
|
<Component Id="Result" Directory="INSTALLFOLDER" Guid="*">
|
|
<RegistryValue Root="HKCU" Key="$key\Result" Name="Folder" Value="[INSTALLFOLDER]" Type="string" KeyPath="yes" />
|
|
</Component>
|
|
</Package>
|
|
</Wix>
|
|
"@
|
|
$wxs = Join-Path $root 'probe.wxs'
|
|
$msi = Join-Path $root 'probe.msi'
|
|
$probe | Set-Content $wxs -Encoding utf8
|
|
& $Wix build $wxs -arch $Arch -o $msi
|
|
if ($LASTEXITCODE -ne 0) { throw 'Could not build the folder-search probe' }
|
|
|
|
$legacy = Join-Path $root 'legacy app café'
|
|
$modern = Join-Path $root 'remembered app'
|
|
$explicit = Join-Path $root 'explicit app'
|
|
$emptied = Join-Path $root 'emptied app'
|
|
New-Item -ItemType Directory -Path $legacy, $modern, $explicit, $emptied | Out-Null
|
|
$legacyExe = Join-Path $legacy 'lightcraft.exe'
|
|
[IO.File]::WriteAllText($legacyExe, 'test placeholder; never executed')
|
|
$programFiles = if ($Arch -eq 'x86' -and ${env:ProgramFiles(x86)}) { ${env:ProgramFiles(x86)} } else { $env:ProgramFiles }
|
|
$cases = @(
|
|
@{ Name = 'fresh'; Expected = (Join-Path $programFiles 'LightCraft') },
|
|
@{ Name = 'legacy-app-paths'; Legacy = $legacyExe; Expected = $legacy },
|
|
# A stale unquoted path with a space used to resolve to the parent of the text before the space.
|
|
@{ Name = 'legacy-folder-deleted'; Legacy = (Join-Path $root 'deleted app\lightcraft.exe'); Expected = (Join-Path $programFiles 'LightCraft') },
|
|
@{ Name = 'legacy-exe-deleted'; Legacy = (Join-Path $emptied 'lightcraft.exe'); Expected = (Join-Path $programFiles 'LightCraft') },
|
|
@{ Name = 'remembered-folder'; Modern = $true; Expected = $modern },
|
|
@{ Name = 'remembered-beats-legacy'; Modern = $true; Legacy = $legacyExe; Expected = $modern },
|
|
@{ Name = 'explicit-beats-legacy'; Legacy = $legacyExe; Explicit = $true; Expected = $explicit },
|
|
@{ Name = 'explicit-beats-both'; Modern = $true; Legacy = $legacyExe; Explicit = $true; Expected = $explicit }
|
|
)
|
|
foreach ($case in $cases) {
|
|
$registry.DeleteSubKeyTree($key, $false)
|
|
if ($case.Legacy) { Set-TestValue 'Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe' '' $case.Legacy }
|
|
if ($case.Modern) { Set-TestValue 'Software\LightCraft' 'InstallFolder' "$modern\" }
|
|
$arguments = @('/i', "`"$msi`"")
|
|
if ($case.Explicit) { $arguments += "INSTALLFOLDER=`"$explicit`"" }
|
|
# Also attempt cleanup if an installation only partially completes.
|
|
$installed = $true
|
|
Invoke-Msi $arguments (Join-Path $root "$($case.Name)-install.log")
|
|
$result = $registry.OpenSubKey("$key\Result")
|
|
if (-not $result) { throw "$($case.Name): no result recorded" }
|
|
try { $actual = [string] $result.GetValue('Folder') } finally { $result.Dispose() }
|
|
if ($actual.TrimEnd('\') -ine $case.Expected.TrimEnd('\')) {
|
|
throw "$($case.Name): expected '$($case.Expected)', got '$actual'"
|
|
}
|
|
Invoke-Msi @('/x', $product) (Join-Path $root "$($case.Name)-uninstall.log")
|
|
$installed = $false
|
|
Write-Output "ok $Arch $($case.Name)"
|
|
}
|
|
$passed = $true
|
|
} finally {
|
|
try {
|
|
if ($installed) { Invoke-Msi @('/x', $product) (Join-Path $root 'cleanup.log') -AllowAbsent }
|
|
} finally {
|
|
$registry.DeleteSubKeyTree($key, $false)
|
|
$tests = $registry.OpenSubKey($testsKey)
|
|
if ($tests) {
|
|
$empty = $tests.SubKeyCount -eq 0 -and $tests.ValueCount -eq 0
|
|
$tests.Dispose()
|
|
if ($empty) { $registry.DeleteSubKey($testsKey, $false) }
|
|
}
|
|
$registry.Dispose()
|
|
# Keep logs on failure; they explain MSI failures that otherwise only report an exit code.
|
|
if ($passed) { Remove-Item -Recurse -Force $root } else { Write-Output "Test artifacts retained: $root" }
|
|
}
|
|
}
|