Building Cross-Platform Rust Binaries: A Multi-Architecture Build Orchestration System
When developing ballistics-engine, a high-performance ballistics calculation library written in Rust, I faced a challenge: how do I efficiently build and distribute binaries for multiple operating systems and architectures? The answer led to the creation of an automated build orchestration system that leverages diverse hardware—from single-board computers to powerful x86_64 servers—to build native binaries for macOS, Linux, FreeBSD, NetBSD, and OpenBSD across both ARM64 and x86_64 architectures. Now, you are probably wondering why I am bothering to show love for the BSD Trilogy; the answer is simple: because I want to. Sure they are a bit esoteric, but I ran FreeBSD for years as my mail server. I still like the BSDs.
This article explores the architecture, implementation, and lessons learned from building a production-grade multi-platform build system that powers https://ballistics.zip, where users can download pre-built binaries for their platform with a simple curl command.
curl --proto '=https' --tlsv1.2 -sSf https://ballistics.zip/install.sh | sh
The Problem: Cross-Platform Distribution
Rust's cross-compilation capabilities are impressive, but they have limitations:
- Cross-compilation complexity: While Rust supports cross-compilation, getting it working reliably for BSD systems (especially with system dependencies) is challenging
- Native testing: You need to test on actual hardware to ensure binaries work correctly
- Binary compatibility: Different BSD versions and configurations require native builds
- Performance verification: Emulated builds may behave differently than native ones
The solution? Build natively on each target platform using actual hardware or high-performance emulation.
Architecture Overview
The build orchestration system consists of three main components:
1. Build Nodes (Physical and Virtual Machines)
- macOS systems (x86_64 and aarch64) - Local builds
- Linux x86_64 server - Remote build via SSH
- FreeBSD ARM64 - Single-board computer (Raspberry Pi 4)
- OpenBSD ARM64 - QEMU VM emulated on x86_64 (rig.localnet)
- NetBSD x86_64 and ARM64 - QEMU VMs
2. Orchestrator (Python-based coordinator)
- Reads build node configuration from
build-nodes.yaml - Executes builds in parallel across all nodes
- Collects artifacts via SSH/SCP
- Generates SHA256 checksums
- Uploads to Google Cloud Storage
- Updates version metadata
3. Distribution (ballistics.zip website)
- Serves install script at https://ballistics.zip
- Hosts binaries in GCS bucket (
gs://ballistics-releases/) - Provides version detection and automatic downloads
- Supports version fallback for platforms with delayed releases
Hardware Infrastructure
Single-Board Computers
- Role: Host for NetBSD ARM64 VM
- CPU: Rockchip RK3588 (8-core ARM Cortex-A76/A55)
- RAM: 16GB
- Why: Native ARM64 hardware for running QEMU VMs
- Host IP: 10.1.1.10
- VM IPs:
- NetBSD ARM64: 10.1.1.15
- OpenBSD ARM64 (native, disabled): 10.1.1.11
- Role: FreeBSD ARM64 native builds
- CPU: Broadcom BCM2711 (quad-core Cortex-A72)
- RAM: 8GB
- Why: Stable FreeBSD support, reliable ARM64 platform
- IP: 10.1.1.7
x86_64 ("rig.localnet")
- Role: Linux builds, BSD VM host, emulated ARM64 builds
- CPU: Intel i9
- RAM: 96GB
- IP: 10.1.1.27 (Linux host), 10.1.1.17 (KVM host)
- VMs Hosted:
- FreeBSD x86_64: 10.1.1.21
- OpenBSD x86_64: 10.1.1.20
- OpenBSD ARM64 (emulated): 10.1.1.23
- NetBSD x86_64: 10.1.1.19
Local macOS Development Machine
- Role: macOS binary builds (both architectures)
- Build Method: Local cargo builds with target flags
- Architectures:
-
aarch64-apple-darwin(Apple Silicon) -
x86_64-apple-darwin(Intel Macs)
A Surprising Discovery: Emulated ARM64 Performance
One of the most interesting findings during development was discovering that emulated ARM64 builds on powerful x86_64 hardware are significantly faster than emulated ARM64 on native ARM64 builds on single-board computers.
Performance Comparison
- Emulated ARM64 on ARM64: ~99+ minutes per build
- Emulated ARM64 on x86_64: 15m 37s ⚡
The emulated build on rig.localnet (running QEMU with KVM acceleration) completed in about 6x less time than the native ARM64 hardware. This is because:
- The x86_64 server has significantly more powerful CPU cores
- QEMU with KVM provides near-native performance for many workloads
- Rust compilation is primarily CPU-bound and benefits from faster single-core performance
- The x86_64 server has faster storage (NVMe vs eMMC/SD card)
As a result, the native OpenBSD ARM64 node on the Orange Pi is now disabled in favor of the emulated version.
Prerequisites
SSH Key-Based Authentication
Critical: The orchestration system requires passwordless SSH access to all remote build nodes. Here's how to set it up:
- Generate SSH key (if you don't have one):
ssh-keygen -t ed25519 -C "build-orchestrator"
- Copy public key to each build node:
# For each build node ssh-copy-id user@build-node-ip # Examples: ssh-copy-id alex@10.1.1.27 # Linux x86_64 ssh-copy-id freebsd@10.1.1.7 # FreeBSD ARM64 ssh-copy-id root@10.1.1.20 # OpenBSD x86_64 ssh-copy-id root@10.1.1.23 # OpenBSD ARM64 emulated ssh-copy-id root@10.1.1.19 # NetBSD x86_64 ssh-copy-id root@10.1.1.15 # NetBSD ARM64
- Test SSH access:
ssh user@build-node-ip "uname -a"
Software Requirements
On Build Orchestrator Machine:
- Python 3.8+
- pyyaml (
pip install pyyaml) - Google Cloud SDK (
gcloudcommand) for GCS uploads - SSH client
On Each Build Node:
- Rust toolchain (
cargo,rustc) - Build essentials (compiler, linker)
-
curl,wget, orftp(for downloading source) - Sufficient disk space (~2GB for build artifacts)
BSD-Specific Requirements
NetBSD: Install curl via pkgsrc (native ftp doesn't support HTTPS)
# Bootstrap pkgsrc cd /usr && ftp -o pkgsrc.tar.gz http://cdn.netbsd.org/pub/pkgsrc/current/pkgsrc.tar.gz tar -xzf pkgsrc.tar.gz cd /usr/pkgsrc/bootstrap && ./bootstrap --prefix=/usr/pkg # Install curl /usr/pkg/bin/pkgin -y update /usr/pkg/bin/pkgin -y install curl
OpenBSD: Native ftp supports HTTPS
pkg_add rust git
FreeBSD: Use pkg for everything
pkg install -y rust git curl
The ballistics.zip Website and Install Script
How It Works
https://ballistics.zip serves as the primary distribution point for pre-built ballistics-engine binaries. The system uses:
-
GCS Bucket:
-
gs://ballistics-releases/- Binary artifacts -
CDN: Google Cloud CDN provides global distribution
-
Install Script: Universal installer that:
-
Detects OS and architecture
- Downloads appropriate binary
- Verifies SHA256 checksum
- Installs to
/usr/local/bin
Usage
Basic installation:
curl -sSL https://ballistics.zip/install.sh | bash
Specific version:
curl -sSL https://ballistics.zip/install.sh | bash -s -- --version 0.13.3
Different install location:
curl -sSL https://ballistics.zip/install.sh | bash -s -- --prefix ~/.local
Install Script Architecture
The install.sh script intelligently handles:
Platform Detection:
OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$ARCH" in x86_64|amd64) ARCH="x86_64" ;; aarch64|arm64) ARCH="aarch64" ;; *) echo "Unsupported architecture: $ARCH"; exit 1 ;; esac PLATFORM="${OS}-${ARCH}" # e.g., "openbsd-aarch64"
Version Fallback: If a requested version isn't available for a platform, the script automatically finds the latest available version:
# If openbsd-aarch64 0.13.3 doesn't exist, fall back to 0.13.2 AVAILABLE_VERSION=$(curl -sL $BASE_URL/versions.txt | grep "^$PLATFORM:" | cut -d: -f2)
Checksum Verification:
EXPECTED_SHA=$(cat "$BINARY.sha256") ACTUAL_SHA=$(sha256sum "$BINARY" | awk '{print $1}') if [ "$EXPECTED_SHA" != "$ACTUAL_SHA" ]; then echo "Checksum verification failed!" exit 1 fi
Build Orchestration System Deep Dive
Configuration: build-nodes.yaml
The heart of the system is build-nodes.yaml, which defines all build targets:
nodes: # macOS builds (local machine) - name: macos-aarch64 host: local target: aarch64-apple-darwin build_command: | cd /tmp && rm -rf ballistics-engine-{version} curl -L -o v{version}.tar.gz https://github.com/ajokela/ballistics-engine/archive/refs/tags/v{version}.tar.gz tar xzf v{version}.tar.gz cd ballistics-engine-{version} cargo build --release --target {target} binary_path: /tmp/ballistics-engine-{version}/target/{target}/release/ballistics enabled: true # Linux x86_64 (remote via SSH) - name: linux-x86_64 host: alex@10.1.1.27 target: x86_64-unknown-linux-gnu build_command: | cd /tmp && rm -rf ballistics-engine-{version} wget -q https://github.com/ajokela/ballistics-engine/archive/refs/tags/v{version}.tar.gz tar xzf v{version}.tar.gz cd ballistics-engine-{version} ~/.cargo/bin/cargo build --release --target {target} binary_path: /tmp/ballistics-engine-{version}/target/{target}/release/ballistics enabled: true # OpenBSD ARM64 emulated (FASTEST ARM64 BUILD!) - name: openbsd-aarch64-emulated host: root@10.1.1.23 target: aarch64-unknown-openbsd build_command: | cd /tmp && rm -rf ballistics-engine-{version} ftp -o v{version}.tar.gz https://github.com/ajokela/ballistics-engine/archive/refs/tags/v{version}.tar.gz tar xzf v{version}.tar.gz cd ballistics-engine-{version} cargo build --release binary_path: /tmp/ballistics-engine-{version}/target/release/ballistics enabled: true # NetBSD x86_64 (HTTPS support via pkgsrc curl) - name: netbsd-x86_64 host: root@10.1.1.19 target: x86_64-unknown-netbsd build_command: | cd /tmp && rm -rf ballistics-engine-{version} /usr/pkg/bin/curl -L -o v{version}.tar.gz https://github.com/ajokela/ballistics-engine/archive/refs/tags/v{version}.tar.gz tar xzf v{version}.tar.gz cd ballistics-engine-{version} /usr/pkg/bin/cargo build --release binary_path: /tmp/ballistics-engine-{version}/target/release/ballistics enabled: true
Orchestrator Workflow
The orchestrator.py script coordinates the entire build process:
Step 1: Parallel Build Execution
def build_on_node(node, version): if node['host'] == 'local': # Local build subprocess.run(build_command, shell=True, check=True) else: # Remote build via SSH ssh_command = f"ssh {node['host']} '{build_command}'" subprocess.run(ssh_command, shell=True, check=True)
Step 2: Artifact Collection
def collect_artifacts(node, version): binary_name = f"ballistics-{version}-{node['name']}" if node['host'] == 'local': shutil.copy(node['binary_path'], f"./{binary_name}") else: # Download via SCP scp_command = f"scp {node['host']}:{node['binary_path']} ./{binary_name}" subprocess.run(scp_command, shell=True, check=True)
Step 3: Checksum Generation
def generate_checksum(binary_path): with open(binary_path, 'rb') as f: sha256 = hashlib.sha256(f.read()).hexdigest() with open(f"{binary_path}.sha256", 'w') as f: f.write(sha256)
Step 4: Upload to GCS
def upload_to_gcs(version): bucket_path = f"gs://ballistics-releases/{version}/" # Upload binaries and checksums subprocess.run(f"gsutil -m cp ballistics-* {bucket_path}", shell=True) # Set public read permissions subprocess.run(f"gsutil -m acl ch -u AllUsers:R {bucket_path}*", shell=True) # Update latest-version.txt with open('latest-version.txt', 'w') as f: f.write(version) subprocess.run("gsutil cp latest-version.txt gs://ballistics-releases/", shell=True)
Running a Build
Dry-run (test without uploading):
cd build-orchestrator ./build.sh --version 0.13.4 --dry-run
Production build:
./build.sh --version 0.13.4
Output:
Building ballistics-engine v0.13.4 =========================================== Enabled build nodes: 7 - macos-aarch64 (local) - macos-x86_64 (local) - linux-x86_64 (alex@10.1.1.27) - freebsd-aarch64 (freebsd@10.1.1.7) - openbsd-aarch64-emulated (root@10.1.1.23) - netbsd-x86_64 (root@10.1.1.19) - netbsd-aarch64 (root@10.1.1.15) Starting parallel builds... [macos-aarch64] Building... (PID: 12345) [linux-x86_64] Building... (PID: 12346) ... Build results: ✓ macos-aarch64 (45s) ✓ linux-x86_64 (28s) ✓ freebsd-aarch64 (6m 32s) ✓ openbsd-aarch64-emulated (15m 37s) ⚡ FASTEST ARM64! ... Uploading to gs://ballistics-releases/0.13.4/ ✓ Uploaded 7 binaries ✓ Uploaded 7 checksums ✓ Updated latest-version.txt Build complete! 🎉 Total time: 16m 12s
Adding New Build Nodes
Interactive Script
The easiest way to add a new node is using the interactive script:
cd build-orchestrator ./add-node.sh
This will prompt you for:
- Node name (e.g., openbsd-aarch64-emulated)
- SSH host (e.g., root@10.1.1.23 or local)
- Rust target triple (e.g., aarch64-unknown-openbsd)
- Build commands (how to download and build)
- Binary location (where the compiled binary is located)
Manual Configuration
Alternatively, edit build-nodes.yaml directly:
- name: your-new-platform host: user@ip-address # or 'local' for local builds target: rust-target-triple build_command: | # Commands to download source and build cd /tmp && rm -rf ballistics-engine-{version} curl -L -o v{version}.tar.gz https://github.com/... tar xzf v{version}.tar.gz cd ballistics-engine-{version} cargo build --release binary_path: /path/to/compiled/binary enabled: true
Variables:
- {version}: Replaced with target version (e.g., 0.13.4)
- {target}: Replaced with Rust target triple
Setting Up a New VM
Example: OpenBSD ARM64 Emulated
- Create VM on host:
ssh alex@rig.localnet cd /opt/bsd-vms/openbsd-arm64-emulated
- Create boot script:
cat > boot.sh << 'EOF' #!/bin/bash exec qemu-system-aarch64 \ -M virt,highmem=off \ -cpu cortex-a57 \ -smp 4 \ -m 2G \ -bios /usr/share/qemu-efi-aarch64/QEMU_EFI.fd \ -drive file=openbsd.qcow2,if=virtio,format=qcow2 \ -netdev bridge,id=net0,br=br0 \ -device virtio-net-pci,netdev=net0,romfile=,mac=52:54:00:12:34:99 \ -nographic EOF chmod +x boot.sh
- Create systemd service:
sudo cat > /etc/systemd/system/openbsd-arm64-emulated-vm.service << 'EOF' [Unit] Description=OpenBSD ARM64 VM (Emulated on x86_64) After=network.target [Service] Type=simple User=alex WorkingDirectory=/opt/bsd-vms/openbsd-arm64-emulated ExecStart=/opt/bsd-vms/openbsd-arm64-emulated/boot.sh Restart=always RestartSec=10 [Install] WantedBy=multi-user.target EOF sudo systemctl enable openbsd-arm64-emulated-vm.service sudo systemctl start openbsd-arm64-emulated-vm.service
-
Configure networking (assign static IP 10.1.1.23)
-
Install build tools inside VM:
ssh root@10.1.1.23 pkg_add rust git
- Test SSH access:
ssh root@10.1.1.23 "cargo --version"
- Add to build-nodes.yaml and test:
./build.sh --version 0.13.3 --dry-run
GitHub Webhook Integration (Optional)
For fully automated builds triggered by GitHub releases:
1. Deploy Webhook Receiver to Cloud Run
cd build-orchestrator gcloud run deploy ballistics-build-webhook \ --source . \ --region us-central1 \ --allow-unauthenticated \ --set-env-vars GITHUB_WEBHOOK_SECRET=your-secret-here
2. Configure GitHub Webhook
- Go to: https://github.com/yourusername/your-repo/settings/hooks
-
Add webhook:
-
Payload URL:
https://ballistics-build-webhook-xxx.run.app/webhook - Content type:
application/json - Secret: Your webhook secret
- Events: Select "Releases" only
3. Test
Create a new release on GitHub, and the webhook will automatically trigger builds for all platforms!
Performance Metrics and Insights
From real-world builds of ballistics-engine v0.13.3:
| Platform | Hardware | Build Time | Notes |
|---|---|---|---|
| macOS aarch64 | Apple M1/M2 | 45s | Native Apple Silicon |
| macOS x86_64 | Intel i7/i9 | 30s | Cross-compile on Apple Silicon |
| Linux x86_64 | Xeon/EPYC | 25s | Fastest overall ⚡ |
| FreeBSD aarch64 | Raspberry Pi 4 | 6m 32s | Native ARM64 hardware |
| OpenBSD aarch64 (emulated) | x86_64 QEMU | 15m 37s | ⚡ FASTEST ARM64 |
| OpenBSD aarch64 (native) | Orange Pi 5 Max | 99+ min | Disabled due to slower speed |
| NetBSD x86_64 | x86_64 VM | 3m 45s | KVM acceleration |
| NetBSD aarch64 | Orange Pi VM | 8m 12s | QEMU on ARM64 host |
Key Insights:
- x86_64 is fastest: Modern x86_64 CPUs dominate for single-threaded compilation
- Emulation wins for ARM64: x86_64 emulating ARM64 beats native ARM64 SBCs
- SBCs are viable: Raspberry Pi and Orange Pi work well for native builds, but slower
- Parallel execution: Running all 7 builds in parallel takes only ~16 minutes (longest pole is FreeBSD ARM64)
Conclusion
Building a custom multi-platform build orchestration system may seem daunting, but the benefits are substantial:
→ Full control: Own your build infrastructure
→ Native builds: Real hardware ensures compatibility
→ Cost-effective: Low operational costs after initial hardware investment
→ Fast iteration: Parallel builds complete in ~16 minutes
→ Flexibility: Easy to add new platforms
→ Learning: Deep understanding of cross-platform development
The surprising discovery that emulated ARM64 on powerful x86_64 hardware outperforms native ARM64 single-board computers has practical implications: you don't always need native hardware for every architecture. Strategic use of emulation can provide better performance while maintaining compatibility.
For projects requiring broad platform support (especially BSD systems not well-served by traditional CI/CD), this approach offers a reliable, maintainable, and cost-effective solution.
Architecture Diagram
v0.13.x] MANUAL[Manual Execution
./build.sh] end subgraph "Build Orchestrator" ORCH[Python Orchestrator
orchestrator.py] CONFIG[Build Configuration
build-nodes.yaml] end subgraph "Build Nodes - Local" MAC_ARM[macOS ARM64
Apple Silicon
~45s] MAC_X86[macOS x86_64
Rosetta 2
~30s] end subgraph "Build Nodes - Remote x86_64" LINUX_X86[Linux x86_64
alex@10.1.1.27
~25s] FREEBSD_X86[FreeBSD x86_64
root@10.1.1.21
~4m] OPENBSD_X86[OpenBSD x86_64
root@10.1.1.20
~12m] NETBSD_X86[NetBSD x86_64
root@10.1.1.19
~3m 45s] end subgraph "Build Nodes - Remote ARM64" FREEBSD_ARM[FreeBSD ARM64
freebsd@10.1.1.7
~6m 32s] OPENBSD_ARM_EMU[OpenBSD ARM64
root@10.1.1.23
Emulated on x86_64
~15m 37s ⚡] NETBSD_ARM[NetBSD ARM64
root@10.1.1.15
~8m 12s] end subgraph "Artifact Collection" COLLECT[SCP Collection
Pull binaries from nodes] CHECKSUM[Generate SHA256
checksums] end subgraph "Distribution" GCS[Google Cloud Storage
gs://ballistics-releases/] WEBSITE[ballistics.zip
Install Script] end GH -->|webhook| ORCH MANUAL -->|CLI| ORCH CONFIG -->|reads| ORCH ORCH -->|SSH parallel builds| MAC_ARM ORCH -->|SSH parallel builds| MAC_X86 ORCH -->|SSH parallel builds| LINUX_X86 ORCH -->|SSH parallel builds| FREEBSD_X86 ORCH -->|SSH parallel builds| OPENBSD_X86 ORCH -->|SSH parallel builds| NETBSD_X86 ORCH -->|SSH parallel builds| FREEBSD_ARM ORCH -->|SSH parallel builds| OPENBSD_ARM_EMU ORCH -->|SSH parallel builds| NETBSD_ARM MAC_ARM -->|binary| COLLECT MAC_X86 -->|binary| COLLECT LINUX_X86 -->|binary| COLLECT FREEBSD_X86 -->|binary| COLLECT OPENBSD_X86 -->|binary| COLLECT NETBSD_X86 -->|binary| COLLECT FREEBSD_ARM -->|binary| COLLECT OPENBSD_ARM_EMU -->|binary| COLLECT NETBSD_ARM -->|binary| COLLECT COLLECT --> CHECKSUM CHECKSUM --> GCS GCS --> WEBSITE style OPENBSD_ARM_EMU fill:#90EE90 style LINUX_X86 fill:#87CEEB style GCS fill:#FFD700 style WEBSITE fill:#FFD700
Diagram Legend
- Green: Fastest ARM64 build (emulated on powerful x86_64)
- Blue: Fastest overall build (native Linux x86_64)
- Yellow: Distribution endpoints
Build Flow
- Trigger: GitHub release webhook or manual execution
- Parallel Execution: All enabled build nodes start simultaneously
- Collection: Orchestrator collects binaries via SCP
- Verification: SHA256 checksums generated for integrity
- Upload: Binaries and checksums uploaded to GCS
- Availability: Install script immediately serves new version













The LattePanda IOTA booting up - x86 performance in a compact form factor
The LattePanda IOTA with PoE expansion board - compact yet feature-rich
Close-up showing the RP2040 co-processor, PoE module, and connectivity options










Figure 7: Example predicted CDM curves compared to ground truth measurements
Figure 8: Production inference performance metrics across different batch sizes