Local Build Box (self-hosted EAS builds on lbb)
Categories:
Local Build Box (lbb)
Mobile builds are moving off EAS cloud to a self-hosted Mac Studio — lbb.thephenom.app (Logan’s machine) — using eas build --local. This reuses the existing eas.json profiles, runs entirely on our own hardware, and costs nothing on Expo.
Why
- Free / off-quota.
eas build --localruns the build on the box, not Expo’s servers. Verified empirically: a full local Android build (which even fetches the keystore from EAS) creates no build record and does not decrement the EAS build quota. Only cloud builds are metered. (Expo’s docs don’t state this explicitly; the account usage proves it.) - No upload limits. Because nothing is uploaded, the EAS Build 2 GB project-archive limit — and the workarounds for the 6 GB c2pa Rust
target/— simply don’t apply. - Fast. The box is a Mac Studio M4 Max, 16-core, 64 GB RAM, ~2.3 TB free (macOS 26). It’s also the right host for iOS (native
xcodebuild→ TestFlight).
Access
The developer build flow does not start with a direct SSH login from your laptop. It goes through Phenom’s C.O.D.E environment:
- Open
code.thephenom.appin a browser and sign in via Cognito. This lands you in your own persistent browser-based VS Code (“box”), already running on the same Mac Studio that hosts the build toolchain below. See the C.O.D.E runbook for the login flow and environment details — this doc doesn’t duplicate them. - From the integrated terminal inside that box, hop to the bare host over the internal
network:
This lands you in the sharedssh builder@host.orb.internalbuilderaccount. Key-based access is set up per-developer by appending your public key to/Users/builder/.ssh/authorized_keys(needs sudo, or the builder password) — ask in#devif you don’t have access yet. - Run the build commands documented below (
expo prebuild,./gradlew bundleRelease, …) from there, underbuilder, against/Users/builder/PhenomApp.
Accounts on the machine: personal logins, plus a dedicated builder account for CI/build
work — the one used for the build flow above.
Toolchain
The Android toolchain is installed self-contained under /Users/Shared (not via Homebrew — /opt/homebrew is owned by another user, so brew is off-limits). Shared location so any build account can use it:
| Component | Location / version |
|---|---|
| JDK | 17 (Temurin) → /Users/Shared/jdk-17/Contents/Home |
| Android SDK | /Users/Shared/android-sdk |
| — platforms | android-34, android-35 |
| — build-tools | 35.0.0 |
| — NDK | 27.1.12297006 |
| — cmake | 3.22.1 |
| — platform-tools | adb |
| Node / bun / eas-cli | system-wide (node, bun, eas-cli) |
Build accounts set in their shell profile (~/.zprofile):
export JAVA_HOME=/Users/Shared/jdk-17/Contents/Home
export ANDROID_HOME=/Users/Shared/android-sdk
export ANDROID_SDK_ROOT="$ANDROID_HOME"
export PATH="$ANDROID_HOME/platform-tools:$JAVA_HOME/bin:$PATH"
Building
From an app checkout on the box, with an Expo login (eas login, or an EXPO_TOKEN robot token — preferred for the shared builder account):
Use the prebuild + Gradle route below, not eas build --local. As of 2026-08-06 the
latter cannot build this app at all, and it leaks the production signing key on every run.
Both problems are described under Why not eas build --local.
cd ~/PhenomApp/PhenomApp
# 1. generate the native project in place (never relocate it — see below)
bunx expo prebuild --platform android --clean --no-install
# 2. Sentry source-map upload needs an auth token the box does not have.
# eas.json's play-internal profile sets these; raw Gradle does not inherit them.
export SENTRY_DISABLE_AUTO_UPLOAD=true
export SENTRY_ALLOW_FAILURE=true
# 3. build the signed AAB
cd android && ./gradlew bundleRelease --no-daemon
# output: android/app/build/outputs/bundle/release/app-release.aab
Prebuild writes the release signingConfig straight from credentials.json, so the AAB comes
out signed with the production upload key (D1:E5:F1:04:…, CN=Project Phenom) with no extra
wiring. Verify before uploading:
keytool -printcert -jarfile app-release.aab | grep SHA1
Why not eas build --local
It cannot build the app. eas build --local copies the project to a temp directory before
building. Expo’s DOM-component transformer bakes an absolute path to PhenomGlobe.tsx (the
"use dom" MapLibre component from #559), and that path does not survive relocation:
[EAGER_BUNDLE] Unable to resolve module …/Users/builder/PhenomApp/PhenomApp/app/components/phenom/map/PhenomGlobe.tsx
Not a CLI-version issue — reproduced identically on eas-cli 20.3.0 and 21.4.0, in a clean
worktree and in the primary checkout. The bundle step itself is fine; running
expo export:embed --eager --platform android in place bundles all 4009 modules plus the DOM
component in under three seconds. Only the relocation breaks it.
It leaks the production signing key. eas build --local passes credentials to its build
plugin as a single base64 argv blob containing the full keystore, keystorePassword and
keyPassword. That means the production upload key is visible in ps for the duration of every
build, and is written verbatim into any captured build log on failure. The Gradle route above
keeps the keystore on disk and never puts it on a command line.
Gradle heap (required)
This app OOMs at the dex-merge (:app:mergeExtDexRelease → java.lang.OutOfMemoryError) on the default ~2 GB Gradle heap. The withGradleHeap config plugin sets org.gradle.jvmargs=-Xmx6144m so local and cloud builds succeed. (android/gradle.properties is regenerated at prebuild, so it must be a plugin, not an edited file.)
Distribution
A universal Android APK for this app is ~492 MB — over Signal’s 100 MB attachment cap, so you share a link/QR, not the file:
- Host the APK (e.g., S3 + a CloudFront/presigned URL, or Firebase App Distribution) → send the download link via Signal, or open/scan on the device.
adb install app.apkif the device is tethered (USB or wireless adb).- Attestation caveat: a sideloaded APK still fails Play Integrity (C2PA signing enrollment returns
403 device attestation). For a build where signing must work end-to-end, deliver via the Play internal-testing track (the AAB), which is Play-recognized. See EAS Deployment.
Status
- ✅ Toolchain (JDK 17 + Android SDK) installed and verified in
/Users/Shared. - ✅ Off-quota / no-upload-limit mechanism confirmed: local builds on the box create no build record and don’t touch the EAS quota (validated on a developer Mac) — see Why.
- ❌
eas build --localitself is unusable for this app — see Why not. The prebuild + Gradle route below is what actually ships (see next line). - ✅
builder-account wiring — shell env and app checkout in place;easauthenticated assmsmatt. Note the toolchain actually used is~/dev-tools/jdk21and~/Library/Android/sdk, not the/Users/Sharedpaths above. - ✅ Signed AAB produced via prebuild + Gradle 2026-08-06 (production key verified).
- ⏳ Artifact hosting (S3 link/QR) for the “build → share to device” flow — planned.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.