feat(installer): default to a per-user ArtCraft folder

This commit is contained in:
Jeremy
2026-10-27 12:01:33 +00:00
parent 9956352be0
commit c19d201748
9 changed files with 737 additions and 194 deletions
+19
View File
@@ -29,3 +29,22 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: bash packaging/linux/verified-download-test.sh
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')
+35
View File
@@ -0,0 +1,35 @@
# Windows installation policy checks
New installations default to the current account under `Programs\ArtCraft\<App>`.
The first setup page offers installation for everyone. Scope is selected before duplicate
checks and directory costing. An update uses the existing context's remembered exact folder;
`INSTALLFOLDER` supplied explicitly is never replaced. It is a secure MSI property so the
chosen folder reaches the elevated installer. Silent machine updates pass `ALLUSERS=1`.
A cross-context copy is refused; changing context means uninstalling the old copy first.
App registration follows the selected context. Existing optional shortcuts and file-handler
choices stay app-owned. No new cleanup operation or model/settings migration is introduced.
Packaged resources remain package-owned; separately downloaded shared models are not caches
and are outside this installer's ownership.
## Repeatable metadata tests
On Windows with WiX 5.0.2 and its matching UI/Util extensions:
```powershell
./packaging/windows/test-install-policy.ps1 -OutputDirectory ./target/install-policy-tests
```
The script builds x64, x86 and arm64 packages with deliberately non-executable payloads. It
uses the actual package source and compiled database. A safe Windows Installer session checks
24 scope/registration/maintenance cases, remembered and explicit custom folders, sequencing,
upgrade identity and the original registration inventory recorded in `install-policy.json`.
The release packaging script runs the compiled checker before signing. Packaging CI runs the
small fixture builds without compiling the Rust application.
Never install or distribute the `NOT-INSTALLABLE.msi` fixtures as an app. Passing these checks
does not prove real installation, elevation, legacy-version upgrades, registry writes, optional
shortcut/menu behaviour, cancellation, rollback or uninstall. Those still require the real
payload in an authorised clean Windows test environment. Cross-architecture metadata is not
execution evidence on those architectures. Existing open installer/association PRs must be
reconciled before publication. Nothing here changes macOS/Linux native packaging conventions.
+3
View File
@@ -0,0 +1,3 @@
# Windows packaging
See [installation policy checks](INSTALLATION.md) for the installer scope and path gates.
+195
View File
@@ -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) }
}
}
+305
View File
@@ -0,0 +1,305 @@
{
"id": "photocraft",
"name": "PhotoCraft",
"upgradeCode": "D9660F32-2D0A-4ABE-B73D-17BE25F1542C",
"sources": [
"photocraft.wxs"
],
"icon": "assets/app-icon/photocraft.ico",
"localization": [
"photocraft.en-us.wxl"
],
"extensions": [
"UI",
"Util"
],
"payloads": [
"photocraft-cli.exe",
"photocraft.exe"
],
"registry": [
[
"-1",
"Software\\Classes\\Photocraft.PhotoshopDocument\\shell\\open\\command",
"",
"\"[#PhotocraftExe]\" \"%1\""
],
[
"-1",
"Software\\Classes\\Photocraft.Image\\shell\\open\\command",
"",
"\"[#PhotocraftExe]\" \"%1\""
],
[
"-1",
"Software\\Classes\\.psd\\OpenWithProgids",
"Photocraft.PhotoshopDocument",
""
],
[
"-1",
"Software\\Classes\\.psb\\OpenWithProgids",
"Photocraft.PhotoshopDocument",
""
],
[
"-1",
"Software\\Classes\\.pdn\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.ora\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.kra\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.krz\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.png\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.jpg\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.jpeg\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.tif\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.tiff\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.webp\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.gif\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.bmp\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.tga\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.exr\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.hdr\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.avif\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.qoi\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\.ico\\OpenWithProgids",
"Photocraft.Image",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe",
"FriendlyAppName",
"PhotoCraft"
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\shell\\open\\command",
"",
"\"[#PhotocraftExe]\" \"%1\""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".pcraft",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".psd",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".psb",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".pdn",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".ora",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".kra",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".krz",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".png",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".jpg",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".jpeg",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".tif",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".tiff",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".webp",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".gif",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".bmp",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".tga",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".exr",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".hdr",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".avif",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".qoi",
""
],
[
"-1",
"Software\\Classes\\Applications\\photocraft.exe\\SupportedTypes",
".ico",
""
],
[
"-1",
"Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\photocraft.exe",
"",
"[#PhotocraftExe]"
],
[
"-1",
"Software\\PhotoCraft",
"InstallDir",
"[INSTALLFOLDER]"
]
],
"mode": "dualScope"
}
+54
View File
@@ -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\PhotoCraft" 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\PhotoCraft" 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\photocraft.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\photocraft.exe" Type="file" /></Property>
<Property Id="CRAFT_U_EXE" Secure="yes"><RegistrySearch Id="CRAFT_ULegacyExe" Root="HKCU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe" Type="file"><FileSearch Id="CRAFT_ULegacyFile" Name="photocraft.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\PhotoCraft" 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\PhotoCraft" 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\photocraft.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\photocraft.exe" Type="file" /></Property>
<Property Id="CRAFT_M_EXE" Secure="yes"><RegistrySearch Id="CRAFT_MLegacyExe" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe" Type="file"><FileSearch Id="CRAFT_MLegacyFile" Name="photocraft.exe" /></RegistrySearch></Property>
<SetProperty Id="CRAFT_MACHINE_DIR" Action="CraftMFromLegacyExe" Value="[CRAFT_M_EXEFOLDER]" After="CraftMFromPATH" Sequence="both" Condition="NOT CRAFT_MACHINE_DIR AND CRAFT_M_EXEFOLDER AND CRAFT_M_EXE" />
<Property Id="CRAFT_MACHINE_DIR" Secure="yes" />
<SetProperty Id="CRAFT_SCOPE" Action="CraftPreferMachineScope" Value="machine" After="CraftMFromLegacyExe" Sequence="ui" Condition="ALLUSERS = 1 OR (CRAFT_MACHINE_DIR AND NOT CRAFT_USER_DIR)" />
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreUserFolder" Value="[CRAFT_USER_DIR]" Before="CostInitialize" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS &lt;&gt; 1 AND CRAFT_USER_DIR" />
<SetProperty Id="INSTALLFOLDER" Action="CraftRestoreMachineFolder" Value="[CRAFT_MACHINE_DIR]" Before="CraftRestoreUserFolder" Sequence="both" Condition="NOT INSTALLFOLDER AND ALLUSERS = 1 AND CRAFT_MACHINE_DIR" />
<Launch Condition="Installed OR ALLUSERS &lt;&gt; 1 OR NOT CRAFT_USER_DIR" Message="This app is already installed for your account. Choose Only for me to update it, or uninstall that copy before changing who it is installed for." />
<Launch Condition="Installed OR ALLUSERS = 1 OR NOT CRAFT_MACHINE_DIR" Message="This app is already installed for everyone. Choose Everyone to update it, or pass ALLUSERS=1 to msiexec. Uninstall that copy before changing who it is installed for." />
<UI>
<TextStyle Id="CraftScopeText" FaceName="Segoe UI" Size="9" />
<TextStyle Id="CraftScopeTitle" FaceName="Segoe UI" Size="12" Bold="yes" />
<Dialog Id="CraftScopeDlg" Width="370" Height="220" Title="[ProductName] Setup" NoMinimize="yes">
<Control Id="Title" Type="Text" X="20" Y="15" Width="330" Height="30" Text="{\CraftScopeTitle}Who should use [ProductName]?" />
<Control Id="Info" Type="Text" X="20" Y="50" Width="330" Height="30" Text="{\CraftScopeText}An update keeps the existing folder. New copies use the ArtCraft folder under Programs." />
<Control Id="Scope" Type="RadioButtonGroup" X="20" Y="88" Width="330" Height="60" Property="CRAFT_SCOPE">
<RadioButtonGroup Property="CRAFT_SCOPE">
<RadioButton Value="user" X="0" Y="0" Width="330" Height="20" Text="Only for me (no administrator permission needed)" />
<RadioButton Value="machine" X="0" Y="30" Width="330" Height="20" Text="Everyone on this computer (asks an administrator)" />
</RadioButtonGroup>
</Control>
<Control Id="Next" Type="PushButton" X="214" Y="182" Width="66" Height="20" Default="yes" Text="Next">
<Publish Property="ALLUSERS" Value="1" Order="1" Condition="CRAFT_SCOPE = &quot;machine&quot;" />
<Publish Property="ALLUSERS" Value="2" Order="2" Condition="CRAFT_SCOPE = &quot;user&quot;" />
<Publish Property="MSIINSTALLPERUSER" Value="{}" Order="3" Condition="CRAFT_SCOPE = &quot;machine&quot;" />
<Publish Property="MSIINSTALLPERUSER" Value="1" Order="4" Condition="CRAFT_SCOPE = &quot;user&quot;" />
<Publish Event="EndDialog" Value="Return" Order="5" />
</Control>
<Control Id="Cancel" Type="PushButton" X="290" Y="182" Width="60" Height="20" Cancel="yes" Text="Cancel"><Publish Event="EndDialog" Value="Exit" /></Control>
</Dialog>
</UI>
<InstallUISequence><Show Dialog="CraftScopeDlg" Before="LaunchConditions" Condition="NOT Installed AND NOT REMOVE AND UILevel = 5" /></InstallUISequence>
</Include>
+3
View File
@@ -158,6 +158,9 @@ Invoke-Native 'wix build' {
Invoke-Native 'MSI shortcut icon validation (ICE50)' {
wix msi validate $Msi -ice ICE50 -intermediateFolder (Join-Path $Stage 'msi-validation')
}
# Inspect the compiled tables and evaluate the duplicate guards with Windows Installer.
# ICE validation alone cannot catch a well-formed condition with its meaning reversed.
& (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
+69 -194
View File
@@ -1,151 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
PhotoCraft 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 photocraft.wxs -arch x64 -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext
-culture en-US -loc photocraft.en-us.wxl -d Version=1.2.3 -d DisplayVersion=1.2.3-rc.1
-d BinDir=... -d IconPath=... -d BannerBmp=... -d DialogBmp=... -o out.msi
Dual-scope package (Microsoft "Single Package Authoring": ALLUSERS=2 with MSIINSTALLPERUSER=1
in the Property table, which Package/Scope="perUserOrMachine" writes). It installs per-user by
default, with no UAC prompt, into %LOCALAPPDATA%\Programs\ArtCraft\PhotoCraft; the setup
wizard's "Install for all users" box, or `msiexec ... ALLUSERS=1`, installs per-machine into
Program Files\ArtCraft\PhotoCraft. In a per-user install Windows Installer itself redirects
the ProgramFiles folder to the user's own Programs folder and the HKMU/HKCR registry roots to
HKCU, so one package serves both contexts.
An install updates the copy it finds in its own context: the remembered folder is read from
HKLM in a per-machine install and from HKCU in a per-user one (RegistrySearch Root="HKMU"),
so an ordinary upgrade stays where the previous version was put. Installing in the other
context while a copy already exists is a visible stop (Launch), not a silent second copy:
the wizard pre-ticks "Install for all users" when a per-machine copy is registered, and a
silent install that would duplicate one fails with a message naming the fix. A scope change
is a deliberate uninstall-and-reinstall, never an ordinary upgrade (MSI major upgrades cannot
cross installation contexts).
The setup wizard names the version: Welcome, install folder with the all-users box
(remembered for upgrades), Ready, a progress page, and a Finish page with a ticked "Launch
PhotoCraft" box. Installs a Start Menu shortcut, makes PhotoCraft the owner of `.pcraft`, and
offers it under "Open with" for PSD/PSB and the image formats it reads (without taking over
the user's default app for them), plus photocraft-cli. `msiexec /qn` still installs silently.
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?ifndef HasCraftFontLicences ?>
<?define HasCraftFontLicences=0 ?>
<?endif ?>
<Package
Name="PhotoCraft"
Manufacturer="Learning Machines LLC"
Version="$(var.Version)"
UpgradeCode="D9660F32-2D0A-4ABE-B73D-17BE25F1542C"
Scope="perUserOrMachine"
Compressed="yes"
Language="1033">
<?endif?>
<Package Name="PhotoCraft" Manufacturer="Learning Machines LLC" Version="$(var.Version)" UpgradeCode="D9660F32-2D0A-4ABE-B73D-17BE25F1542C" Scope="perUserOrMachine" Compressed="yes" Language="1033">
<SummaryInformation Description="PhotoCraft image editor" 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 PhotoCraft is already installed." />
<MajorUpgrade AllowSameVersionUpgrades="yes" DowngradeErrorMessage="A newer version of PhotoCraft is already installed." />
<MediaTemplate EmbedCab="yes" CompressionLevel="high" />
<!-- MSI caches this icon using its Id as the filename. Shortcuts to an .exe need an .exe
icon identifier (ICE50), even when the source is an ICO. -->
<!-- MSI caches this icon using its Id as the filename. Advertised shortcuts to an
.exe need an .exe icon identifier (ICE50), even when the source is an ICO. -->
<Icon Id="PhotocraftIcon.exe" SourceFile="$(var.IconPath)" />
<Property Id="ARPPRODUCTICON" Value="PhotocraftIcon.exe" />
<Property Id="ARPURLINFOABOUT" Value="https://getartcraft.com/apps/photocraft" />
<Property Id="ARPURLUPDATEINFO" Value="https://getartcraft.com/apps/photocraft" />
<Property Id="ARPHELPLINK" Value="https://discord.gg/artcraft" />
<!-- The suite's own folder under the machine's (per-user: the user's) Programs folder:
Program Files\ArtCraft\PhotoCraft, or %LOCALAPPDATA%\Programs\ArtCraft\PhotoCraft. -->
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="ArtCraftSuiteFolder" Name="ArtCraft">
<Directory Id="CraftSuiteFolder" Name="ArtCraft">
<Directory Id="INSTALLFOLDER" Name="PhotoCraft" />
</Directory>
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder" />
<!-- An upgrade installs where the user put the previous version, remembered per context
(HKLM for a per-machine install, HKCU for a per-user one; written by PhotocraftApp).
RegistrySearch has no dual root, so both records are read and the remembered folder is
the first that exists, preferring the user's own copy. The launcher passes INSTALLFOLDER
explicitly for updates, so this mainly serves direct installs. -->
<Property Id="PERMACHINEINSTALLDIR">
<RegistrySearch Id="PerMachineInstallDir" Root="HKLM" Key="Software\PhotoCraft" Name="InstallDir" Type="raw" />
</Property>
<Property Id="PERUSERINSTALLDIR">
<RegistrySearch Id="PerUserInstallDir" Root="HKCU" Key="Software\PhotoCraft" Name="InstallDir" Type="raw" />
</Property>
<SetProperty Id="INSTALLFOLDER" Action="RememberPerUserFolder" Value="[PERUSERINSTALLDIR]" After="AppSearch" Sequence="both"
Condition="NOT INSTALLFOLDER AND PERUSERINSTALLDIR" />
<SetProperty Id="INSTALLFOLDER" Action="RememberPerMachineFolder" Value="[PERMACHINEINSTALLDIR]" After="RememberPerUserFolder" Sequence="both"
Condition="NOT INSTALLFOLDER AND PERMACHINEINSTALLDIR" />
<!-- A registered per-machine copy makes the wizard default to updating it in place. -->
<SetProperty Id="ALLUSERSINSTALL" Value="1" After="AppSearch" Sequence="first"
Condition="NOT ALLUSERSINSTALL AND PERMACHINEINSTALLDIR" />
<!-- A duplicate in the other context is a visible stop, never a silent second copy. -->
<Launch Condition="ALLUSERS = 1 OR NOT PERUSERINSTALLDIR"
Message="PhotoCraft is already installed for your user account only. Leave &quot;Install for all users&quot; unticked to update it in place, or uninstall the other copy first." />
<Launch Condition="ALLUSERS &lt;&gt; 1 OR NOT PERMACHINEINSTALLDIR"
Message="PhotoCraft is already installed for all users of this computer. Tick &quot;Install for all users&quot; to update it in place (or pass ALLUSERS=1 on the msiexec command line), or uninstall the other copy first." />
<!-- An upgrade installs where the user put the previous version (written by PhotocraftApp). -->
<Feature Id="Main" Title="PhotoCraft" Level="1">
<?if $(var.HasCraftFontLicences) = 1 ?>
<ComponentGroupRef Id="CraftFontLicences" />
<?endif ?>
<?endif?>
<ComponentGroupRef Id="PhotocraftFiles" />
</Feature>
<!-- The setup wizard: WixUI_InstallDir without its licence page (the code is MIT OR
Apache-2.0; there is nothing to accept), with our folder page (PhotocraftInstallDirDlg:
the stock one plus the install-scope box) in place of the stock one. A later NewDialog
on the same button wins. -->
Apache-2.0; there is nothing to accept). A later NewDialog on the same button wins. -->
<ui:WixUI Id="WixUI_InstallDir" InstallDirectory="INSTALLFOLDER" />
<UI>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="PhotocraftInstallDirDlg" Order="2" Condition="NOT Installed" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="PhotocraftInstallDirDlg" Order="2" Condition="NOT Installed" />
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchPhotocraft"
Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
<Dialog Id="PhotocraftInstallDirDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)">
<!-- As WixUI_InstallDir does for its own folder page: take the folder, check it, go on. -->
<Publish Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Event="DoAction" Value="WixUIValidatePath_$(sys.BUILDARCHSHORT)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<!-- The scope box: ALLUSERS=1 asks for the per-machine context (WixUI_Advanced's
idiom); otherwise the package's own per-user default applies. An ALLUSERS given
on the msiexec command line is left alone. -->
<Publish Property="ALLUSERS" Value="1" Order="4" Condition="ALLUSERSINSTALL = &quot;1&quot;" />
<Publish Property="ALLUSERS" Value="2" Order="5" Condition="NOT ALLUSERSINSTALL = &quot;1&quot; AND NOT ALLUSERS = &quot;1&quot;" />
<Publish Property="MSIINSTALLPERUSER" Value="1" Order="6" Condition="NOT ALLUSERSINSTALL = &quot;1&quot; AND NOT ALLUSERS = &quot;1&quot;" />
<Publish Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
</Control>
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)">
<Publish Event="NewDialog" Value="WelcomeDlg" />
</Control>
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
<Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgDescription)" />
<Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgTitle)" />
<Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
<Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="FolderLabel" Type="Text" X="20" Y="60" Width="290" Height="30" NoPrefix="yes" Text="!(loc.InstallDirDlgFolderLabel)" />
<Control Id="Folder" Type="PathEdit" X="20" Y="100" Width="320" Height="18" Property="WIXUI_INSTALLDIR" Indirect="yes" />
<Control Id="ChangeFolder" Type="PushButton" X="20" Y="120" Width="56" Height="17" Text="!(loc.InstallDirDlgChange)">
<Publish Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Event="SpawnDialog" Value="BrowseDlg" Order="2" />
</Control>
<Control Id="PerUserNote" Type="Text" X="20" Y="146" Width="330" Height="24" NoPrefix="yes" Text="!(loc.PhotocraftInstallDirNote)" />
<Control Id="AllUsers" Type="CheckBox" X="20" Y="176" Width="330" Height="17" Property="ALLUSERSINSTALL" CheckBoxValue="1" Text="!(loc.PhotocraftAllUsers)" />
</Dialog>
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2" Condition="NOT Installed" />
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2" />
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchPhotocraft" Condition="WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 AND NOT Installed" />
</UI>
<!-- Our own art, generated by package.ps1 from the app icon (not WiX's stock bitmaps). -->
<WixVariable Id="WixUIBannerBmp" Value="$(var.BannerBmp)" />
@@ -169,19 +65,14 @@
<ComponentGroup Id="CraftFontLicences" Directory="INSTALLFOLDER">
<Files Include="$(var.DocsDir)\**" />
</ComponentGroup>
<?endif ?>
<?endif?>
<ComponentGroup Id="PhotocraftFiles" Directory="INSTALLFOLDER">
<!-- Keyed on an HKMU registry value, not the file: in a per-user install a component
under the user's profile cannot use a file as its KeyPath (ICE38), and HKMU writes
HKCU there while remaining HKLM for a per-machine install. -->
<Component Id="PhotocraftApp" Guid="A6DE41B7-A80F-4C59-A17E-BB68065BF67E">
<File Id="PhotocraftExe" Source="$(var.BinDir)\photocraft.exe" KeyPath="no" />
<!-- Read back by the next version's installer in the same context, and by the ArtCraft
Launcher (detect.rs) in both. -->
<RegistryValue Root="HKMU" Key="Software\PhotoCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" KeyPath="yes" />
<File Id="PhotocraftExe" Source="$(var.BinDir)\photocraft.exe" KeyPath="yes">
<Shortcut Id="StartMenuShortcut" Directory="ProgramMenuFolder" Name="PhotoCraft" Description="PhotoCraft image editor" WorkingDirectory="INSTALLFOLDER" Advertise="yes" Icon="PhotocraftIcon.exe" IconIndex="0" />
</File>
<!-- .pcraft: PhotoCraft's own format; PhotoCraft is its default app. ProgIds register
under HKCR, which a per-user install redirects to HKCU\Software\Classes. -->
<!-- .pcraft: PhotoCraft's own format; PhotoCraft is its default app. -->
<ProgId Id="Photocraft.Document" Description="PhotoCraft Document" Icon="PhotocraftExe" IconIndex="0">
<Extension Id="pcraft" ContentType="application/x-photocraft">
<Verb Id="open" Command="Open" TargetFile="PhotocraftExe" Argument="&quot;%1&quot;" />
@@ -189,77 +80,61 @@
</ProgId>
<!-- PSD/PSB and images: offered under "Open with", never forced as the default. -->
<ProgId Id="Photocraft.PhotoshopDocument" Description="PSD Document" Icon="PhotocraftExe" IconIndex="0" />
<RegistryValue Root="HKCR" Key="Software\Classes\Photocraft.PhotoshopDocument\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Photocraft.PhotoshopDocument\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<ProgId Id="Photocraft.Image" Description="Image" Icon="PhotocraftExe" IconIndex="0" />
<RegistryValue Root="HKCR" Key="Software\Classes\Photocraft.Image\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.psd\OpenWithProgids" Name="Photocraft.PhotoshopDocument" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.psb\OpenWithProgids" Name="Photocraft.PhotoshopDocument" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.pdn\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.ora\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.kra\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.krz\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.png\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.jpg\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.jpeg\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.tif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.tiff\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.webp\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.gif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.bmp\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.tga\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.exr\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.hdr\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.avif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.qoi\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\.ico\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Photocraft.Image\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.psd\OpenWithProgids" Name="Photocraft.PhotoshopDocument" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.psb\OpenWithProgids" Name="Photocraft.PhotoshopDocument" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.pdn\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.ora\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.kra\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.krz\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.png\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.jpg\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.jpeg\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.tif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.tiff\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.webp\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.gif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.bmp\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.tga\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.exr\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.hdr\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.avif\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.qoi\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\.ico\OpenWithProgids" Name="Photocraft.Image" Value="" Type="string" />
<!-- "Open with" app list (HKCR: per-user installs register their own classes) and the
Run dialog (Win+R "photocraft", HKMU: HKCU in a per-user install). -->
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe" Name="FriendlyAppName" Value="PhotoCraft" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".pcraft" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".psd" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".psb" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".pdn" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".ora" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".kra" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".krz" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".png" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".jpg" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".jpeg" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tif" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tiff" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".webp" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".gif" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".bmp" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tga" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".exr" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".hdr" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".avif" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".qoi" Value="" Type="string" />
<RegistryValue Root="HKCR" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".ico" Value="" Type="string" />
<!-- "Open with" app list and the Run dialog (Win+R "photocraft"). -->
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe" Name="FriendlyAppName" Value="PhotoCraft" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\shell\open\command" Value="&quot;[#PhotocraftExe]&quot; &quot;%1&quot;" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".pcraft" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".psd" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".psb" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".pdn" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".ora" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".kra" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".krz" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".png" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".jpg" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".jpeg" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tif" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tiff" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".webp" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".gif" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".bmp" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".tga" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".exr" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".hdr" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".avif" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".qoi" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Classes\Applications\photocraft.exe\SupportedTypes" Name=".ico" Value="" Type="string" />
<RegistryValue Root="HKMU" Key="Software\Microsoft\Windows\CurrentVersion\App Paths\photocraft.exe" Value="[#PhotocraftExe]" Type="string" />
<!-- The folders we create go when we go (ICE64): both in a per-user and a per-machine
install the suite folder belongs to us alone. -->
<RemoveFolder Id="RemoveInstallFolder" Directory="INSTALLFOLDER" On="uninstall" />
<RemoveFolder Id="RemoveSuiteFolder" Directory="ArtCraftSuiteFolder" On="uninstall" />
<RegistryValue Root="HKMU" Key="Software\PhotoCraft" Name="InstallDir" Value="[INSTALLFOLDER]" Type="string" />
</Component>
<!-- The Start Menu shortcut is its own component keyed on a registry value (as the
ArtCraft Launcher's MSI does): an advertised shortcut must target its component's
keypath, which is a registry value in a per-user-capable component, and ICE38 wants
per-user components keyed in the registry anyway. -->
<Component Id="PhotocraftShortcut" Directory="ProgramMenuFolder" Guid="2F3E6B67-62B0-4F2E-BA63-F1EBB337A067">
<Shortcut Id="StartMenuShortcut" Name="PhotoCraft"
Description="PhotoCraft image editor" Target="[#PhotocraftExe]"
WorkingDirectory="INSTALLFOLDER" Icon="PhotocraftIcon.exe" IconIndex="0" />
<RegistryValue Root="HKMU" Key="Software\PhotoCraft" Name="StartMenuShortcut" Value="1" Type="integer" KeyPath="yes" />
</Component>
<Component Id="PhotocraftCli" Guid="EE4D91D4-8C52-43E4-BA06-939EEBB2CCC2">
<File Id="PhotocraftCliExe" Source="$(var.BinDir)\photocraft-cli.exe" KeyPath="no" />
<RegistryValue Root="HKMU" Key="Software\PhotoCraft" Name="CliInstalled" Value="1" Type="integer" KeyPath="yes" />
<Component Id="PhotocraftCli">
<File Id="PhotocraftCliExe" Source="$(var.BinDir)\photocraft-cli.exe" KeyPath="yes" />
</Component>
</ComponentGroup>
</Fragment>
</Wix>
</Wix>
+54
View File
@@ -0,0 +1,54 @@
<#
.SYNOPSIS
Build non-executable installer metadata fixtures; never install them.
#>
param(
[Parameter(Mandatory = $true)] [string] $OutputDirectory,
[ValidateSet('x64','x86','arm64')] [string[]] $Architectures = @('x64','x86','arm64')
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
$policy = Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'install-policy.json') | ConvertFrom-Json
[void](New-Item -ItemType Directory -Force -Path $OutputDirectory)
$output = (Resolve-Path -LiteralPath $OutputDirectory).Path
Add-Type -AssemblyName System.Drawing
foreach ($arch in $Architectures) {
$stage = Join-Path $output $arch
[void](New-Item -ItemType Directory -Force -Path $stage)
foreach ($name in $policy.payloads) {
$destination = Join-Path $stage $name
[void](New-Item -ItemType Directory -Force -Path ([IO.Path]::GetDirectoryName($destination)))
Set-Content -LiteralPath $destination -Encoding Ascii -Value 'NON-EXECUTABLE MSI METADATA TEST FIXTURE'
}
foreach ($folder in @('docs','models','notices')) {
$path = Join-Path $stage $folder
[void](New-Item -ItemType Directory -Force -Path $path)
Set-Content -LiteralPath (Join-Path $path "$folder-fixture.txt") -Value 'Metadata test fixture, not a user file or model.'
}
foreach ($model in @('metadata-one.rten','metadata-two.rten')) {
Set-Content -LiteralPath (Join-Path (Join-Path $stage 'models') $model) -Encoding Ascii -Value 'NON-EXECUTABLE MODEL METADATA FIXTURE'
}
foreach ($spec in @(@('banner.bmp',493,58),@('dialog.bmp',493,312))) {
$bitmap = [Drawing.Bitmap]::new($spec[1],$spec[2],[Drawing.Imaging.PixelFormat]::Format24bppRgb)
try { $bitmap.Save((Join-Path $stage $spec[0]),[Drawing.Imaging.ImageFormat]::Bmp) }
finally { $bitmap.Dispose() }
}
Set-Content -LiteralPath (Join-Path $stage 'licence.rtf') -Encoding Ascii -Value '{\rtf1 Metadata test fixture.}'
$sources = @($policy.sources | ForEach-Object { Join-Path $PSScriptRoot $_ })
$msi = Join-Path $stage "$($policy.id)-NOT-INSTALLABLE.msi"
$extensions = @($policy.extensions | ForEach-Object { '-ext'; "WixToolset.$_.wixext" })
& wix build @sources -arch $arch -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."
}