Issue #18: Windows MSI: finish dialog with Launch LightCraft

Interactive MSI installs now walk Welcome -> Ready to install -> progress
-> "Completed the LightCraft Setup Wizard" (Finish) with "LightCraft has
been installed" text and a checked-by-default "Launch LightCraft"
checkbox. Failure and cancellation end on WiX's standard FatalError /
UserExit pages; silent (/qn) and basic-UI (/passive) installs show no
dialogs and launch nothing.

- lightcraft.wxs: custom UI set = WixUI_Minimal without the licence page
  (MIT OR Apache-2.0 asks nothing of installers), maintenance dialogs for
  repair/remove, ARPNOMODIFY. Launch uses the Util extension's
  WixUnelevatedShellExec via Explorer (runs as the signed-in user, never
  elevated), Return="ignore", only when NOT Installed and the box is ticked.
- package.ps1: add WixToolset.UI/Util.wixext 5.0.2 to the global cache,
  pass -ext ... -culture en-us, and generate plain side/banner bitmaps
  (brand blue / white) so the MSI carries no WiX sample artwork.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 1c5feb7ab3)
This commit is contained in:
Brandon Thomas 2026-10-05 01:57:09 -04:00 committed by hxw
parent 65ad290e89
commit 45621f5679
3 changed files with 126 additions and 3 deletions

View File

@ -147,6 +147,7 @@ jobs:
if: inputs.version != ''
shell: bash
run: cargo xtask version set "$LIGHTCRAFT_VERSION"
# package.ps1 adds the matching WixToolset.UI/Util.wixext (5.0.2) to the global extension cache.
- name: Install WiX
shell: pwsh
run: dotnet tool install --global wix --version 5.0.2

View File

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
LightCraft MSI (WiX Toolset v5: `dotnet tool install -g wix`). Built by package.ps1:
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 -d Version=1.2.3 -d BinDir=... -d IconPath=... -o out.msi
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, a Start Menu shortcut, `.pcraft` owned by LightCraft,
and LightCraft offered under "Open with" for PSD/PSB and the image formats it reads (without
@ -38,8 +40,87 @@
<Feature Id="Main" Title="LightCraft" Level="1">
<ComponentGroupRef Id="LightcraftFiles" />
</Feature>
<UIRef Id="LightcraftUI" />
</Package>
<!--
Interactive install UI: Welcome -> Ready to install -> progress -> "Completed" (Finish), with a
"Launch LightCraft" checkbox (checked by default). Failure and cancellation end on WiX's standard
FatalError / UserExit dialogs instead of the completion page. Maintenance (running the MSI again,
Repair/Remove from Apps) uses the standard maintenance dialogs.
This is WixUI_Minimal without its licence-acceptance page: LightCraft is MIT OR Apache-2.0,
which ask nothing of people who just install and run it, so a click-through EULA would only
add a step. Silent (/qn) and basic-UI (/passive, /qb) installs never show these dialogs, so
they are unaffected, and nothing is launched.
The dialogs, strings and error/cancel pages come from WixToolset.UI.wixext; the side and
banner bitmaps are plain panels generated by package.ps1 (no third-party artwork).
-->
<Fragment>
<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_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch LightCraft" />
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1" />
<!-- "Launch LightCraft" on Finish. WixUnelevatedShellExec (Util extension) asks the desktop
shell (Explorer) to start the program, so it runs as the signed-in user at normal
integrity even when the installer UI itself is elevated (e.g. msiexec started from an
admin prompt); it never runs from the elevated/SYSTEM server side. It is a UI-only control
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" />
<UI Id="LightcraftUI">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<DialogRef Id="WelcomeDlg" />
<DialogRef Id="VerifyReadyDlg" />
<DialogRef Id="MaintenanceWelcomeDlg" />
<DialogRef Id="MaintenanceTypeDlg" />
<!-- First install / upgrade (and patches): Welcome -> Ready to install. -->
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="1"
Condition="NOT Installed OR 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" />
<!-- 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="EndDialog" Value="Return" Order="999" />
<!-- One feature, nothing to change: no "Change" in Apps & features. -->
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
<Fragment>
<ComponentGroup Id="LightcraftFiles" Directory="INSTALLFOLDER">
<Component Id="LightcraftApp" Guid="8A1D3A39-F7C1-47B1-97AE-40C80BF82F4A">

View File

@ -13,6 +13,8 @@
Needs: Rust (MSVC toolchain + the target), the Windows SDK (rc.exe, signtool.exe),
and WiX v5: dotnet tool install --global wix --version 5.0.2
The WiX UI and Util extensions (installer dialogs, "Launch LightCraft" on Finish) are added to
the global WiX extension cache by this script (wix extension add -g ..., needs network once).
.EXAMPLE
pwsh packaging/windows/package.ps1 -Arch x64
@ -32,6 +34,29 @@ function Invoke-Native([string] $What, [scriptblock] $Block) {
if ($LASTEXITCODE -ne 0) { throw "$What failed with exit code $LASTEXITCODE" }
}
# A 24-bit BMP of two flat colours: $SplitX pixels of $Left, then $Right (colours as R,G,B).
# Used for the installer's side/banner bitmaps so the MSI ships no third-party artwork.
function New-PanelBitmap([string] $Path, [int] $Width, [int] $Height, [int] $SplitX, [byte[]] $Left, [byte[]] $Right) {
$stride = [int][Math]::Ceiling($Width * 3 / 4) * 4
$row = New-Object byte[] $stride
for ($x = 0; $x -lt $Width; $x++) {
$c = if ($x -lt $SplitX) { $Left } else { $Right }
$row[$x * 3] = $c[2]; $row[$x * 3 + 1] = $c[1]; $row[$x * 3 + 2] = $c[0] # BMP pixels are B,G,R
}
$pixels = $stride * $Height
$ms = [IO.MemoryStream]::new()
$w = [IO.BinaryWriter]::new($ms)
$w.Write([byte]0x42); $w.Write([byte]0x4D) # 'BM'
$w.Write([uint32](54 + $pixels)); $w.Write([uint32]0); $w.Write([uint32]54)
$w.Write([uint32]40); $w.Write([int32]$Width); $w.Write([int32]$Height) # BITMAPINFOHEADER
$w.Write([uint16]1); $w.Write([uint16]24); $w.Write([uint32]0); $w.Write([uint32]$pixels)
$w.Write([int32]2835); $w.Write([int32]2835); $w.Write([uint32]0); $w.Write([uint32]0)
for ($y = 0; $y -lt $Height; $y++) { $w.Write($row) }
$w.Flush()
[IO.File]::WriteAllBytes($Path, $ms.ToArray())
$w.Dispose()
}
# The version lives in one place: [workspace.package] version in the root Cargo.toml.
$Version = $env:LIGHTCRAFT_VERSION
if (-not $Version) {
@ -91,10 +116,26 @@ Copy-Item (Join-Path $Bin 'lightcraft.exe'), (Join-Path $Bin 'lightcraft-cli.exe
& (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'lightcraft.exe') (Join-Path $Stage 'lightcraft-cli.exe')
# ---- MSI ---------------------------------------------------------------------------------------
# Extensions must match the WiX tool version (5.0.2, see release.yml). Re-adding is harmless.
$WixVersion = '5.0.2'
$WixExtensions = 'WixToolset.UI.wixext', 'WixToolset.Util.wixext'
foreach ($ext in $WixExtensions) {
Invoke-Native "wix extension add $ext/$WixVersion" { wix extension add -g "$ext/$WixVersion" }
}
$ExtArgs = foreach ($ext in $WixExtensions) { '-ext'; "$ext/$WixVersion" }
# Installer dialog bitmaps (WixUI sizes): a brand-blue side panel on the welcome/finish pages and
# a plain white banner on the inner pages. Generated here, never committed.
$UiDialogBmp = Join-Path $Stage 'ui-dialog.bmp'
$UiBannerBmp = Join-Path $Stage 'ui-banner.bmp'
New-PanelBitmap $UiDialogBmp 493 312 164 ([byte[]](0x4E, 0x7B, 0xFB)) ([byte[]](0xFF, 0xFF, 0xFF))
New-PanelBitmap $UiBannerBmp 493 58 0 ([byte[]](0xFF, 0xFF, 0xFF)) ([byte[]](0xFF, 0xFF, 0xFF))
$Msi = Join-Path $Dist "lightcraft-$Version-windows-$Arch.msi"
Invoke-Native 'wix build' {
wix build (Join-Path $PSScriptRoot 'lightcraft.wxs') -arch $Arch `
wix build (Join-Path $PSScriptRoot 'lightcraft.wxs') -arch $Arch -culture en-us @ExtArgs `
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\lightcraft.ico')" `
-d "UiDialogBmp=$UiDialogBmp" -d "UiBannerBmp=$UiBannerBmp" `
-o $Msi
}
# wix writes its debug symbols (.wixpdb) next to the MSI; keep them out of the release assets.