Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Implement post-build symbol stripping for Android - #126023

Open
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717
Open

Implement post-build symbol stripping for Android#126023
Zurisen wants to merge 3 commits into
dotnet:mainfrom
Zurisen:android-symbol-stripping-115717

Conversation

@Zurisen

Copy link
Copy Markdown
Contributor

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.

CopilotAI review requested due to automatic review settings March 24, 2026 11:22
@github-actionsgithub-actionsBot added the area-Infrastructure-coreclr Only use for closed issues label Mar 24, 2026
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 24, 2026
@Zurisen

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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.

FileDescription
src/tasks/MobileBuildTasks/Android/AndroidProject.csRemoves strip-related CMake build-type switching and adds StripBinaryInPlace() using NDK llvm-strip.
src/tasks/AndroidAppBuilder/ApkBuilder.csKeeps 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}");

CopilotAIMar 24, 2026

Copy link

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
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
NdkToolstools=newNdkTools(targetArchitecture,GetHostOS(),apiLevel);
stringexecExt=Utils.IsWindows()?".exe":"";
stringllvmStripPath=Path.Combine(tools.ToolPrefixPath,$"llvm-strip{execExt}");
privatestringGetLlvmStripPath()
{
if(string.IsNullOrEmpty(androidToolchainPath))
{
thrownewInvalidOperationException($"{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(ndkRootDirisnull)
{
thrownewInvalidOperationException($"Unable to determine Android NDK root from toolchain path '{androidToolchainPath}'.");
}
stringhostTag=GetHostOS()switch
{
NdkToolchainHostOS.Windows=>"windows-x86_64",
NdkToolchainHostOS.MacOS=>"darwin-x86_64",
NdkToolchainHostOS.Linux=>"linux-x86_64",
_ =>thrownewInvalidOperationException($"Unsupported host OS '{GetHostOS()}'.")
};
stringexecExt=Utils.IsWindows()?".exe":string.Empty;
returnPath.Combine(ndkRootDir.FullName,"toolchains","llvm","prebuilt",hostTag,"bin",$"llvm-strip{execExt}");
}
publicvoidStripBinaryInPlace(stringfilePath,stringapiLevel=DefaultMinApiLevel)
{
stringllvmStripPath=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!);

CopilotAIMar 24, 2026

Copy link

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&&projectis not null)
project.StripBinaryInPlace(Path.Combine(OutputDir,destRelative),MinApiLevel!);
if(StripDebugSymbols)
{
if(projectisnull)
{
thrownewInvalidOperationException("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!);

CopilotAIMar 24, 2026

Copy link

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.
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.
Fixdotnet#115717
@dotnet-policy-servicedotnet-policy-serviceBot added the linkable-framework Issues associated with delivering a linker friendly framework label Apr 30, 2026
@Zurisen
Zurisenforce-pushed the android-symbol-stripping-115717 branch from 168f034 to bf703e2CompareApril 30, 2026 19:22
@kg
kg removed their request for review May 1, 2026 04:39
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-android': @vitek-karas, @simonrozsival, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.

@simonrozsival

Copy link
Copy Markdown
Member

/azp run runtime-android

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@github-actions

github-actionsBot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Workflow state for the Holistic Review Orchestrator.

{
"version": 5,
"last_dispatched_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_dispatched_base_ref": "main",
"last_dispatched_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_reviewed_commit": "e00eccfee757337cad80cdd65fe585601931989e",
"last_reviewed_base_ref": "main",
"last_reviewed_base_sha": "a31b4923d5b3cc51681c997b7c4834960cc5337a",
"last_recorded_worker_run_id": "29679139864",
"review_attempt_commit": "",
"review_attempt_base_ref": "",
"review_attempt_count": 0,
"max_review_attempts": 5,
"review_history_format": "holistic-review-disclosure-v1",
"review_history": [
{
"commit": "e00eccfee757337cad80cdd65fe585601931989e",
"review_id": 4730522953
}
]
}

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Holistic Review

Motivation: Build machines were running out of disk space when Android native libraries retained debug symbols. The prior approach stripped symbols at compile time via -DCMAKE_BUILD_TYPE=MinSizeRel plus a -s flag, but that path was also coupled to android:debuggable=false, which breaks adb shell run-as access relied upon by the test infrastructure (issue #115717). The PR decouples size reduction from debuggability.

Approach: AndroidProject.GenerateCMake/BuildCMake now always build in Debug config (dropping the stripDebugSymbols parameters and the MinSizeRel branch), and a new StripBinaryInPlace method invokes the NDK's llvm-strip --strip-debug on the produced .so files. ApkBuilder hoists the AndroidProject instance so the packaging loop can strip each copied .so when StripDebugSymbols=true, always passes --debug-mode to aapt (keeping android:debuggable=true), and removes the CoreCLR-specific exclusion of libmscordbi.so/libmscordaccore.so. This is a reasonable, well-targeted design that matches the stated goal.

Summary: The change is coherent and the CMake-side simplification is clean. The main concern is a behavioral regression risk for the NativeAOT path: project remains null when IsNativeAOT is true, so a NativeAOT build with StripDebugSymbols=true will now throw at packaging time instead of stripping (see inline comment on ApkBuilder.cs). The default for StripDebugSymbols is false, so the common path is unaffected, but the coupling of stripping capability to the presence of the CMake project should be resolved so NativeAOT is either supported or explicitly left at prior behavior. Minor: the removed CoreCLR debugger-lib exclusion means libmscordbi.so/libmscordaccore.so are now always packaged (only --strip-debug'd), slightly increasing APK size vs. before for stripped CoreCLR builds — intentional per the debuggability goal, but worth confirming. Since the author notes no local Android testing was possible, validation via the Android CI legs is important before merge.

Detailed Findings

  • NativeAOT + StripDebugSymbols now throws (inline on ApkBuilder.cs): the hoisted project is only assigned in the non-NativeAOT branch, so the new guard converts a previously-packaging configuration into an InvalidOperationException.
  • Dead parameter left in place (non-blocking): AndroidProject.Build(..., bool stripDebugSymbols = false, ...) (used by LibraryBuilder) still accepts but ignores stripDebugSymbols. Not in scope of the changed lines, but the option no longer has any effect for the CMake path; consider a follow-up to reconcile the Library/Apple builder stripping story so behavior is consistent across mobile builders.
  • Host tag mapping (informational): StripBinaryInPlace hardcodes darwin-x86_64/windows-x86_64/linux-x86_64 NDK prebuilt tags, which matches how the NDK ships llvm prebuilts today; fine as-is.

Note

This review was generated by this repository's Holistic Review agentic workflow to complement the built-in Copilot review.

Generated by Holistic Review · 76.6 AIC · ⌖ 10.9 AIC · ⊞ 10K

File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
if (StripDebugSymbols)
{
if (project is null)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This guard turns a previously-working configuration into a hard failure for NativeAOT. When IsNativeAOT is true, project is never assigned (it stays null), so any NativeAOT Android build that enables StripDebugSymbols will now throw InvalidOperationException here instead of packaging. The .so files for NativeAOT come straight from AppDir and previously were packaged as-is regardless of StripDebugSymbols. Two options: (1) construct a lightweight AndroidProject (or factor StripBinaryInPlace so it doesn't require the CMake project state) so NativeAOT can also strip, or (2) scope the stripping/guard to the non-NativeAOT path so NativeAOT keeps its prior behavior. As written, this is a functional regression for NativeAOT + stripping rather than a defensive check for an impossible state.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Infrastructure-coreclrOnly use for closed issuescommunity-contributionIndicates that the PR has been added by a community memberlinkable-frameworkIssues associated with delivering a linker friendly frameworkos-android

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Android] Implement post-build symbol stripping

5 participants

@Zurisen@simonrozsival@marek-safar@kotlarmilos