Keel Prebuild

keel prebuild <platform> is the Expo-prebuild equivalent: it turns a managed Keel JS project (no android//ios/ folder) into a compilable native project you build locally with the platform toolchain. It is the local counterpart of cloud dev-client — the developer’s own machine is the build host, so no macOS/Android-SDK runner infrastructure is needed on our side. Both platforms are verified end-to-end (the built app mounts its embedded bundle and renders on a device/simulator).

# Android → sideloadable APK
keel prebuild android            # generate ./android from app.json + package.json
cd android && ./gradlew assembleRelease
# → app/build/outputs/apk/release/app-release.apk  (sideload-ready, debug-signed)

# iOS → .app (macOS + Xcode).  prebuild runs xcodegen + pod install for you.
keel prebuild ios
cd ios && xcodebuild -workspace <Name>.xcworkspace -scheme <Name> \
  -destination 'platform=iOS Simulator,name=iPhone 16' build

The produced app embeds the project’s JS (assets/index.android.bundle as Hermes bytecode on Android; main.jsbundle on iOS) and mounts it through KeelRuntimeHost — no dev server, no network. It is a standalone app with its own applicationId/bundleId + label, not the shared Keel Go tester.

app.json — the identity config

Prebuild reads app identity from app.json (Expo-parallel; keel.json stays build/OTA config). Only android.package is really needed:

{
  "name": "My App",                       // launcher label (falls back to slug)
  "slug": "my-app",                        // falls back to package.json name
  "version": "1.0.0",                      // versionName (falls back to package.json version)
  "android": {
    "package": "com.example.myapp",        // applicationId — derived if absent (with a warn)
    "versionCode": 1
  },
  "ios": {
    "bundleIdentifier": "com.example.myapp",
    "appleTeamId": "AB12CD34EF",            // Apple Developer Team ID — enables signed .ipa
    "distribution": "development"           // development | ad-hoc | app-store
  }
}

If android.package is missing, prebuild derives com.keelapp.<slug> and warns — pin it, the applicationId is your app’s permanent identity.

iOS signing → .ipa → store

Set ios.appleTeamId (your Apple Developer Team ID) and keel prebuild ios wires automatic signing into the Xcode project + emits an ios/ExportOptions.plist. Then the archive/export produces a signed .ipa (Xcode resolves your cert + provisioning for that team):

keel prebuild ios          # generates signed project + ExportOptions.plist
cd ios
xcodebuild -workspace <Name>.xcworkspace -scheme <Name> \
  -destination 'generic/platform=iOS' -archivePath build/<Name>.xcarchive archive
xcodebuild -exportArchive -archivePath build/<Name>.xcarchive \
  -exportOptionsPlist ExportOptions.plist -exportPath build/ipa
# → build/ipa/<Name>.ipa
keel submit ios --build build/ipa/<Name>.ipa --to ios_appstore \
  --app-id <bundle> --version 1.0.0 --build-number 1 --credentials creds.yml

keel submit ios (the appstore driver) mints an App Store Connect JWT from your .p8 key, uploads via xcrun altool, waits for the build to go VALID, and files a TestFlight review submission. All signing/upload credentials are yours (cert, provisioning, ASC API key) — prebuild + submit are the pipeline around them. Without ios.appleTeamId, prebuild builds a simulator-only (unsigned) .app.

How it works

Prebuild copies a parameterized template of the Keel Go Android host (which is not a standard RN ReactNativeHost app — modules register through the global KeelModuleRegistrar, and the bundle mounts through KeelRuntimeHost), baking in absolute toolchain paths so the project builds wherever node_modules is hoisted:

  1. Resolves the node_modules root (walks up; handles workspace hoisting).
  2. Generates android/ with the app’s applicationId / label / version, entry pointed at the project’s index.tsx.
  3. Wires Keel’s own autolinker — gradle runs keel-module autolink at build time to generate KeelModulesProvider.kt + the module include/deps scripts, so npm add @keel-ai/foo needs no host edits.
  4. Writes react-native.config.js opting the Keel modules out of RN community autolinking (Keel links them itself).

The native foundation comes from the prebuilt com.appunvs:keel-module-core AAR (maven); the KeelView Fabric component compiles in-situ from the @keel-ai/modules-core npm package’s cpp/.

Requirements

Checked by keel doctor. Android: JDK 17, Android SDK + NDK 26.x, and com.appunvs:keel-module-core resolvable from maven (mavenLocal during development). iOS (macOS only): Xcode 26+, xcodegen, CocoaPods, and KeelModuleCore.xcframework under keel/build/ios/ (SPM .binaryTarget).

The project needs these devDeps — RN 0.85 unbundled them:

  • @react-native-community/cli — metro bundle refuses to run without it.
  • hermes-compiler — ships hermesc (Android); prebuild points react.hermesCommand at node_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc.

What prebuild generates

  • Android: android/ (gradle project) + react-native.config.js. You run ./gradlew assembleRelease.
  • iOS: ios/ (XcodeGen project.yml + Podfile + <Name>/MainApp.swift + embedded main.jsbundle + autolinked Swift providers), and prebuild runs xcodegen generate + pod install for you (like expo prebuild). You run xcodebuild … build — with a concrete -destination, not -sdk (-sdk iphonesimulator forces the @KeelModule macro plugin onto the simulator SDK → Pitfall 17).

v1 limitations

  • iOS is debug-signed for the simulator / local device. Store / TestFlight distribution needs a real signing identity + provisioning profile (that’s keel submit’s input) and, for a physical device, an Apple Developer team.
  • Android release is debug-signed so assembleRelease produces a sideloadable APK out of the box. Point signingConfigs { create("release") } at a real keystore to ship.
  • android/ / ios/ are build artifacts — regenerate with --clean, don’t hand-edit.

Community RN native modules

Both platforms autolink genuine community RN native modules (react-native-screens, react-native-safe-area-context, …) alongside Keel’s own @keel-ai/* / keel-module-* modules — install the module (npm add), re-run prebuild, done. Two autolinkers run in parallel (this is how Expo does it): Keel’s keel-module autolink for Keel modules, and RN’s community autolinker for the rest (react-native.config.js marks the Keel modules android:null so they aren’t double-linked).

  • Android wires RN community autolinking via com.facebook.react.settings + react.autolinkLibrariesWithApp(); MainActivity registers the community Java ViewManagers through KeelRuntimeHost.additionalPackages = PackageList(...). Needs NDK 27.1 (RN 0.85’s prefab Fabric headers use C++20 std::format, which NDK 26’s libc++ gates) and the RN-standard rootProject.ext.compileSdkVersion — both are baked into the generated project.
  • iOS gets community modules free via the Podfile’s use_native_modules!.

Notes for maintainers

Bringing up prebuild surfaced gaps in the release-build + community-autolink paths that Keel Go’s Android project shares but had never exercised (Keel Go was only ever built assembleDebug, never a clean release, and opts community modules out):

  1. autolinking.json — Keel Go passed only on a stale checked-in build/ copy. Prebuild now applies com.facebook.react.settings + autolinkLibrariesFromCommand (the “JsonUtils NoClassDefFound” fear was stale).
  2. RN 0.85 moved hermesc into the hermes-compiler package; fixed via react.hermesCommand.
  3. R8 strips kotlin.jvm.functions.Function1.invoke (libkeel.so’s JNI_OnLoad resolves it by name → NoSuchMethodError abort). Fixed with keep rules; these now ship in keel-module-core.aar (consumer-rules.pro moved into defaultConfig — the publish is the DEBUG variant, so it can’t sit in buildTypes.release). App-level keeps kept as belt-and-suspenders.
  4. @react-native-community/cli must be a project devDep (RN 0.85 unbundled it).
  5. Community Fabric modules need NDK 27.1 (RN prefab std::format), rootProject.ext.compileSdkVersion, and — for Java ViewManagers — KeelRuntimeHost.additionalPackages = PackageList(...).

A clean assembleRelease of keel/go/android would hit (1)–(3) as well.