From 0adbbb6924bb99acbcec3f112b77b516c2e7a7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 7 Jun 2026 06:13:53 +0200 Subject: [PATCH 1/4] feat: add linux-arm64 build target (aarch64 cross-compile) Pairs with the perry compiler's new linux-arm64 target so this worker can produce aarch64 Linux server binaries (e.g. for an AWS Lambda arm64 matrix), alongside the existing x86_64 linux target. - BuildTarget::LinuxArm64 variant; determine_target maps "linux-arm64"/"linux-aarch64" to it. - compiler_target = Some("linux-arm64") so perry gets --target and cross-compiles (unlike native x86_64 linux which passes no --target). Falling through to BuildTarget::Linux would have silently produced an x86_64 binary. - Packaging reuses run_linux_pipeline (the binary is just aarch64). - compiler.rs: cross env for cc-rs (CC/CXX/AR_aarch64_unknown_linux_gnu + cargo linker var) so user-project native C/C++ deps build for aarch64. The build container must provide gcc-aarch64-linux-gnu. - Advertise "linux-arm64" capability so the hub routes such jobs here. --- src/build/compiler.rs | 21 +++++++++++++++++++++ src/build/pipeline.rs | 9 ++++++++- src/worker.rs | 2 +- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/build/compiler.rs b/src/build/compiler.rs index 3db58db..7177206 100644 --- a/src/build/compiler.rs +++ b/src/build/compiler.rs @@ -396,6 +396,27 @@ async fn compile_in_docker( cmd.arg("-e").arg(format!("CFLAGS_x86_64_pc_windows_msvc={cflags}")); cmd.arg("-e").arg(format!("CXXFLAGS_x86_64_pc_windows_msvc={cflags}")); } + Some("linux-arm64") | Some("linux-aarch64") => { + // aarch64 Linux cross-compile from an x86_64 host. Point cc-rs at + // the GNU cross toolchain so user-project native C/C++ deps build + // for aarch64 instead of the host. perry's own final link also uses + // aarch64-linux-gnu-gcc (see perry link/platform_cmd.rs), so the + // build container must provide the `gcc-aarch64-linux-gnu` package. + for cc_var in &["CC_aarch64_unknown_linux_gnu", "CC_aarch64-unknown-linux-gnu"] { + cmd.arg("-e").arg(format!("{cc_var}=aarch64-linux-gnu-gcc")); + } + for cxx_var in &["CXX_aarch64_unknown_linux_gnu", "CXX_aarch64-unknown-linux-gnu"] { + cmd.arg("-e").arg(format!("{cxx_var}=aarch64-linux-gnu-g++")); + } + for ar_var in &["AR_aarch64_unknown_linux_gnu", "AR_aarch64-unknown-linux-gnu"] { + cmd.arg("-e").arg(format!("{ar_var}=aarch64-linux-gnu-ar")); + } + // cargo's own linker var for the aarch64 leg (build-deps / proc-macro + // stay on host; only the target leg is redirected). + cmd.arg("-e").arg( + "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc", + ); + } _ => {} } diff --git a/src/build/pipeline.rs b/src/build/pipeline.rs index 8f3a034..26a2aef 100644 --- a/src/build/pipeline.rs +++ b/src/build/pipeline.rs @@ -79,6 +79,9 @@ async fn run_pipeline( BuildTarget::Ios => Some("ios"), BuildTarget::Macos => Some("macos"), BuildTarget::Tvos => Some("tvos"), + // aarch64 Linux is a cross-compile even on a Linux host, so it must + // pass an explicit --target (unlike native x86_64 Linux below). + BuildTarget::LinuxArm64 => Some("linux-arm64"), BuildTarget::Linux => None, // native compilation on Linux host }; // For ios-game-loop: swap the runtime with the game-loop variant @@ -140,7 +143,7 @@ async fn run_pipeline( } match target { - BuildTarget::Linux => { + BuildTarget::Linux | BuildTarget::LinuxArm64 => { run_linux_pipeline(request, cancelled, progress, tmpdir, &actual_binary, &project_dir) .await } @@ -722,6 +725,7 @@ fn copy_artifact( #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum BuildTarget { Linux, + LinuxArm64, Android, Windows, Ios, @@ -737,6 +741,9 @@ fn determine_target(targets: &[String]) -> BuildTarget { "ios" => return BuildTarget::Ios, "macos" => return BuildTarget::Macos, "tvos" => return BuildTarget::Tvos, + // aarch64 Linux: cross-compiled (the builder host is x86_64) and + // packaged the same as x86_64 Linux — the binary is just aarch64. + "linux-arm64" | "linux-aarch64" => return BuildTarget::LinuxArm64, _ => {} } } diff --git a/src/worker.rs b/src/worker.rs index 6f47ec9..b53bd03 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -543,7 +543,7 @@ async fn connect_and_run(config: &WorkerConfig) -> Result<(), String> { // Send worker_hello let perry_version = get_perry_version(&config.perry_binary); let hello = WorkerMessage::WorkerHello { - capabilities: vec!["linux".into(), "android".into(), "windows".into(), "ios".into(), "macos".into(), "tvos".into()], + capabilities: vec!["linux".into(), "linux-arm64".into(), "android".into(), "windows".into(), "ios".into(), "macos".into(), "tvos".into()], name: config.worker_name.clone().unwrap_or_else(|| { hostname::get() .map(|h| h.to_string_lossy().to_string()) From 9dfb591dcf1f5c41029de430792ad327a689ade1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 7 Jun 2026 16:34:26 +0200 Subject: [PATCH 2/4] fix(tvos): route precompiled tvOS bundle to the sign worker #16 tagged the tvOS artifact as ios-precompiled (so the .app cross-built on Linux gets signed -> .ipa -> App Store Connect on the macOS sign worker), but the build-complete message's needs_finishing match only covered windows/ios/macos -> tvOS fell through to None. The hub then treated the tvOS build as fully done and never dispatched the sign step, so the publish hung waiting for a signed artifact that never came. Add "tvos" => Some("ios") so tvOS finishes via the same ios-sign path its precompiled bundle is already routed to (verified: iOS signs fine on that worker; tvOS now follows the identical hand-off). --- src/worker.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/worker.rs b/src/worker.rs index b53bd03..6dae58a 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -862,6 +862,7 @@ async fn handle_build( "windows" => Some("windows"), "ios" => Some("ios"), "macos" => Some("macos"), + "tvos" => Some("ios"), _ => None, }, "artifacts": [{"name": artifact_name, "size": size, "sha256": sha256}] From 347924a203e153fcaa4de005413a5390b9d6e0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 7 Jun 2026 17:22:57 +0200 Subject: [PATCH 3/4] fix(tvos/watchos): route precompiled bundles to platform-specific sign jobs Emit needs_finishing tvos/watchos (not flatten to ios) so the hub creates tvos-sign / watchos-sign jobs; the macOS sign worker now has dedicated TvosSign/WatchosSign pipelines (skip iOS icon mangling, altool --type tvos). Supersedes the earlier ios-routing stopgap. --- src/worker.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/worker.rs b/src/worker.rs index 6dae58a..f5a11a0 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -823,6 +823,7 @@ async fn handle_build( // re-queued for signing — the unsigned .app was returned as a // "final" artifact and never reached TestFlight. "tvos" => "ios-precompiled", + "watchos" => "ios-precompiled", "macos" => "macos-precompiled", other => other, }; @@ -862,7 +863,8 @@ async fn handle_build( "windows" => Some("windows"), "ios" => Some("ios"), "macos" => Some("macos"), - "tvos" => Some("ios"), + "tvos" => Some("tvos"), + "watchos" => Some("watchos"), _ => None, }, "artifacts": [{"name": artifact_name, "size": size, "sha256": sha256}] From 586b5dce11395ef249782a0027227119afd0a35b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Mon, 8 Jun 2026 09:45:59 +0200 Subject: [PATCH 4/4] feat: advertise watchos capability (route watchos build jobs to worker) --- src/worker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/worker.rs b/src/worker.rs index f5a11a0..46fc49a 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -543,7 +543,7 @@ async fn connect_and_run(config: &WorkerConfig) -> Result<(), String> { // Send worker_hello let perry_version = get_perry_version(&config.perry_binary); let hello = WorkerMessage::WorkerHello { - capabilities: vec!["linux".into(), "linux-arm64".into(), "android".into(), "windows".into(), "ios".into(), "macos".into(), "tvos".into()], + capabilities: vec!["linux".into(), "linux-arm64".into(), "android".into(), "windows".into(), "ios".into(), "macos".into(), "tvos".into(), "watchos".into()], name: config.worker_name.clone().unwrap_or_else(|| { hostname::get() .map(|h| h.to_string_lossy().to_string())