mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-27 20:26:48 -06:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11bb67386c | ||
|
|
1decf0ef51 | ||
|
|
d33616dc95 | ||
|
|
89ac453efa | ||
|
|
3b71e37b70 | ||
|
|
a4c50a6dac | ||
|
|
a614e683ed | ||
|
|
e7d778046b | ||
|
|
998396ad9f | ||
|
|
df42619b1e | ||
|
|
0b6d243946 |
@@ -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')
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -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 = "1"" />
|
||||
<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 = "1"" />
|
||||
</Property>
|
||||
<SetProperty Id="DESKTOPSHORTCUT" Action="LoadDesktopShortcutPreference" Value="1" After="CraftLoadOption1U" Sequence="first" Condition="NOT DESKTOPSHORTCUT AND SAVEDDESKTOPSHORTCUT = "1"" />
|
||||
<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 = "1"" />
|
||||
|
||||
<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 &desktop shortcut" />
|
||||
<Control Id="Install" Type="PushButton" X="165" Y="118" Width="65" Height="20"
|
||||
Default="yes" ElevationShield="yes" Text="&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 &desktop shortcut" />
|
||||
<Control Id="Install" Type="PushButton" X="165" Y="118" Width="65" Height="20" Default="yes" DefaultCondition="ALLUSERS = 1" ElevationShield="yes" HideCondition="ALLUSERS <> 1" ShowCondition="ALLUSERS = 1" Text="&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 <> 1" HideCondition="ALLUSERS = 1" ShowCondition="ALLUSERS <> 1" X="165" Y="118" Width="65" Height="20" Text="&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 <> 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=""[#GridcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.Workbook\shell\open\command" Value=""[#GridcraftExe]" "%1"" 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=""[#GridcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\Classes\GridCraft.TextData\shell\open\command" Value=""[#GridcraftExe]" "%1"" 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=""[#GridcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.Workbook\shell\open\command" Value=""[#GridcraftExe]" "%1"" 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=""[#GridcraftExe]",0" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Classes\GridCraft.TextData\shell\open\command" Value=""[#GridcraftExe]" "%1"" 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 = "1"">
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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 <> 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 <> 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 <> 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 <> 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 = "machine"" />
|
||||
<Publish Property="ALLUSERS" Value="{}" Order="2" Condition="CRAFT_SCOPE = "user"" />
|
||||
<Publish Property="MSIINSTALLPERUSER" Value="{}" Order="3" Condition="CRAFT_SCOPE = "machine"" />
|
||||
<Publish Property="MSIINSTALLPERUSER" Value="1" Order="4" Condition="CRAFT_SCOPE = "user"" />
|
||||
<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>
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
Reference in New Issue
Block a user