Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:
platform: darwin-arm64
- runner: ubuntu-24.04
platform: linux-x64
ssh-platform: linux/amd64
- runner: ubuntu-24.04-arm
platform: linux-arm64
ssh-platform: linux/arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
Expand All @@ -38,6 +42,8 @@ jobs:
if-no-files-found: error
- name: Real SSH acceptance on an offline host
if: runner.os == 'Linux'
env:
OXBIT_SSH_TEST_PLATFORM: ${{ matrix.ssh-platform }}
run: bun run remote:test
build:
needs: remote-runtime
Expand All @@ -51,6 +57,8 @@ jobs:
platform: darwin-arm64
- runner: ubuntu-24.04
platform: linux-x64
- runner: ubuntu-24.04-arm
platform: linux-arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/language-servers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
runner: [macos-26, ubuntu-24.04]
runner: [macos-26, ubuntu-24.04, ubuntu-24.04-arm]
runs-on: ${{ matrix.runner }}
env:
OXBIT_LSP_REPORT: evidence/language-milestone1/real-servers.json
Expand Down
63 changes: 62 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
platform: darwin-arm64
- runner: ubuntu-24.04
platform: linux-x64
- runner: ubuntu-24.04-arm
platform: linux-arm64
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -103,6 +105,65 @@ jobs:
name: remote-manifest
path: release-files/remote-runtime-manifest.json
if-no-files-found: error
# Linux arm64 is published when it builds; its failure does not block the release.
linux:
needs: remote-runtime
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
platform: linux-x64
- runner: ubuntu-24.04-arm
platform: linux-arm64
runs-on: ${{ matrix.runner }}
continue-on-error: ${{ matrix.platform == 'linux-arm64' }}
timeout-minutes: 120
env:
OXBIT_REMOTE_PAYLOADS: ${{ github.workspace }}/.desktop-cache/remote-payloads
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "24.20.0"
- uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.2"
- name: Linux native dependencies (Ubuntu 24.04 minimum)
run: |
sudo apt-get update
sudo apt-get install -y build-essential python3 pkg-config libssl-dev libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf libxdo-dev libfuse2t64 xdg-utils xvfb dbus-x11
- run: rustup toolchain install 1.97.1 --profile minimal
- run: bun install --frozen-lockfile
- run: bun run build
- uses: actions/download-artifact@v4
with:
pattern: remote-runtime-*
path: .desktop-cache/remote-payloads
- name: Build and sign the updater AppImage and .deb
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
OXBIT_UPDATER_PUBLIC_KEY: ${{ vars.OXBIT_UPDATER_PUBLIC_KEY }}
run: bun run desktop:release
- name: Installed package smoke checks
run: |
bundle=apps/desktop/src-tauri/target/release/bundle
sudo apt-get install -y ./"$bundle"/deb/*.deb
xvfb-run -a dbus-run-session -- bun run desktop:test:installed -- /usr/bin/oxbit-desktop
sudo apt-get remove -y "$(dpkg-deb --field "$bundle"/deb/*.deb Package)"
APPIMAGE_EXTRACT_AND_RUN=1 xvfb-run -a dbus-run-session -- node scripts/desktop/installed-smoke.mjs "$(realpath "$bundle"/appimage/*.AppImage)"
- name: Collect release files
run: |
bundle=apps/desktop/src-tauri/target/release/bundle
mkdir -p release-files
cp "$bundle"/deb/*.deb "$bundle"/appimage/*.AppImage "$bundle"/appimage/*.AppImage.sig release-files/
ls -l release-files
- uses: actions/upload-artifact@v4
with:
name: release-${{ matrix.platform }}
path: release-files/
if-no-files-found: error
# The pinned manifest records the darwin payload after the macos job signs and repacks it.
ios:
needs: [remote-runtime, macos]
Expand Down Expand Up @@ -181,7 +242,7 @@ jobs:
path: release-files/
if-no-files-found: error
publish:
needs: [macos, ios]
needs: [macos, ios, linux]
if: github.ref_type == 'tag' && github.repository == 'yannelli/oxbit'
runs-on: ubuntu-24.04
permissions:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/ssh-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function SshDialog({
</label>
<p>
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.
</p>
{error && <p role="alert">{error}</p>}
<div className="dialog-actions">
Expand Down
30 changes: 20 additions & 10 deletions apps/ios/src-tauri/src/remote_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -125,23 +126,32 @@ 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}");
}

#[test]
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());
}

Expand All @@ -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(),
Expand All @@ -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());
}
Expand Down
9 changes: 7 additions & 2 deletions apps/ios/src-tauri/src/ssh/live_runtime_task_tests.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand Down Expand Up @@ -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();
Expand Down
17 changes: 13 additions & 4 deletions apps/ios/src-tauri/src/ssh/live_runtime_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ use std::{

pub(super) type Seen = Arc<Mutex<Vec<Event>>>;

/// 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.
Expand All @@ -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| {
Expand Down Expand Up @@ -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!(
Expand Down
3 changes: 2 additions & 1 deletion apps/ios/src-tauri/src/ssh/runtime_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)),
}
}
Expand Down
9 changes: 7 additions & 2 deletions apps/ios/src-tauri/src/ssh/runtime_protocol_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ fn frame(value: Value) -> Vec<u8> {
#[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!(
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion apps/ios/src/ssh-connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function SshConnect({ preset, mode = "files", progress, lastFolders = {},
event.preventDefault();
if (host && !busy) perform(() => connect(host));
}}>
{runtime && <p>Oxbit installs its runtime on the server and runs terminals, tasks, and agents there. Linux x64 and macOS Apple Silicon servers are supported.</p>}
{runtime && <p>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.</p>}
{hosts && !hosts.length ? <p>Add a server in SSH Hosts and Keys first.</p> : <>
<label>Server<select aria-label="Server" value={hostId} onChange={event => {
setHostId(event.target.value);
Expand Down
Loading
Loading