diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml
index efac5e149..c09f5bd2b 100644
--- a/.github/workflows/windows-arm64.yml
+++ b/.github/workflows/windows-arm64.yml
@@ -26,8 +26,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: wordcraft
app-dir: WordCraft
+ install-relative-path: Programs/ArtCraft/WordCraft
+ install-scope: perUser
diff --git a/packaging/windows/check-install-policy.ps1 b/packaging/windows/check-install-policy.ps1
index dd2895643..00b683d70 100644
--- a/packaging/windows/check-install-policy.ps1
+++ b/packaging/windows/check-install-policy.ps1
@@ -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) {
diff --git a/packaging/windows/install-policy.wxi b/packaging/windows/install-policy.wxi
index 0a23e558b..ffb78406f 100644
--- a/packaging/windows/install-policy.wxi
+++ b/packaging/windows/install-policy.wxi
@@ -50,5 +50,9 @@
-
+
+
+
+
+
diff --git a/packaging/windows/package.ps1 b/packaging/windows/package.ps1
index 3289a7ba6..9a0941f34 100644
--- a/packaging/windows/package.ps1
+++ b/packaging/windows/package.ps1
@@ -160,7 +160,9 @@ foreach ($build in @(@($Msi, 'perMachine'), @($UserMsi, 'perUserOrMachine'))) {
-o $out
}
# Check the compiled scope, path and registration contract before signing.
- & (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $Msi
+ $shell = (Get-Process -Id $PID).Path
+ & $shell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $out
+ if ($LASTEXITCODE -ne 0) { throw "Compiled installer policy failed for $out." }
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($out, '.wixpdb'))
& (Join-Path $PSScriptRoot 'sign.ps1') $out
diff --git a/packaging/windows/test-install-policy.ps1 b/packaging/windows/test-install-policy.ps1
index 21199ca7f..a574122c0 100644
--- a/packaging/windows/test-install-policy.ps1
+++ b/packaging/windows/test-install-policy.ps1
@@ -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-packaging-loop.ps1')
Add-Type -AssemblyName System.Drawing
foreach ($arch in $Architectures) {
$stage = Join-Path $output $arch
diff --git a/packaging/windows/test-packaging-loop.ps1 b/packaging/windows/test-packaging-loop.ps1
new file mode 100644
index 000000000..94b73c6c5
--- /dev/null
+++ b/packaging/windows/test-packaging-loop.ps1
@@ -0,0 +1,21 @@
+<# Check the real packaging loop without compiling, signing or installing. #>
+$ErrorActionPreference = 'Stop'
+$tokens = $null; $errors = $null
+$ast = [Management.Automation.Language.Parser]::ParseFile((Join-Path $PSScriptRoot 'package.ps1'), [ref]$tokens, [ref]$errors)
+if ($errors.Count) { throw 'The packaging script does not parse.' }
+$loop = @($ast.FindAll({ param($node)
+ $node -is [Management.Automation.Language.ForEachStatementAst] -and $node.Variable.VariablePath.UserPath -eq 'build'
+}, $true))
+if ($loop.Count -ne 1) { throw 'Expected one artifact packaging loop.' }
+$checks = @($loop[0].FindAll({ param($node)
+ $node -is [Management.Automation.Language.CommandAst] -and $node.InvocationOperator.ToString() -eq 'Ampersand' -and
+ @($node.CommandElements | Where-Object { $_.Extent.Text -match 'check-install-policy.ps1' }).Count -gt 0
+}, $true))
+if ($checks.Count -ne 1) { throw 'Every packaging iteration needs its own policy check.' }
+$elements = $checks[0].CommandElements
+$index = 0
+while ($index -lt $elements.Count -and $elements[$index].Extent.Text -ne '-MsiPath') { $index++ }
+if ($index + 1 -ge $elements.Count -or $elements[$index + 1].Extent.Text -ne '$out') {
+ throw 'The policy check must inspect the current artifact ($out), not the first MSI.'
+}
+Write-Output 'PASS each WordCraft artifact is checked before signing.'