From 202fa9940401d127a660032bc1b985be6f27ccc0 Mon Sep 17 00:00:00 2001 From: Ryan Yannelli Date: Fri, 9 Oct 2026 01:11:31 -0400 Subject: [PATCH 1/3] Add the linux-arm64 remote runtime and accept arm64 SSH hosts - Pin Node 24.20.0 and ripgrep 15.1.0 for linux-arm64; remote:prepare builds darwin-arm64, linux-x64, and linux-arm64. A Linux host reuses its native node-pty build; other Linux targets compile in Docker with per-platform node:24.20.0-bookworm manifest digests. - The release manifest, the iOS pinned-manifest parser, and their tests require all three platforms. - Desktop and iOS SSH map Linux/aarch64 to linux-arm64 and name arm64 in the unsupported-host message and connection dialogs. - OXBIT_SSH_TEST_PLATFORM=linux/arm64 runs remote:test and ios:ssh-live against an arm64 sshd container with the linux-arm64 payload. bundle-tools.mjs reads the per-arch linuxBundleTools lists from binaries.json. binaries.json lists Tauri's AppImage helpers per arch (x64, arm64) and refreshes the x86_64 linuxdeploy and linuxdeploy-plugin-appimage pins, which changed upstream under their mutable release tags. --- README.md | 4 +- apps/desktop/src/ssh-dialog.tsx | 2 +- apps/ios/src-tauri/src/remote_runtime.rs | 30 ++++--- .../src/ssh/live_runtime_task_tests.rs | 9 +- .../src-tauri/src/ssh/live_runtime_tests.rs | 17 +++- .../ios/src-tauri/src/ssh/runtime_protocol.rs | 3 +- .../src/ssh/runtime_protocol_tests.rs | 9 +- apps/ios/src/ssh-connect.tsx | 2 +- apps/runtime/src/ssh.ts | 11 +-- docs/ios-ssh.md | 4 +- docs/remote-ssh.md | 27 ++++-- scripts/desktop/binaries.json | 90 +++++++++++++------ scripts/desktop/bundle-tools.mjs | 9 +- scripts/ios/ssh-live.mjs | 16 ++-- scripts/remote/prepare.mjs | 32 ++++--- scripts/remote/release-manifest.mjs | 2 +- scripts/remote/smoke.ts | 16 +++- tests/ios/ssh-runtime.spec.ts | 4 +- tests/remote-release-manifest.test.ts | 14 +-- 19 files changed, 207 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 68791560..99e444c0 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,11 @@ allowed origins, and storage. ### Desktop The Tauri desktop app bundles Node, ripgrep, the terminal addon, and managed -language services. It targets Apple Silicon with macOS 26+ and Ubuntu 24.04+ x64. +language services. It targets Apple Silicon with macOS 26+ and Ubuntu 24.04+ x64 and arm64. Run `bun run desktop:dev` for development or `bun run desktop:build` for local installers. Use `oxbit --desktop [path]` to open an installed app. -**Connect over SSH…** opens a remote folder on Linux x64 or macOS Apple Silicon. +**Connect over SSH…** opens a remote folder on Linux x64, Linux arm64, or macOS Apple Silicon. Oxbit installs its headless runtime through SSH; the remote host does not need Node or npm for the bundled runtime. diff --git a/apps/desktop/src/ssh-dialog.tsx b/apps/desktop/src/ssh-dialog.tsx index f00e9248..adf35ad2 100644 --- a/apps/desktop/src/ssh-dialog.tsx +++ b/apps/desktop/src/ssh-dialog.tsx @@ -101,7 +101,7 @@ export function SshDialog({

Uses your SSH config and keys. Connect once in a terminal to verify a - new host. Supports Linux x64 and macOS Apple Silicon. + new host. Supports Linux x64 and arm64, and macOS Apple Silicon.

{error &&

{error}

}
diff --git a/apps/ios/src-tauri/src/remote_runtime.rs b/apps/ios/src-tauri/src/remote_runtime.rs index 7588a186..590bb3d5 100644 --- a/apps/ios/src-tauri/src/remote_runtime.rs +++ b/apps/ios/src-tauri/src/remote_runtime.rs @@ -5,7 +5,7 @@ use std::collections::BTreeMap; include!(concat!(env!("OUT_DIR"), "/remote_runtime_manifest.rs")); -pub const PLATFORMS: [&str; 2] = ["darwin-arm64", "linux-x64"]; +pub const PLATFORMS: [&str; 3] = ["darwin-arm64", "linux-x64", "linux-arm64"]; const RELEASE_DOWNLOADS: &str = "https://github.com/yannelli/oxbit/releases/download"; #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -99,20 +99,21 @@ mod tests { const DARWIN: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; const LINUX: &str = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"; + const ARM: &str = "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; fn manifest(version: &str, platforms: &str) -> String { format!(r#"{{"version":"{version}","platforms":{{{platforms}}}}}"#) } - fn both() -> String { + fn all() -> String { format!( - r#""darwin-arm64":{{"sha256":"{DARWIN}","size":52000000}},"linux-x64":{{"sha256":"{LINUX}","size":48000000}}"# + r#""darwin-arm64":{{"sha256":"{DARWIN}","size":52000000}},"linux-x64":{{"sha256":"{LINUX}","size":48000000}},"linux-arm64":{{"sha256":"{ARM}","size":47000000}}"# ) } #[test] fn parses_a_release_manifest_and_builds_download_urls() { - let parsed = parse(&manifest("0.4.0-alpha.1", &both()), "0.4.0-alpha.1").unwrap(); + let parsed = parse(&manifest("0.4.0-alpha.1", &all()), "0.4.0-alpha.1").unwrap(); assert_eq!( parsed.download("linux-x64").unwrap(), Download { @@ -125,12 +126,20 @@ mod tests { parsed.download("darwin-arm64").unwrap().url, "https://github.com/yannelli/oxbit/releases/download/v0.4.0-alpha.1/remote-runtime-darwin-arm64.tar.gz" ); + assert_eq!( + parsed.download("linux-arm64").unwrap(), + Download { + url: "https://github.com/yannelli/oxbit/releases/download/v0.4.0-alpha.1/remote-runtime-linux-arm64.tar.gz".into(), + sha256: ARM.into(), + size: 47_000_000, + } + ); assert!(parsed.download("darwin-x64").is_err()); } #[test] fn rejects_a_manifest_for_another_version() { - let error = parse(&manifest("0.3.3", &both()), "0.3.4").unwrap_err(); + let error = parse(&manifest("0.3.3", &all()), "0.3.4").unwrap_err(); assert!(error.contains("0.3.3"), "{error}"); } @@ -138,10 +147,11 @@ mod tests { fn rejects_missing_or_unknown_platforms() { let darwin_only = format!(r#""darwin-arm64":{{"sha256":"{DARWIN}","size":1}}"#); assert!(parse(&manifest("0.3.4", &darwin_only), "0.3.4").is_err()); - let with_intel = format!( - r#"{},"darwin-x64":{{"sha256":"{DARWIN}","size":1}}"#, - both() + let without_arm = format!( + r#""darwin-arm64":{{"sha256":"{DARWIN}","size":1}},"linux-x64":{{"sha256":"{LINUX}","size":1}}"# ); + assert!(parse(&manifest("0.3.4", &without_arm), "0.3.4").is_err()); + let with_intel = format!(r#"{},"darwin-x64":{{"sha256":"{DARWIN}","size":1}}"#, all()); assert!(parse(&manifest("0.3.4", &with_intel), "0.3.4").is_err()); } @@ -154,7 +164,7 @@ mod tests { (DARWIN, "0"), ] { let platforms = format!( - r#""darwin-arm64":{{"sha256":"{digest}","size":{size}}},"linux-x64":{{"sha256":"{LINUX}","size":1}}"# + r#""darwin-arm64":{{"sha256":"{digest}","size":{size}}},"linux-x64":{{"sha256":"{LINUX}","size":1}},"linux-arm64":{{"sha256":"{ARM}","size":1}}"# ); assert!( parse(&manifest("0.3.4", &platforms), "0.3.4").is_err(), @@ -163,7 +173,7 @@ mod tests { } let extra = format!( r#"{{"version":"0.3.4","schema":1,"platforms":{{{}}}}}"#, - both() + all() ); assert!(parse(&extra, "0.3.4").is_err()); } diff --git a/apps/ios/src-tauri/src/ssh/live_runtime_task_tests.rs b/apps/ios/src-tauri/src/ssh/live_runtime_task_tests.rs index 2ae34f6b..f11d858d 100644 --- a/apps/ios/src-tauri/src/ssh/live_runtime_task_tests.rs +++ b/apps/ios/src-tauri/src/ssh/live_runtime_task_tests.rs @@ -1,7 +1,7 @@ //! Live remote runtime cases on the Docker sshd fixture: the wget install, a task service reached //! through `taskForward`, and the relaunch budget after repeated failures. use super::{ - live_runtime_tests::{live, payload, probe, source, start, Live, Seen}, + live_runtime_tests::{live, payload, probe, remote_platform, source, start, Live, Seen}, live_tests::{docker, exec}, runtime_process::Event, runtime_protocol::{Downloader, Install}, @@ -68,7 +68,12 @@ async fn live_runtime_wget_tasks_and_budget() { } = live().await; let (_, sha256) = payload(); let node = format!("/root/.oxbit/remote/runtimes/{sha256}/bin/node"); - let hosted = || source(format!("{served}/linux-x64.tar.gz"), scratch.clone()); + let hosted = || { + source( + format!("{served}/{}.tar.gz", remote_platform()), + scratch.clone(), + ) + }; exec(&container, "mv /usr/bin/curl /usr/bin/curl.off"); let seen: Seen = Arc::default(); diff --git a/apps/ios/src-tauri/src/ssh/live_runtime_tests.rs b/apps/ios/src-tauri/src/ssh/live_runtime_tests.rs index 83cc5821..089c743f 100644 --- a/apps/ios/src-tauri/src/ssh/live_runtime_tests.rs +++ b/apps/ios/src-tauri/src/ssh/live_runtime_tests.rs @@ -21,15 +21,21 @@ use std::{ pub(super) type Seen = Arc>>; +/// The fixture's runtime platform; scripts/ios/ssh-live.mjs sets it from the container architecture. +pub(super) fn remote_platform() -> String { + std::env::var("OXBIT_SSH_TEST_REMOTE_PLATFORM").unwrap_or_else(|_| "linux-x64".into()) +} + pub(super) fn payload() -> (PathBuf, String) { let directory = PathBuf::from(env("OXBIT_SSH_TEST_PAYLOAD_DIR")); let manifest: serde_json::Value = serde_json::from_slice(&std::fs::read(directory.join("manifest.json")).unwrap()).unwrap(); - let sha256 = manifest["platforms"]["linux-x64"]["sha256"] + let platform = remote_platform(); + let sha256 = manifest["platforms"][&platform]["sha256"] .as_str() .unwrap() .to_string(); - (directory.join("linux-x64.tar.gz"), sha256) + (directory.join(format!("{platform}.tar.gz")), sha256) } /// `url` is what the host downloads; the device side copies the local archive. @@ -39,7 +45,7 @@ pub(super) fn source(url: String, scratch: PathBuf) -> Source { let download = Download { url, sha256, size }; Source { lookup: Arc::new(move |platform| { - assert_eq!(platform, "linux-x64"); + assert_eq!(platform, remote_platform()); Ok(download.clone()) }), fetch: Arc::new(move |download| { @@ -222,7 +228,10 @@ async fn live_remote_runtime() { exec(&container, "rm -rf /root/.oxbit/remote/runtimes"); let seen: Seen = Arc::default(); - let hosted = source(format!("{served}/linux-x64.tar.gz"), scratch.clone()); + let hosted = source( + format!("{served}/{}.tar.gz", remote_platform()), + scratch.clone(), + ); let runtime = start(&pool, "live", hosted.clone(), &seen).await; let ready = runtime.ensure(true).await.unwrap(); assert_eq!( diff --git a/apps/ios/src-tauri/src/ssh/runtime_protocol.rs b/apps/ios/src-tauri/src/ssh/runtime_protocol.rs index 2f301e75..06e1455b 100644 --- a/apps/ios/src-tauri/src/ssh/runtime_protocol.rs +++ b/apps/ios/src-tauri/src/ssh/runtime_protocol.rs @@ -5,10 +5,11 @@ use crate::fs_core::{Error, Result}; pub fn platform(system: &str, machine: &str) -> Result<&'static str> { match (system.trim(), machine.trim()) { ("Linux", "x86_64") => Ok("linux-x64"), + ("Linux", "aarch64") => Ok("linux-arm64"), ("Darwin", "arm64") => Ok("darwin-arm64"), _ => Err(Error::new( "REMOTE_UNSUPPORTED", - "Remote SSH supports Linux x64 (glibc) and macOS Apple Silicon.", + "Remote SSH supports Linux x64 and arm64 (glibc) and macOS Apple Silicon.", )), } } diff --git a/apps/ios/src-tauri/src/ssh/runtime_protocol_tests.rs b/apps/ios/src-tauri/src/ssh/runtime_protocol_tests.rs index 33eb9a39..fcb4de85 100644 --- a/apps/ios/src-tauri/src/ssh/runtime_protocol_tests.rs +++ b/apps/ios/src-tauri/src/ssh/runtime_protocol_tests.rs @@ -13,16 +13,17 @@ fn frame(value: Value) -> Vec { #[test] fn maps_uname_to_the_desktop_platform_names() { assert_eq!(platform("Linux", "x86_64").unwrap(), "linux-x64"); + assert_eq!(platform("Linux", "aarch64").unwrap(), "linux-arm64"); assert_eq!(platform("Darwin\n", "arm64\n").unwrap(), "darwin-arm64"); for (system, machine) in [ - ("Linux", "aarch64"), + ("Linux", "armv7l"), ("Darwin", "x86_64"), ("FreeBSD", "amd64"), ] { let error = platform(system, machine).unwrap_err(); assert_eq!( error.message, - "Remote SSH supports Linux x64 (glibc) and macOS Apple Silicon." + "Remote SSH supports Linux x64 and arm64 (glibc) and macOS Apple Silicon." ); } assert_eq!( @@ -33,6 +34,10 @@ fn maps_uname_to_the_desktop_platform_names() { parse_probe("Darwin\narm64\nwget\n").unwrap(), ("darwin-arm64", Some(Downloader::Wget)) ); + assert_eq!( + parse_probe("Linux\naarch64\ncurl\n").unwrap(), + ("linux-arm64", Some(Downloader::Curl)) + ); assert_eq!( parse_probe("Linux\nx86_64\nnone\n").unwrap(), ("linux-x64", None) diff --git a/apps/ios/src/ssh-connect.tsx b/apps/ios/src/ssh-connect.tsx index 47baa9ba..5cb3635e 100644 --- a/apps/ios/src/ssh-connect.tsx +++ b/apps/ios/src/ssh-connect.tsx @@ -106,7 +106,7 @@ export function SshConnect({ preset, mode = "files", progress, lastFolders = {}, event.preventDefault(); if (host && !busy) perform(() => connect(host)); }}> - {runtime &&

Oxbit installs its runtime on the server and runs terminals, tasks, and agents there. Linux x64 and macOS Apple Silicon servers are supported.

} + {runtime &&

Oxbit installs its runtime on the server and runs terminals, tasks, and agents there. Linux x64 and arm64 servers and macOS Apple Silicon servers are supported.

} {hosts && !hosts.length ?

Add a server in SSH Hosts and Keys first.

: <>