diff --git a/.github/scripts/bundle-windows.ps1 b/.github/scripts/bundle-windows.ps1
index 46bee01a..a1263f26 100644
--- a/.github/scripts/bundle-windows.ps1
+++ b/.github/scripts/bundle-windows.ps1
@@ -50,7 +50,7 @@ Copy-Item README.md "$Stage/README.md"
# 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' }[$Target]
+$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"
@@ -68,9 +68,14 @@ Copy-Item "assets/windows/conpty/LICENSE.txt" "$Stage/LICENSE-ConPTY.txt"
# naming the directories it searched.
#
# The *filename* is a contract too, not just the directory: `wsl.rs` looks for
-# `
/`, so this string has to stay whatever
-# `install::asset::ASSET_X86_64` says it is.
-$ServerAsset = "tty7-server-linux-x86_64-musl"
+# `/`, 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
@@ -94,6 +99,15 @@ if ($PackageUpdater) {
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
@@ -104,6 +118,7 @@ if (-not $Iscc) { $Iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" }
"/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" }
diff --git a/.github/scripts/windows-installer.iss b/.github/scripts/windows-installer.iss
index 8c610b10..70afae0b 100644
--- a/.github/scripts/windows-installer.iss
+++ b/.github/scripts/windows-installer.iss
@@ -7,6 +7,7 @@
; /DStageDir= staged payload (app, CLI, updater, marker, resources)
; /DOutputDir= where the setup exe is written
; /DOutputName= setup exe filename, without ".exe"
+; /DArchitectures= Inno architecture: x64compatible or arm64
;
; Defaults to a per-user install ({localappdata}\Programs\tty7 — no UAC
; prompt), with an "install for all users" escape hatch in the dialog. The
@@ -19,6 +20,9 @@
#ifndef VersionInfoVersion
#error Missing /DVersionInfoVersion — this script is meant to be compiled via bundle-windows.ps1
#endif
+#ifndef Architectures
+ #error Missing /DArchitectures — this script is meant to be compiled via bundle-windows.ps1
+#endif
[Setup]
; Never change AppId: it is how Windows ties upgrades + the uninstall entry
@@ -35,8 +39,8 @@ DefaultDirName={autopf}\tty7
DisableProgramGroupPage=yes
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
-ArchitecturesAllowed=x64compatible
-ArchitecturesInstallIn64BitMode=x64compatible
+ArchitecturesAllowed={#Architectures}
+ArchitecturesInstallIn64BitMode={#Architectures}
MinVersion=10.0
LicenseFile={#StageDir}\LICENSE.txt
SetupIconFile=..\..\assets\favicon.ico
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 341d973c..9bff9042 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -84,11 +84,23 @@ jobs:
os: windows
arch: x86_64
target: x86_64-pc-windows-msvc
+ wsl-server: tty7-server-linux-x86_64-musl
+ # Cross-compiled from the x64 runner, like `server-windows`'s ARM64
+ # leg. Experimental until it has shipped once: a failure here drops
+ # the ARM64 packages from the release instead of holding it up, and
+ # ARM64 machines still have the x64 build under emulation.
+ - runner: windows-latest
+ os: windows
+ arch: arm64
+ target: aarch64-pc-windows-msvc
+ wsl-server: tty7-server-linux-aarch64-musl
+ experimental: true
- runner: ubuntu-latest
os: linux
arch: x86_64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
+ continue-on-error: ${{ matrix.experimental == true }}
steps:
- name: Checkout tty7
uses: actions/checkout@v4
@@ -121,6 +133,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: tty7
+ # Two Windows legs share a runner OS; without the target in the key
+ # they would overwrite each other's cache.
+ key: ${{ matrix.target }}
# The updater ships on every GUI platform now: inside the macOS bundle,
# beside the Windows app, and inside the Linux AppImage.
@@ -200,7 +215,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@v8
with:
- name: nightly-tty7-server-linux-x86_64-musl
+ name: nightly-${{ matrix.wsl-server }}
path: tty7/bundled-server
- name: Package Windows installer + zip
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 384a72f1..fea9f31b 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -35,11 +35,23 @@ jobs:
os: windows
arch: x86_64
target: x86_64-pc-windows-msvc
+ wsl-server: tty7-server-linux-x86_64-musl
+ # Cross-compiled from the x64 runner, like `server-windows`'s ARM64
+ # leg. Experimental until it has shipped once: a failure here drops
+ # the ARM64 packages from the release instead of holding it up, and
+ # ARM64 machines still have the x64 build under emulation.
+ - runner: windows-latest
+ os: windows
+ arch: arm64
+ target: aarch64-pc-windows-msvc
+ wsl-server: tty7-server-linux-aarch64-musl
+ experimental: true
- runner: ubuntu-latest
os: linux
arch: x86_64
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.runner }}
+ continue-on-error: ${{ matrix.experimental == true }}
steps:
- name: Checkout tty7
uses: actions/checkout@v4
@@ -69,6 +81,9 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: tty7
+ # Two Windows legs share a runner OS; without the target in the key
+ # they would overwrite each other's cache.
+ key: ${{ matrix.target }}
# `--locked` because a release must ship the dependency set the tag
# recorded, not whatever cargo would re-resolve at build time. Safe here
@@ -147,7 +162,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@v8
with:
- name: release-tty7-server-linux-x86_64-musl
+ name: release-${{ matrix.wsl-server }}
path: tty7/bundled-server
- name: Package Windows installer + zip
@@ -388,9 +403,10 @@ jobs:
# redistributable — which a headless server is the likeliest machine to lack.
# Symbols are stripped through the profile instead.
#
- # ARM64 is new ground for this repository's Windows builds (the app itself
- # ships x64 only), so its leg may fail without holding up the release; an
- # ARM64 host then gets a clear "could not download" instead of a server.
+ # ARM64 is new ground for this repository's Windows builds (the app's own
+ # ARM64 leg in `build` is experimental too), so its leg may fail without
+ # holding up the release; an ARM64 host then gets a clear "could not
+ # download" instead of a server.
server-windows:
strategy:
fail-fast: false
diff --git a/assets/windows/conpty/README.md b/assets/windows/conpty/README.md
index 9e9f6911..8492aa2e 100644
--- a/assets/windows/conpty/README.md
+++ b/assets/windows/conpty/README.md
@@ -46,8 +46,11 @@ curl.exe -sL -o conpty.zip "https://api.nuget.org/v3-flatcontainer/microsoft.win
Expand-Archive conpty.zip -DestinationPath conpty-pkg
Copy-Item conpty-pkg/runtimes/win-x64/native/conpty.dll x64/conpty.dll
Copy-Item conpty-pkg/build/native/runtimes/x64/OpenConsole.exe x64/OpenConsole.exe
+Copy-Item conpty-pkg/runtimes/win-arm64/native/conpty.dll arm64/conpty.dll
+Copy-Item conpty-pkg/build/native/runtimes/arm64/OpenConsole.exe arm64/OpenConsole.exe
```
-Only `x86_64-pc-windows-msvc` is released today, so only `x64/` is vendored. The
-package also carries `arm64` and `x86`; adding an architecture means adding the
-directory here and one `Copy-Item` in `.github/scripts/bundle-windows.ps1`.
+`x64/` and `arm64/` match the two released Windows targets
+(`x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`). The package also carries
+`x86`; adding an architecture means adding the directory here, one arm in
+`build.rs`, and one entry in `.github/scripts/bundle-windows.ps1`'s `$ConptyArch`.
diff --git a/assets/windows/conpty/arm64/OpenConsole.exe b/assets/windows/conpty/arm64/OpenConsole.exe
new file mode 100644
index 00000000..7693efea
Binary files /dev/null and b/assets/windows/conpty/arm64/OpenConsole.exe differ
diff --git a/assets/windows/conpty/arm64/conpty.dll b/assets/windows/conpty/arm64/conpty.dll
new file mode 100644
index 00000000..369a2553
Binary files /dev/null and b/assets/windows/conpty/arm64/conpty.dll differ
diff --git a/build.rs b/build.rs
index 581d8394..71fadedf 100644
--- a/build.rs
+++ b/build.rs
@@ -31,8 +31,9 @@ fn stage_bundled_conpty() {
let arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
let vendored = match arch.as_str() {
"x86_64" => "x64",
- // Every other Windows architecture is unreleased, so nothing is
- // vendored for it and the in-box conhost stays in charge.
+ "aarch64" => "arm64",
+ // 32-bit Windows is unreleased, so nothing is vendored for it and the
+ // in-box conhost stays in charge.
_ => return,
};
let source = PathBuf::from("assets/windows/conpty").join(vendored);
diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx
index f1e7c366..8efec900 100644
--- a/docs/getting-started/installation.mdx
+++ b/docs/getting-started/installation.mdx
@@ -27,12 +27,17 @@ AppImage bundles its own X11/Wayland/font libraries.
- Two shapes, both x86-64:
+ Two shapes, each built for x86-64 (`x86_64`) and ARM64 (`arm64`, e.g.
+ Snapdragon laptops):
| File | Use it when |
|---|---|
- | `tty7--windows-x86_64-setup.exe` | You want a normal install with Start-menu entries and an uninstaller. |
- | `tty7--windows-x86_64.zip` | You want it portable — unzip anywhere and run `tty7-app.exe`. |
+ | `tty7--windows--setup.exe` | You want a normal install with Start-menu entries and an uninstaller. |
+ | `tty7--windows-.zip` | You want it portable — unzip anywhere and run `tty7-app.exe`. |
+
+ The x86-64 build also runs on ARM64 Windows under emulation, but the
+ `arm64` one is native. Installing either over the other upgrades the same
+ install.
The installer offers one optional setup task, off by default: **Add "Open in
tty7" to the folder context menu**. It writes shell verbs under `HKCU`, so
diff --git a/src/core/update.rs b/src/core/update.rs
index 35316fd3..789d3cc5 100644
--- a/src/core/update.rs
+++ b/src/core/update.rs
@@ -2687,8 +2687,14 @@ impl WindowsUpdateLayout {
#[cfg(target_os = "windows")]
fn windows_package_for_layout(version: &str, layout: &WindowsUpdateLayout) -> Option {
+ // Labelled like the macOS packages (`arm64`), not like the server assets
+ // (`aarch64`): these are the desktop app's names. An x64 build running
+ // under emulation on ARM64 keeps taking x64 packages — switching it to
+ // the native build is the user's reinstall, not something an update does.
let arch = if cfg!(target_arch = "x86_64") {
"x86_64"
+ } else if cfg!(target_arch = "aarch64") {
+ "arm64"
} else {
return None;
};
@@ -3732,12 +3738,20 @@ mod tests {
assert_eq!(
windows_package_for_layout("26.8.2", &WindowsUpdateLayout::Inno(directory.clone()))
.as_deref(),
- Some("tty7-26.8.2-windows-x86_64-setup.exe")
+ Some(if cfg!(target_arch = "aarch64") {
+ "tty7-26.8.2-windows-arm64-setup.exe"
+ } else {
+ "tty7-26.8.2-windows-x86_64-setup.exe"
+ })
);
assert_eq!(
windows_package_for_layout("26.8.2", &WindowsUpdateLayout::Portable(directory))
.as_deref(),
- Some("tty7-26.8.2-windows-x86_64.zip")
+ Some(if cfg!(target_arch = "aarch64") {
+ "tty7-26.8.2-windows-arm64.zip"
+ } else {
+ "tty7-26.8.2-windows-x86_64.zip"
+ })
);
}