Files
lightcraft/packaging/windows/package.ps1
T
LabDennis 1f4aaaec47 Fonts: finish Thai fallback delivery for desktop and web releases
Retain the complete reviewed change, including prior integration resolutions,
on current upstream.

#736: deliver Thai UI fonts in desktop and web releases

Use the merged craft-fonts input containing Noto Sans Thai across release,
FreeBSD and Nix builds. Retain the small Thai face in the wasm subset and
reject required release inputs that lack it. Exercise the reported Thai
folder name through all UI families and include original font licence texts
in Windows installers as well as portable packages.

Refs #736; completes delivery of the fallback added in #750.

Validation: UI tests, UI-only strict Clippy, 28 wasm checks, HEIF checks,
assets, layers, parity and formatting pass. All 21 unrelated workspace
failures reproduce with upstream Rust sources and the old font input;
existing Rust 1.95 metadata/engine Clippy findings remain unchanged.
2026-10-14 18:52:15 +02:00

177 lines
9.5 KiB
PowerShell

<#
.SYNOPSIS
Build, sign and package LightCraft for Windows.
.DESCRIPTION
Produces, in $env:DIST (default: dist/release):
lightcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
lightcraft-<version>-windows-<arch>-portable.zip lightcraft.exe + lightcraft-cli.exe
The binaries link the C runtime statically (+crt-static), so neither the MSI nor the portable
zip needs the Visual C++ redistributable. Signing is delegated to sign.ps1 (skipped with a
warning when no signing secrets are set).
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
pwsh packaging/windows/package.ps1 -Arch x86 -SkipBuild
pwsh packaging/windows/package.ps1 -Arch arm64 # cross-compiled; needs the MSVC ARM64 build tools
#>
param(
[ValidateSet('x64', 'x86', 'arm64')] [string] $Arch = 'x64',
[switch] $SkipBuild
)
$ErrorActionPreference = 'Stop'
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
function Invoke-Native([string] $What, [scriptblock] $Block) {
Write-Output "==> $What"
& $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) {
$inPkg = $false
foreach ($line in Get-Content (Join-Path $Root 'Cargo.toml')) {
if ($line -match '^\s*\[') { $inPkg = ($line.Trim() -eq '[workspace.package]'); continue }
if ($inPkg -and $line -match '^\s*version\s*=\s*"([^"]+)"') { $Version = $Matches[1]; break }
}
}
if (-not $Version) { throw 'could not read [workspace.package] version from Cargo.toml' }
# MSI ProductVersion is numeric (major.minor.build); pre-release tags are dropped there.
$MsiVersion = ($Version -split '-')[0]
$Target = switch ($Arch) { 'x64' { 'x86_64-pc-windows-msvc' } 'x86' { 'i686-pc-windows-msvc' } 'arm64' { 'aarch64-pc-windows-msvc' } }
$Dist = if ($env:DIST) { $env:DIST } else { Join-Path $Root 'dist\release' }
$TargetDir = if ($env:CARGO_TARGET_DIR) { $env:CARGO_TARGET_DIR } else { Join-Path $Root 'target' }
New-Item -ItemType Directory -Force -Path $Dist | Out-Null
if (-not $env:LIGHTCRAFT_BUILD_SHA) { $env:LIGHTCRAFT_BUILD_SHA = (git -C $Root rev-parse HEAD 2>$null) }
if (-not $env:LIGHTCRAFT_BUILD_DATE) { $env:LIGHTCRAFT_BUILD_DATE = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') }
Write-Output "LightCraft $Version for Windows $Arch ($Target)"
if (-not $SkipBuild) {
# Static CRT: no VC++ redistributable needed. Scoped to the target so host build scripts and
# proc-macros are unaffected.
$flagVar = 'CARGO_TARGET_' + ($Target.ToUpper() -replace '-', '_') + '_RUSTFLAGS'
[Environment]::SetEnvironmentVariable($flagVar, '-C target-feature=+crt-static')
# Fail the build (rather than warn) if the icon/VERSIONINFO can't be embedded.
$env:LIGHTCRAFT_REQUIRE_WINRES = '1'
Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p lightcraft -p lightcraft-cli --features lightcraft/heif,lightcraft-cli/heif --target $Target }
}
$Bin = Join-Path $TargetDir "$Target\release"
# Check both binaries' PE headers before packaging. Machine (COFF header) must match -Arch, so an
# x64 build can never ship labelled arm64. Subsystem (optional header): 2 = Windows GUI, 3 = console.
# The app must be GUI (no console window opens with it); the CLI must stay console so its output
# reaches the terminal.
function Get-PeHeader([string] $Path) {
$bytes = [System.IO.File]::ReadAllBytes($Path)
$pe = [BitConverter]::ToInt32($bytes, 0x3C)
return @{ Machine = [BitConverter]::ToUInt16($bytes, $pe + 4); Subsystem = [BitConverter]::ToUInt16($bytes, $pe + 0x5C) }
}
$Machine = switch ($Arch) { 'x64' { 0x8664 } 'x86' { 0x14C } 'arm64' { 0xAA64 } }
foreach ($check in @(@('lightcraft.exe', 2), @('lightcraft-cli.exe', 3))) {
$h = Get-PeHeader (Join-Path $Bin $check[0])
if ($h.Machine -ne $Machine) { throw "$($check[0]) is for machine 0x$('{0:X}' -f $h.Machine), expected 0x$('{0:X}' -f $Machine) ($Arch)" }
if ($h.Subsystem -ne $check[1]) { throw "$($check[0]) has PE subsystem $($h.Subsystem), expected $($check[1])" }
Write-Output "ok $($check[0]): $Arch, PE subsystem $($h.Subsystem)"
}
$Stage = Join-Path $TargetDir "windows-package\$Arch"
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
Copy-Item (Join-Path $Bin 'lightcraft.exe'), (Join-Path $Bin 'lightcraft-cli.exe') $Stage
# Include the embedded fonts' unmodified OFL texts in both installer and portable releases.
. (Join-Path $PSScriptRoot 'font-licences.ps1')
Write-FontLicences (Join-Path $Stage 'FONT-LICENCES.txt') (Join-Path $Root 'assets\fonts\OFL-Inter.txt') $env:CRAFT_FONTS_DIR
& (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 -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.
Remove-Item -Force -ErrorAction SilentlyContinue ([IO.Path]::ChangeExtension($Msi, '.wixpdb'))
& (Join-Path $PSScriptRoot 'sign.ps1') $Msi
# ---- portable zip ------------------------------------------------------------------------------
$Portable = Join-Path $TargetDir "windows-package\lightcraft-$Version-windows-$Arch-portable"
Remove-Item -Recurse -Force $Portable -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Portable | Out-Null
Copy-Item (Join-Path $Stage '*.exe') $Portable
Copy-Item (Join-Path $Stage 'FONT-LICENCES.txt') $Portable
foreach ($f in 'README.md', 'LICENSE', 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE') {
$p = Join-Path $Root $f
if (Test-Path $p) { Copy-Item $p $Portable }
}
# Builds made with CRAFT_FONTS_DIR (storytold/craft-fonts) embed its fonts: ship their OFL licences.
if ($env:CRAFT_FONTS_DIR) {
Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue | ForEach-Object {
Copy-Item $_.FullName (Join-Path $Portable "OFL-$($_.Directory.Name).txt")
}
}
$Zip = Join-Path $Dist "lightcraft-$Version-windows-$Arch-portable.zip"
Remove-Item -Force $Zip -ErrorAction SilentlyContinue
Compress-Archive -Path $Portable -DestinationPath $Zip
# Smoke-test the CLI when this machine can run it. An ARM64 build made on an x64 runner can't run
# here; .github/workflows/windows-arm64.yml installs and runs it on ARM64 instead.
$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
if ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') {
Invoke-Native 'lightcraft-cli --version' { & (Join-Path $Stage 'lightcraft-cli.exe') --version }
} else {
Write-Output "skipping lightcraft-cli --version: an $Arch build doesn't run on this $HostArch machine"
}
Get-Item $Msi, $Zip | Format-Table Name, Length