ReviewOS

also looking at this

craft-native/craft

perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols

#217
Merged glennmichael123 wants to merge fix/android-strip-libcraft into main
6 files +235 -14
9 days ago

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.so

AGP 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

  1. Strip, and where. In build.zig, for every non-Debug build, with the NDK's llvm-objcopy. That is the tool AGP would have used, and the NDK is already a hard requirement for building these libraries (#205).
  2. 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 to zig-out/android-symbols/<abi>/libcraft.so.debug, linked from the library by .gnu_debuglink. That is what ndk-stack -sym and Play Console's native debug symbols expect. A -Doptimize=Debug build installs the library as built.
  3. 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: unimplemented

lib/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.so still carries any .debug_* section;
  • it has no .gnu_debuglink;
  • android-symbols/x86_64/libcraft.so.debug is 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.zig configures on Zig 1441 and 1963.
  • The NDK build and the strip itself run only in this PR's Android emulator job; there is no NDK on the machine this was written on. That job builds build-android-all -Doptimize=ReleaseSafe, then the runner checks the result and runs both legs on the stripped library, including registered:103, declines:0 on the runtime leg.

6 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.