Merge pull request #466 from tylerjenningsw/enhance/windows-installer-folder

# Conflicts:
#	contributors/people.toml
This commit is contained in:
Claude committed 2026-10-10 12:51:31 +00:00
commit 6039e86a04
8 files changed
+381 -6

No files matched your search

+24 -2
View File
@@ -2,9 +2,9 @@
<!--
EffectCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
wix build effectcraft.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
wix build effectcraft.wxs installer-ui.wxs -arch x64 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
Per-machine install into Program Files, a Start Menu shortcut, `.pcraft` owned by EffectCraft,
Per-machine install into Program Files (or a folder the user picks), a Start Menu shortcut, `.pcraft` owned by EffectCraft,
and EffectCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
taking over the user's default app for them).
-->
@@ -23,6 +23,8 @@
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of EffectCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<!-- The setup wizard (installer-ui.wxs): pick the folder on a fresh install. -->
<UIRef Id="EffectcraftUI" />
<Icon Id="EffectcraftIcon.ico" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="EffectcraftIcon.ico" />
@@ -30,6 +32,25 @@
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/effectcraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<!-- An upgrade, repair or silent reinstall goes where the installed copy already is: the App Paths
"Path" value written below, or for releases before it (0.6.0 and earlier, which could be put
elsewhere with INSTALLFOLDER=...) the folder of the effectcraft.exe that App Paths names. A
"file" search with no FileSearch inside yields that file's folder. A folder passed on the
command line (INSTALLFOLDER=...) wins. -->
<Property Id="INSTALLFOLDER" Secure="yes" />
<Property Id="PREVIOUSINSTALLFOLDER" Secure="yes">
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Type="directory"
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\effectcraft.exe" Name="Path" />
</Property>
<Property Id="PREVIOUSEXEFOLDER" Secure="yes">
<RegistrySearch Id="PreviousExeFolder" Root="HKLM" Type="file"
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\effectcraft.exe" />
</Property>
<SetProperty Id="INSTALLFOLDER" Value="[PREVIOUSINSTALLFOLDER]" After="AppSearch" Sequence="both"
Condition="PREVIOUSINSTALLFOLDER AND NOT INSTALLFOLDER" />
<SetProperty Id="INSTALLFOLDER" Action="SetINSTALLFOLDERFromExe" Value="[PREVIOUSEXEFOLDER]"
After="SetINSTALLFOLDER" Sequence="both" Condition="PREVIOUSEXEFOLDER AND NOT INSTALLFOLDER" />
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="EffectCraft" />
</StandardDirectory>
@@ -50,6 +71,7 @@
</File>
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\effectcraft.exe" Value="[#EffectcraftExe]" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\effectcraft.exe" Name="Path" Value="[INSTALLFOLDER]" Type="string" />
</Component>
<Component Id="EffectcraftCli">
+201
View File
@@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
EffectCraft installer wizard: original dialogs built from Windows Installer's own controls and
events (no WixUI library), included by effectcraft.wxs with <UIRef Id="EffectcraftUI" />.
fresh install: DestDlg (folder, Change... opens BrowseDlg) -> ReadyDlg -> ProgressDlg -> ExitDlg
upgrade: ReadyDlg (the installed copy's folder, not editable) -> ProgressDlg -> ExitDlg
already there: MaintDlg (Repair or Remove) -> ProgressDlg -> ExitDlg
Silent installs (/qn) skip all of this; pass INSTALLFOLDER="..." to choose the folder there.
Folder controls edit INSTALLFOLDER indirectly through _BrowseProperty; SetTargetPath checks the
path and applies it, and Windows Installer reports a bad path itself.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<UI Id="EffectcraftUI">
<TextStyle Id="EcText" FaceName="Segoe UI" Size="9" />
<TextStyle Id="EcTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
<Property Id="DefaultUIFont" Value="EcText" />
<Property Id="ErrorDialog" Value="ErrorDlg" />
<Property Id="_BrowseProperty" Value="INSTALLFOLDER" />
<!-- Choose the destination folder (fresh installs only). -->
<Dialog Id="DestDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Choose where to install [ProductName]" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="30" NoPrefix="yes" Text="[ProductName] will be installed in this folder. Type a different folder, or click Change to browse for one." />
<Control Id="FolderLabel" Type="Text" X="20" Y="85" Width="330" Height="12" Text="&amp;Destination folder:" />
<Control Id="Folder" Type="PathEdit" X="20" Y="100" Width="260" Height="18" Property="_BrowseProperty" Indirect="yes" />
<Control Id="ChangeFolder" Type="PushButton" X="290" Y="99" Width="60" Height="20" Text="&amp;Change...">
<Publish Event="SpawnDialog" Value="BrowseDlg" />
</Control>
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&amp;Back" Disabled="yes" />
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="&amp;Next">
<Publish Event="SetTargetPath" Value="[_BrowseProperty]" Order="1" />
<Publish Event="NewDialog" Value="ReadyDlg" Order="2" />
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
</Dialog>
<!-- Folder browser: drive/folder list, up one level, new folder. -->
<Dialog Id="BrowseDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Browse for a folder" />
<Control Id="LookInLabel" Type="Text" X="20" Y="47" Width="44" Height="12" Text="&amp;Look in:" />
<Control Id="LookIn" Type="DirectoryCombo" X="66" Y="45" Width="220" Height="80" Property="_BrowseProperty" Indirect="yes" Fixed="yes" Remote="yes">
<Subscribe Event="IgnoreChange" Attribute="IgnoreChange" />
</Control>
<Control Id="Up" Type="PushButton" X="292" Y="44" Width="28" Height="18" Text="&amp;Up" ToolTip="Up one level">
<Publish Event="DirectoryListUp" Value="0" />
</Control>
<Control Id="NewFolder" Type="PushButton" X="322" Y="44" Width="28" Height="18" Text="Ne&amp;w" ToolTip="Create a new folder">
<Publish Event="DirectoryListNew" Value="0" />
</Control>
<Control Id="Folders" Type="DirectoryList" X="20" Y="68" Width="330" Height="120" Property="_BrowseProperty" Indirect="yes" Sunken="yes" TabSkip="no" />
<Control Id="PathLabel" Type="Text" X="20" Y="196" Width="330" Height="12" Text="&amp;Folder name:" />
<Control Id="Path" Type="PathEdit" X="20" Y="210" Width="330" Height="18" Property="_BrowseProperty" Indirect="yes" />
<Control Id="OK" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="OK">
<Publish Event="SetTargetPath" Value="[_BrowseProperty]" Order="1" />
<Publish Event="EndDialog" Value="Return" Order="2" />
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
<Publish Event="Reset" Value="0" Order="1" />
<Publish Event="EndDialog" Value="Return" Order="2" />
</Control>
</Dialog>
<!-- Last stop before anything changes: shows the folder that will be used. -->
<Dialog Id="ReadyDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Ready to install [ProductName]" />
<Control Id="InstallText" Type="Text" X="20" Y="45" Width="330" Height="24" NoPrefix="yes" Text="[ProductName] [ProductVersion] will be installed in:" HideCondition="WIX_UPGRADE_DETECTED" />
<Control Id="UpgradeText" Type="Text" X="20" Y="45" Width="330" Height="24" NoPrefix="yes" Text="[ProductName] is already installed. Version [ProductVersion] will replace it in the same folder:" Hidden="yes" ShowCondition="WIX_UPGRADE_DETECTED" />
<Control Id="Destination" Type="Text" X="20" Y="72" Width="330" Height="36" NoPrefix="yes" Text="[INSTALLFOLDER]" />
<Control Id="Prompt" Type="Text" X="20" Y="116" Width="330" Height="24" NoPrefix="yes" Text="Click Install to continue." />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&amp;Back" DisableCondition="WIX_UPGRADE_DETECTED">
<Publish Event="NewDialog" Value="DestDlg" />
</Control>
<Control Id="Install" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" ElevationShield="yes" Text="&amp;Install">
<Publish Event="EndDialog" Value="Return" />
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
</Dialog>
<!-- Opening the MSI when this version is already installed. -->
<Dialog Id="MaintDlg" Width="370" Height="270" Title="[ProductName] Setup">
<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">
<!-- "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>
<Control Id="Remove" Type="PushButton" X="20" Y="108" Width="80" Height="20" ElevationShield="yes" Text="Re&amp;move">
<Publish Event="Remove" Value="ALL" Order="1" />
<Publish Event="EndDialog" Value="Return" Order="2" />
</Control>
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
</Dialog>
<Dialog Id="ProgressDlg" Width="370" Height="270" Title="[ProductName] Setup" Modeless="yes">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Working on [ProductName]" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="24" NoPrefix="yes" Text="Please wait. This can take a minute." />
<Control Id="Action" Type="Text" X="20" Y="80" Width="330" Height="12" NoPrefix="yes">
<Subscribe Event="ActionText" Attribute="Text" />
</Control>
<Control Id="Progress" Type="ProgressBar" X="20" Y="96" Width="330" Height="12" ProgressBlocks="yes" Text="Progress">
<Subscribe Event="SetProgress" Attribute="Progress" />
</Control>
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="Cancel">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
</Dialog>
<Dialog Id="CancelDlg" Width="260" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="Text" Type="Text" X="10" Y="15" Width="240" Height="30" NoPrefix="yes" Text="Cancel [ProductName] setup? Nothing will be changed." />
<Control Id="Yes" Type="PushButton" X="132" Y="57" Width="56" Height="17" Text="&amp;Yes">
<Publish Event="EndDialog" Value="Exit" />
</Control>
<Control Id="No" Type="PushButton" X="194" Y="57" Width="56" Height="17" Default="yes" Cancel="yes" Text="&amp;No">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<!-- Windows Installer's messages (a bad folder, low disk space, ...) and files in use. -->
<Dialog Id="ErrorDlg" Width="270" Height="105" Title="[ProductName] Setup" ErrorDialog="yes">
<Control Id="ErrorText" Type="Text" X="10" Y="10" Width="250" Height="60" NoPrefix="yes" TabSkip="no" Text="Error" />
<Control Id="Y" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="&amp;Yes"><Publish Event="EndDialog" Value="ErrorYes" /></Control>
<Control Id="A" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="Abort"><Publish Event="EndDialog" Value="ErrorAbort" /></Control>
<Control Id="C" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="Cancel"><Publish Event="EndDialog" Value="ErrorCancel" /></Control>
<Control Id="I" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="&amp;Ignore"><Publish Event="EndDialog" Value="ErrorIgnore" /></Control>
<Control Id="N" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="&amp;No"><Publish Event="EndDialog" Value="ErrorNo" /></Control>
<Control Id="O" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="OK"><Publish Event="EndDialog" Value="ErrorOk" /></Control>
<Control Id="R" Type="PushButton" X="100" Y="80" Width="56" Height="17" TabSkip="yes" Text="&amp;Retry"><Publish Event="EndDialog" Value="ErrorRetry" /></Control>
</Dialog>
<Dialog Id="FilesInUse" Width="370" Height="270" Title="[ProductName] Setup" KeepModeless="yes">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Close these programs" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="24" NoPrefix="yes" Text="These programs are using files setup needs to update. Close them, then click Retry." />
<Control Id="List" Type="ListBox" X="20" Y="75" Width="330" Height="140" Property="FileInUseProcess" Sunken="yes" TabSkip="yes" />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Retry" Type="PushButton" X="168" Y="243" Width="56" Height="17" Default="yes" Text="&amp;Retry"><Publish Event="EndDialog" Value="Retry" /></Control>
<Control Id="Ignore" Type="PushButton" X="236" Y="243" Width="56" Height="17" Text="&amp;Ignore"><Publish Event="EndDialog" Value="Ignore" /></Control>
<Control Id="Exit" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="E&amp;xit"><Publish Event="EndDialog" Value="Exit" /></Control>
</Dialog>
<!-- The three endings: finished, cancelled, failed. -->
<Dialog Id="ExitDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}[ProductName] setup is complete" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="36" NoPrefix="yes" Text="Click Finish to close setup." />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Finish" Type="PushButton" X="304" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Return" />
</Control>
</Dialog>
<Dialog Id="UserExitDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Setup was cancelled" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="36" NoPrefix="yes" Text="[ProductName] was not changed. Run setup again to install it." />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Finish" Type="PushButton" X="304" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Exit" />
</Control>
</Dialog>
<Dialog Id="FatalErrorDlg" Width="370" Height="270" Title="[ProductName] Setup">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="20" Transparent="yes" NoPrefix="yes" Text="{\EcTitle}Setup did not finish" />
<Control Id="Description" Type="Text" X="20" Y="45" Width="330" Height="48" NoPrefix="yes" Text="Something went wrong, and setup put back what was there before. Run setup again, or report it at https://github.com/storytold/effectcraft/issues." />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Finish" Type="PushButton" X="304" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="&amp;Finish">
<Publish Event="EndDialog" Value="Exit" />
</Control>
</Dialog>
<InstallUISequence>
<Show Dialog="DestDlg" Before="ProgressDlg" Condition="NOT Installed AND NOT WIX_UPGRADE_DETECTED" />
<Show Dialog="ReadyDlg" After="DestDlg" Condition="NOT Installed AND WIX_UPGRADE_DETECTED" />
<Show Dialog="MaintDlg" After="ReadyDlg" Condition="Installed AND NOT REMOVE AND NOT REINSTALL" />
<Show Dialog="ProgressDlg" Before="ExecuteAction" />
<Show Dialog="ExitDlg" OnExit="success" />
<Show Dialog="UserExitDlg" OnExit="cancel" />
<Show Dialog="FatalErrorDlg" OnExit="error" />
</InstallUISequence>
<!-- msiexec /a (an administrative image) gets the same progress and endings. -->
<AdminUISequence>
<Show Dialog="ProgressDlg" Before="ExecuteAction" />
<Show Dialog="ExitDlg" OnExit="success" />
<Show Dialog="UserExitDlg" OnExit="cancel" />
<Show Dialog="FatalErrorDlg" OnExit="error" />
</AdminUISequence>
</UI>
</Fragment>
</Wix>
+1 -1
View File
@@ -93,7 +93,7 @@ Copy-Item (Join-Path $Bin 'effectcraft.exe'), (Join-Path $Bin 'effectcraft-cli.e
# ---- MSI ---------------------------------------------------------------------------------------
$Msi = Join-Path $Dist "effectcraft-$Version-windows-$Arch.msi"
Invoke-Native 'wix build' {
wix build (Join-Path $PSScriptRoot 'effectcraft.wxs') -arch $Arch `
wix build (Join-Path $PSScriptRoot 'effectcraft.wxs') (Join-Path $PSScriptRoot 'installer-ui.wxs') -arch $Arch `
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\effectcraft.ico')" `
-o $Msi
}
+124
View File
@@ -0,0 +1,124 @@
<#
.SYNOPSIS
Install-test the EffectCraft MSI's folder handling with stub binaries (no Rust build).
.DESCRIPTION
Builds the MSI around two stand-in executables as 0.0.1 the way 0.6.0 and earlier were (no App
Paths "Path" value), then as 0.0.2 and 0.0.3, validates them (ICE), and, silently and per-machine:
1. installs 0.0.1 into a folder with spaces and a non-ASCII name (INSTALLFOLDER=...),
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 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
and upgrades do with the folder. Needs an elevated shell and WiX v5 (`wix`) on PATH.
Run by .github/workflows/packaging-lint.yml.
.EXAMPLE
pwsh packaging/windows/test-msi.ps1
#>
param([string] $Work = (Join-Path ([IO.Path]::GetTempPath()) 'effectcraft-msi-test'))
$ErrorActionPreference = 'Stop'
trap { Write-Output "FAILED: $_"; exit 1 }
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$AppPaths = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\effectcraft.exe'
function Invoke-Native([string] $What, [scriptblock] $Block) {
Write-Output "==> $What"
& $Block
if ($LASTEXITCODE -ne 0) { throw "$What failed with exit code $LASTEXITCODE" }
}
function Invoke-Msiexec([string] $What, [string[]] $Arguments) {
$log = Join-Path $Work "$($What -replace '\W', '-').log"
$p = Start-Process msiexec.exe -ArgumentList ($Arguments + '/qn', '/l*v', "`"$log`"") -Wait -PassThru
if ($p.ExitCode -ne 0) { Get-Content $log -Tail 60; throw "msiexec ($What) exited $($p.ExitCode)" }
Write-Output "ok $What"
}
function Assert-InstalledIn([string] $Dir, [switch] $NoPathValue) {
foreach ($exe in 'effectcraft.exe', 'effectcraft-cli.exe') {
if (-not (Test-Path -LiteralPath (Join-Path $Dir $exe))) { throw "$exe is not in $Dir" }
}
$key = Get-ItemProperty -LiteralPath $AppPaths
$want = Join-Path $Dir 'effectcraft.exe'
if ($key.'(default)' -ne $want) { throw "App Paths points at '$($key.'(default)')', expected '$want'" }
if ($NoPathValue) {
if ($null -ne $key.Path) { throw "the legacy package wrote an App Paths Path value" }
} elseif ($key.Path.TrimEnd('\') -ne $Dir.TrimEnd('\')) {
throw "App Paths Path is '$($key.Path)', expected '$Dir'"
}
Write-Output "ok installed in $Dir"
}
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
$Bin = Join-Path $Work 'bin'
New-Item -ItemType Directory -Force -Path $Bin | Out-Null
# 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')
[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'
$LegacyWxs = Join-Path $Work 'effectcraft-legacy.wxs'
$pathValue = '(?m)^.*<RegistryValue [^>]*Name="Path"[^>]*/>\r?\n'
$text = Get-Content -Raw $Wxs
if ($text -notmatch $pathValue) { throw "no App Paths Path value in $Wxs to leave out" }
[IO.File]::WriteAllText($LegacyWxs, ($text -replace $pathValue, ''))
$Msi = @{}
foreach ($v in '0.0.1', '0.0.2', '0.0.3') {
$Msi[$v] = Join-Path $Work "effectcraft-$v.msi"
$src = if ($v -eq '0.0.1') { $LegacyWxs } else { $Wxs }
Invoke-Native "wix build $v" {
wix build $src (Join-Path $PSScriptRoot 'installer-ui.wxs') -arch x64 `
-d "Version=$v" -d "BinDir=$Bin" -d "IconPath=$(Join-Path $Root 'assets\app-icon\effectcraft.ico')" -o $Msi[$v]
}
Invoke-Native "wix msi validate $v" { wix msi validate $Msi[$v] }
}
# Spaces and a non-ASCII letter (written as a char so the script's own encoding doesn't matter).
$Custom = Join-Path $env:SystemDrive "EffectCraft Test $([char]0x00DC)nicode\My Apps"
$Default = Join-Path $env:ProgramFiles 'EffectCraft'
if (Test-Path -LiteralPath $AppPaths) { throw 'EffectCraft is already installed here; run this on a clean machine' }
Invoke-Msiexec 'install the legacy package to a custom folder' @('/i', "`"$($Msi['0.0.1'])`"", "INSTALLFOLDER=`"$Custom`"")
Assert-InstalledIn $Custom -NoPathValue
if (Test-Path -LiteralPath $Default) { throw "a custom install also wrote $Default" }
Invoke-Msiexec 'upgrade the legacy install without a folder' @('/i', "`"$($Msi['0.0.2'])`"")
Assert-InstalledIn $Custom
if (Test-Path -LiteralPath $Default) { throw "the upgrade moved the install to $Default" }
Invoke-Msiexec 'upgrade again without a folder' @('/i', "`"$($Msi['0.0.3'])`"")
Assert-InstalledIn $Custom
# Read single values: Get-ItemProperty throws on some machines' malformed Uninstall entries.
$arp = @(Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' |
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 ', ')" }
# 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' }
if (Test-Path -LiteralPath $AppPaths) { throw 'uninstall left the App Paths registration behind' }
Invoke-Msiexec 'install to the default folder' @('/i', "`"$($Msi['0.0.3'])`"")
Assert-InstalledIn $Default
Invoke-Msiexec 'uninstall the default install' @('/x', "`"$($Msi['0.0.3'])`"")
if (Test-Path -LiteralPath (Join-Path $Default 'effectcraft.exe')) { throw 'uninstall left effectcraft.exe behind' }
Write-Output 'MSI folder handling ok'