mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 04:26:55 -06:00
fix(installer): address scope and ARM64 review feedback
This commit is contained in:
@@ -28,8 +28,10 @@ permissions:
|
||||
|
||||
jobs:
|
||||
windows-arm64:
|
||||
# Intentionally tracked at @main (not SHA-pinned) so org-wide CI improvements, security fixes, and build optimizations propagate without PR churn across all craftapp repos.
|
||||
uses: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@main
|
||||
# Immutable supporting workflow revision until the new install contract lands upstream.
|
||||
uses: Jeremy8776/craft-common-actions/.github/workflows/windows-arm64.yml@f4666d5cbf35f9bdeda24f1f88c7fb5c6adf5276
|
||||
with:
|
||||
app-name: effectcraft
|
||||
app-dir: EffectCraft
|
||||
install-relative-path: Programs/ArtCraft/EffectCraft
|
||||
install-scope: perUser
|
||||
|
||||
@@ -131,9 +131,12 @@ try {
|
||||
}
|
||||
$ui = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Action`, `Sequence` FROM `InstallUISequence`' 2)) { $ui[$row[0]] = [int]$row[1] }
|
||||
if (-not ($ui['AppSearch'] -lt $ui['CraftScopeDlg'] -and $ui['CraftScopeDlg'] -lt $ui['LaunchConditions'] -and
|
||||
if (-not ($ui['AppSearch'] -lt $ui['CraftPreferMachineScope'] -and
|
||||
$ui['CraftPreferMachineScope'] -lt $ui['CraftScopeDlg'] -and
|
||||
$ui['CraftScopeDlg'] -lt $ui['FindRelatedProducts'] -and
|
||||
$ui['FindRelatedProducts'] -lt $ui['LaunchConditions'] -and
|
||||
$ui['CraftScopeDlg'] -lt $ui['CraftRestoreUserFolder'] -and $ui['CraftRestoreUserFolder'] -lt $ui['CostInitialize'])) {
|
||||
throw 'Scope must be chosen after discovery and before duplicate checks, folder restoration and costing.'
|
||||
throw 'Scope must be chosen after AppSearch and before FindRelatedProducts, launch checks and costing.'
|
||||
}
|
||||
$execute = @{}
|
||||
foreach ($row in (Read-Rows $database 'SELECT `Action`, `Sequence` FROM `InstallExecuteSequence`' 2)) { $execute[$row[0]] = [int]$row[1] }
|
||||
@@ -144,7 +147,18 @@ try {
|
||||
}
|
||||
# Switching the first-page choice must reset BOTH properties, including a back-and-forth change.
|
||||
$events = @(Read-Rows $database 'SELECT `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_` = ''CraftScopeDlg'' AND `Control_` = ''Next'' ORDER BY `Ordering`' 4)
|
||||
foreach ($choice in @('user','machine','user')) {
|
||||
# Model the full-UI upgrade entry with an existing machine-wide copy. Run
|
||||
# the actual preference action, then the compiled Next events before discovery.
|
||||
Set-SessionProperty $session 'ALLUSERS' '2'
|
||||
Set-SessionProperty $session 'CRAFT_SCOPE' 'user'
|
||||
Set-SessionProperty $session 'CRAFT_USER_DIR' ''
|
||||
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' 'E:\Existing machine app'
|
||||
$preference = @(Read-Rows $database 'SELECT `Condition` FROM `InstallUISequence` WHERE `Action` = ''CraftPreferMachineScope''' 1)[0]
|
||||
if ($session.EvaluateCondition($preference[0]) -ne 1 -or $session.DoAction('CraftPreferMachineScope') -ne 1 -or
|
||||
(Get-SessionProperty $session 'CRAFT_SCOPE') -ne 'machine') {
|
||||
throw 'Full-UI machine upgrades must initially select the existing machine context.'
|
||||
}
|
||||
foreach ($choice in @('machine','user','machine','user')) {
|
||||
Set-SessionProperty $session 'CRAFT_SCOPE' $choice
|
||||
foreach ($event in $events) {
|
||||
if ($event[0] -notmatch '^\[([^\]]+)\]$') { continue }
|
||||
@@ -176,6 +190,52 @@ try {
|
||||
if ((Get-SessionProperty $session $option[1]) -ne $expected) { throw 'Saved installer choices leaked across contexts.' }
|
||||
}
|
||||
}
|
||||
# Follow the compiled control conditions, not the source's intended appearance.
|
||||
$controls = @(Read-Rows $database 'SELECT `Dialog_`, `Control`, `Type`, `Attributes`, `Text` FROM `Control`' 5)
|
||||
$conditions = @()
|
||||
if (@(Read-Rows $database 'SELECT `Name` FROM `_Tables` WHERE `Name` = ''ControlCondition''' 1).Count) {
|
||||
$conditions = @(Read-Rows $database 'SELECT `Dialog_`, `Control_`, `Action`, `Condition` FROM `ControlCondition`' 4)
|
||||
}
|
||||
function Test-ControlVisible($Control) {
|
||||
$visible = ([int]$Control[3] -band 1) -ne 0
|
||||
foreach ($condition in $conditions) {
|
||||
if ($condition[0] -eq $Control[0] -and $condition[1] -eq $Control[1] -and $session.EvaluateCondition($condition[3]) -eq 1) {
|
||||
if ($condition[2] -eq 'Show') { $visible = $true }
|
||||
if ($condition[2] -eq 'Hide') { $visible = $false }
|
||||
}
|
||||
}
|
||||
return $visible
|
||||
}
|
||||
foreach ($button in @($controls | Where-Object { $_[0] -in 'InstallWelcome','InstallMaintenance','DesktopShortcutDlg' -and ([int]$_[3] -band 8388608) -ne 0 })) {
|
||||
$plain = @($controls | Where-Object { $_[0] -eq $button[0] -and $_[1] -eq ($button[1] + 'User') })
|
||||
if ($plain.Count -ne 1 -or ([int]$plain[0][3] -band 8388608)) { throw 'Scope-aware buttons need an unshielded per-user counterpart.' }
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
if ((Test-ControlVisible $button) -ne ($scope -eq '1') -or (Test-ControlVisible $plain[0]) -ne ($scope -ne '1')) {
|
||||
throw 'The elevation shield must follow the selected scope.'
|
||||
}
|
||||
}
|
||||
$dialog = $button[0]; $shieldId = $button[1]; $plainId = $plain[0][1]
|
||||
$shieldEvents = @(Read-Rows $database "SELECT ``Event``, ``Argument``, ``Condition``, ``Ordering`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$shieldId' ORDER BY ``Ordering``" 4 | ForEach-Object { $_ -join '|' })
|
||||
$plainEvents = @(Read-Rows $database "SELECT ``Event``, ``Argument``, ``Condition``, ``Ordering`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$plainId' ORDER BY ``Ordering``" 4 | ForEach-Object { $_ -join '|' })
|
||||
if (($shieldEvents -join "`n") -ne ($plainEvents -join "`n")) { throw 'Per-user and machine buttons must perform the same operation.' }
|
||||
}
|
||||
foreach ($scope in @('', '2', '1')) {
|
||||
Set-SessionProperty $session 'ALLUSERS' $scope
|
||||
foreach ($text in @($controls | Where-Object { $_[4] -like '*for all users*' })) {
|
||||
if ((Test-ControlVisible $text) -ne ($scope -eq '1')) { throw 'The welcome-page scope text must follow the selected scope.' }
|
||||
}
|
||||
}
|
||||
if ($policy.id -in 'deckcraft','filmcraft','soundcraft') {
|
||||
foreach ($route in @(@('WelcomeDlg','Next','InstallDirDlg'),@('InstallDirDlg','Back','WelcomeDlg'))) {
|
||||
$dialog = $route[0]; $control = $route[1]
|
||||
$routes = @(Read-Rows $database "SELECT ``Argument``, ``Condition`` FROM ``ControlEvent`` WHERE ``Dialog_`` = '$dialog' AND ``Control_`` = '$control' AND ``Event`` = 'NewDialog' ORDER BY ``Ordering``" 2)
|
||||
Set-SessionProperty $session 'Installed' ''
|
||||
$destination = ''
|
||||
foreach ($event in $routes) { if ($session.EvaluateCondition($event[1]) -eq 1) { $destination = $event[0] } }
|
||||
if ($destination -ne $route[2]) { throw 'Skipping the licence page must work in both navigation directions.' }
|
||||
}
|
||||
}
|
||||
$registry = @(Read-Rows $database 'SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`' 4)
|
||||
if (@($registry | Where-Object { $_[0] -in '1', '2' }).Count) { throw 'Registration must follow the selected context, not force HKCU or HKLM.' }
|
||||
if (@($registry | Where-Object { $_[0] -eq '0' -and $_[1] -match '^Software\\Classes(\\|$)' }).Count) {
|
||||
|
||||
@@ -50,5 +50,9 @@
|
||||
<Control Id="Cancel" Type="PushButton" X="290" Y="182" Width="60" Height="20" Cancel="yes" Text="Cancel"><Publish Event="EndDialog" Value="Exit" /></Control>
|
||||
</Dialog>
|
||||
</UI>
|
||||
<InstallUISequence><Show Dialog="CraftScopeDlg" Before="LaunchConditions" Condition="NOT Installed AND NOT REMOVE AND UILevel = 5" /></InstallUISequence>
|
||||
<InstallUISequence>
|
||||
<Show Dialog="CraftScopeDlg" After="CraftPreferMachineScope" Condition="NOT Installed AND NOT REMOVE AND UILevel = 5" />
|
||||
<!-- Related-product discovery runs once, so full UI must choose its context first. -->
|
||||
<FindRelatedProducts After="CraftScopeDlg" />
|
||||
</InstallUISequence>
|
||||
</Include>
|
||||
|
||||
@@ -66,7 +66,10 @@
|
||||
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="&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="&Install">
|
||||
<Control Id="Install" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="&Install">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
<Control Id="InstallUser" Type="PushButton" DefaultCondition="ALLUSERS <> 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="236" Y="243" Width="56" Height="17" Text="&Install">
|
||||
<Publish Event="EndDialog" Value="Return" />
|
||||
</Control>
|
||||
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="Cancel">
|
||||
@@ -78,7 +81,7 @@
|
||||
<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="&Repair">
|
||||
<Control Id="Repair" Type="PushButton" X="20" Y="80" Width="80" Height="20" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="&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. -->
|
||||
@@ -86,7 +89,19 @@
|
||||
<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&move">
|
||||
<Control Id="RepairUser" Type="PushButton" DefaultCondition="ALLUSERS <> 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="20" Y="80" Width="80" Height="20" Text="&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" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="Re&move">
|
||||
<Publish Event="Remove" Value="ALL" Order="1" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="2" />
|
||||
</Control>
|
||||
<Control Id="RemoveUser" Type="PushButton" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="20" Y="108" Width="80" Height="20" Text="Re&move">
|
||||
<Publish Event="Remove" Value="ALL" Order="1" />
|
||||
<Publish Event="EndDialog" Value="Return" Order="2" />
|
||||
</Control>
|
||||
|
||||
@@ -12,6 +12,7 @@ $root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$policy = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'install-policy.json') | ConvertFrom-Json
|
||||
[void](New-Item -ItemType Directory -Force -Path $OutputDirectory)
|
||||
$output = (Resolve-Path -LiteralPath $OutputDirectory).Path
|
||||
& (Join-Path $PSScriptRoot 'test-legacy-fixture.ps1') -Work (Join-Path $output 'legacy-source')
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
foreach ($arch in $Architectures) {
|
||||
$stage = Join-Path $output $arch
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<# Exercise the legacy source transformation without building or installing. #>
|
||||
param([Parameter(Mandatory = $true)] [string] $Work)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[void](New-Item -ItemType Directory -Force -Path $Work)
|
||||
$source = Get-Content -Raw (Join-Path $PSScriptRoot 'test-msi.ps1')
|
||||
$start = $source.IndexOf('$Wxs =')
|
||||
$end = $source.IndexOf('$Msi = @{}', $start)
|
||||
if ($start -lt 0 -or $end -le $start) { throw 'Could not locate the legacy source transformation.' }
|
||||
# Run the test harness's exact source-producing block, with only scratch outputs.
|
||||
$block = $source.Substring($start, $end - $start).Replace('$PSScriptRoot', ("'" + $PSScriptRoot.Replace("'", "''") + "'"))
|
||||
Invoke-Expression $block
|
||||
[xml]$legacy = Get-Content -Raw $LegacyWxs
|
||||
$ns = New-Object Xml.XmlNamespaceManager($legacy.NameTable)
|
||||
$ns.AddNamespace('w', 'http://wixtoolset.org/schemas/v4/wxs')
|
||||
if ($legacy.SelectNodes('//w:RegistryValue[@Name="Path" or @Name="InstallDir" or @Name="InstallFolder"]', $ns).Count) {
|
||||
throw 'Legacy fixture must omit every modern folder record so discovery uses the executable fallback.'
|
||||
}
|
||||
if (-not $legacy.SelectNodes('//w:RegistryValue[contains(@Value,"EffectcraftExe")]', $ns).Count) {
|
||||
throw 'Legacy fixture must retain its executable registration.'
|
||||
}
|
||||
Write-Output 'PASS legacy fixture exposes only the executable-path fallback.'
|
||||
@@ -72,10 +72,10 @@ Copy-Item (Join-Path $env:SystemRoot 'System32\cmd.exe') (Join-Path $Bin 'effect
|
||||
[IO.File]::WriteAllText((Join-Path $Bin 'NOTICE'), 'Test fixture notice')
|
||||
|
||||
# 0.0.1 stands in for releases before this installer change: the same package without the App
|
||||
# Paths "Path" value.
|
||||
# Paths "Path" value or modern InstallDir/InstallFolder records.
|
||||
$Wxs = Join-Path $PSScriptRoot 'effectcraft.wxs'
|
||||
$LegacyWxs = Join-Path $Work 'effectcraft-legacy.wxs'
|
||||
$pathValue = '(?m)^.*<RegistryValue [^>]*Name="Path"[^>]*/>\r?\n'
|
||||
$pathValue = '(?m)^.*<RegistryValue [^>]*Name="(?:Path|InstallDir|InstallFolder)"[^>]*/>\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, ''))
|
||||
|
||||
Reference in New Issue
Block a user