Closes #204.
What was wrong
Since #199, every generated Android app's APK carries the Zig JNI library, and at -Doptimize=ReleaseSafe each one was built with debug info:
5,761,056 zig-out/android/arm64-v8a/libcraft.so
5,565,632 zig-out/android/x86_64/libcraft.soAGP normally strips native libraries at packaging time with the NDK's strip. A generated project configures no NDK, so that task had nothing to run, and roughly 11 MB of mostly DWARF shipped in a universal APK.
The three questions from the issue
- Strip, and where. In
build.zig, for every non-Debug build, with the NDK'sllvm-objcopy. That is the tool AGP would have used, and the NDK is already a hard requirement for building these libraries (#205). - Is the debug info worth keeping? In the APK, no: nothing on a device reads it. Next to the build, yes. Only the DWARF is stripped (
--strip-debug, not--strip-all), so the symbol table stays and a native crash tombstone still names functions. The DWARF moves tozig-out/android-symbols/<abi>/libcraft.so.debug, linked from the library by.gnu_debuglink. That is whatndk-stack -symand Play Console's native debug symbols expect. A-Doptimize=Debugbuild installs the library as built. - ABI splits. Not done here. Which ABIs an artifact carries is a packaging decision for each app, and Play already delivers per-ABI splits from an AAB. That leaves one question the issue raised unanswered by this PR, deliberately.
Why not Zig's ObjCopy step
It was the first thing tried. b.addObjCopy(..., .{ .strip = .debug, .separate_debug_file = ... }) configures fine on both 0.17.0-dev.1441 and 1963, and then fails at build time:
failed command: zig objcopy --strip-debug --extract-to=.../libcraft.so.debug ...
error: unimplementedlib/compiler/objcopy.zig reaches fatal("unimplemented") for any ELF to ELF copy, in every strip mode. --strip-all alone fails the same way. So the step runs llvm-objcopy twice through addSystemCommand: once with --only-keep-debug, then with --strip-debug --add-gnu-debuglink=<debug file>. androidTool finds the binary under the NDK's host prebuilt directory, the same way androidSysroot finds the sysroot. An NDK without it fails the build step naming the path, and does not stop zig build test.
How it is held
The Android E2E runner now inspects the library it is about to ship before either leg starts. It reads the ELF section table directly (elfSectionNames), because a macOS host has no readelf. It refuses to run when:
x86_64/libcraft.sostill carries any.debug_*section;- it has no
.gnu_debuglink; android-symbols/x86_64/libcraft.so.debugis missing, or holds no.debug_info, which means the strip discarded the DWARF rather than moving it.
protocol.test.ts builds minimal ELF64 files in memory to cover each case, including the exact shape #204 found. The reader was also run against a real Zig-built ELF shared library, which lists .debug_info, .debug_line and the rest. The runner logs the shipped size, so the saving shows up in the job log.
Verification
- Locally:
bun run test,bun run typecheck,bun run verify:packages, Pickier, the protocol tests and the android package tests.build.zigconfigures on Zig 1441 and 1963. - The NDK build and the strip itself run only in this PR's
Android emulatorjob; there is no NDK on the machine this was written on. That job buildsbuild-android-all -Doptimize=ReleaseSafe, then the runner checks the result and runs both legs on the stripped library, includingregistered:103, declines:0on the runtime leg.