From 87267e98d833b39857712591e2999f63c1d17a2d Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 13:20:05 +0200 Subject: [PATCH] fix(android): ship the AAR's native libraries with their debug info A consuming app's `ndk.debugSymbolLevel` has only the merged native libraries to extract from, and both halves of this repo were throwing that away: `build_native.py` stripped with `--strip-unneeded`, and AGP's own StripDebugSymbolsTask stripped again on the way into the AAR. Play reported every frame of a crash inside the core as a bare address. Fixing only the script is invisible - AGP undoes it, and the published AAR comes out byte-identical to the fully stripped one. So `build_native.py` now leaves the libraries alone and `packaging.jniLibs.keepDebugSymbols` holds AGP off them. That is 60-72 MB per ABI of DWARF, and it is the point: play turns it into `file:line` with the inline chain intact. None of it reaches a device, which serves APKs the consuming app's own build strips - the weight is paid by maven central and by developer builds. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Lqc6gWzBBnoqvdaQ9HPoEa --- android/README.md | 9 +++++++++ android/build.gradle.kts | 9 +++++++++ android/build_native.py | 15 +++++++-------- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/android/README.md b/android/README.md index c5a49c70..b88ae8bb 100644 --- a/android/README.md +++ b/android/README.md @@ -77,6 +77,15 @@ The odrcore build is a normal one — `ODR_JNI=ON`, static core linked into `libodr_jni.so` — driven by the `android-` conan profiles in `.github/config/conan/profiles`, which pin the NDK and API 26. +The libraries ship unstripped — 60-72 MB per ABI, most of the AAR — so that a +consuming app's `ndk.debugSymbolLevel` can hand play what it needs to symbolicate +a crash inside the core. Devices never see it: play serves APKs built from the +stripped copies, so the weight is on maven central and developer builds only. + +It takes both `build_native.py` not stripping and the +`packaging.jniLibs.keepDebugSymbols` rule in `build.gradle.kts`. Without the +second the first is invisible. + ## Testing ```bash diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 8ebc590d..7320fceb 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -102,6 +102,15 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + packaging { + jniLibs { + // AGP strips the libraries on their way into the AAR unless told not + // to, silently — the published AAR comes out byte-identical to a + // fully stripped one however `build_native.py` built it. + keepDebugSymbols += "**/*.so" + } + } + // pushing the instrumented apk onto a cold emulator outlasts ddmlib's // default timeout, which surfaces as a ShellCommandUnresponsiveException // rather than as a failing test diff --git a/android/build_native.py b/android/build_native.py index fc6e4bdb..2df19b23 100644 --- a/android/build_native.py +++ b/android/build_native.py @@ -14,6 +14,10 @@ `libc++_shared.so` has to be shipped because the android profiles build against the shared c++ runtime and nothing else in a consuming app pulls it in. + +Neither is stripped: the NDK's unconditional `-g` is what a consumer's +`ndk.debugSymbolLevel` turns into symbolicated play crash reports. Costs 60-72 MB +per ABI, and needs the `keepDebugSymbols` rule in `build.gradle.kts` to survive. """ import argparse @@ -65,13 +69,8 @@ def libcxx_shared(ndk: Path, triple: str) -> Path: return matches[0] -def strip(ndk: Path, library: Path) -> None: - """The NDK compiles with `-g` in every configuration, so a release build of - the bindings is ~70 MB until it is stripped.""" - matches = sorted(ndk.glob("toolchains/llvm/prebuilt/*/bin/llvm-strip")) - if not matches: - raise SystemExit(f"no llvm-strip under {ndk}") - run([matches[0], "--strip-unneeded", library]) +def report(library: Path) -> None: + print(f" {library.name}: {library.stat().st_size // 1024} KiB", flush=True) def build(architecture: str, conan: str, build_profile: str, output: Path) -> None: @@ -106,7 +105,7 @@ def build(architecture: str, conan: str, build_profile: str, output: Path) -> No for source in (cmake_dir / "jni" / "libodr_jni.so", libcxx_shared(ndk, triple)): target = jni_libs / source.name shutil.copy2(source, target) - strip(ndk, target) + report(target) def main() -> int: