mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-27 19:47:35 -06:00
feat(installer): default to a per-user ArtCraft folder
This commit is contained in:
@@ -37,3 +37,22 @@ jobs:
|
||||
run: |
|
||||
./packaging/windows/test-install-folder.ps1 -Arch x64
|
||||
./packaging/windows/test-install-folder.ps1 -Arch x86
|
||||
|
||||
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
|
||||
- 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
|
||||
& (Join-Path $tools 'wix.exe') extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2
|
||||
if ($LASTEXITCODE -ne 0) { throw 'WiX 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')
|
||||
|
||||
@@ -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,195 @@
|
||||
<#
|
||||
.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))
|
||||
}
|
||||
$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' }
|
||||
if (-not $explicit) {
|
||||
if ($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['CraftScopeDlg'] -and $ui['CraftScopeDlg'] -lt $ui['LaunchConditions'] -and
|
||||
$ui['CraftScopeDlg'] -lt $ui['CraftRestoreUserFolder'] -and $ui['CraftRestoreUserFolder'] -lt $ui['CostInitialize'])) {
|
||||
throw 'Scope must be chosen after discovery and before duplicate checks, folder restoration and costing.'
|
||||
}
|
||||
$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.'
|
||||
}
|
||||
}
|
||||
# Switching the first-page choice must reset BOTH properties, including a back-and-forth change.
|
||||
$events = @(Read-Rows $database 'SELECT `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_` = ''CraftScopeDlg'' AND `Control_` = ''Next'' ORDER BY `Ordering`' 4)
|
||||
foreach ($choice in @('user','machine','user')) {
|
||||
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 -ne '2' -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.' }
|
||||
}
|
||||
}
|
||||
$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) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "lightcraft",
|
||||
"name": "LightCraft",
|
||||
"upgradeCode": "229BB552-5EFD-479A-BA86-8CB6D926390C",
|
||||
"sources": [
|
||||
"lightcraft.wxs"
|
||||
],
|
||||
"icon": "assets/app-icon/lightcraft.ico",
|
||||
"localization": [],
|
||||
"extensions": [
|
||||
"UI",
|
||||
"Util"
|
||||
],
|
||||
"payloads": [
|
||||
"FONT-LICENCES.txt",
|
||||
"NOTICE",
|
||||
"lightcraft-cli.exe",
|
||||
"lightcraft.exe"
|
||||
],
|
||||
"registry": [
|
||||
[
|
||||
"-1",
|
||||
"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\lightcraft.exe",
|
||||
"",
|
||||
"[#LightcraftExe]"
|
||||
],
|
||||
[
|
||||
"-1",
|
||||
"Software\\LightCraft",
|
||||
"InstallFolder",
|
||||
"[INSTALLFOLDER]"
|
||||
]
|
||||
],
|
||||
"mode": "dualScope"
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?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. -->
|
||||
<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\LightCraft" 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\LightCraft" 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\lightcraft.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\lightcraft.exe" Type="file" /></Property>
|
||||
<Property Id="CRAFT_U_EXE" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyExe" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file"><FileSearch Id="CRAFT_ULegacyFile" Name="lightcraft.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\LightCraft" 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\LightCraft" 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\lightcraft.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\lightcraft.exe" Type="file" /></Property>
|
||||
<Property Id="CRAFT_M_EXE" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyExe" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file"><FileSearch Id="CRAFT_MLegacyFile" Name="lightcraft.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" />
|
||||
<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="2" 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" Before="LaunchConditions" Condition="NOT Installed AND NOT REMOVE AND UILevel = 5" /></InstallUISequence>
|
||||
</Include>
|
||||
@@ -1,30 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
LightCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`, plus the UI and Util extensions:
|
||||
`wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2`). Built by package.ps1:
|
||||
|
||||
wix build lightcraft.wxs -arch x64 -ext WixToolset.UI.wixext/5.0.2 -ext WixToolset.Util.wixext/5.0.2
|
||||
-d Version=1.2.3 -d BinDir=... -d IconPath=... -d UiDialogBmp=... -d UiBannerBmp=... -o out.msi
|
||||
|
||||
Per-machine install into Program Files (or the folder chosen in the installer, or
|
||||
`INSTALLFOLDER=...` on the msiexec command line; upgrades keep it), a Start Menu shortcut, `.pcraft` owned by LightCraft,
|
||||
and LightCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
|
||||
taking over the user's default app for them).
|
||||
-->
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package
|
||||
Name="LightCraft"
|
||||
Manufacturer="Learning Machines LLC"
|
||||
Version="$(var.Version)"
|
||||
UpgradeCode="229BB552-5EFD-479A-BA86-8CB6D926390C"
|
||||
Scope="perMachine"
|
||||
Compressed="yes"
|
||||
Language="1033">
|
||||
<Package Name="LightCraft" Manufacturer="Learning Machines LLC" Version="$(var.Version)" UpgradeCode="229BB552-5EFD-479A-BA86-8CB6D926390C" Scope="perUserOrMachine" Compressed="yes" Language="1033">
|
||||
<SummaryInformation Description="LightCraft photo library and raw developer" 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 LightCraft is already installed." />
|
||||
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of LightCraft is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
|
||||
|
||||
<Icon Id="LightcraftIcon.ico" SourceFile="$(var.IconPath)" />
|
||||
@@ -34,7 +15,9 @@
|
||||
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
|
||||
|
||||
<StandardDirectory Id="ProgramFiles6432Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="LightCraft" />
|
||||
<Directory Id="CraftSuiteFolder" Name="ArtCraft">
|
||||
<Directory Id="INSTALLFOLDER" Name="LightCraft" />
|
||||
</Directory>
|
||||
</StandardDirectory>
|
||||
<!-- The folder of the installed version (written by LightcraftApp), so an upgrade installs
|
||||
where the user chose instead of moving back to Program Files (issue #399). Searched into
|
||||
@@ -42,28 +25,21 @@
|
||||
<Property Id="PREVIOUSINSTALLFOLDER">
|
||||
<RegistrySearch Id="PreviousInstallFolder" Root="HKLM" Key="Software\LightCraft" Name="InstallFolder" Type="raw" />
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLFOLDER" Value="[PREVIOUSINSTALLFOLDER]" After="AppSearch" Sequence="both"
|
||||
Condition="PREVIOUSINSTALLFOLDER AND NOT INSTALLFOLDER" />
|
||||
<!-- Older MSIs (including 0.5.0) only recorded the executable in App Paths. Type="file"
|
||||
without a FileSearch returns its containing directory. Keep the searches separate so
|
||||
the newer InstallFolder value and an explicit INSTALLFOLDER always take precedence. -->
|
||||
<Property Id="LEGACYINSTALLFOLDER">
|
||||
<RegistrySearch Id="LegacyInstallFolder" Root="HKLM"
|
||||
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file" />
|
||||
<RegistrySearch Id="LegacyInstallFolder" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file" />
|
||||
</Property>
|
||||
<!-- That directory search doesn't check the executable exists, and when its unquoted path is
|
||||
gone it falls back to the text before the first space: a deleted
|
||||
"C:\Program Files\LightCraft\lightcraft.exe" yields "C:\". Only use the folder when this
|
||||
search, which does require lightcraft.exe, finds it too. -->
|
||||
<Property Id="LEGACYINSTALLEXE">
|
||||
<RegistrySearch Id="LegacyInstallExe" Root="HKLM"
|
||||
Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file">
|
||||
<RegistrySearch Id="LegacyInstallExe" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Type="file">
|
||||
<FileSearch Id="LegacyInstallExeFile" Name="lightcraft.exe" />
|
||||
</RegistrySearch>
|
||||
</Property>
|
||||
<SetProperty Id="INSTALLFOLDER" Action="SetInstallFolderFromAppPaths" Value="[LEGACYINSTALLFOLDER]"
|
||||
After="SetINSTALLFOLDER" Sequence="both"
|
||||
Condition="LEGACYINSTALLFOLDER AND LEGACYINSTALLEXE AND NOT PREVIOUSINSTALLFOLDER AND NOT INSTALLFOLDER" />
|
||||
<StandardDirectory Id="ProgramMenuFolder" />
|
||||
|
||||
<Feature Id="Main" Title="LightCraft" Level="1">
|
||||
@@ -96,8 +72,7 @@
|
||||
<WixVariable Id="WixUIDialogBmp" Value="$(var.UiDialogBmp)" />
|
||||
<WixVariable Id="WixUIBannerBmp" Value="$(var.UiBannerBmp)" />
|
||||
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT"
|
||||
Value="LightCraft has been installed. You can start it at any time from the Start menu." />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALTEXT" Value="LightCraft has been installed. You can start it at any time from the Start menu." />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch LightCraft" />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
|
||||
|
||||
@@ -108,8 +83,7 @@
|
||||
event, so silent and passive installs never run it. Return="ignore": a failed launch must
|
||||
not turn a successful install into an error. -->
|
||||
<Property Id="WixUnelevatedShellExecTarget" Value="[#LightcraftExe]" />
|
||||
<CustomAction Id="LaunchLightcraft" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)"
|
||||
DllEntry="WixUnelevatedShellExec" Execute="immediate" Impersonate="yes" Return="ignore" />
|
||||
<CustomAction Id="LaunchLightcraft" BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" DllEntry="WixUnelevatedShellExec" Execute="immediate" Impersonate="yes" Return="ignore" />
|
||||
|
||||
<UI Id="LightcraftUI">
|
||||
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
|
||||
@@ -152,35 +126,26 @@
|
||||
whose OK validates too. -->
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="2"
|
||||
Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3"
|
||||
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4"
|
||||
Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1"" />
|
||||
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
|
||||
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
|
||||
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="3"
|
||||
Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4"
|
||||
Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
|
||||
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"" />
|
||||
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1"
|
||||
Condition="NOT Installed" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="3"
|
||||
Condition="Installed AND PATCH" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1" Condition="NOT Installed" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="3" Condition="Installed AND PATCH" />
|
||||
|
||||
<!-- Maintenance: Welcome -> Repair / Remove -> Ready. -->
|
||||
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg" />
|
||||
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg" />
|
||||
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg" />
|
||||
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2"
|
||||
Condition="Installed AND NOT PATCH" />
|
||||
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2" Condition="Installed AND NOT PATCH" />
|
||||
|
||||
<!-- Completion page: optionally launch, then close. -->
|
||||
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchLightcraft" Order="1"
|
||||
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
|
||||
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchLightcraft" Order="1" Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
|
||||
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
|
||||
|
||||
<!-- One feature, nothing to change: no "Change" in Apps & features. -->
|
||||
@@ -194,13 +159,12 @@
|
||||
<ComponentGroup Id="LightcraftFiles" Directory="INSTALLFOLDER">
|
||||
<Component Id="LightcraftApp" Guid="8A1D3A39-F7C1-47B1-97AE-40C80BF82F4A">
|
||||
<File Id="LightcraftExe" Source="$(var.BinDir)\lightcraft.exe" KeyPath="yes">
|
||||
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="LightCraft"
|
||||
Description="LightCraft photo library and raw developer" WorkingDirectory="INSTALLFOLDER" Advertise="yes"
|
||||
Icon="LightcraftIcon.ico" IconIndex="0" />
|
||||
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="LightCraft" Description="LightCraft photo library and raw developer" WorkingDirectory="INSTALLFOLDER" Advertise="yes" Icon="LightcraftIcon.ico" IconIndex="0" />
|
||||
</File>
|
||||
|
||||
<RegistryValue Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Value="[#LightcraftExe]" Type="string" />
|
||||
<RegistryValue Root="HKLM" Key="Software\LightCraft" Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\lightcraft.exe" Value="[#LightcraftExe]" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\LightCraft" Name="InstallFolder" Value="[INSTALLFOLDER]" Type="string" />
|
||||
<RegistryValue Root="HKMU" Key="Software\LightCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" />
|
||||
</Component>
|
||||
|
||||
<Component Id="LightcraftCli">
|
||||
|
||||
@@ -147,6 +147,8 @@ Invoke-Native 'wix build' {
|
||||
-d "UiDialogBmp=$UiDialogBmp" -d "UiBannerBmp=$UiBannerBmp" `
|
||||
-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
|
||||
|
||||
@@ -41,7 +41,10 @@ try {
|
||||
$ns.AddNamespace('w', 'http://wixtoolset.org/schemas/v4/wxs')
|
||||
# Copy the real search/action authoring, changing only registry roots/keys for isolation.
|
||||
# Do not reimplement folder resolution here: removing the fallback must fail the legacy case.
|
||||
$nodes = $source.SelectNodes('/w:Wix/w:Package/w:Property[w:RegistrySearch] | /w:Wix/w:Package/w:SetProperty[@Id="INSTALLFOLDER"]', $ns)
|
||||
[xml] $scope = Get-Content (Join-Path $PSScriptRoot 'install-policy.wxi') -Raw
|
||||
$scopeNs = [Xml.XmlNamespaceManager]::new($scope.NameTable)
|
||||
$scopeNs.AddNamespace('w', 'http://wixtoolset.org/schemas/v4/wxs')
|
||||
$nodes = $scope.SelectNodes('/w:Include/w:Property | /w:Include/w:SetProperty[@Sequence="both"]', $scopeNs)
|
||||
$authoring = foreach ($node in $nodes) {
|
||||
$copy = $node.CloneNode($true)
|
||||
foreach ($search in $copy.SelectNodes('.//w:RegistrySearch', $ns)) {
|
||||
@@ -50,11 +53,11 @@ try {
|
||||
}
|
||||
$copy.OuterXml
|
||||
}
|
||||
$directory = $source.SelectSingleNode('/w:Wix/w:Package/w:StandardDirectory[w:Directory[@Id="INSTALLFOLDER"]]', $ns).OuterXml
|
||||
$directory = $source.SelectSingleNode('/w:Wix/w:Package/w:StandardDirectory[.//w:Directory[@Id="INSTALLFOLDER"]]', $ns).OuterXml
|
||||
$probe = @"
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package Name="LightCraft folder search test $id" Manufacturer="LightCraft tests"
|
||||
Version="1.0.0" ProductCode="$product" UpgradeCode="$upgrade" Scope="perUser">
|
||||
Version="1.0.0" ProductCode="$product" UpgradeCode="$upgrade" Scope="perUserOrMachine">
|
||||
$($authoring -join "`n")
|
||||
$directory
|
||||
<Feature Id="Main"><ComponentRef Id="Result" /></Feature>
|
||||
@@ -77,7 +80,7 @@ try {
|
||||
New-Item -ItemType Directory -Path $legacy, $modern, $explicit, $emptied | Out-Null
|
||||
$legacyExe = Join-Path $legacy 'lightcraft.exe'
|
||||
[IO.File]::WriteAllText($legacyExe, 'test placeholder; never executed')
|
||||
$programFiles = if ($Arch -eq 'x86' -and ${env:ProgramFiles(x86)}) { ${env:ProgramFiles(x86)} } else { $env:ProgramFiles }
|
||||
$programFiles = Join-Path ([Environment]::GetFolderPath('LocalApplicationData')) 'Programs\ArtCraft'
|
||||
$cases = @(
|
||||
@{ Name = 'fresh'; Expected = (Join-Path $programFiles 'LightCraft') },
|
||||
@{ Name = 'legacy-app-paths'; Legacy = $legacyExe; Expected = $legacy },
|
||||
|
||||
@@ -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 -wx @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