mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 03:27:03 -06:00
* fix(windows): keep the shortcut icon after an update (#1114) The MSI's Start menu shortcut took its icon from the WiX <Icon>, which Windows Installer caches under %APPDATA%\Microsoft\Installer\{ProductCode}\. Every update is a major upgrade with a new ProductCode, so the old folder is deleted. A taskbar pin is a copy of the shortcut that the update never touches, so it kept pointing at the deleted file and lost its icon. Drop the Icon from the shortcut so it shows the icon embedded in deckcraft.exe, whose path survives updates. The <Icon> stays for ARPPRODUCTICON. A test keeps shortcuts from naming an MSI icon again. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * contributors: credit Sofia Alves Figueredo by real name Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(windows): make the Start menu shortcut non-advertised (#1114) Addresses the review comment: without an MSI Icon, a non-advertised shortcut targets the installed exe directly and takes its embedded icon. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * fix(windows): package.ps1 rejects a deckcraft.exe with no embedded icon (#1114) The shortcuts now take the exe's own icon, and -SkipBuild packages an existing exe that may come from a build without DECKCRAFT_REQUIRE_WINRES=1. Count its icons with ExtractIconEx and stop before WiX when there are none. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
157 lines
8.5 KiB
PowerShell
157 lines
8.5 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Build, sign and package DeckCraft for Windows.
|
|
|
|
.DESCRIPTION
|
|
Produces, in $env:DIST (default: dist/release):
|
|
deckcraft-<version>-windows-<arch>.msi per-machine installer (WiX v5)
|
|
deckcraft-<version>-windows-<arch>-portable.zip deckcraft.exe + deckcraft-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
|
|
|
|
.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" }
|
|
}
|
|
|
|
# The version lives in one place: [workspace.package] version in the root Cargo.toml.
|
|
$Version = $env:DECKCRAFT_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:DECKCRAFT_BUILD_SHA) { $env:DECKCRAFT_BUILD_SHA = (git -C $Root rev-parse HEAD 2>$null) }
|
|
if (-not $env:DECKCRAFT_BUILD_DATE) { $env:DECKCRAFT_BUILD_DATE = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd') }
|
|
|
|
Write-Output "DeckCraft $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:DECKCRAFT_REQUIRE_WINRES = '1'
|
|
Invoke-Native "cargo build ($Target)" { cargo build --release --locked -p deckcraft -p deckcraft-cli --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 @(@('deckcraft.exe', 2), @('deckcraft-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)"
|
|
}
|
|
|
|
# The Start menu shortcut is not advertised and names no MSI icon, so it shows the icon embedded in
|
|
# deckcraft.exe (#1114). -SkipBuild takes whatever binary is already there, which may come from a
|
|
# build without DECKCRAFT_REQUIRE_WINRES=1 whose resource compilation failed: refuse an exe with no
|
|
# icon instead of shipping shortcuts with a generic one. ExtractIconEx with index -1 counts them.
|
|
Add-Type -Namespace DeckCraft -Name Shell -MemberDefinition '[DllImport("shell32.dll", CharSet = CharSet.Unicode)] public static extern uint ExtractIconEx(string file, int index, IntPtr[] large, IntPtr[] small, uint icons);'
|
|
$Icons = [DeckCraft.Shell]::ExtractIconEx((Join-Path $Bin 'deckcraft.exe'), -1, $null, $null, 0)
|
|
if ($Icons -eq 0) { throw 'deckcraft.exe has no embedded icon: rebuild it with DECKCRAFT_REQUIRE_WINRES=1 (omit -SkipBuild)' }
|
|
Write-Output "ok deckcraft.exe: $Icons embedded icon group(s)"
|
|
$Stage = Join-Path $TargetDir "windows-package\$Arch"
|
|
Remove-Item -Recurse -Force $Stage -ErrorAction SilentlyContinue
|
|
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
|
|
# Dependency font notices are required even without optional craft-fonts.
|
|
Copy-Item -LiteralPath (Join-Path $Root 'NOTICE') -Destination $Stage
|
|
Copy-Item (Join-Path $Bin 'deckcraft.exe'), (Join-Path $Bin 'deckcraft-cli.exe') $Stage
|
|
|
|
& (Join-Path $PSScriptRoot 'sign.ps1') (Join-Path $Stage 'deckcraft.exe') (Join-Path $Stage 'deckcraft-cli.exe')
|
|
|
|
# ---- font notices for the MSI ------------------------------------------------------------------
|
|
# The portable ZIP already carries these notices; stage the complete files before MSI creation too.
|
|
$Docs = Join-Path $Stage 'font-licences'
|
|
New-Item -ItemType Directory -Force -Path $Docs | Out-Null
|
|
$HasCraftFontLicences = 0
|
|
if ($env:CRAFT_FONTS_DIR) {
|
|
foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -File -ErrorAction SilentlyContinue) {
|
|
Copy-Item -LiteralPath $ofl.FullName -Destination (Join-Path $Docs "OFL-$($ofl.Directory.Name).txt")
|
|
$HasCraftFontLicences = 1
|
|
}
|
|
}
|
|
# ---- end font notices for the MSI --------------------------------------------------------------
|
|
|
|
# ---- MSI ---------------------------------------------------------------------------------------
|
|
$Msi = Join-Path $Dist "deckcraft-$Version-windows-$Arch.msi"
|
|
Invoke-Native 'wix build' {
|
|
wix build (Join-Path $PSScriptRoot 'deckcraft.wxs') -arch $Arch `
|
|
-d "Version=$MsiVersion" -d "BinDir=$Stage" -d "IconPath=$(Join-Path $Root 'assets\app-icon\deckcraft.ico')" `
|
|
-d "DocsDir=$Docs" -d "HasCraftFontLicences=$HasCraftFontLicences" `
|
|
-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\deckcraft-$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 (CRAFT_FONTS_DIR) embed its fonts: ship their licences.
|
|
if ($env:CRAFT_FONTS_DIR) {
|
|
foreach ($ofl in Get-ChildItem -Path (Join-Path $env:CRAFT_FONTS_DIR 'fonts\*\OFL.txt') -ErrorAction SilentlyContinue) {
|
|
Copy-Item $ofl.FullName (Join-Path $Portable "OFL-$($ofl.Directory.Name).txt")
|
|
}
|
|
}
|
|
$Zip = Join-Path $Dist "deckcraft-$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.
|
|
$HostArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant()
|
|
if ($Arch -ne 'arm64' -or $HostArch -eq 'arm64') {
|
|
Invoke-Native 'deckcraft-cli --version' { & (Join-Path $Stage 'deckcraft-cli.exe') --version }
|
|
} else {
|
|
Write-Output "skipping deckcraft-cli --version: an $Arch build doesn't run on this $HostArch machine"
|
|
}
|
|
Get-Item $Msi, $Zip | Format-Table Name, Length
|