<# .SYNOPSIS Build and package SolidCraft for Windows. .DESCRIPTION Produces, in $env:DIST (default: dist/release): solidcraft--windows-.msi per-machine installer (WiX v5) solidcraft--windows--portable.zip solidcraft.exe + solidcraft-cli.exe + licences (+ the solvecraft-cli.exe compatibility alias) SolidCraft--windows-.exe the app alone (x64 only: a plain download) The binaries link the C runtime statically (+crt-static), so nothing else needs installing. The icon comes from $env:SOLIDCRAFT_ICONS (solidcraft.ico, made by `solidcraft-cli icon`), or is drawn by the CLI just built when this machine can run it. Nothing is signed. Needs: Rust (MSVC toolchain + the target) and WiX v5: dotnet tool install --global wix --version 5.0.2 wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2 Adapted from PhotoCraft's packaging (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md). .EXAMPLE pwsh packaging/windows/package.ps1 -Arch x64 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" } } # The version lives in one place: [workspace.package] version in the root Cargo.toml. $Version = $env:SOLIDCRAFT_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 Write-Output "SolidCraft $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') Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p solidcraft -p solidcraft-cli --target $Target } } $Bin = Join-Path $TargetDir "$Target\release" # Check both binaries' PE headers: Machine must match -Arch; Subsystem 2 = GUI (the app opens no # console window), 3 = console (the CLI's 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 } } # solvecraft-cli.exe: the old CLI name, forwarding to solidcraft-cli.exe (compatibility shim, # docs/compatibility.md). Built by `-p solidcraft-cli` with the CLI. foreach ($check in @(@('solidcraft.exe', 2), @('solidcraft-cli.exe', 3), @('solvecraft-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" & (Join-Path $PSScriptRoot 'check-icon.ps1') -Path (Join-Path $Bin 'solidcraft.exe') Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue New-Item -ItemType Directory -Force -Path $Stage | Out-Null Copy-Item (Join-Path $Bin 'solidcraft.exe'), (Join-Path $Bin 'solidcraft-cli.exe'), (Join-Path $Bin 'solvecraft-cli.exe') $Stage foreach ($f in 'LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE', 'THIRD-PARTY-LICENSES.txt') { Copy-Item (Join-Path $Root $f) $Stage } $HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant() $CanRun = ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') $Icon = if ($env:SOLIDCRAFT_ICONS) { Join-Path $env:SOLIDCRAFT_ICONS 'solidcraft.ico' } else { Join-Path $Stage 'solidcraft.ico' } if (-not (Test-Path $Icon)) { if (-not $CanRun) { throw "no icon: set SOLIDCRAFT_ICONS to a folder with solidcraft.ico (an $Arch CLI can't run on this $HostArch machine)" } Invoke-Native 'draw the icon' { & (Join-Path $Stage 'solidcraft-cli.exe') icon --out $Icon } } # ---- MSI --------------------------------------------------------------------------------------- $Msi = Join-Path $Dist "solidcraft-$Version-windows-$Arch.msi" Invoke-Native 'wix build' { wix build (Join-Path $PSScriptRoot 'solidcraft.wxs') -arch $Arch ` -ext WixToolset.UI.wixext -ext WixToolset.Util.wixext ` -culture en-US -loc (Join-Path $PSScriptRoot 'solidcraft.en-us.wxl') ` -d "Version=$MsiVersion" -d "DisplayVersion=$Version" -d "BinDir=$Stage" -d "IconPath=$Icon" ` -o $Msi } Invoke-Native 'MSI shortcut icon validation (ICE50)' { wix msi validate $Msi -ice ICE50 -intermediateFolder (Join-Path $Stage 'msi-validation') } # 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')) # ---- portable zip ------------------------------------------------------------------------------ $Portable = Join-Path $TargetDir "windows-package\solidcraft-$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-MIT', 'LICENSE-APACHE', 'NOTICE', 'THIRD-PARTY-LICENSES.txt') { Copy-Item (Join-Path $Root $f) $Portable } $Zip = Join-Path $Dist "solidcraft-$Version-windows-$Arch-portable.zip" Remove-Item -Force $Zip -ErrorAction SilentlyContinue Compress-Archive -Path $Portable -DestinationPath $Zip # ---- the app as a plain download (x64) --------------------------------------------------------- $Outputs = @($Msi, $Zip) if ($Arch -eq 'x64') { $Exe = Join-Path $Dist "SolidCraft-$Version-windows-$Arch.exe" Copy-Item (Join-Path $Stage 'solidcraft.exe') $Exe $Outputs += $Exe } if ($CanRun) { Invoke-Native 'solidcraft-cli --version' { & (Join-Path $Stage 'solidcraft-cli.exe') --version } } else { Write-Output "skipping solidcraft-cli --version: an $Arch build doesn't run on this $HostArch machine" } Get-Item $Outputs | Format-Table Name, Length