Compare commits

..
Author SHA1 Message Date
aiongg 11bb67386c Merge pull request #2704 from Jeremy8776/feat/install-scope-policy
Windows installation policy: GridCraft
2026-10-27 17:01:41 -04:00
aiongg 1decf0ef51 ci: use storytold/craft-common-actions at the merged installer-policy commit 2026-10-27 20:51:47 +00:00
Jeremy d33616dc95 fix(installer): check the exact architecture-specific machine folder
The pre-signing check matched any folder named Program Files, so it rejected
a valid MSI when Windows resolves Program Files elsewhere, accepted the 64-bit
root for an x86 package and accepted a different app name. Build the expected
folder from the package architecture's resolved root and the policy name.
2026-10-27 19:12:54 +00:00
aiongg 89ac453efa Merge pull request #2605 from dotCooCoo/feat/format-chart-pane-stacked-types
Format Chart Area: Chart Type offers the stacked and 100% stacked types and marks the current one
2026-10-27 13:46:40 -04:00
Jeremy 3b71e37b70 fix(installer): resolve the wizard scope and folder correctly
Real msiexec runs (per-user and all-users, silent and wizard) found two defects
that the metadata fixtures cannot see:

- "Only for me" published ALLUSERS=2, so FindRelatedProducts treated the run as
  per-machine and skipped the existing per-user copy. An interactive upgrade
  left two registered products and uninstall left files behind. The page now
  leaves ALLUSERS unset for a per-user choice.
- Windows Installer resolves Program Files once, for the context it started in,
  so choosing "Everyone" in the wizard installed into the user profile with a
  machine-wide registration. The wizard now sets the new-install folder itself
  after the choice (Program Files, Program Files (x86) for 32-bit, or the
  per-user Programs folder). Silent installs are unchanged.

check-install-policy.ps1 now fails if either regresses.
2026-10-27 15:32:27 +00:00
Jeremy a4c50a6dac docs(installer): note that the 32-bit all-users folder is Program Files (x86); tidy wording 2026-10-27 12:25:19 +00:00
Jeremy a614e683ed docs(installer): describe the per-user default install location and scope 2026-10-27 12:09:03 +00:00
Jeremy e7d778046b fix(installer): harden install-policy checks and packaging-lint workflow 2026-10-27 12:01:24 +00:00
Jeremy 998396ad9f fix(installer): address scope and ARM64 review feedback 2026-10-27 12:01:24 +00:00
Jeremy df42619b1e feat(installer): default to a per-user ArtCraft folder 2026-10-27 12:01:24 +00:00
dotCooCoo 0b6d243946 Format Chart Area: Chart Type offers the stacked and 100% stacked types and marks the current one
The Chart Type section of the Format Chart Area pane listed nine types,
with stacked column as the only stacked one, so a chart could not be
switched there to stacked bar, 100% stacked column or bar, stacked
line, or stacked or 100% stacked area. It now lists them after their
family's entries, and "Stacked" is renamed "Stacked Column".

Each entry is drawn selected when it is the chart's current type, as
the Legend entries are, using the same type and subtype mapping that
chart.set applies.
2026-10-25 16:53:40 -07:00
16 changed files with 846 additions and 122 deletions
+24
View File
@@ -21,3 +21,27 @@ jobs:
lint:
# 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/packaging-lint.yml@main
windows-install-policy:
timeout-minutes: 10
name: Windows installation policy (no app build)
runs-on: windows-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install the pinned packaging toolchain
shell: pwsh
run: |
$tools = Join-Path $env:RUNNER_TEMP 'craft-install-policy-wix'
dotnet tool install --tool-path $tools wix --version 5.0.2
if ($LASTEXITCODE -ne 0) { throw 'WiX installation failed.' }
$tools >> $env:GITHUB_PATH
$policy = Get-Content -Raw -LiteralPath packaging/windows/install-policy.json | ConvertFrom-Json
foreach ($extension in @($policy.extensions)) {
& (Join-Path $tools 'wix.exe') extension add -g "WixToolset.$extension.wixext/5.0.2"
if ($LASTEXITCODE -ne 0) { throw "WiX extension $extension installation failed." }
}
- name: Build and check non-executable installer fixtures
shell: pwsh
run: ./packaging/windows/test-install-policy.ps1 -OutputDirectory (Join-Path $env:RUNNER_TEMP 'craft-install-policy-tests')
+4 -2
View File
@@ -19,8 +19,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: storytold/craft-common-actions/.github/workflows/windows-arm64.yml@8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f
with:
app-name: gridcraft
app-dir: GridCraft
install-relative-path: Programs/ArtCraft/GridCraft
install-scope: perUser
Generated
+48 -37
View File
@@ -195,6 +195,15 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arboard"
version = "3.7.0"
@@ -932,7 +941,7 @@ dependencies = [
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"skrifa 0.44.0",
"skrifa",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@@ -960,7 +969,7 @@ dependencies = [
"verifysign",
"winreg",
"winsafe",
"zip",
"zip 9.1.0",
]
[[package]]
@@ -1098,6 +1107,17 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e"
[[package]]
name = "derive_arbitrary"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.6",
]
[[package]]
name = "dify"
version = "0.8.0"
@@ -1408,7 +1428,7 @@ dependencies = [
"parking_lot",
"profiling",
"self_cell",
"skrifa 0.44.0",
"skrifa",
"smallvec",
"unicode-bidi",
"unicode-general-category",
@@ -1603,9 +1623,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.12.7"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
dependencies = [
"bytemuck",
]
@@ -1816,7 +1836,7 @@ dependencies = [
"hashbrown 0.17.1",
"log",
"peniko",
"skrifa 0.44.0",
"skrifa",
"smallvec",
"vello_common",
]
@@ -1970,9 +1990,9 @@ dependencies = [
"gridcraft-engine",
"gridcraft-mcp",
"log",
"quick-xml 0.42.0",
"quick-xml 0.41.0",
"serde_json",
"zip",
"zip 3.0.0",
]
[[package]]
@@ -2004,7 +2024,7 @@ dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
"zip",
"zip 3.0.0",
]
[[package]]
@@ -2073,7 +2093,7 @@ dependencies = [
"log",
"serde",
"serde_json",
"skrifa 0.48.0",
"skrifa",
"sys-locale",
"toml",
"web-sys",
@@ -2106,11 +2126,11 @@ dependencies = [
"gridcraft-formula",
"gridcraft-functions",
"gridcraft-model",
"quick-xml 0.42.0",
"quick-xml 0.41.0",
"sha1 0.11.0",
"sha2 0.11.1",
"thiserror 2.0.21",
"zip",
"zip 3.0.0",
]
[[package]]
@@ -2161,7 +2181,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
dependencies = [
"bitflags 2.13.2",
"bytemuck",
"read-fonts 0.41.0",
"read-fonts",
"smallvec",
]
@@ -3963,19 +3983,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "read-fonts"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
dependencies = [
"bytemuck",
"foldhash 0.2.0",
"font-types",
"once_cell",
"yoke",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
@@ -4698,17 +4705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
dependencies = [
"bytemuck",
"read-fonts 0.41.0",
]
[[package]]
name = "skrifa"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5213b4825bac3587046330052d61bfac7bd3bd0d203c758d7c2ef726fcea034"
dependencies = [
"bytemuck",
"read-fonts 0.45.1",
"read-fonts",
]
[[package]]
@@ -6636,6 +6633,20 @@ dependencies = [
"syn 3.0.6",
]
[[package]]
name = "zip"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308"
dependencies = [
"arbitrary",
"crc32fast",
"flate2",
"indexmap",
"memchr",
"zopfli",
]
[[package]]
name = "zip"
version = "9.1.0"
+3 -3
View File
@@ -37,8 +37,8 @@ log = "0.4"
chrono = { version = "0.4.45", default-features = false, features = ["std"] }
rayon = "1"
regex = { version = "1", default-features = false, features = ["std", "unicode", "perf"] }
quick-xml = "0.42"
zip = { version = "9", default-features = false, features = ["deflate"] }
quick-xml = "0.41"
zip = { version = "3", default-features = false, features = ["deflate"] }
sha2 = { version = "0.11", default-features = false }
# Decrypting password-protected workbooks ([MS-OFFCRYPTO]): AES and SHA-1, pure Rust (wasm-safe).
aes = { version = "0.9", default-features = false }
@@ -56,7 +56,7 @@ arboard = { version = "3.6", default-features = false }
sys-locale = "0.3"
# Font parser egui's epaint uses; the UI validates system font files with it before handing them
# to egui, which panics on a file it can't parse. Same version as epaint's, so no second copy.
skrifa = { version = "0.48", default-features = false, features = ["std"] }
skrifa = { version = "0.44", default-features = false, features = ["std"] }
# Real font bytes for tests (Ubuntu Light, compiled into epaint).
epaint_default_fonts = "0.37"
# One native window per workbook: the window list, Window menu titles and the immediate-viewport
+9 -2
View File
@@ -185,16 +185,23 @@ fn format_chart(app: &mut SheetApp, ui: &mut Ui) {
ui.horizontal_wrapped(|ui| {
for (l, k, s) in [
("Column", "column", ""),
("Stacked", "column", "stacked"),
("Stacked Column", "column", "stacked"),
("100% Stacked Column", "column", "stacked100"),
("Bar", "bar", ""),
("Stacked Bar", "bar", "stacked"),
("100% Stacked Bar", "bar", "stacked100"),
("Line", "line", "markers"),
("Stacked Line", "line", "stacked"),
("Area", "area", ""),
("Stacked Area", "area", "stacked"),
("100% Stacked Area", "area", "stacked100"),
("Pie", "pie", ""),
("Doughnut", "doughnut", ""),
("Scatter", "scatter", ""),
("Radar", "radar", ""),
] {
if ui.button(l).clicked() {
let cur = chart.kind == gridcraft_engine::cmd::insert::chart_kind(k, s);
if ui.selectable_label(cur, l).clicked() {
app.run_or_alert("chart.set", json!({"chart": id, "type": k, "subtype": s}));
}
}
@@ -0,0 +1,91 @@
//! The Format Chart Area pane's Chart Type section changes the chart's type and marks the current one.
use egui::accesskit::{Role, Toggled};
use egui_kittest::kittest::{NodeT, Queryable};
use gridcraft_engine::Session;
use gridcraft_engine::model::ChartKind;
use gridcraft_ui_egui::SheetApp;
use serde_json::json;
type Harness = egui_kittest::Harness<'static, SheetApp>;
/// A two-series chart made with `insert.chart`'s `type` `t` and `subtype` `sub`, with Format Chart Area open on its Chart Type section.
fn open_chart_type(t: &str, sub: &str) -> (Harness, u32) {
let mut session = Session::new();
session.new_workbook();
session.execute("range.setValues", json!({"range": "A1", "values": [["Quarter", "North", "South"], ["Q1", 10, 30], ["Q2", 30, 10]]})).unwrap();
session.execute("selection.set", json!({"range": "A1:C3"})).unwrap();
let chart = session.execute("insert.chart", json!({"type": t, "subtype": sub, "at": "E3"})).unwrap();
let id = chart["chart"].as_u64().unwrap() as u32;
let mut h = crate::support::harness(session, 4);
h.state_mut().selected_chart = Some(id);
h.state_mut().grid.pane = Some("formatChart".into());
h.run_steps(2);
h.get_by_role_and_label(Role::DisclosureTriangle, "Chart Type").click();
h.run_steps(2);
(h, id)
}
fn kind(h: &Harness, id: u32) -> ChartKind {
h.state().session.active().unwrap().wb.active().unwrap().charts.iter().find(|c| c.id == id).unwrap().kind
}
/// Whether the pane's `label` button is drawn as the chart's current type. The pane sits on the
/// right, past the Chart Type header's left edge; the ribbon's buttons are elsewhere.
fn marked(h: &Harness, label: &str) -> bool {
let left = h.get_by_role_and_label(Role::DisclosureTriangle, "Chart Type").rect().left();
let button = h.get_all_by_role_and_label(Role::Button, label).find(|n| n.rect().left() >= left - 1.0).unwrap();
button.accesskit_node().toggled() == Some(Toggled::True)
}
fn click(h: &mut Harness, label: &str) {
let left = h.get_by_role_and_label(Role::DisclosureTriangle, "Chart Type").rect().left();
h.get_all_by_role_and_label(Role::Button, label).find(|n| n.rect().left() >= left - 1.0).unwrap().click();
h.run_steps(2);
}
#[test]
fn chart_type_offers_the_stacked_and_hundred_percent_stacked_types() {
let (mut h, id) = open_chart_type("column", "");
for (label, want) in [
("Stacked Column", ChartKind::ColumnStacked),
("100% Stacked Column", ChartKind::ColumnStacked100),
("Stacked Bar", ChartKind::BarStacked),
("100% Stacked Bar", ChartKind::BarStacked100),
("Stacked Line", ChartKind::LineStacked),
("Stacked Area", ChartKind::AreaStacked),
("100% Stacked Area", ChartKind::AreaStacked100),
("Column", ChartKind::ColumnClustered),
] {
click(&mut h, label);
assert_eq!(kind(&h, id), want, "{label} changes the chart's type");
}
}
#[test]
fn chart_type_marks_the_chart_s_current_type() {
for (t, sub, label) in
[("bar", "stacked100", "100% Stacked Bar"), ("area", "stacked", "Stacked Area"), ("column", "", "Column"), ("line", "markers", "Line")]
{
let (h, _) = open_chart_type(t, sub);
for other in [
"Column",
"Stacked Column",
"100% Stacked Column",
"Bar",
"Stacked Bar",
"100% Stacked Bar",
"Line",
"Stacked Line",
"Area",
"Stacked Area",
"100% Stacked Area",
"Pie",
"Doughnut",
"Scatter",
"Radar",
] {
assert_eq!(marked(&h, other), other == label, "{other} on a {t} {sub} chart");
}
}
}
+1
View File
@@ -38,6 +38,7 @@ mod find_dialog_focus;
mod floating_pictures;
mod font_box_filter;
mod font_size_box;
mod format_chart_type;
mod format_painter;
mod formula_bar;
mod formula_bar_expand;
+3 -1
View File
@@ -122,7 +122,9 @@ open dist/release/gridcraft-*-macos-*.dmg
is GUI, `gridcraft-cli.exe` console), signs both executables, builds the MSI with WiX v5, signs
the MSI, and zips a portable build.
- `gridcraft.wxs`: per-machine install into Program Files, advertised Start Menu shortcut, App
- `gridcraft.wxs`: a dual-scope package (per-user by default into
`%LOCALAPPDATA%\Programs\ArtCraft\GridCraft`, per-machine into `Program Files\ArtCraft\GridCraft` (`Program Files (x86)` for the 32-bit build),
see `packaging/windows/INSTALLATION.md`), advertised Start Menu shortcut, App
Paths (Win+R `gridcraft`). File types: ProgIds `GridCraft.Workbook` (`.xlsx`, `.xlsm`) and
`GridCraft.TextData` (`.csv`, `.tsv`), listed under each extension's `OpenWithProgids` and
in `Capabilities` + `RegisteredApplications`, so GridCraft appears in *Open with* and
+35
View File
@@ -0,0 +1,35 @@
# Windows installation policy checks
New installations default to the current account under `Programs\ArtCraft\<App>`.
The first setup page offers installation for everyone. Scope is selected before duplicate
checks and directory costing. An update uses the existing context's remembered exact folder;
`INSTALLFOLDER` supplied explicitly is never replaced. It is a secure MSI property so the
chosen folder reaches the elevated installer. Silent machine updates pass `ALLUSERS=1`.
A cross-context copy is refused; changing context means uninstalling the old copy first.
App registration follows the selected context. Existing optional shortcuts and file-handler
choices stay app-owned. No new cleanup operation or model/settings migration is introduced.
Packaged resources remain package-owned; separately downloaded shared models are not caches
and are outside this installer's ownership.
## Repeatable metadata tests
On Windows with WiX 5.0.2 and its matching UI/Util extensions:
```powershell
./packaging/windows/test-install-policy.ps1 -OutputDirectory ./target/install-policy-tests
```
The script builds x64, x86 and arm64 packages with deliberately non-executable payloads. It
uses the actual package source and compiled database. A safe Windows Installer session checks
24 scope/registration/maintenance cases, remembered and explicit custom folders, sequencing,
upgrade identity and the original registration inventory recorded in `install-policy.json`.
The release packaging script runs the compiled checker before signing. Packaging CI runs the
small fixture builds without compiling the Rust application.
Never install or distribute the `NOT-INSTALLABLE.msi` fixtures as an app. Passing these checks
does not prove real installation, elevation, legacy-version upgrades, registry writes, optional
shortcut/menu behaviour, cancellation, rollback or uninstall. Those still require the real
payload in an authorised clean Windows test environment. Cross-architecture metadata is not
execution evidence on those architectures. Existing open installer/association PRs must be
reconciled before publication. Nothing here changes macOS/Linux native packaging conventions.
+296
View File
@@ -0,0 +1,296 @@
<#
.SYNOPSIS
Check the compiled installation policy without installing any product.
.DESCRIPTION
Uses Windows Installer's safe session (OpenPackage option 1). Only property-setting custom
actions are exercised. No AppSearch, install, repair, removal or file-writing action runs.
#>
param([Parameter(Mandatory = $true)] [string] $MsiPath)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$policy = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'install-policy.json') | ConvertFrom-Json
function Read-Rows($Database, [string] $Sql, [int] $Columns) {
$view = $Database.OpenView($Sql)
try {
[void]$view.Execute()
while ($null -ne ($record = $view.Fetch())) {
try {
$values = @(for ($i = 1; $i -le $Columns; $i++) {
$record.GetType().InvokeMember('StringData', 'GetProperty', $null, $record, @($i))
})
,$values
} finally { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($record) }
}
} finally {
[void]$view.Close()
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($view)
}
}
function Set-SessionProperty($Session, [string] $Name, [string] $Value) {
[void]$Session.GetType().InvokeMember('Property', 'SetProperty', $null, $Session, @($Name, $Value))
}
function Get-SessionProperty($Session, [string] $Name) {
$Session.GetType().InvokeMember('Property', 'GetProperty', $null, $Session, @($Name))
}
function Get-PackageTemplate($Database) {
# Summary information property 7 is the platform: "Intel;1033", "x64;1033" or "Arm64;1033".
$summary = $Database.GetType().InvokeMember('SummaryInformation', 'GetProperty', $null, $Database, @(0))
try { [string]$summary.GetType().InvokeMember('Property', 'GetProperty', $null, $summary, @(7)) }
finally { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($summary) }
}
$installer = $null; $database = $null; $session = $null
try {
$installer = New-Object -ComObject WindowsInstaller.Installer
$file = (Resolve-Path -LiteralPath $MsiPath).Path
$database = $installer.OpenDatabase($file, 0)
$session = $installer.OpenPackage($file, 1)
$properties = @{}
foreach ($row in (Read-Rows $database 'SELECT `Property`, `Value` FROM `Property`' 2)) { $properties[$row[0]] = $row[1] }
$secureProperties = @($properties['SecureCustomProperties'] -split ';')
if ('INSTALLFOLDER' -notin $secureProperties) {
throw 'The selected installation folder must survive elevation (secure INSTALLFOLDER).'
}
if ($policy.mode -eq 'perUser') {
if ($properties.ContainsKey('ALLUSERS')) { throw 'Launcher must remain per-user; machine self-update is not part of this change.' }
if ($properties['UpgradeCode'].Trim('{}') -ne $policy.upgradeCode) { throw 'Launcher upgrade identity changed.' }
$dirs = @{}
foreach ($row in (Read-Rows $database 'SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`' 3)) { $dirs[$row[0]] = @($row[1],$row[2]) }
if ($dirs['INSTALLFOLDER'][0] -ne 'CraftSuiteFolder' -or $dirs['CraftSuiteFolder'][1] -ne 'ArtCraft' -or
$dirs['CraftSuiteFolder'][0] -ne 'LocalProgramsFolder' -or $dirs['LocalProgramsFolder'][0] -ne 'LocalAppDataFolder') {
throw 'Launcher new-install folder must be under the per-user ArtCraft suite root.'
}
foreach ($explicit in @('', 'F:\Chosen exact launcher')) {
Set-SessionProperty $session 'PREVIOUSINSTALLFOLDER' 'E:\Existing exact launcher'
Set-SessionProperty $session 'INSTALLFOLDER' $explicit
$row = @(Read-Rows $database 'SELECT `Condition` FROM `InstallUISequence` WHERE `Action` = ''CraftRestoreLauncherFolder''' 1)[0]
if ($session.EvaluateCondition($row[0]) -eq 1) {
if ($session.DoAction('CraftRestoreLauncherFolder') -ne 1) { throw 'Launcher restore action failed.' }
}
$expected = if ($explicit) { $explicit } else { 'E:\Existing exact launcher' }
if ((Get-SessionProperty $session 'INSTALLFOLDER') -ne $expected) { throw 'Launcher custom or existing path was lost.' }
}
$rows = @(Read-Rows $database 'SELECT `Root`, `Key`, `Name`, `Value` FROM `Registry`' 4)
if (@($rows | Where-Object { $_[0] -ne '1' }).Count) { throw 'Launcher registration must stay under the current account.' }
foreach ($entry in $policy.registry) {
if (@($rows | Where-Object { $_[0] -eq $entry[0] -and $_[1] -eq $entry[1] -and $_[2] -eq $entry[2] -and $_[3] -eq $entry[3] }).Count -ne 1) {
throw 'Launcher registration or shortcut preference changed unexpectedly.'
}
}
Write-Output 'PASS ArtCraft Launcher: per-user scope, suite folder, remembered/custom paths and existing registration. No installation performed.'
return
}
if ($properties['ALLUSERS'] -ne '2' -or $properties['MSIINSTALLPERUSER'] -ne '1') {
throw 'Policy regression: new packages must default to the per-user dual-scope context.'
}
if ($properties['UpgradeCode'].Trim('{}') -ne $policy.upgradeCode) { throw 'The existing upgrade identity must not change.' }
$directories = @{}
foreach ($row in (Read-Rows $database 'SELECT `Directory`, `Directory_Parent`, `DefaultDir` FROM `Directory`' 3)) {
$directories[$row[0]] = @($row[1], $row[2])
}
if ($directories['INSTALLFOLDER'][0] -ne 'CraftSuiteFolder' -or
$directories['CraftSuiteFolder'][1] -ne 'ArtCraft') { throw 'Expected the app under the ArtCraft suite folder.' }
$programs = $directories['CraftSuiteFolder'][0]
if ($programs -eq 'ProgramFiles6432Folder') { $programs = $directories[$programs][0] }
if ($programs -notin 'ProgramFilesFolder', 'ProgramFiles64Folder') {
throw 'Use Windows Installer native folder redirection, not a hard-coded profile path.'
}
$guards = @(Read-Rows $database 'SELECT `Condition`, `Description` FROM `LaunchCondition`' 2 |
Where-Object { $_[0] -match 'CRAFT_(USER|MACHINE)_DIR' })
if ($guards.Count -ne 2) { throw 'Expected both duplicate-installation guards.' }
$cases = 0
foreach ($scope in @('', '2', '1')) {
foreach ($user in @('', 'D:\Existing user app')) {
foreach ($machine in @('', 'E:\Existing machine app')) {
foreach ($installed in @('', '1')) {
Set-SessionProperty $session 'ALLUSERS' $scope
Set-SessionProperty $session 'Installed' $installed
Set-SessionProperty $session 'CRAFT_USER_DIR' $user
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' $machine
$allowed = $true
foreach ($guard in $guards) {
$value = $session.EvaluateCondition($guard[0])
if ($value -notin 0, 1) { throw 'Invalid duplicate guard.' }
if ($value -eq 0) { $allowed = $false }
}
$opposite = if ($scope -eq '1') { $user } else { $machine }
$expected = [bool]$installed -or -not [bool]$opposite
if ($allowed -ne $expected) { throw "Scope regression: ALLUSERS='$scope', user=$([bool]$user), machine=$([bool]$machine), maintenance=$([bool]$installed)." }
$cases++
}
}
}
}
# Execute the actual authored type-51 actions in a safe session, not copied resolver logic.
foreach ($scope in @('', '2', '1')) {
foreach ($explicit in @('', 'F:\Chosen exact app')) {
Set-SessionProperty $session 'ALLUSERS' $scope
Set-SessionProperty $session 'INSTALLFOLDER' $explicit
Set-SessionProperty $session 'CRAFT_USER_DIR' 'D:\Existing user app'
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' 'E:\Existing machine app'
$action = if ($scope -eq '1') { 'CraftRestoreMachineFolder' } else { 'CraftRestoreUserFolder' }
# Evaluate the compiled condition, so the test exercises it instead of calling the action directly.
$restore = @(Read-Rows $database ('SELECT `Condition` FROM `InstallExecuteSequence` WHERE `Action` = ''{0}''' -f $action) 1)[0]
$selected = $session.EvaluateCondition($restore[0]) -eq 1
if ($selected -eq [bool]$explicit) { throw "$action must run only when no folder was supplied (condition: $($restore[0]))." }
if ($selected -and $session.DoAction($action) -ne 1) { throw 'Remembered-folder property action failed.' }
$expected = if ($explicit) { $explicit } elseif ($scope -eq '1') { 'E:\Existing machine app' } else { 'D:\Existing user app' }
if ((Get-SessionProperty $session 'INSTALLFOLDER') -ne $expected) { throw 'Custom or remembered folder was not preserved.' }
}
}
$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['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 AppSearch and before FindRelatedProducts, launch checks and costing.'
}
# Windows Installer resolves Program Files for the context it started in, before the wizard's scope choice.
# The wizard therefore sets the new-install folder itself, and only in the wizard.
$defaults = @(Read-Rows $database 'SELECT `Action`, `Sequence`, `Condition` FROM `InstallUISequence`' 3 | Where-Object { $_[0] -like 'CraftDefault*' })
if ($defaults.Count -lt 2) { throw 'The wizard must set the new-install folder after the scope page; Program Files is resolved before the choice.' }
foreach ($row in $defaults) {
if (-not ($ui['CraftScopeDlg'] -lt [int]$row[1] -and [int]$row[1] -lt $ui['CostInitialize'])) { throw "$($row[0]) must run after the scope page and before costing." }
}
if (@(Read-Rows $database 'SELECT `Action` FROM `InstallExecuteSequence`' 1 | Where-Object { $_[0] -like 'CraftDefault*' }).Count) {
throw 'Silent installs must not use the wizard-only default folder actions.'
}
# The machine default comes from the environment of the package architecture, not from a folder name.
# x86 packages use Program Files (x86) on 64-bit Windows; x64 and Arm64 packages use the 64-bit root.
$machineRoot = if ((Get-PackageTemplate $database) -match '^Intel') {
if (Get-SessionProperty $session 'VersionNT64') { ${env:ProgramFiles(x86)} } else { $env:ProgramFiles }
} elseif ($env:ProgramW6432) { $env:ProgramW6432 } else { $env:ProgramFiles }
$machineFolder = (Join-Path $machineRoot "ArtCraft\$($policy.name)") + '\'
foreach ($scope in @('1', '')) {
Set-SessionProperty $session 'ALLUSERS' $scope
Set-SessionProperty $session 'INSTALLFOLDER' ''
Set-SessionProperty $session 'CRAFT_MACHINE_DIR' ''
Set-SessionProperty $session 'CRAFT_USER_DIR' ''
foreach ($row in @($defaults | Sort-Object { [int]$_[1] })) {
if ($session.EvaluateCondition($row[2]) -eq 1 -and $session.DoAction($row[0]) -ne 1) { throw "$($row[0]) failed." }
}
$folder = Get-SessionProperty $session 'INSTALLFOLDER'
$wanted = if ($scope -eq '1') { $machineFolder } else { (Join-Path $env:LOCALAPPDATA "Programs\ArtCraft\$($policy.name)") + '\' }
if ($folder -ine $wanted) { throw "Wizard default folder for ALLUSERS='$scope' is '$folder', expected '$wanted'." }
}
$execute = @{}
foreach ($row in (Read-Rows $database 'SELECT `Action`, `Sequence` FROM `InstallExecuteSequence`' 2)) { $execute[$row[0]] = [int]$row[1] }
foreach ($action in @('CraftRestoreUserFolder','CraftRestoreMachineFolder')) {
if (-not ($execute['AppSearch'] -lt $execute[$action] -and $execute[$action] -lt $execute['CostInitialize'])) {
throw 'Silent updates must restore the folder before directory costing.'
}
}
foreach ($resolver in @('CraftUFromLegacyExe','CraftMFromLegacyExe')) {
if (-not ($execute['AppSearch'] -lt $execute[$resolver] -and $execute[$resolver] -lt $execute['LaunchConditions'])) {
throw 'Both legacy folder resolvers must run after AppSearch and before the launch checks.'
}
}
# 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)
# 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 }
$property = $Matches[1]
if ($session.EvaluateCondition($event[2]) -eq 1) {
Set-SessionProperty $session $property $(if ($event[1] -eq '{}') { '' } else { $event[1] })
}
}
$allUsers = Get-SessionProperty $session 'ALLUSERS'
$perUser = Get-SessionProperty $session 'MSIINSTALLPERUSER'
if (($choice -eq 'machine' -and ($allUsers -ne '1' -or $perUser)) -or
($choice -eq 'user' -and ($allUsers -or $perUser -ne '1'))) {
throw 'Scope selector does not reset the requested installation context.'
}
}
$actions = @(Read-Rows $database 'SELECT `Action`, `Source`, `Target`, `Type` FROM `CustomAction`' 4)
$savedOptions = @($actions | Where-Object { $_[0] -match '^CraftLoadOption[0-9]+M$' })
foreach ($option in $savedOptions) {
if ([int]$option[3] -ne 51) { throw 'Saved-option test may run only property-setting actions.' }
$number = [regex]::Match($option[0], '[0-9]+').Value
foreach ($scope in @('2','1')) {
Set-SessionProperty $session 'ALLUSERS' $scope
Set-SessionProperty $session "CRAFT_OPTION_${number}_M" 'saved-machine-choice'
Set-SessionProperty $session "CRAFT_OPTION_${number}_U" 'saved-user-choice'
Set-SessionProperty $session $option[1] ''
$action = "CraftLoadOption$number" + $(if ($scope -eq '1') { 'M' } else { 'U' })
if ($session.DoAction($action) -ne 1) { throw 'Saved-option property action failed.' }
$expected = if ($scope -eq '1') { 'saved-machine-choice' } else { 'saved-user-choice' }
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) {
throw 'HKCR keys must be relative to Classes.'
}
foreach ($entry in $policy.registry) {
$found = @($registry | Where-Object { $_[0] -eq $entry[0] -and $_[1] -eq $entry[1] -and $_[2] -eq $entry[2] -and $_[3] -eq $entry[3] })
if ($found.Count -ne 1) { throw "Lost or changed registration: $($entry[1]) / $($entry[2])" }
}
$installDir = @($registry | Where-Object { $_[0] -eq '-1' -and $_[1] -eq "Software\$($policy.name)" -and $_[2] -eq 'InstallDir' -and $_[3] -eq '[INSTALLFOLDER]' })
if ($installDir.Count -ne 1) { throw 'A context-sensitive installation-folder record is required.' }
Write-Output "PASS $($policy.name): $cases scope cases, folder preservation, action sequencing and registry contract. No installation performed."
} finally {
foreach ($com in @($session, $database, $installer)) {
if ($null -ne $com) { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($com) }
}
}
+52 -75
View File
@@ -1,34 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
GridCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
wix build gridcraft.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, an optional desktop shortcut,
App Paths (Win+R `gridcraft`),
and GridCraft offered under "Open with" (and in Settings > Default apps) for .xlsx, .xlsm,
.csv and .tsv, without taking over the user's default app for them.
UpgradeCode is fixed for GridCraft (generated 2026-10-07): never change it, or upgrades
install side by side instead of replacing the old version.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?ifndef HasCraftFontLicences ?>
<?define HasCraftFontLicences=0 ?>
<?endif ?>
<Package
Name="GridCraft"
Manufacturer="Learning Machines LLC"
Version="$(var.Version)"
UpgradeCode="5511327B-F305-41BE-92D0-EB84944E7C4D"
Scope="perMachine"
Compressed="yes"
Language="1033">
<?endif?>
<Package Name="GridCraft" Manufacturer="Learning Machines LLC" Version="$(var.Version)" UpgradeCode="5511327B-F305-41BE-92D0-EB84944E7C4D" Scope="perUserOrMachine" Compressed="yes" Language="1033">
<SummaryInformation Description="GridCraft spreadsheet" Manufacturer="Learning Machines LLC" />
<?include $(sys.SOURCEFILEDIR)install-policy.wxi?>
<!-- Same-version upgrades let successive pre-releases of one X.Y.Z replace each other
(MSI versions are numeric, so 1.2.3-rc.1 and 1.2.3-rc.2 are both 1.2.3). -->
<MajorUpgrade AllowSameVersionUpgrades="yes"
DowngradeErrorMessage="A newer version of GridCraft is already installed." />
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of GridCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<Icon Id="GridcraftIcon.ico" SourceFile="$(var.IconPath)" />
@@ -42,16 +22,10 @@
execute sequence cannot reload the old choice after the user unchecks the box. -->
<Property Id="DESKTOPSHORTCUT" Secure="yes" />
<Property Id="SAVEDDESKTOPSHORTCUT" Secure="yes">
<RegistrySearch Id="FindDesktopShortcutPreference" Root="HKLM"
Key="Software\GridCraft\Installer" Name="DesktopShortcut" Type="raw" />
</Property>
<SetProperty Id="DESKTOPSHORTCUT" Action="LoadDesktopShortcutPreference" Value="1"
After="AppSearch" Sequence="first"
Condition="NOT DESKTOPSHORTCUT AND SAVEDDESKTOPSHORTCUT = &quot;1&quot;" />
<SetProperty Id="DESKTOPSHORTCUT" Action="DefaultDesktopShortcut" Value="0"
After="LoadDesktopShortcutPreference" Sequence="first" Condition="NOT DESKTOPSHORTCUT" />
<SetProperty Id="DesktopShortcutChoice" Value="1" After="DefaultDesktopShortcut"
Sequence="ui" Condition="DESKTOPSHORTCUT = &quot;1&quot;" />
</Property>
<SetProperty Id="DESKTOPSHORTCUT" Action="LoadDesktopShortcutPreference" Value="1" After="CraftLoadOption1U" Sequence="first" Condition="NOT DESKTOPSHORTCUT AND SAVEDDESKTOPSHORTCUT = &quot;1&quot;" />
<SetProperty Id="DESKTOPSHORTCUT" Action="DefaultDesktopShortcut" Value="0" After="LoadDesktopShortcutPreference" Sequence="first" Condition="NOT DESKTOPSHORTCUT" />
<SetProperty Id="DesktopShortcutChoice" Value="1" After="DefaultDesktopShortcut" Sequence="ui" Condition="DESKTOPSHORTCUT = &quot;1&quot;" />
<Property Id="DefaultUIFont" Value="GridcraftUIFont" />
<Property Id="ErrorDialog" Value="GridcraftErrorDlg" />
@@ -60,12 +34,16 @@
<TextStyle Id="GridcraftUIFont" FaceName="Segoe UI" Size="9" />
<Dialog Id="DesktopShortcutDlg" Width="320" Height="150" Title="GridCraft Setup" NoMinimize="yes">
<Control Id="Heading" Type="Text" X="15" Y="15" Width="290" Height="20" Text="Install GridCraft" />
<Control Id="Description" Type="Text" X="15" Y="40" Width="290" Height="28"
Text="Install GridCraft for everyone who uses this computer." />
<Control Id="DesktopShortcutCheckbox" Type="CheckBox" X="15" Y="76" Width="290" Height="18"
Property="DesktopShortcutChoice" CheckBoxValue="1" Text="Create a &amp;desktop shortcut" />
<Control Id="Install" Type="PushButton" X="165" Y="118" Width="65" Height="20"
Default="yes" ElevationShield="yes" Text="&amp;Install">
<Control Id="Description" Type="Text" X="15" Y="40" Width="290" Height="28" Text="Install GridCraft using the account option you selected." />
<Control Id="DesktopShortcutCheckbox" Type="CheckBox" X="15" Y="76" Width="290" Height="18" Property="DesktopShortcutChoice" CheckBoxValue="1" Text="Create a &amp;desktop shortcut" />
<Control Id="Install" Type="PushButton" X="165" Y="118" Width="65" Height="20" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS &lt;&gt; 1" ShowCondition="ALLUSERS = 1" Text="&amp;Install">
<!-- An unchecked checkbox clears its property. Publish an explicit zero so the
elevated execute sequence receives an unambiguous opt-out. -->
<Publish Property="DESKTOPSHORTCUT" Value="1" Condition="DesktopShortcutChoice" />
<Publish Property="DESKTOPSHORTCUT" Value="0" Condition="NOT DesktopShortcutChoice" />
<Publish Event="EndDialog" Value="Return" />
</Control>
<Control Id="InstallUser" Type="PushButton" DefaultCondition="ALLUSERS &lt;&gt; 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS &lt;&gt; 1" X="165" Y="118" Width="65" Height="20" Text="&amp;Install">
<!-- An unchecked checkbox clears its property. Publish an explicit zero so the
elevated execute sequence receives an unambiguous opt-out. -->
<Publish Property="DESKTOPSHORTCUT" Value="1" Condition="DesktopShortcutChoice" />
@@ -104,8 +82,7 @@
</Control>
</Dialog>
<Dialog Id="FilesInUse" Width="320" Height="190" Title="GridCraft Setup" NoMinimize="yes">
<Control Id="Description" Type="Text" X="15" Y="15" Width="290" Height="35"
Text="Close the applications listed below, then choose Retry. Ignore may require a restart." />
<Control Id="Description" Type="Text" X="15" Y="15" Width="290" Height="35" Text="Close the applications listed below, then choose Retry. Ignore may require a restart." />
<Control Id="Files" Type="ListBox" X="15" Y="55" Width="290" Height="85" Property="FileInUseProcess" />
<Control Id="Retry" Type="PushButton" X="95" Y="155" Width="65" Height="20" Default="yes" Text="Retry">
<Publish Event="EndDialog" Value="Retry" />
@@ -151,7 +128,9 @@
</AdminUISequence>
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="GridCraft" />
<Directory Id="CraftSuiteFolder" Name="ArtCraft">
<Directory Id="INSTALLFOLDER" Name="GridCraft" />
</Directory>
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<StandardDirectory Id="DesktopFolder" />
@@ -159,54 +138,55 @@
<Feature Id="Main" Title="GridCraft" Level="1">
<?if $(var.HasCraftFontLicences) = 1 ?>
<ComponentGroupRef Id="CraftFontLicences" />
<?endif ?>
<?endif?>
<ComponentGroupRef Id="GridcraftFiles" />
</Feature>
</Package>
<Property Id="CRAFT_OPTION_1_M" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_1_MSearch" Root="HKLM" Key="Software\GridCraft\Installer" Name="DesktopShortcut" Type="raw" />
</Property><SetProperty Id="SAVEDDESKTOPSHORTCUT" Action="CraftLoadOption1M" Value="[CRAFT_OPTION_1_M]" After="LaunchConditions" Sequence="both" Condition="ALLUSERS = 1" /><Property Id="CRAFT_OPTION_1_U" Secure="yes"><RegistrySearch Id="CRAFT_OPTION_1_USearch" Root="HKCU" Key="Software\GridCraft\Installer" Name="DesktopShortcut" Type="raw" />
</Property><SetProperty Id="SAVEDDESKTOPSHORTCUT" Action="CraftLoadOption1U" Value="[CRAFT_OPTION_1_U]" After="CraftLoadOption1M" Sequence="both" Condition="ALLUSERS &lt;&gt; 1" /></Package>
<Fragment>
<?if $(var.HasCraftFontLicences) = 1 ?>
<ComponentGroup Id="CraftFontLicences" Directory="INSTALLFOLDER">
<Files Include="$(var.DocsDir)\**" />
</ComponentGroup>
<?endif ?>
<?endif?>
<ComponentGroup Id="GridcraftFiles" Directory="INSTALLFOLDER">
<Component Id="GridcraftApp" Guid="0507D4B6-12FF-4882-84E1-59083E74A4EB">
<File Id="GridcraftExe" Source="$(var.BinDir)\gridcraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="GridCraft"
Description="GridCraft spreadsheet" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
Icon="GridcraftIcon.ico" IconIndex="0" />
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="GridCraft" Description="GridCraft spreadsheet" WorkingDirectory="INSTALLFOLDER" Advertise="yes" Icon="GridcraftIcon.ico" IconIndex="0" />
</File>
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Value="[#GridcraftExe]" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Value="[#GridcraftExe]" Type="string" />
<!-- ProgIds: how Explorer opens a file with GridCraft. Nothing here sets HKCR\.<ext>'s
default value, so the user's current default app is left alone. -->
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.Workbook" Value="GridCraft Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.Workbook\DefaultIcon" Value="&quot;[#GridcraftExe]&quot;,0" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.Workbook\shell\open\command" Value="&quot;[#GridcraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.TextData" Value="GridCraft Text Data" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.TextData\DefaultIcon" Value="&quot;[#GridcraftExe]&quot;,0" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.TextData\shell\open\command" Value="&quot;[#GridcraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.Workbook" Value="GridCraft Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.Workbook\DefaultIcon" Value="&quot;[#GridcraftExe]&quot;,0" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.Workbook\shell\open\command" Value="&quot;[#GridcraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.TextData" Value="GridCraft Text Data" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.TextData\DefaultIcon" Value="&quot;[#GridcraftExe]&quot;,0" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.TextData\shell\open\command" Value="&quot;[#GridcraftExe]&quot; &quot;%1&quot;" Type="string" />
<!-- "Open with": Windows reads only the value names under OpenWithProgids; the data is
ignored (WiX rejects an empty Value, so it repeats the ProgId). -->
<RegistryValue Root="HKLM" Key="Software\Classes\.xlsx\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.xlsm\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.xlsx\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.xlsm\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<!-- .xlsb and .ods are imported (data only) into a new workbook that saves as .xlsx. -->
<RegistryValue Root="HKLM" Key="Software\Classes\.xlsb\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.ods\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.csv\OpenWithProgids" Name="GridCraft.TextData" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKLM" Key="Software\Classes\.tsv\OpenWithProgids" Name="GridCraft.TextData" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.xlsb\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.ods\OpenWithProgids" Name="GridCraft.Workbook" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.csv\OpenWithProgids" Name="GridCraft.TextData" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.tsv\OpenWithProgids" Name="GridCraft.TextData" Value="GridCraft.TextData" Type="string" />
<!-- Settings > Default apps: lets the user pick GridCraft for these types. -->
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities" Name="ApplicationName" Value="GridCraft" Type="string" />
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities" Name="ApplicationDescription" Value="Open-source spreadsheet for .xlsx, CSV and TSV files" Type="string" />
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".xlsx" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".xlsm" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".csv" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKLM" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".tsv" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKLM" Key="Software\RegisteredApplications" Name="GridCraft" Value="Software\GridCraft\Capabilities" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities" Name="ApplicationName" Value="GridCraft" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities" Name="ApplicationDescription" Value="Open-source spreadsheet for .xlsx, CSV and TSV files" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".xlsx" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".xlsm" Value="GridCraft.Workbook" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".csv" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Capabilities\FileAssociations" Name=".tsv" Value="GridCraft.TextData" Type="string" />
<RegistryValue Root="HKMU" Key="Software\RegisteredApplications" Name="GridCraft" Value="Software\GridCraft\Capabilities" Type="string" />
<RegistryValue Root="HKMU" Key="Software\GridCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" />
</Component>
<Component Id="GridcraftCli">
@@ -216,16 +196,13 @@
<!-- Persist both on and off, independently of the optional shortcut. AppSearch reads
this before MajorUpgrade removes the previous product. -->
<Component Id="DesktopShortcutPreference">
<RegistryValue Root="HKLM" Key="Software\GridCraft\Installer" Name="DesktopShortcut"
Value="[DESKTOPSHORTCUT]" Type="string" KeyPath="yes" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Installer" Name="DesktopShortcut" Value="[DESKTOPSHORTCUT]" Type="string" KeyPath="yes" />
</Component>
<Component Id="DesktopShortcutComponent" Directory="DesktopFolder" Condition="DESKTOPSHORTCUT = &quot;1&quot;">
<Shortcut Id="DesktopShortcut" Name="GridCraft" Description="GridCraft spreadsheet"
Target="[#GridcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="GridcraftIcon.ico" IconIndex="0" />
<Shortcut Id="DesktopShortcut" Name="GridCraft" Description="GridCraft spreadsheet" Target="[#GridcraftExe]" WorkingDirectory="INSTALLFOLDER" Icon="GridcraftIcon.ico" IconIndex="0" />
<!-- HKMU resolves to HKLM for this per-machine package, so another account can
uninstall the marker too. MSI root -1 also satisfies ICE43 for this shortcut. -->
<RegistryValue Root="HKMU" Key="Software\GridCraft\Installer" Name="DesktopShortcutComponent"
Value="1" Type="integer" KeyPath="yes" />
<RegistryValue Root="HKMU" Key="Software\GridCraft\Installer" Name="DesktopShortcutComponent" Value="1" Type="integer" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
+150
View File
@@ -0,0 +1,150 @@
{
"id": "gridcraft",
"name": "GridCraft",
"upgradeCode": "5511327B-F305-41BE-92D0-EB84944E7C4D",
"sources": [
"gridcraft.wxs"
],
"icon": "assets/app-icon/gridcraft.ico",
"localization": [],
"extensions": [],
"payloads": [
"gridcraft-cli.exe",
"gridcraft.exe"
],
"registry": [
[
"-1",
"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\gridcraft.exe",
"",
"[#GridcraftExe]"
],
[
"-1",
"Software\\Classes\\GridCraft.Workbook",
"",
"GridCraft Workbook"
],
[
"-1",
"Software\\Classes\\GridCraft.Workbook\\DefaultIcon",
"",
"\"[#GridcraftExe]\",0"
],
[
"-1",
"Software\\Classes\\GridCraft.Workbook\\shell\\open\\command",
"",
"\"[#GridcraftExe]\" \"%1\""
],
[
"-1",
"Software\\Classes\\GridCraft.TextData",
"",
"GridCraft Text Data"
],
[
"-1",
"Software\\Classes\\GridCraft.TextData\\DefaultIcon",
"",
"\"[#GridcraftExe]\",0"
],
[
"-1",
"Software\\Classes\\GridCraft.TextData\\shell\\open\\command",
"",
"\"[#GridcraftExe]\" \"%1\""
],
[
"-1",
"Software\\Classes\\.xlsx\\OpenWithProgids",
"GridCraft.Workbook",
"GridCraft.Workbook"
],
[
"-1",
"Software\\Classes\\.xlsm\\OpenWithProgids",
"GridCraft.Workbook",
"GridCraft.Workbook"
],
[
"-1",
"Software\\Classes\\.xlsb\\OpenWithProgids",
"GridCraft.Workbook",
"GridCraft.Workbook"
],
[
"-1",
"Software\\Classes\\.ods\\OpenWithProgids",
"GridCraft.Workbook",
"GridCraft.Workbook"
],
[
"-1",
"Software\\Classes\\.csv\\OpenWithProgids",
"GridCraft.TextData",
"GridCraft.TextData"
],
[
"-1",
"Software\\Classes\\.tsv\\OpenWithProgids",
"GridCraft.TextData",
"GridCraft.TextData"
],
[
"-1",
"Software\\GridCraft\\Capabilities",
"ApplicationName",
"GridCraft"
],
[
"-1",
"Software\\GridCraft\\Capabilities",
"ApplicationDescription",
"Open-source spreadsheet for .xlsx, CSV and TSV files"
],
[
"-1",
"Software\\GridCraft\\Capabilities\\FileAssociations",
".xlsx",
"GridCraft.Workbook"
],
[
"-1",
"Software\\GridCraft\\Capabilities\\FileAssociations",
".xlsm",
"GridCraft.Workbook"
],
[
"-1",
"Software\\GridCraft\\Capabilities\\FileAssociations",
".csv",
"GridCraft.TextData"
],
[
"-1",
"Software\\GridCraft\\Capabilities\\FileAssociations",
".tsv",
"GridCraft.TextData"
],
[
"-1",
"Software\\RegisteredApplications",
"GridCraft",
"Software\\GridCraft\\Capabilities"
],
[
"-1",
"Software\\GridCraft\\Installer",
"DesktopShortcut",
"[DESKTOPSHORTCUT]"
],
[
"-1",
"Software\\GridCraft\\Installer",
"DesktopShortcutComponent",
"#1"
]
],
"mode": "dualScope"
}
+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Include xmlns="http://wixtoolset.org/schemas/v4/wxs">
<!-- Choose scope before launch checks and directory costing. Never move an existing copy. -->
<!-- Only for me leaves ALLUSERS unset. Publishing ALLUSERS=2 here makes FindRelatedProducts treat the run as
per-machine and skip the existing per-user copy, so an interactive upgrade installs beside it
(two registered products, and uninstall leaves files behind). Checked with real msiexec runs. -->
<Property Id="INSTALLFOLDER" Secure="yes" />
<Property Id="CRAFT_SCOPE" Value="user" Secure="yes" />
<Property Id="CRAFT_U_DIR" Secure="yes"><RegistrySearch Id="CRAFT_U_DIRSearch" Root="HKCU" Key="Software\GridCraft" Name="InstallDir" Type="raw" /></Property>
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromDIR" Value="[CRAFT_U_DIR]" After="AppSearch" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_DIR" />
<Property Id="CRAFT_U_FOLDER" Secure="yes"><RegistrySearch Id="CRAFT_U_FOLDERSearch" Root="HKCU" Key="Software\GridCraft" Name="InstallFolder" Type="raw" /></Property>
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromFOLDER" Value="[CRAFT_U_FOLDER]" After="CraftUFromDIR" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_FOLDER" />
<Property Id="CRAFT_U_PATH" Secure="yes"><RegistrySearch Id="CRAFT_U_PATHSearch" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Name="Path" Type="raw" /></Property>
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromPATH" Value="[CRAFT_U_PATH]" After="CraftUFromFOLDER" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_PATH" />
<Property Id="CRAFT_U_EXEFOLDER" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyFolder" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Type="file" /></Property>
<Property Id="CRAFT_U_EXE" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyExe" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Type="file"><FileSearch Id="CRAFT_ULegacyFile" Name="gridcraft.exe" /></RegistrySearch></Property>
<SetProperty Id="CRAFT_USER_DIR" Action="CraftUFromLegacyExe" Value="[CRAFT_U_EXEFOLDER]" After="CraftUFromPATH" Sequence="both" Condition="NOT CRAFT_USER_DIR AND CRAFT_U_EXEFOLDER AND CRAFT_U_EXE" />
<Property Id="CRAFT_USER_DIR" Secure="yes" />
<Property Id="CRAFT_M_DIR" Secure="yes"><RegistrySearch Id="CRAFT_M_DIRSearch" Root="HKLM" Key="Software\GridCraft" Name="InstallDir" Type="raw" /></Property>
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromDIR" Value="[CRAFT_M_DIR]" After="CraftUFromLegacyExe" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_DIR" />
<Property Id="CRAFT_M_FOLDER" Secure="yes"><RegistrySearch Id="CRAFT_M_FOLDERSearch" Root="HKLM" Key="Software\GridCraft" Name="InstallFolder" Type="raw" /></Property>
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromFOLDER" Value="[CRAFT_M_FOLDER]" After="CraftMFromDIR" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_FOLDER" />
<Property Id="CRAFT_M_PATH" Secure="yes"><RegistrySearch Id="CRAFT_M_PATHSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Name="Path" Type="raw" /></Property>
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromPATH" Value="[CRAFT_M_PATH]" After="CraftMFromFOLDER" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_PATH" />
<Property Id="CRAFT_M_EXEFOLDER" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyFolder" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Type="file" /></Property>
<Property Id="CRAFT_M_EXE" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyExe" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\gridcraft.exe" Type="file"><FileSearch Id="CRAFT_MLegacyFile" Name="gridcraft.exe" /></RegistrySearch></Property>
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromLegacyExe" Value="[CRAFT_M_EXEFOLDER]" After="CraftMFromPATH" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_EXEFOLDER AND CRAFT_M_EXE" />
<Property Id="CRAFT_MACHINE_DIR" Secure="yes" />
<SetProperty Id="CRAFT_SCOPE" Action="CraftPreferMachineScope" Value="machine" After="CraftMFromLegacyExe" Sequence="ui" Condition="ALLUSERS = 1 OR (CRAFT_MACHINE_DIR AND NOT CRAFT_USER_DIR)" />
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreUserFolder" Value="[CRAFT_USER_DIR]" Before="CostInitialize" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS &lt;&gt; 1 AND CRAFT_USER_DIR" />
<!-- The wizard can change the context after Windows Installer has already resolved Program Files for the first one.
Give the new-install folder explicitly so Everyone never lands in the profile and Only for me never lands in Program Files. -->
<?if $(sys.BUILDARCH) = x86 ?>
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder" Value="[%ProgramFiles(x86)]\ArtCraft\GridCraft\" After="CraftRestoreUserFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND VersionNT64" />
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder32" Value="[%ProgramFiles]\ArtCraft\GridCraft\" After="CraftDefaultMachineFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND NOT VersionNT64" />
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultUserFolder" Value="[LocalAppDataFolder]Programs\ArtCraft\GridCraft\" After="CraftDefaultMachineFolder32" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS &lt;&gt; 1" />
<?else?>
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultMachineFolder" Value="[%ProgramW6432]\ArtCraft\GridCraft\" After="CraftRestoreUserFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1" />
<SetProperty Id="INSTALLFOLDER" Action="CraftDefaultUserFolder" Value="[LocalAppDataFolder]Programs\ArtCraft\GridCraft\" After="CraftDefaultMachineFolder" Sequence="ui" Condition="NOT INSTALLFOLDER AND ALLUSERS &lt;&gt; 1" />
<?endif?>
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreMachineFolder" Value="[CRAFT_MACHINE_DIR]" Before="CraftRestoreUserFolder" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND CRAFT_MACHINE_DIR" />
<Launch Condition="Installed OR ALLUSERS &lt;&gt; 1 OR NOT CRAFT_USER_DIR" Message="This app is already installed for your account. Choose Only for me to update it, or uninstall that copy before changing who it is installed for." />
<Launch Condition="Installed OR ALLUSERS = 1 OR NOT CRAFT_MACHINE_DIR" Message="This app is already installed for everyone. Choose Everyone to update it, or pass ALLUSERS=1 to msiexec. Uninstall that copy before changing who it is installed for." />
<UI>
<TextStyle Id="CraftScopeText" FaceName="Segoe UI" Size="9" />
<TextStyle Id="CraftScopeTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
<Dialog Id="CraftScopeDlg" Width="370" Height="220" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="30" Text="{\CraftScopeTitle}Who should use [ProductName]?" />
<Control Id="Info" Type="Text" X="20" Y="50" Width="330" Height="30" Text="{\CraftScopeText}An update keeps the existing folder. New copies use the ArtCraft folder under Programs." />
<Control Id="Scope" Type="RadioButtonGroup" X="20" Y="88" Width="330" Height="60" Property="CRAFT_SCOPE">
<RadioButtonGroup Property="CRAFT_SCOPE">
<RadioButton Value="user" X="0" Y="0" Width="330" Height="20" Text="Only for me (no administrator permission needed)" />
<RadioButton Value="machine" X="0" Y="30" Width="330" Height="20" Text="Everyone on this computer (asks an administrator)" />
</RadioButtonGroup>
</Control>
<Control Id="Next" Type="PushButton" X="214" Y="182" Width="66" Height="20" Default="yes" Text="Next">
<Publish Property="ALLUSERS" Value="1" Order="1" Condition="CRAFT_SCOPE = &quot;machine&quot;" />
<Publish Property="ALLUSERS" Value="{}" Order="2" Condition="CRAFT_SCOPE = &quot;user&quot;" />
<Publish Property="MSIINSTALLPERUSER" Value="{}" Order="3" Condition="CRAFT_SCOPE = &quot;machine&quot;" />
<Publish Property="MSIINSTALLPERUSER" Value="1" Order="4" Condition="CRAFT_SCOPE = &quot;user&quot;" />
<Publish Event="EndDialog" Value="Return" Order="5" />
</Control>
<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" 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>
+2
View File
@@ -113,6 +113,8 @@ Invoke-Native 'wix build' {
-d "DocsDir=$Docs" -d "HasCraftFontLicences=$HasCraftFontLicences" `
-o $Msi
}
# Check the compiled scope, path and registration contract before signing.
& (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $Msi
# 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($Msi, '.wixpdb'))
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
+3 -2
View File
@@ -16,7 +16,7 @@ if ($env:GITHUB_ACTIONS -ne 'true' -or $env:RUNNER_OS -ne 'Windows' -or $env:RUN
}
if (-not $WorkDir) { $WorkDir = Join-Path $env:RUNNER_TEMP 'gridcraft-shortcut-smoke' }
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$InstallDir = Join-Path $env:ProgramFiles 'GridCraft'
$InstallDir = Join-Path $env:ProgramFiles 'ArtCraft\GridCraft'
$DesktopLink = Join-Path ([Environment]::GetFolderPath('CommonDesktopDirectory')) 'GridCraft.lnk'
$StartLink = Join-Path ([Environment]::GetFolderPath('CommonPrograms')) 'GridCraft.lnk'
$ChoiceKey = 'HKLM:\Software\GridCraft\Installer'
@@ -65,7 +65,8 @@ function Get-ProductCode([string] $Msi) {
function Invoke-Msi([string] $Name, [string[]] $Arguments) {
$log = Join-Path $WorkDir "$Name.log"
# /norestart is unconditional. Each operation and the workflow have time limits.
$process = Start-Process msiexec.exe -ArgumentList ($Arguments + @('/qn', '/norestart', '/l*v', "`"$log`"")) -PassThru
# Keep this existing regression on the explicit all-users route, including cross-account removal.
$process = Start-Process msiexec.exe -ArgumentList ($Arguments + @('ALLUSERS=1', '/qn', '/norestart', '/l*v', "`"$log`"")) -PassThru
try {
if (-not $process.WaitForExit(120000)) {
$process.Kill()
+54
View File
@@ -0,0 +1,54 @@
<#
.SYNOPSIS
Build non-executable installer metadata fixtures; never install them.
#>
param(
[Parameter(Mandatory = $true)] [string] $OutputDirectory,
[ValidateSet('x64','x86','arm64')] [string[]] $Architectures = @('x64','x86','arm64')
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$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
Add-Type -AssemblyName System.Drawing
foreach ($arch in $Architectures) {
$stage = Join-Path $output $arch
[void](New-Item -ItemType Directory -Force -Path $stage)
foreach ($name in $policy.payloads) {
$destination = Join-Path $stage $name
[void](New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($destination)))
Set-Content -LiteralPath $destination -Encoding Ascii -Value 'NON-EXECUTABLE MSI METADATA TEST FIXTURE'
}
foreach ($folder in @('docs','models','notices')) {
$path = Join-Path $stage $folder
[void](New-Item -ItemType Directory -Force -Path $path)
Set-Content -LiteralPath (Join-Path $path "$folder-fixture.txt") -Value 'Metadata test fixture, not a user file or model.'
}
foreach ($model in @('metadata-one.rten','metadata-two.rten')) {
Set-Content -LiteralPath (Join-Path (Join-Path $stage 'models') $model) -Encoding Ascii -Value 'NON-EXECUTABLE MODEL METADATA FIXTURE'
}
foreach ($spec in @(@('banner.bmp',493,58),@('dialog.bmp',493,312))) {
$bitmap = [Drawing.Bitmap]::new($spec[1],$spec[2],[Drawing.Imaging.PixelFormat]::Format24bppRgb)
try { $bitmap.Save((Join-Path $stage $spec[0]),[Drawing.Imaging.ImageFormat]::Bmp) }
finally { $bitmap.Dispose() }
}
Set-Content -LiteralPath (Join-Path $stage 'licence.rtf') -Encoding Ascii -Value '{\rtf1 Metadata test fixture.}'
$sources = @($policy.sources | ForEach-Object { Join-Path $PSScriptRoot $_ })
$msi = Join-Path $stage "$($policy.id)-NOT-INSTALLABLE.msi"
$extensions = @($policy.extensions | ForEach-Object { '-ext'; "WixToolset.$_.wixext" })
& wix build @sources -arch $arch @extensions `
-culture en-US -d 'Version=0.0.1' -d 'DisplayVersion=0.0.1-metadata-test' `
-d "BinDir=$stage" -d "IconPath=$(Join-Path $root $policy.icon)" `
-d "DocsDir=$(Join-Path $stage 'docs')" -d "ModelsDir=$(Join-Path $stage 'models')" `
-d "AppNoticesDir=$(Join-Path $stage 'notices')" -d 'HasCraftFontLicences=1' -d 'HasCli=1' `
-d "BannerBmp=$(Join-Path $stage 'banner.bmp')" -d "DialogBmp=$(Join-Path $stage 'dialog.bmp')" `
-d "UiBannerBmp=$(Join-Path $stage 'banner.bmp')" -d "UiDialogBmp=$(Join-Path $stage 'dialog.bmp')" `
-d "LicenseRtf=$(Join-Path $stage 'licence.rtf')" -o $msi @($policy.localization | ForEach-Object { '-loc'; Join-Path $PSScriptRoot $_ })
if ($LASTEXITCODE -ne 0) { throw "WiX validation failed for $arch (exit $LASTEXITCODE)." }
$shell = (Get-Process -Id $PID).Path
& $shell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot 'check-install-policy.ps1') -MsiPath $msi
if ($LASTEXITCODE -ne 0) { throw "Compiled installer policy failed for $arch (exit $LASTEXITCODE)." }
Write-Output "PASS $arch metadata-only package. This is not an executable installer."
}