diff --git a/.github/scripts/bundle-windows.ps1 b/.github/scripts/bundle-windows.ps1
index e82a8a0c..40fd3cae 100644
--- a/.github/scripts/bundle-windows.ps1
+++ b/.github/scripts/bundle-windows.ps1
@@ -33,7 +33,11 @@ Copy-Item README.md "$Stage/README.md"
# 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.
-$ServerAsset = "tty7-server-x86_64-unknown-linux-musl"
+#
+# 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"
$ServerSrc = "bundled-server/$ServerAsset"
if (Test-Path $ServerSrc) {
New-Item -ItemType Directory -Force -Path "$Stage/server" | Out-Null
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 43baa178..e152ae33 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -143,7 +143,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@v7
with:
- name: nightly-server-x86_64-unknown-linux-musl
+ name: nightly-tty7-server-linux-x86_64-musl
path: tty7/bundled-server
- name: Package Windows installer + zip
@@ -174,10 +174,15 @@ jobs:
if: needs.plan.outputs.build == 'true'
strategy:
fail-fast: false
+ # `target` is the build triple; `asset` is the published filename. Kept
+ # apart for the reason release.yml spells out: the triple's vendor field is
+ # `unknown`, and a download name has no business carrying it.
matrix:
- target:
- - x86_64-unknown-linux-musl
- - aarch64-unknown-linux-musl
+ include:
+ - target: x86_64-unknown-linux-musl
+ asset: tty7-server-linux-x86_64-musl
+ - target: aarch64-unknown-linux-musl
+ asset: tty7-server-linux-aarch64-musl
runs-on: ubuntu-latest
env:
RUSTFLAGS: -C strip=symbols
@@ -245,14 +250,14 @@ jobs:
set -euo pipefail
mkdir -p dist
cp "target/${{ matrix.target }}/release/tty7-server" \
- "dist/tty7-server-${{ matrix.target }}"
- chmod +x "dist/tty7-server-${{ matrix.target }}"
+ "dist/${{ matrix.asset }}"
+ chmod +x "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v7
if: steps.probe.outputs.present == 'true'
with:
- name: nightly-server-${{ matrix.target }}
- path: tty7/dist/tty7-server-${{ matrix.target }}
+ name: nightly-${{ matrix.asset }}
+ path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# Single publish step after all platforms succeed, so the rolling release is
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5fbc5f20..b5a3c182 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -121,7 +121,7 @@ jobs:
continue-on-error: true
uses: actions/download-artifact@v7
with:
- name: release-server-x86_64-unknown-linux-musl
+ name: release-tty7-server-linux-x86_64-musl
path: tty7/bundled-server
- name: Package Windows installer + zip
@@ -161,10 +161,16 @@ jobs:
server-musl:
strategy:
fail-fast: false
+ # `target` is the build triple; `asset` is the published filename. They
+ # are deliberately not the same string — the triple's vendor field is
+ # `unknown`, which says nothing to anyone reading the releases page. See
+ # `install::asset::ASSET_X86_64`, which pins these two names as literals.
matrix:
- target:
- - x86_64-unknown-linux-musl
- - aarch64-unknown-linux-musl
+ include:
+ - target: x86_64-unknown-linux-musl
+ asset: tty7-server-linux-x86_64-musl
+ - target: aarch64-unknown-linux-musl
+ asset: tty7-server-linux-aarch64-musl
runs-on: ubuntu-latest
env:
RUSTFLAGS: -C strip=symbols
@@ -234,14 +240,14 @@ jobs:
set -euo pipefail
mkdir -p dist
cp "target/${{ matrix.target }}/release/tty7-server" \
- "dist/tty7-server-${{ matrix.target }}"
- chmod +x "dist/tty7-server-${{ matrix.target }}"
+ "dist/${{ matrix.asset }}"
+ chmod +x "dist/${{ matrix.asset }}"
- uses: actions/upload-artifact@v7
if: steps.probe.outputs.present == 'true'
with:
- name: release-server-${{ matrix.target }}
- path: tty7/dist/tty7-server-${{ matrix.target }}
+ name: release-${{ matrix.asset }}
+ path: tty7/dist/${{ matrix.asset }}
if-no-files-found: error
# Single assembly step, after all four platforms succeed. The release object is
diff --git a/crates/tty7-core/src/daemon/install/asset.rs b/crates/tty7-core/src/daemon/install/asset.rs
index 1dbff30d..ce65ea3a 100644
--- a/crates/tty7-core/src/daemon/install/asset.rs
+++ b/crates/tty7-core/src/daemon/install/asset.rs
@@ -10,9 +10,23 @@
use std::fmt;
/// The release asset for a 64-bit x86 Linux box.
-pub const ASSET_X86_64: &str = "tty7-server-x86_64-unknown-linux-musl";
-/// The release asset for a 64-bit ARM Linux box.
-pub const ASSET_AARCH64: &str = "tty7-server-aarch64-unknown-linux-musl";
+///
+/// **`--musl`, not the Rust target triple.** These names used to be
+/// `${{ matrix.target }}` pasted into a filename, which put `unknown` — the
+/// triple's *vendor* field, meaning "no particular vendor" — in front of anyone
+/// reading the releases page. Of the triple's four fields only two say anything
+/// to whoever downloads this: the architecture, which is what `asset_for_uname`
+/// picks by, and `musl`, which is why one file runs on any distribution. The
+/// order matches the GUI assets the same release publishes
+/// (`tty7--linux-x86_64.tar.gz`), so one release is one naming scheme.
+///
+/// The build target keeps the triple wherever it really is one — `cargo
+/// zigbuild --target`, the `target//release` path, the cache key. This
+/// is a *download* name, and the two are no longer spelled the same on purpose.
+pub const ASSET_X86_64: &str = "tty7-server-linux-x86_64-musl";
+/// The release asset for a 64-bit ARM Linux box. See [`ASSET_X86_64`] for the
+/// naming.
+pub const ASSET_AARCH64: &str = "tty7-server-linux-aarch64-musl";
/// The sha256 manifest published beside every asset in a release.
pub const CHECKSUMS_ASSET: &str = "checksums.txt";
@@ -382,7 +396,7 @@ mod tests {
fn download_urls_point_at_the_release_the_tag_names() {
assert_eq!(
download_url(&release_tag("26.7.5"), ASSET_X86_64),
- "https://github.com/l0ng-ai/tty7/releases/download/v26.7.5/tty7-server-x86_64-unknown-linux-musl"
+ "https://github.com/l0ng-ai/tty7/releases/download/v26.7.5/tty7-server-linux-x86_64-musl"
);
assert_eq!(
download_url(&release_tag("26.7.6-nightly.20260727"), CHECKSUMS_ASSET),
@@ -390,6 +404,33 @@ mod tests {
);
}
+ /// **The asset names, pinned as literals.**
+ ///
+ /// They are one half of a contract whose other half is a `cp` in two
+ /// workflow files, and checking them against the consts they come from
+ /// would assert nothing. A literal here is what makes changing one side
+ /// without the other a failing test rather than a 404 on a user's machine.
+ ///
+ /// Including the absence of `unknown`: that word only ever reached these
+ /// names by way of `${{ matrix.target }}`, and a build triple pasted into a
+ /// download name is worth failing on rather than explaining again.
+ #[test]
+ fn asset_names_are_the_ones_the_release_workflow_publishes() {
+ assert_eq!(ASSET_X86_64, "tty7-server-linux-x86_64-musl");
+ assert_eq!(ASSET_AARCH64, "tty7-server-linux-aarch64-musl");
+ for asset in [ASSET_X86_64, ASSET_AARCH64] {
+ assert!(
+ !asset.contains("unknown"),
+ "{asset} carries the triple's vendor field"
+ );
+ }
+ // `checksums::expected_digest` matches the filename field whole, and
+ // says outright that it relies on no asset name being a substring of
+ // another. Two names is the whole set, so check it here.
+ assert!(!ASSET_X86_64.contains(ASSET_AARCH64));
+ assert!(!ASSET_AARCH64.contains(ASSET_X86_64));
+ }
+
/// Path construction, including the `mkdir` chain. Asserted literally: these
/// strings are what an SFTP server sees, and a `\` in any of them (which is
/// what `PathBuf::join` would produce on a Windows client) would create a file
diff --git a/crates/tty7-core/src/daemon/install/checksums.rs b/crates/tty7-core/src/daemon/install/checksums.rs
index 8a9960a9..0c403943 100644
--- a/crates/tty7-core/src/daemon/install/checksums.rs
+++ b/crates/tty7-core/src/daemon/install/checksums.rs
@@ -96,9 +96,9 @@ fn parse_hex(s: &str) -> Option {
/// The digest `manifest` records for `asset`.
///
/// **The filename field is matched whole, never by substring.**
-/// `tty7-server-x86_64-unknown-linux-musl` happens not to be a substring of any
-/// other asset today, but that is an accident of the current release contents,
-/// not a property anyone maintains — and a substring match that drifted would
+/// `tty7-server-linux-x86_64-musl` happens not to be a substring of any other
+/// asset today, but that is an accident of the current release contents, not a
+/// property anyone maintains — and a substring match that drifted would
/// silently verify one binary's bytes against another's digest.
///
/// The coreutils format is ``; the second space is `*`
@@ -239,9 +239,9 @@ mod tests {
#[test]
fn a_malformed_entry_aborts() {
for bad in [
- "abc tty7-server-x86_64-unknown-linux-musl",
- "zz786850e387550fdab836ed7e6dc881de23001b4b4d8ec3a1a0b9d5e0d5c0f1x tty7-server-x86_64-unknown-linux-musl",
- " tty7-server-x86_64-unknown-linux-musl",
+ "abc tty7-server-linux-x86_64-musl",
+ "zz786850e387550fdab836ed7e6dc881de23001b4b4d8ec3a1a0b9d5e0d5c0f1x tty7-server-linux-x86_64-musl",
+ " tty7-server-linux-x86_64-musl",
] {
let err = expected_digest(bad, ASSET_X86_64).unwrap_err();
assert!(
diff --git a/crates/tty7-core/src/daemon/install/mod.rs b/crates/tty7-core/src/daemon/install/mod.rs
index b9f2e09f..f2aef501 100644
--- a/crates/tty7-core/src/daemon/install/mod.rs
+++ b/crates/tty7-core/src/daemon/install/mod.rs
@@ -396,7 +396,7 @@ pub struct InstallRequest {
pub host: String,
/// The version about to be installed (= the client's own version).
pub version: String,
- /// The release asset name, e.g. `tty7-server-x86_64-unknown-linux-musl`.
+ /// The release asset name, e.g. `tty7-server-linux-x86_64-musl`.
pub asset: &'static str,
/// The URL it was downloaded from.
pub source_url: String,
diff --git a/crates/tty7-core/src/daemon/install/wsl.rs b/crates/tty7-core/src/daemon/install/wsl.rs
index 40f7a815..bbd0eb94 100644
--- a/crates/tty7-core/src/daemon/install/wsl.rs
+++ b/crates/tty7-core/src/daemon/install/wsl.rs
@@ -768,13 +768,24 @@ impl RemoteOps for WslRemoteOps {
/// Overrides where the bundled Linux server binaries are looked for. Exists so
/// this can be tested, and so a developer running an unpackaged build can point
/// at a `cargo build --target x86_64-unknown-linux-musl` output.
+///
+/// **The file in it has to carry the asset name**, not cargo's — [`Self::locate`]
+/// joins the directory with
+/// [`asset::ASSET_X86_64`](crate::daemon::install::asset::ASSET_X86_64) and
+/// nothing translates, so a cross-compile has to be copied to
+/// `tty7-server-linux-x86_64-musl` rather than left as plain `tty7-server`.
+///
+/// [`Self::locate`]: BundledServerBinary::locate
pub const BUNDLED_DIR_ENV: &str = "TTY7_BUNDLED_SERVER_DIR";
/// The subdirectory beside the client executable that a packaged build puts the
/// Linux server binaries in. **This is the contract with the release workflow**:
/// the Windows installer must place
-/// `/server/tty7-server-x86_64-unknown-linux-musl` (and the
-/// `aarch64` one, for WSL on ARM Windows).
+/// `/server/tty7-server-linux-x86_64-musl` (and the `aarch64` one,
+/// for WSL on ARM Windows) — the filenames
+/// [`asset::ASSET_X86_64`](crate::daemon::install::asset::ASSET_X86_64) names,
+/// because [`BundledServerBinary::locate`] joins the directory with the asset
+/// name and nothing translates between the two.
pub const BUNDLED_SUBDIR: &str = "server";
/// Directories a bundled Linux server binary is looked for in, most specific
@@ -1681,10 +1692,7 @@ mod tests {
.load("26.7.5", super::super::asset::ASSET_X86_64)
.expect_err("nothing bundled yet");
let msg = err.to_string();
- assert!(
- msg.contains("tty7-server-x86_64-unknown-linux-musl"),
- "{msg}"
- );
+ assert!(msg.contains("tty7-server-linux-x86_64-musl"), "{msg}");
assert!(msg.contains(&tmp.display().to_string()), "{msg}");
assert!(matches!(err, InstallError::MissingBundled { .. }));
@@ -2074,7 +2082,7 @@ mod tests {
.run()
.expect_err("no aarch64 binary bundled");
let msg = err.to_string();
- assert!(msg.contains("aarch64-unknown-linux-musl"), "{msg}");
+ assert!(msg.contains("linux-aarch64-musl"), "{msg}");
// With it bundled, the same install goes through.
std::fs::write(dir.join(super::super::asset::ASSET_AARCH64), b"\x7fELF arm").unwrap();
diff --git a/src/ui/remote_connect.rs b/src/ui/remote_connect.rs
index 89496160..9a40463b 100644
--- a/src/ui/remote_connect.rs
+++ b/src/ui/remote_connect.rs
@@ -1151,7 +1151,7 @@ mod tests {
InstallRequest {
host: "me@build-box:22".into(),
version: "0.9.1".into(),
- asset: "tty7-server-x86_64-unknown-linux-musl",
+ asset: "tty7-server-linux-x86_64-musl",
source_url: "https://example.invalid/v0.9.1/tty7-server".into(),
remote_path: "/home/me/.local/share/tty7/bin/tty7-server-0.9.1".into(),
size_bytes: 9_437_184,