From d66c00b7c81ccb1a788fc936e3056336fd233872 Mon Sep 17 00:00:00 2001 From: Zurisen Date: Tue, 24 Mar 2026 12:14:33 +0100 Subject: [PATCH 1/2] Implement post-build symbol stripping for Android Build machines run out of disk space without symbol stripping, but enabling symbol stripping at compile time removes debug symbols and sets android:debuggable=false, which breaks adb shell run-as access. This change modifies the Android build to: - Always build native libraries in Debug mode with symbols - Strip debug symbols post-build using llvm-strip from the NDK - Always set android:debuggable=true in the APK manifest This preserves debuggability while reducing binary size. Fix #115717 --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 32 ++++++++------- .../Android/AndroidProject.cs | 39 ++++++++----------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index df0a274e9d4895..b6d5bf580c30ae 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -330,6 +330,7 @@ public ApkBuilder(TaskLoggingHelper logger) } } string abi; + AndroidProject? project = null; if (IsNativeAOT) { abi = AndroidProject.DetermineAbi(runtimeIdentifier); @@ -400,12 +401,19 @@ public ApkBuilder(TaskLoggingHelper logger) } File.WriteAllText(Path.Combine(OutputDir, monodroidSource), monodroidContent); - AndroidProject project = new AndroidProject("monodroid", runtimeIdentifier, AndroidNdk, logger); - project.GenerateCMake(OutputDir, MinApiLevel, StripDebugSymbols); - project.BuildCMake(OutputDir, StripDebugSymbols); + project = new AndroidProject("monodroid", runtimeIdentifier, AndroidNdk, logger); + project.GenerateCMake(OutputDir, MinApiLevel); + project.BuildCMake(OutputDir); abi = project.Abi; - // TODO: https://github.com/dotnet/runtime/issues/115717 + if (StripDebugSymbols) + { + // Strip debug symbols post-build to reduce binary size while keeping the app debuggable. + // This preserves android:debuggable=true so adb shell run-as continues to work. + string libMonodroidPath = Path.Combine(OutputDir, "monodroid", "libmonodroid.so"); + if (File.Exists(libMonodroidPath)) + project.StripBinaryInPlace(libMonodroidPath, MinApiLevel!); + } } // 2. Compile Java files @@ -486,7 +494,9 @@ public ApkBuilder(TaskLoggingHelper logger) // 3. Generate APK - string debugModeArg = StripDebugSymbols ? string.Empty : "--debug-mode"; + // Always keep the app debuggable to enable adb shell run-as access during test runs. + // Symbol stripping is done post-build via llvm-strip rather than by setting android:debuggable=false. + string debugModeArg = "--debug-mode"; string apkFile = Path.Combine(OutputDir, "bin", $"{ProjectName}.unaligned.apk"); Utils.RunProcess(logger, androidSdkHelper.AaptPath, $"package -f -m -F {apkFile} -A assets -M AndroidManifest.xml -I {androidJar} {debugModeArg}", workingDir: OutputDir); @@ -501,15 +511,6 @@ public ApkBuilder(TaskLoggingHelper logger) else { var excludedLibs = new HashSet { "libmonodroid.so" }; - if (IsCoreCLR) - { - if (StripDebugSymbols) - { - // exclude debugger support libs - excludedLibs.Add("libmscordbi.so"); - excludedLibs.Add("libmscordaccore.so"); - } - } if (!StaticLinkedRuntime) dynamicLibs.AddRange(Directory.GetFiles(AppDir, "*.so").Where(file => !excludedLibs.Contains(Path.GetFileName(file)))); } @@ -554,8 +555,9 @@ public ApkBuilder(TaskLoggingHelper logger) } } - // NOTE: we can run android-strip tool from NDK to shrink native binaries here even more. File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true); + if (StripDebugSymbols && project is not null) + project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!); Utils.RunProcess(logger, androidSdkHelper.AaptPath, $"add {apkFile} {NormalizePathToUnix(destRelative)}", workingDir: OutputDir); } Utils.RunProcess(logger, androidSdkHelper.AaptPath, $"add {apkFile} classes.dex", workingDir: OutputDir); diff --git a/src/tasks/MobileBuildTasks/Android/AndroidProject.cs b/src/tasks/MobileBuildTasks/Android/AndroidProject.cs index b3c3ab55ba85da..5d07d38abb6fb4 100644 --- a/src/tasks/MobileBuildTasks/Android/AndroidProject.cs +++ b/src/tasks/MobileBuildTasks/Android/AndroidProject.cs @@ -50,49 +50,42 @@ public void Build(string workingDir, ClangBuildOptions buildOptions, bool stripD Utils.RunProcess(logger, tools.ClangPath, workingDir: workingDir, args: clangArgs); } - public void GenerateCMake(string workingDir, bool stripDebugSymbols) + public void GenerateCMake(string workingDir) { - GenerateCMake(workingDir, DefaultMinApiLevel, stripDebugSymbols); + GenerateCMake(workingDir, DefaultMinApiLevel); } - public void GenerateCMake(string workingDir, string apiLevel = DefaultMinApiLevel, bool stripDebugSymbols = false) + public void GenerateCMake(string workingDir, string apiLevel = DefaultMinApiLevel) { // force ninja generator on Windows, the VS generator causes issues with the built-in Android support in VS var generator = Utils.IsWindows() ? "-G Ninja" : ""; string cmakeGenArgs = $"{generator} -DCMAKE_TOOLCHAIN_FILE={androidToolchainPath} -DANDROID_ABI=\"{Abi}\" -DANDROID_STL=none -DTARGETS_ANDROID=1 " + $"-DANDROID_PLATFORM=android-{apiLevel} -B {projectName}"; - if (stripDebugSymbols) - { - // Use "-s" to strip debug symbols, it complains it's unused but it works - cmakeGenArgs += " -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_FLAGS=\"-s -Wno-unused-command-line-argument\""; - } - else - { - cmakeGenArgs += " -DCMAKE_BUILD_TYPE=Debug"; - } + // Always build with debug info; symbol stripping is done post-build via StripBinaryInPlace + cmakeGenArgs += " -DCMAKE_BUILD_TYPE=Debug"; Utils.RunProcess(logger, Cmake, workingDir: workingDir, args: cmakeGenArgs); } - public string BuildCMake(string workingDir, bool stripDebugSymbols = false) + public string BuildCMake(string workingDir) { - string cmakeBuildArgs = $"--build {projectName}"; - - if (stripDebugSymbols) - { - cmakeBuildArgs += " --config MinSizeRel"; - } - else - { - cmakeBuildArgs += " --config Debug"; - } + string cmakeBuildArgs = $"--build {projectName} --config Debug"; Utils.RunProcess(logger, Cmake, workingDir: workingDir, args: cmakeBuildArgs); return Path.Combine(workingDir, projectName); } + 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}"); + logger.LogMessage(MessageImportance.High, $"Stripping debug symbols from {filePath}"); + Utils.RunProcess(logger, llvmStripPath, args: $"--strip-debug \"{filePath}\""); + } + private static string BuildClangArgs(ClangBuildOptions buildOptions) { StringBuilder ret = new StringBuilder(); From 6e838d480549646386c98e1f4a98c59baf1bd399 Mon Sep 17 00:00:00 2001 From: Zurisen Date: Tue, 24 Mar 2026 13:59:33 +0100 Subject: [PATCH 2/2] Address Copilot review feedback --- src/tasks/AndroidAppBuilder/ApkBuilder.cs | 17 +++++++---------- .../MobileBuildTasks/Android/AndroidProject.cs | 15 +++++++++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/tasks/AndroidAppBuilder/ApkBuilder.cs b/src/tasks/AndroidAppBuilder/ApkBuilder.cs index b6d5bf580c30ae..1c194e79ed8f54 100644 --- a/src/tasks/AndroidAppBuilder/ApkBuilder.cs +++ b/src/tasks/AndroidAppBuilder/ApkBuilder.cs @@ -406,14 +406,7 @@ public ApkBuilder(TaskLoggingHelper logger) project.BuildCMake(OutputDir); abi = project.Abi; - if (StripDebugSymbols) - { - // Strip debug symbols post-build to reduce binary size while keeping the app debuggable. - // This preserves android:debuggable=true so adb shell run-as continues to work. - string libMonodroidPath = Path.Combine(OutputDir, "monodroid", "libmonodroid.so"); - if (File.Exists(libMonodroidPath)) - project.StripBinaryInPlace(libMonodroidPath, MinApiLevel!); - } + } // 2. Compile Java files @@ -556,8 +549,12 @@ public ApkBuilder(TaskLoggingHelper logger) } File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true); - if (StripDebugSymbols && project is not null) - project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative), MinApiLevel!); + if (StripDebugSymbols) + { + if (project is null) + throw new InvalidOperationException("StripDebugSymbols is enabled, but no Android project is available to strip native libraries during APK packaging."); + project.StripBinaryInPlace(Path.Combine(OutputDir, destRelative)); + } Utils.RunProcess(logger, androidSdkHelper.AaptPath, $"add {apkFile} {NormalizePathToUnix(destRelative)}", workingDir: OutputDir); } Utils.RunProcess(logger, androidSdkHelper.AaptPath, $"add {apkFile} classes.dex", workingDir: OutputDir); diff --git a/src/tasks/MobileBuildTasks/Android/AndroidProject.cs b/src/tasks/MobileBuildTasks/Android/AndroidProject.cs index 5d07d38abb6fb4..d5ce2f8a501f41 100644 --- a/src/tasks/MobileBuildTasks/Android/AndroidProject.cs +++ b/src/tasks/MobileBuildTasks/Android/AndroidProject.cs @@ -20,6 +20,7 @@ public sealed class AndroidProject private TaskLoggingHelper logger; private string abi; + private string androidNdkPath; private string androidToolchainPath; private string projectName; private string targetArchitecture; @@ -33,6 +34,7 @@ public AndroidProject(string projectName, string runtimeIdentifier, TaskLoggingH public AndroidProject(string projectName, string runtimeIdentifier, string androidNdkPath, TaskLoggingHelper logger) { + this.androidNdkPath = androidNdkPath; androidToolchainPath = Path.Combine(androidNdkPath, "build", "cmake", "android.toolchain.cmake").Replace('\\', '/'); abi = DetermineAbi(runtimeIdentifier); targetArchitecture = GetTargetArchitecture(runtimeIdentifier); @@ -77,11 +79,16 @@ public string BuildCMake(string workingDir) return Path.Combine(workingDir, projectName); } - public void StripBinaryInPlace(string filePath, string apiLevel = DefaultMinApiLevel) + public void StripBinaryInPlace(string filePath) { - NdkTools tools = new NdkTools(targetArchitecture, GetHostOS(), apiLevel); - string execExt = Utils.IsWindows() ? ".exe" : ""; - string llvmStripPath = Path.Combine(tools.ToolPrefixPath, $"llvm-strip{execExt}"); + string hostTag = GetHostOS() switch + { + "windows" => "windows-x86_64", + "osx" => "darwin-x86_64", + _ => "linux-x86_64" + }; + string execExt = Utils.IsWindows() ? ".exe" : string.Empty; + string llvmStripPath = Path.Combine(androidNdkPath, "toolchains", "llvm", "prebuilt", hostTag, "bin", $"llvm-strip{execExt}"); logger.LogMessage(MessageImportance.High, $"Stripping debug symbols from {filePath}"); Utils.RunProcess(logger, llvmStripPath, args: $"--strip-debug \"{filePath}\""); }