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

# Conflicts:
#	contributors/people.toml
This commit is contained in:
Claude 2026-10-10 12:51:31 +00:00
commit 6039e86a04
No known key found for this signature in database
8 changed files with 381 additions and 6 deletions

View File

@ -1,5 +1,6 @@
# Quick static checks for the release pipeline (seconds, no Rust build): workflow syntax,
# packaging shell/PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata.
# packaging shell/PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata. Plus a
# Windows MSI install test with stub binaries (install folder, upgrade, repair, uninstall).
name: Packaging lint
on:
@ -46,7 +47,7 @@ jobs:
}
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
xmllint --noout packaging/windows/effectcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml*
xmllint --noout packaging/windows/*.wxs packaging/macos/*.plist* packaging/linux/*.xml*
# Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon.
if grep -oE '<Icon Id="[^"]*"' packaging/windows/effectcraft.wxs | grep -vE '\.(ico|exe)"$'; then
echo "::error::Icon Id must end in .ico or .exe"; exit 1
@ -66,3 +67,16 @@ jobs:
EOF
- name: FreeBSD package layout (stub binaries)
run: bash packaging/freebsd/package.sh --dry-run
windows-msi:
name: Windows MSI install folder (stub binaries)
runs-on: windows-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 5.0.2
- name: Install, upgrade, repair and uninstall
shell: pwsh
run: ./packaging/windows/test-msi.ps1

View File

@ -264,7 +264,9 @@ send us right now.
| arm64 (Snapdragon and other ARM PCs) | `effectcraft-<ver>-windows-arm64.msi` | `effectcraft-<ver>-windows-arm64-portable.zip` |
| x86 (32-bit) | `effectcraft-<ver>-windows-x86.msi` | `effectcraft-<ver>-windows-x86-portable.zip` |
Installers and executables are code-signed.
Installers and executables are code-signed. The installer asks where to install EffectCraft
(Program Files by default), and later versions install into the same folder. To install silently
into a folder of your choice: `msiexec /i effectcraft-<ver>-windows-x64.msi /qn INSTALLFOLDER="D:\Apps\EffectCraft"`.
### macOS

View File

@ -39,3 +39,8 @@ sync_github_name = false
[people.Aavirash]
display_name = "Asquareportal"
[people.tylerjenningsw]
real_name = "Tyler Jennings"
display_name = "tylerjenningsw"
sync_github_name = false

View File

@ -66,6 +66,13 @@ jobs are refused by the environment's branch rule, and no draft release is made.
The ARM64 MSI is installed and run on ARM64 hardware by
[`windows-arm64.yml`](../.github/workflows/windows-arm64.yml).
The MSI's setup wizard ([`installer-ui.wxs`](../packaging/windows/installer-ui.wxs)) lets people
choose the install folder; upgrades and repairs reuse it, and `INSTALLFOLDER="..."` picks it for a
silent (`/qn`) install. [`packaging-lint.yml`](../.github/workflows/packaging-lint.yml) install-tests
that with stub binaries ([`test-msi.ps1`](../packaging/windows/test-msi.ps1)): a custom folder,
an upgrade that keeps it, a repair, uninstall, and the Program Files default. The wizard pages
themselves are checked by hand.
Every binary reports its version: `effectcraft --version`, `effectcraft-cli --version` and
*Help › About EffectCraft*.

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">

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>

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
}

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'