mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
173 lines
9.2 KiB
PowerShell
173 lines
9.2 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
|
|
|
|
& (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
|
|
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
|