perf(android): ship libcraft.so without its DWARF, and keep the DWARF for symbols - #217
Merged
Merged
Conversation
✅ Binary Size Report
Size limits
|
✅ Binary load timeWhat this measures
Both binaries are measured interleaved on this runner and compared by |
… for symbols
Every generated app's APK carries the Zig JNI library, and at ReleaseSafe
each was about 5.5 MB, most of it debug info nothing read. AGP would strip
it with the NDK's strip, but a generated project configures no NDK, so it
never did.
Release builds now run the NDK's llvm-objcopy after linking:
--only-keep-debug writes zig-out/android-symbols/<abi>/libcraft.so.debug,
and --strip-debug --add-gnu-debuglink produces the library the generator
installs. Only the DWARF goes: the symbol table stays so a tombstone still
names functions, and ndk-stack or Play Console can take the .debug files.
Debug builds install the library as built.
Not Zig's own ObjCopy step: ELF to ELF copying is fatal("unimplemented") in
zig objcopy at 0.17.0-dev.1963, for every strip mode. The NDK is already
required to build these libraries at all.
The Android E2E runner now refuses to start when the x86_64 library still
carries any .debug_ section, has no .gnu_debuglink, or has no symbols file
holding .debug_info beside it. It reads the ELF section table itself, and
protocol.test.ts covers the shape #204 found.
Closes #204
glennmichael123
force-pushed
the
fix/android-strip-libcraft
branch
from
September 16, 2026 17:14
7094453 to
3b34336
Compare
Contributor
Author
|
Measured on this PR's Android emulator job, which built 5,565,632 bytes (5.31 MiB) before → 1.34 MiB shipped, about a quarter of the size. The runtime leg ran on the stripped library: it loaded, bound all 103 natives, and declined nothing. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #204.
What was wrong
Since #199, every generated Android app's APK carries the Zig JNI library, and at
-Doptimize=ReleaseSafeeach one was built with debug info: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
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 (fix(android): make every Zig decline speak, and prove Zig answers #205).--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.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:lib/compiler/objcopy.zigreachesfatal("unimplemented")for any ELF to ELF copy, in every strip mode.--strip-allalone fails the same way. So the step runsllvm-objcopytwice throughaddSystemCommand: once with--only-keep-debug, then with--strip-debug --add-gnu-debuglink=<debug file>.androidToolfinds the binary under the NDK's host prebuilt directory, the same wayandroidSysrootfinds the sysroot. An NDK without it fails the build step naming the path, and does not stopzig 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 noreadelf. It refuses to run when:x86_64/libcraft.sostill carries any.debug_*section;.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.tsbuilds 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_lineand the rest. The runner logs the shipped size, so the saving shows up in the job log.Verification
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.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.