Skip to content

Implement post-build symbol stripping for Android#126023

Open
Zurisen wants to merge 2 commits intodotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 2 commits intodotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen
Copy link
Contributor

@Zurisen Zurisen commented Mar 24, 2026

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 + -s flag) also set android:debuggable=false in the APK manifest, which breaks adb shell run-as access needed by test infrastructure.

Solution: Decouple symbol stripping from debuggability by:

  1. Always building native libraries in Debug mode (preserves symbols during build)
  2. Stripping debug symbols post-build using llvm-strip from the Android NDK
  3. Always keeping android:debuggable=true in the APK manifest

This 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.cs

    • Removed stripDebugSymbols parameter from GenerateCMake() and BuildCMake() methods
    • Changed CMake to always use CMAKE_BUILD_TYPE=Debug instead of conditionally using MinSizeRel
    • Added new StripBinaryInPlace() method that uses llvm-strip --strip-debug from NDK toolchain
  • src/tasks/AndroidAppBuilder/ApkBuilder.cs

    • Hoisted AndroidProject variable declaration to enable post-build stripping
    • Added post-build stripping of libmonodroid.so when StripDebugSymbols=true
    • Changed AAPT packaging to always pass --debug-mode (sets android:debuggable=true)
    • Removed conditional exclusion of CoreCLR debugger libraries (libmscordbi.so, libmscordaccore.so)
    • Added stripping of all .so files during APK packaging when StripDebugSymbols=true

Testing

  • Code builds successfully (./build.cmd clr+libs -rc release)
  • MobileBuildTasks project compiles with 0 errors, 0 warnings
  • AndroidAppBuilder project compiles with 0 errors, 0 warnings
  • Android device testing - deferred to CI and maintainer review

Note: I don't have a local Android test environment configured. The implementation follows the standard approach of using NDK's llvm-strip tool 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):

Compile with -s flag → Stripped binary + android:debuggable=false

After (this PR):

Compile in Debug mode → Binary with symbols + android:debuggable=true
llvm-strip --strip-debug → Stripped binary + android:debuggable=true ✓

The llvm-strip --strip-debug command 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.

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
Copilot AI review requested due to automatic review settings March 24, 2026 11:22
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @agocke, @dotnet/runtime-infrastructure
See info in area-owners.md if you want to be subscribed.

@Zurisen
Copy link
Contributor Author

Zurisen commented Mar 24, 2026

@dotnet-policy-service agree

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-debug post-build stripping helper.
  • Update APK packaging to always set debuggable mode and to strip .so files during/after packaging when StripDebugSymbols=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.

Comment on lines +80 to +84
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}");
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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();

Copilot uses AI. Check for mistakes.
Comment on lines +559 to +560
if (StripDebugSymbols && project is not null)
project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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!);
}

Copilot uses AI. Check for mistakes.
Comment on lines +413 to +415
string libMonodroidPath = Path.Combine(OutputDir, "monodroid", "libmonodroid.so");
if (File.Exists(libMonodroidPath))
project.StripBinaryInPlace(libMonodroidPath, MinApiLevel!);
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

2 participants