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
packages/zig/build.zigmodified+82-10
Changes to packages/zig/build.zig
@@ -2471,11 +2471,9 @@ pub fn build(b: *std.Build) void {
24712471 });
24722472 linkAndroidLibc(b, android_arm64_jni, ndk, "aarch64-linux-android");
24732473
2474 const android_arm64_jni_install = b.addInstallArtifact(android_arm64_jni, .{
2475 .dest_dir = .{ .override = .{ .custom = "android/arm64-v8a" } },
2476 });
2477 build_android.dependOn(&android_arm64_jni_install.step);
2478 build_android_all.dependOn(&android_arm64_jni_install.step);
2474 const android_arm64_jni_install = installAndroidJni(b, android_arm64_jni, ndk, "arm64-v8a", debug_build);
2475 build_android.dependOn(android_arm64_jni_install);
2476 build_android_all.dependOn(android_arm64_jni_install);
24792477 } else {
24802478 // Not built at all rather than built without bionic. The library would
24812479 // compile, install, ship in the APK and then fail at `dlopen` with two
@@ -2521,11 +2519,9 @@ pub fn build(b: *std.Build) void {
25212519
25222520 // `x86_64`, matching the jniLibs directory the loader looks in — not
25232521 // `x86`, which is the 32-bit ABI and would be silently ignored.
2524 const android_x86_jni_install = b.addInstallArtifact(android_x86_jni, .{
2525 .dest_dir = .{ .override = .{ .custom = "android/x86_64" } },
2526 });
2527 build_android_x86.dependOn(&android_x86_jni_install.step);
2528 build_android_all.dependOn(&android_x86_jni_install.step);
2522 const android_x86_jni_install = installAndroidJni(b, android_x86_jni, ndk, "x86_64", debug_build);
2523 build_android_x86.dependOn(android_x86_jni_install);
2524 build_android_all.dependOn(android_x86_jni_install);
25292525 }
25302526
25312527 const android_x86_lib = b.addLibrary(.{
@@ -2618,6 +2614,82 @@ fn androidSysroot(b: *std.Build, ndk: []const u8) ?[]const u8 {
26182614 return null;
26192615}
26202616
2617/// A tool from the NDK's LLVM toolchain, or null when this NDK has none for
2618/// this host. Found the way `androidSysroot` finds the sysroot, and for the
2619/// same reason.
2620fn androidTool(b: *std.Build, ndk: []const u8, name: []const u8) ?[]const u8 {
2621 const io = b.graph.io;
2622 for ([_][]const u8{ "darwin-x86_64", "darwin-arm64", "linux-x86_64", "windows-x86_64" }) |host| {
2623 const exe = if (std.mem.startsWith(u8, host, "windows")) b.fmt("{s}.exe", .{name}) else name;
2624 const candidate = b.pathJoin(&.{ ndk, "toolchains/llvm/prebuilt", host, "bin", exe });
2625 std.Io.Dir.cwd().access(io, candidate, .{}) catch continue;
2626 return candidate;
2627 }
2628 return null;
2629}
2630
2631/// Install a JNI library where the Android generator looks for it, without its
2632/// DWARF, and install the DWARF beside it for symbolication (#204).
2633///
2634/// Every generated app's APK carries this library, and at `ReleaseSafe` each
2635/// one was about 5.5 MB, most of it debug info nothing read: AGP strips native
2636/// libraries by running the NDK's strip, and a generated project configures no
2637/// NDK, so it never did. So it happens here, with the same tool AGP would use.
2638///
2639/// `--strip-debug` and not `--strip-all`. The symbol table stays, which is what
2640/// turns a native crash tombstone's addresses into function names, and it is a
2641/// small fraction of what the DWARF was. The DWARF goes to
2642/// `android-symbols/<abi>/libcraft.so.debug`, linked from the stripped library
2643/// by `.gnu_debuglink`, which is the shape `ndk-stack` and Play Console's
2644/// native debug symbols both take.
2645///
2646/// `llvm-objcopy` rather than Zig's own ObjCopy step, because ELF to ELF
2647/// copying is `fatal("unimplemented")` in `zig objcopy` at 0.17.0-dev.1963;
2648/// every strip mode fails. The NDK is already required to build these at all.
2649///
2650/// A Debug build installs the library as built: whoever asked for Debug wants
2651/// the debug info in place.
2652fn installAndroidJni(
2653 b: *std.Build,
2654 lib: *std.Build.Step.Compile,
2655 ndk: []const u8,
2656 abi: []const u8,
2657 keep_debug_info: bool,
2658) *std.Build.Step {
2659 const dest = b.fmt("android/{s}", .{abi});
2660 if (keep_debug_info) {
2661 const install = b.addInstallArtifact(lib, .{ .dest_dir = .{ .override = .{ .custom = dest } } });
2662 return &install.step;
2663 }
2664
2665 const objcopy = androidTool(b, ndk, "llvm-objcopy") orelse {
2666 return &b.addFail(b.fmt(
2667 "-Dandroid-ndk={s} has no toolchains/llvm/prebuilt/<host>/bin/llvm-objcopy, " ++
2668 "which strips libcraft.so's debug info for release builds. " ++
2669 "Point it at an NDK root, or build with -Doptimize=Debug to keep it.",
2670 .{ndk},
2671 )).step;
2672 };
2673
2674 const keep = b.addSystemCommand(&.{ objcopy, "--only-keep-debug" });
2675 keep.addFileArg(lib.getEmittedBin());
2676 const debug_info = keep.addOutputFileArg("libcraft.so.debug");
2677
2678 const strip = b.addSystemCommand(&.{ objcopy, "--strip-debug" });
2679 strip.addPrefixedFileArg("--add-gnu-debuglink=", debug_info);
2680 strip.addFileArg(lib.getEmittedBin());
2681 const stripped = strip.addOutputFileArg("libcraft.so");
2682
2683 const install = b.addInstallFileWithDir(stripped, .{ .custom = dest }, "libcraft.so");
2684 const symbols = b.addInstallFileWithDir(
2685 debug_info,
2686 .{ .custom = b.fmt("android-symbols/{s}", .{abi}) },
2687 "libcraft.so.debug",
2688 );
2689 install.step.dependOn(&symbols.step);
2690 return &install.step;
2691}
2692
26212693/// Link one Android artifact against the NDK's bionic.
26222694///
26232695/// A `--libc` file, not include and library paths. Paths alone are what