CraftNative calls System.loadLibrary("craft") inside a try/catch and sets
isAvailable = false on UnsatisfiedLinkError
(CraftNative.kt.template:111-118). Every wrapper short-circuits on that, and
every @JavascriptInterface method falls through to its Kotlin body — e.g.
CraftNative.getDeviceInfo(activity)?.let { return it }
(CraftBridge.kt.template:2341), if (CraftNative.haptic(activity, style)) return
(:1491).
Nothing ever puts the library where loadLibrary would find it. Repo-wide,
jniLibs appears only in docs, one build.zig comment (:2461) and one
CraftNative.kt.template comment (:105). packages/android/src/index.ts
copies no .so, and build.gradle.kts.app.template has no sourceSets, no
ndk { abiFilters } and no packaging block.
So isAvailable is always false, and all 38 bridge_android_*.zig modules —
the entire Android half of the Zig bridge migration — are built by
zig build build-android and never loaded by anything.
The artifacts exist and are correctly named for it:
zig build build-android→zig-out/android/arm64-v8a/libcraft.sozig build build-android-x86→zig-out/android/x86_64/libcraft.so
Both are named craft, so one loadLibrary("craft") serves both ABIs
(build.zig:2428-2441, :2450-2467). Neither needs the NDK: the JNI modules
deliberately omit link_libc so they build with nothing but Zig
(android_dispatch.zig:77-89).
Two things make this quiet rather than loud. The fallback is by design, so there is no error anywhere — an app with no library behaves exactly like an app whose library failed to bind. And there is no Zig-side log channel on Android to tell them apart (#TBD-LOG).
Worth adding an assertion that the .so is actually inside the APK, since
unzip -l app-debug.apk | grep lib/x86_64/libcraft.so is the only
externally checkable proof. The Android leg of the mobile E2E harness
(scripts/mobile-e2e/android.ts, added in #199) reports zigDispatches: 0
and says why, so the report states which side answered rather than leaving it
open — but it cannot assert the Zig path until this is fixed.
Found while building #199; the iOS side has the equivalent wiring
(renderRuntimeSettings in packages/ios/src/index.ts:305-313) and the iOS
E2E leg proves it works, which is what made the gap visible.