Mac remote builds
iOS XCTest cannot run on Windows or Linux. The DVAI-Bridge dev workflow solves this with an SSH-based wrapper: a small PowerShell script on the dev machine connects to a Mac, git pulls the worktree there, runs xcodebuild, and streams output back. CI uses the same Mac as a self-hosted GitHub Actions runner.
This page covers the developer-side setup. All values shown are placeholders — your Mac's hostname, IP, username, and repo path stay in a gitignored config file and never reach the repository.
Why we use it
- iOS XCTest requires a Mac runner. Renting CI minutes on hosted macOS is expensive and slow to spin up; a self-hosted Mac caches simulators and DerivedData and runs ~3× faster.
- Most contributors work primarily on Windows / Linux. Round-tripping to a Mac shell is the smallest unit of friction that keeps the ergonomics workable.
- CI uses the same Mac via a self-hosted ARM64 runner. Same machine, same toolchain, no "works on my CI doesn't work locally" surprises.
Setup (one-time)
1. Mac side
On the Mac that will host both your remote builds and the CI runner:
- Xcode 26+ installed via the App Store. Open Xcode once to accept the license and let it download the iOS SDK + the Metal toolchain.
- A simulator runtime that matches the destination string in
scripts/mac-side-build.sh(currentlyiOS 18.5, iPhone 16— adjust the script if you target a different runtime). - JDK 21 + Android command-line tools, only if you want to run Android instrumented tests on the Mac too. Most contributors run Android tests directly on their dev box.
- Clone the repo to a path of your choosing, e.g.
<path-to-repo-on-mac>/dvai-edge.
2. SSH key auth from dev box → Mac
# On the dev box (Windows: PowerShell or Git Bash; Linux: any shell):
ssh-keygen -t ed25519 -C "dvai-mac"
ssh-copy-id <your-username>@<example-mac-host>(On Windows without ssh-copy-id, paste the contents of ~/.ssh/id_ed25519.pub into ~/.ssh/authorized_keys on the Mac manually.)
Add an SSH alias in ~/.ssh/config:
Host <your-ssh-alias>
HostName <example-mac-host>
User <your-username>
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30Verify:
ssh <your-ssh-alias> 'uname -a && xcodebuild -version'3. Local config file
In the worktree, create scripts/mac.local.json. This file is gitignored (.gitignore already has scripts/mac.local.json and scripts/*.local.json and .env.local):
{
"sshAlias": "<your-ssh-alias>",
"repoPath": "<path-to-repo-on-mac>/dvai-edge"
}Or, equivalently, set environment variables instead of the file:
export DVAI_MAC_SSH_ALIAS=<your-ssh-alias>
export DVAI_MAC_REPO_PATH=<path-to-repo-on-mac>/dvai-edgeThe wrapper reads the file first, then falls back to env vars.
4. Mac-side helpers
scripts/mac-side-{build,test,clean}.sh are committed in the repo and run on the Mac (over SSH). You don't need to install or copy anything extra — git pull on the Mac picks them up.
Usage
From the dev box, in the worktree root:
# Build only (compile-check):
pwsh -File scripts/mac-build.ps1 -Action build -Target capacitor-llama
pwsh -File scripts/mac-build.ps1 -Action build -Target capacitor-foundation
pwsh -File scripts/mac-build.ps1 -Action build -Target capacitor-mediapipe
# Run tests:
pwsh -File scripts/mac-build.ps1 -Action test -Target capacitor-llama
pwsh -File scripts/mac-build.ps1 -Action test -Target capacitor-foundation
pwsh -File scripts/mac-build.ps1 -Action test -Target capacitor-mediapipe
# Filter to a single test:
pwsh -File scripts/mac-build.ps1 -Action test -Target capacitor-llama \
-Filter DVAICapacitorLlamaTests/LlamaHandlersTest/testStreamingFinishFrame
# Clean DerivedData / build artifacts:
pwsh -File scripts/mac-build.ps1 -Action clean -Target capacitor-llamaThe wrapper:
- Reads
mac.local.json(or env vars) for the SSH alias + repo path. - SSHes to the Mac and
git pull --ff-onlys the repo there. - Runs
bash scripts/mac-side-<action>.sh <target> [<filter>]. - Streams stdout / stderr back; exits with the remote exit code.
You commit and push from the dev box; the Mac pulls the same branch on each invocation. Don't edit on the Mac — drift between the two trees is the most common source of "passes on my Mac, fails in CI" surprises.
Self-hosted CI runner
The same Mac is registered as a GitHub Actions self-hosted runner. The workflows reference it via the generic label set [self-hosted, macOS, ARM64] — no machine-specific identifiers.
Registration:
- In the GitHub repo settings → Actions → Runners → "New self-hosted runner."
- Follow the OS-specific instructions on the Mac. Use the labels
self-hosted,macOS,ARM64. The token shown is single-use and expires within ~1 hour. - Run the runner as a launchd service:
./svc.sh install && ./svc.sh start. - Verify a workflow can pick it up —
gh workflow run test-ios-llama.ymlfrom any clone.
Troubleshooting
"xcodebuild: error: Result bundle path already exists"
mac-side-test.sh already runs rm -rf build/test-results.xcresult before each test pass. If you still hit this, the test was killed mid-run and a stale lock survived. SSH in and clear it manually:
ssh <your-ssh-alias> 'cd <path-to-repo-on-mac>/dvai-edge/packages/dvai-bridge-capacitor-llama/ios && rm -rf build/'Simulator runtime mismatch
xcodebuild: error: Unable to find a destination matching ...The destination string in scripts/mac-side-{build,test}.sh is hard-pinned (currently iPhone 16, iOS 18.5). If your Mac doesn't have that runtime installed, either install it via Xcode → Settings → Components, or override per-invocation:
ssh <your-ssh-alias> 'IOS_DEST="platform=iOS Simulator,name=iPhone 15,OS=17.5" \
bash <path-to-repo-on-mac>/dvai-edge/scripts/mac-side-test.sh capacitor-llama'Stale node_modules on the Mac
The Mac's pnpm install state is independent of yours. If a freshly added native dependency fails to resolve:
ssh <your-ssh-alias> 'cd <path-to-repo-on-mac>/dvai-edge && pnpm install'CI runner gone offline
Self-hosted runners go offline if the Mac sleeps. Disable App Nap and "Put hard disks to sleep when possible" in System Settings → Energy. For headless boxes, configure the Mac to wake-on-LAN and stay awake on power.
Privacy hardening
scripts/mac.local.jsonis gitignored. Never commit it.mac-build.ps1reads from the local file or env vars; the script itself contains no real hostnames, IPs, or paths.- Workflow YAMLs reference only the generic
[self-hosted, macOS, ARM64]label set. - The self-hosted runner registration token is single-use (~1 hour TTL). Even if leaked it cannot register a second runner.
If you find a real Mac identifier in any committed file, treat it as a bug and open a PR removing it.
See also
- Testing — full layer-by-layer test guide.
- Handler parity — why running Swift tests remotely matters.
