Implement post-build symbol stripping for Android#126023
Implement post-build symbol stripping for Android#126023Zurisen wants to merge 2 commits intodotnet:mainfrom
Conversation
Build machines run out of disk space without symbol stripping, but enabling symbol stripping at compile time removes debug symbols and sets android:debuggable=false, which breaks adb shell run-as access. This change modifies the Android build to: - Always build native libraries in Debug mode with symbols - Strip debug symbols post-build using llvm-strip from the NDK - Always set android:debuggable=true in the APK manifest This preserves debuggability while reducing binary size. Fix dotnet#115717
|
Tagging subscribers to this area: @agocke, @dotnet/runtime-infrastructure |
|
@dotnet-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Implements post-build symbol stripping for Android native libraries to reduce disk usage on build machines while keeping APKs debuggable for test infrastructure (e.g., enabling adb shell run-as).
Changes:
- Update Android CMake generation/build to always use Debug configuration and introduce an
llvm-strip --strip-debugpost-build stripping helper. - Update APK packaging to always set debuggable mode and to strip
.sofiles during/after packaging whenStripDebugSymbols=true.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/tasks/MobileBuildTasks/Android/AndroidProject.cs |
Removes strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip. |
src/tasks/AndroidAppBuilder/ApkBuilder.cs |
Keeps APKs debuggable unconditionally and invokes post-build stripping for libmonodroid.so and packaged .so files when enabled. |
| public void StripBinaryInPlace(string filePath, string apiLevel = DefaultMinApiLevel) | ||
| { | ||
| NdkTools tools = new NdkTools(targetArchitecture, GetHostOS(), apiLevel); | ||
| string execExt = Utils.IsWindows() ? ".exe" : ""; | ||
| string llvmStripPath = Path.Combine(tools.ToolPrefixPath, $"llvm-strip{execExt}"); |
There was a problem hiding this comment.
StripBinaryInPlace() locates llvm-strip via NdkTools, which in turn uses the global Ndk.NdkPath (probing ANDROID_NDK_ROOT / fixed install locations) rather than the Android NDK path passed into AndroidProject. If the build is using an NDK path provided via MSBuild (AndroidNdk) without setting ANDROID_NDK_ROOT, this will likely fail to find llvm-strip (or pick a different NDK than CMake used). Consider deriving the llvm-strip path from the AndroidProject-provided NDK root (store it as a field), or otherwise ensure NdkTools is initialized from that same NDK path to avoid mismatches/regressions.
| public void StripBinaryInPlace(string filePath, string apiLevel = DefaultMinApiLevel) | |
| { | |
| NdkTools tools = new NdkTools(targetArchitecture, GetHostOS(), apiLevel); | |
| string execExt = Utils.IsWindows() ? ".exe" : ""; | |
| string llvmStripPath = Path.Combine(tools.ToolPrefixPath, $"llvm-strip{execExt}"); | |
| private string GetLlvmStripPath() | |
| { | |
| if (string.IsNullOrEmpty(androidToolchainPath)) | |
| { | |
| throw new InvalidOperationException($"{nameof(androidToolchainPath)} must be set before stripping binaries."); | |
| } | |
| // androidToolchainPath is expected to be <ndkRoot>/build/cmake/android.toolchain.cmake | |
| // so the NDK root is two levels up. | |
| DirectoryInfo? toolchainDir = Directory.GetParent(androidToolchainPath); | |
| DirectoryInfo? ndkRootDir = toolchainDir?.Parent; | |
| if (ndkRootDir is null) | |
| { | |
| throw new InvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'."); | |
| } | |
| string hostTag = GetHostOS() switch | |
| { | |
| NdkToolchainHostOS.Windows => "windows-x86_64", | |
| NdkToolchainHostOS.MacOS => "darwin-x86_64", | |
| NdkToolchainHostOS.Linux => "linux-x86_64", | |
| _ => throw new InvalidOperationException($"Unsupported host OS '{GetHostOS()}'.") | |
| }; | |
| string execExt = Utils.IsWindows() ? ".exe" : string.Empty; | |
| return Path.Combine(ndkRootDir.FullName, "toolchains", "llvm", "prebuilt", hostTag, "bin", $"llvm-strip{execExt}"); | |
| } | |
| public void StripBinaryInPlace(string filePath, string apiLevel = DefaultMinApiLevel) | |
| { | |
| string llvmStripPath = GetLlvmStripPath(); |
| if (StripDebugSymbols && project is not null) | ||
| project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!); |
There was a problem hiding this comment.
When StripDebugSymbols is enabled, stripping during APK packaging is gated on project is not null. For NativeAOT builds project stays null, so none of the packaged .so files will be stripped even though StripDebugSymbols=true. If stripping is intended to apply to NativeAOT (or any path that doesn't create an AndroidProject), consider creating an AndroidProject (or a dedicated NDK-tool locator) for the active RID purely for stripping so the packaging loop strips all copied .so files consistently.
| if (StripDebugSymbols && project is not null) | |
| project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!); | |
| if (StripDebugSymbols) | |
| { | |
| if (project is null) | |
| { | |
| throw new InvalidOperationException("StripDebugSymbols is enabled, but no Android project is available to strip native libraries during APK packaging."); | |
| } | |
| project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!); | |
| } |
| string libMonodroidPath = Path.Combine(OutputDir, "monodroid", "libmonodroid.so"); | ||
| if (File.Exists(libMonodroidPath)) | ||
| project.StripBinaryInPlace(libMonodroidPath, MinApiLevel!); |
There was a problem hiding this comment.
With StripDebugSymbols enabled, libmonodroid.so is stripped in-place here and then stripped again after it’s copied into OutputDir/lib/... in the packaging loop. This causes an extra llvm-strip invocation without changing the resulting APK contents. Consider stripping only once (either in-place post-build or on the copied file) to reduce work.
Description
Fixes #115717
This PR implements post-build symbol stripping for Android to solve the disk space issue on build machines while preserving app debuggability for test infrastructure.
Problem: Build machines run out of disk space without symbol stripping. However, the previous approach of stripping symbols at compile time (
-DCMAKE_BUILD_TYPE=MinSizeRel+-sflag) also setandroid:debuggable=falsein the APK manifest, which breaksadb shell run-asaccess needed by test infrastructure.Solution: Decouple symbol stripping from debuggability by:
llvm-stripfrom the Android NDKandroid:debuggable=truein the APK manifestThis allows the build to produce both small binaries (via post-build stripping) and debuggable APKs (via manifest flag).
Changes
Modified Files:
src/tasks/MobileBuildTasks/Android/AndroidProject.csstripDebugSymbolsparameter fromGenerateCMake()andBuildCMake()methodsCMAKE_BUILD_TYPE=Debuginstead of conditionally usingMinSizeRelStripBinaryInPlace()method that usesllvm-strip --strip-debugfrom NDK toolchainsrc/tasks/AndroidAppBuilder/ApkBuilder.csAndroidProjectvariable declaration to enable post-build strippinglibmonodroid.sowhenStripDebugSymbols=true--debug-mode(setsandroid:debuggable=true)libmscordbi.so,libmscordaccore.so).sofiles during APK packaging whenStripDebugSymbols=trueTesting
./build.cmd clr+libs -rc release)Note: I don't have a local Android test environment configured. The implementation follows the standard approach of using NDK's
llvm-striptool for post-build symbol removal, which is the recommended practice for Android native libraries.Technical Details
The key architectural change is when symbols are stripped:
Before (problematic):
After (this PR):
The
llvm-strip --strip-debugcommand removes only debug sections (.debug_*,.symtab, etc.) while preserving dynamic symbols needed for runtime operation, resulting in significantly smaller binaries without affecting app functionality or debuggability.Related Issues
This unblocks work on #111491 (Enable building CoreCLR for Android) by ensuring test infrastructure can function properly with optimized builds.