Files
tty7/.github/scripts/bundle-windows.ps1
T
l0ng-ai fa3d777c29 feat(release): ship a native Windows ARM64 build (#1043)
Release and nightly now build the desktop app for aarch64-pc-windows-msvc
alongside x64, publishing tty7-<version>-windows-arm64-setup.exe and
tty7-<version>-windows-arm64.zip.

- Cross-compiled on the x64 Windows runner, like server-windows' ARM64
  leg, and marked experimental until it has shipped once: a failure drops
  the ARM64 packages instead of holding up the release.
- Vendors Microsoft's arm64 ConPTY pair from the same package version as
  the x64 one, so ARM64 panes keep the OSC 11 fix (#345).
- Each Windows package bundles the WSL server for its own architecture.
- The installer's architecture comes in as a define: x64compatible for the
  x64 build (still installable on ARM64 under emulation), arm64 for the
  native one. One AppId, so either upgrades the other.
- The in-app updater on an ARM64 build asks for the arm64 packages; an x64
  build under emulation keeps taking x64 ones.

Claude-Session: https://claude.ai/code/session_011mDkkQhwx4RJJBHee3yVpq
2026-09-30 22:04:46 +08:00

132 lines
6.9 KiB
PowerShell

# Usage: bundle-windows.ps1 <target-triple> <arch-label>
# Package the release binary twice from one staged payload:
# dist/tty7-<version>-windows-<arch>.zip portable (unzip anywhere)
# dist/tty7-<version>-windows-<arch>-setup.exe Inno Setup installer
# (Program Files or per-user, Start Menu shortcut, "Apps" uninstall entry)
#
# Fonts are embedded via include_bytes! and the app icon is compiled into the
# executable as a resource (see build.rs). So the payload is tty7-app.exe plus a
# sibling completions\ dir (loaded at runtime — see terminal::signature), the
# bundled ConPTY pair (loaded by the DLL search path — see below) and the
# license/readme. Windows release artifacts are intentionally unsigned. The
# in-app updater verifies the published SHA-256 checksum and PE file version
# before and after waiting for the GUI to exit.
$ErrorActionPreference = 'Stop'
$Target = $args[0]
$Arch = $args[1]
$Version = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"').Matches[0].Groups[1].Value
# Inno accepts the full semantic version for AppVersion, but the PE version
# resource only accepts numeric components. Keep both values so Nightly and
# other prerelease builds retain their display version without breaking ISCC.
$VersionCore = ($Version -split '[-+]', 2)[0]
$VersionInfoVersion = "${VersionCore}.0"
$Name = "tty7-$Version-windows-$Arch"
$Stage = "dist/$Name"
$PackageUpdater = $env:TTY7_PACKAGE_UPDATE_HELPER -ne '0'
Remove-Item -Recurse -Force dist -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $Stage | Out-Null
Copy-Item "target/$Target/release/tty7-app.exe" "$Stage/tty7-app.exe"
# The CLI, staged beside the GUI so both the zip and the installer carry it.
# `core::cli_install` resolves it relative to tty7-app.exe and puts that
# directory on the user's PATH.
Copy-Item "target/$Target/release/tty7.exe" "$Stage/tty7.exe"
if ($PackageUpdater) {
# The installed copy is never executed in place during an update. The GUI
# first copies it to a private staging directory so Inno can replace every
# installed executable without colliding with Windows image locks.
Copy-Item "target/$Target/release/tty7-updater.exe" "$Stage/tty7-updater.exe"
}
New-Item -ItemType Directory -Force -Path "$Stage/completions" | Out-Null
Copy-Item "assets/completions/*.json" "$Stage/completions/"
Copy-Item LICENSE "$Stage/LICENSE.txt"
Copy-Item README.md "$Stage/README.md"
# Microsoft's redistributable ConPTY, which `portable-pty` prefers over the
# in-box conhost when it sits beside the running executable — and the daemon is
# `tty7-app.exe --daemon`, so beside it is exactly here. The in-box host
# swallows a pane's OSC 11 background query (#345); this pair forwards it. The
# two files are one unit, so a missing half is an error rather than a warning.
# See assets/windows/conpty/README.md.
$ConptyArch = @{ 'x86_64-pc-windows-msvc' = 'x64'; 'aarch64-pc-windows-msvc' = 'arm64' }[$Target]
if (-not $ConptyArch) { throw "no vendored ConPTY for $Target - see assets/windows/conpty/README.md" }
foreach ($File in 'conpty.dll', 'OpenConsole.exe') {
$Source = "assets/windows/conpty/$ConptyArch/$File"
if (-not (Test-Path $Source)) { throw "missing bundled ConPTY file: $Source" }
Copy-Item $Source "$Stage/$File"
}
Copy-Item "assets/windows/conpty/LICENSE.txt" "$Stage/LICENSE-ConPTY.txt"
# The Linux musl `tty7-server`, staged at server/ so a WSL distro can be handed
# the binary this client shipped with (WSL downloads nothing). The
# lookup path is a contract with `daemon::install::wsl` — it searches
# <dir of tty7-app.exe>/server/<asset> first — so this directory name is not free to
# change on its own. Missing is a warning, not an error, matching `server-musl`'s
# own skip-don't-fail probe; WSL then fails at connect time with a message
# naming the directories it searched.
#
# The *filename* is a contract too, not just the directory: `wsl.rs` looks for
# `<dir>/<asset name>`, so these strings have to stay whatever
# `install::asset::ASSET_X86_64` / `ASSET_AARCH64` say they are. A WSL distro
# runs the host's architecture, so each package carries the one that matches it.
$ServerAsset = @{
'x86_64-pc-windows-msvc' = 'tty7-server-linux-x86_64-musl'
'aarch64-pc-windows-msvc' = 'tty7-server-linux-aarch64-musl'
}[$Target]
if (-not $ServerAsset) { throw "no bundled WSL server asset known for $Target" }
$ServerSrc = "bundled-server/$ServerAsset"
if (Test-Path $ServerSrc) {
New-Item -ItemType Directory -Force -Path "$Stage/server" | Out-Null
Copy-Item $ServerSrc "$Stage/server/$ServerAsset"
Write-Host "OK bundled $ServerAsset"
} else {
Write-Warning "no $ServerAsset to bundle - this build cannot serve WSL distros"
}
# The marker tells the in-app updater which of the two Windows layouts it is
# running from, and therefore which release asset can replace it. It says
# nothing about where updates come from: that is always the latest stable
# release. The Inno payload gets the mutually exclusive marker below.
if ($PackageUpdater) {
Set-Content -Path "$Stage/.tty7-portable" -Value 'portable-v1' -NoNewline -Encoding ascii
}
Compress-Archive -Path "$Stage/*" -DestinationPath "dist/$Name.zip" -Force
if ($PackageUpdater) {
# The Inno payload must never retain the mutually exclusive portable marker.
Remove-Item -LiteralPath "$Stage/.tty7-portable" -Force
Set-Content -Path "$Stage/.tty7-inno-install" -Value 'inno-v1' -NoNewline -Encoding ascii
}
# Inno's architecture identifier for this payload. `x64compatible` also admits
# ARM64 Windows, which runs the x64 build under emulation — so an ARM64 machine
# can take either installer, and both upgrade the same install (one AppId).
$InstallArch = @{
'x86_64-pc-windows-msvc' = 'x64compatible'
'aarch64-pc-windows-msvc' = 'arm64'
}[$Target]
if (-not $InstallArch) { throw "no Inno architecture known for $Target" }
# Installer, built from the same staged payload. ISCC is on PATH on GitHub's
# windows-latest image; fall back to the default install location.
$Iscc = (Get-Command ISCC.exe -ErrorAction SilentlyContinue).Source
if (-not $Iscc) { $Iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" }
& $Iscc `
"/DAppVersion=$Version" `
"/DVersionInfoVersion=$VersionInfoVersion" `
"/DStageDir=$((Resolve-Path $Stage).Path)" `
"/DOutputDir=$((Resolve-Path dist).Path)" `
"/DOutputName=$Name-setup" `
"/DArchitectures=$InstallArch" `
.github/scripts/windows-installer.iss
if ($LASTEXITCODE -ne 0) { throw "ISCC exited with $LASTEXITCODE" }
# The staging directory stays. It is what ISCC compiled the installer from, and
# reading the compiled setup.exe back would need innoextract, which the runners
# do not carry — so `verify-windows-package.ps1` reads the payload here instead.
# It never reaches the release: both workflows upload named file globs
# (*.zip, *-setup.exe, …), and a directory matches none of them.
Write-Host "OK dist/$Name.zip"
Write-Host "OK dist/$Name-setup.exe"