BUILD
Building the universal apps
Read [ARCHITECTURE.md] for the why. This is the how. Three things build:
1. libzero — the engine as an in-process library (zero_serve). Proven on
the desktop, cross-compiled per mobile platform.
2. Android shell (mobile/android) — Kotlin + NDK, one APK for phone /
tablet / Android TV / Fire TV.
3. iOS shell (mobile/ios) — Swift + WKWebView, iPhone / iPad.
These build in CI, not on a dev machine
Like the engine binaries and the desktop apps, the mobile apps build in GitHub Actions and publish to the DigitalOcean Space, fronted by oneaurica.com. The apps have their OWN workflow (.github/workflows/mobile.yml), decoupled from the engine release — building an app does NOT republish the engine or bump dist/VERSION, so testing the app never pushes an engine update to your users. Tag the app (or run it from the Actions tab):
git tag app-v0.0.1 && git push origin app-v0.0.1 # builds + publishes the apps only
• android job (ubuntu runner): builds the engine, runs zero seed, builds
OpenSSL + curl for the NDK so the app is fully networked (mesh, oracle, store hydration; falls back to offline-seeded only if that dep build fails), then builds a signed release AAB + APK and:
• uploads the AAB to Google Play (internal track → Play auto-updates your
testers, no reinstall), gated on PLAY_SERVICE_ACCOUNT_JSON;
• publishes the release APK to the Space at a stable link
(dist/mobile/Zero/Zero-android.apk) so you can install on test devices *immediately*, without waiting on Play review.
• ios job (macOS runner): builds libzero.xcframework, generates the Xcode
project, compiles for the simulator as a smoke test — needs no Apple account. Distribution deferred.
One-time setup (yours — accounts + keys, not code)
1. Signing key (the Play *upload* key): mobile/android/make-keystore.sh →
add the 4 printed secrets (ANDROID_KEYSTORE_B64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD).
2. Play Console ($25 one-time): create the app with package
com.oneaurica.zero, upload the first AAB manually (Play requires the first release of a new app by hand before the API can push), create a service account (*Release manager* role), add its JSON as PLAY_SERVICE_ACCOUNT_JSON. After that, every CI run pushes to the internal track automatically. Keep Play App Signing on (default): Play holds the distribution key; your keystore is only the upload key.
The sections below are for local iteration only (optional) — you never have to build on your machine to ship.
0. Prove the engine in-process (desktop — do this first)
No mobile toolchain needed. This is the exact call the mobile shells make:
make embed-test
./embed-test app 8088 mobile # class, port, door
curl http://127.0.0.1:8088/home # -> HTTP 200, your home page
zero_serve runs the engine in-process (no child process), binds loopback via the mobile door (owner-trusted), and serves real pages. If this works, the mobile architecture works — the shells only change *who calls zero_serve*.
The one gotcha, baked into every build: the native registry is populated by a static initializer in registry_gen.o. A linker drops an unreferenced archive member, so a plain link boots an EMPTY primitive table (no native function ...). Force every member in: -force_load (Makefile / Apple), -all_load (iOS xcframework, see project.yml), or compile the sources straight into the .so (Android CMake). All three are already set up.
1. The native-deps reality (libcurl + OpenSSL)
The engine's non-Apple/non-Windows path uses libcurl (HTTP) and OpenSSL (crypto). Neither the Android NDK nor the iOS public SDK ships them. Two tiers:
crypto
HTTP
first build
iOS
CommonCrypto (real, libSystem)
libcurl — gap
ZERO_NO_NET (offline-seeded)
Android
OpenSSL — gap
libcurl — gap
ZERO_NO_NET, or vendor prebuilts
ZERO_NO_NET (a real compile flag, wired through remote_store.h, functions/http/http_request.cpp, functions/crypto/crypto_util.h) drops both deps: the store is baked into the app and copied in on first launch, so the engine boots and serves the seed with no network. iOS keeps real CommonCrypto even under ZERO_NO_NET (the __APPLE__ branch wins); Android gets a non-secure placeholder digest until OpenSSL is wired — fine for the owner-trusted local door, NOT for auth/passkeys/SigV4.
Lifting ZERO_NO_NET: either vendor prebuilt curl+OpenSSL (Android: see mobile/android/fetch-deps.sh), or — cleaner, and the recommended path — add a native HTTP branch that mirrors the existing _WIN32 WinHTTP precedent: NSURLSession on Apple, HttpURLConnection-over-JNI on Android. That makes both mobile platforms dependency-free, exactly as Windows already is.
2. Bake the store seed (both platforms, first build)
The offline-seeded build needs the class library present. The engine verb zero seed pulls it deterministically from the store (same manifest-based fetch as runtime hydration) into a flat set of class folders — this is what CI uses:
make # build the engine once
./zero seed mobile/android/app/src/main/assets/store_seed # _core + the home chain
cp -R mobile/android/app/src/main/assets/store_seed mobile/ios/Resources/store_seed
zero seed <dir> [class...] defaults to the chain the personal home needs (_core, app, home, thing, …); pass class names to seed others (a scanner, a storefront). mobile/android/seed.sh is the alternative that copies from a local ~/zero.9.stores instead of fetching.
3. Android
mobile/android/seed.sh
cd mobile/android
./gradlew assembleDebug -PzeroNoNet=true # offline-seeded first APK
# install on a device / TV:
adb install app/build/outputs/apk/debug/app-debug.apk
• One APK installs on phone, tablet, Android TV and Fire TV (the manifest
marks touchscreen + leanback not-required and declares both launchers).
• For the full engine (outbound HTTP, store hydration): run
mobile/android/fetch-deps.sh, drop prebuilt curl+OpenSSL under app/src/main/cpp/prebuilt/<abi>/, then build without -PzeroNoNet.
• The CMake compiles the engine sources directly into libzeroengine.so, so the
registry static-init is always linked (no force-load needed there).
4. iOS
mobile/android/seed.sh && cp -R mobile/android/app/src/main/assets/store_seed mobile/ios/Resources/store_seed
cd mobile/ios
./build-libzero.sh # -> build/libzero.xcframework (ZERO_NO_NET, real CommonCrypto)
brew install xcodegen && xcodegen generate
open Zero.xcodeproj # set your signing team, run on a device
• iPhone / iPad today. Apple TV (tvOS) is deferred: tvOS has no
WKWebView, so the webview-door model can't run there. The Apple TV path needs a native SwiftUI front-end driven by the in-process engine over loopback (JSON), or it waits — meanwhile Android TV / Fire TV carry the 10-foot experience (they have WebView). See the table in ARCHITECTURE.md.
• The app links libzero.xcframework with -all_load (registry), plus
Security + CoreFoundation (ECDSA), exactly like the macOS desktop shell.
5. What's verified vs what needs your toolchain
• Verified here (desktop): libzero builds; zero_serve serves in-process on
loopback via the mobile door (HTTP 200, real page); ZERO_PORT override; ZERO_NO_NET compiles the network/HTTP paths; normal engine unregressed.
• Needs the Android SDK/NDK: the APK link + on-device run.
• Needs Xcode: the xcframework cross-compile + on-device run.
The hard, cross-cutting risk (an in-process, no-fork engine with a populated native table) is the part that's proven. The rest is platform packaging.
6. Native rendering (no webview) — render = native
Every shell can DRAW the engine's output two ways. By default it points a webview at the engine (HTML, unchanged). When its manifest declares render = native, it instead asks the engine for a scene (Accept: application/vnd.zero.scene+json) and draws it with that platform's native widgets — AppKit, UIKit, GTK, Win32, android.widget. No browser between the user and the controls. See [../roadmap/native-rendering.md] for the protocol, the component class library (ui-kit/), and the node taxonomy.
make desktop APP=native # ZeroNative.app -> the AppKit scene renderer
./verify-scene.sh # prove one action renders HTML + a scene + a form POST
• The flag is per-app (a manifest key), so native is opt-in and additive: an
app that doesn't set it keeps the webview door, and an HTML page that isn't scene-authored is untouched. Adopt screens one at a time.
• The renderers live beside each shell: desktop/scene_mac.h (macOS, the
reference), desktop/main_gtk.c (Linux), desktop/main_win.cpp (Windows), mobile/android/.../SceneRenderer.kt, mobile/ios/Sources/SceneRenderer.swift.
• This is the only viewer that works on Apple TV (tvOS has no WebView) —
the scene protocol is what unblocks the 10-foot SwiftUI front-end.
[../roadmap/native-rendering.md]: ../roadmap/native-rendering.md
[ARCHITECTURE.md]: ARCHITECTURE.md