Description
With EmbedAssembliesIntoApk=true, an Android app that has at least one ProjectReference never builds incrementally. Every build — including one run immediately after a successful build, with no file touched — re-runs _BuildApkEmbed, then re-zipaligns and re-signs the APK. That is ~30s in the hello-world repro below, and ~45s in our real app, on every build.
The APK packaging targets themselves look correct: _BuildApkEmbed has proper Inputs/Outputs, and the input it names really is newer than the APK. The problem is upstream of it — the referenced library is recompiled on every build, which invalidates everything downstream.
Removing the single ProjectReference drops the no-change rebuild from 32.5s to 1.0s.
Note: This bug report was written by Claude (AI assistant) based on testing and guidance from the project developer.
Steps to Reproduce
git clone -b android-apk-repack-every-build https://github.com/Qythyx/bug_repros.git
cd bug_repros
dotnet build ReproApp/ReproApp.csproj -f net10.0-android # first build
dotnet build ReproApp/ReproApp.csproj -f net10.0-android # no changes — still ~32s
The repro is nothing but dotnet new maui + dotnet new classlib with one ProjectReference added. The only non-template properties in ReproApp.csproj are:
<TargetFrameworks>net10.0-android</TargetFrameworks>
<RuntimeIdentifiers Condition="'$(TargetFramework)' == 'net10.0-android'">android-arm64</RuntimeIdentifiers>
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
ReproLib.csproj is an unmodified dotnet new classlib retargeted to net10.0-android.
Measured on the second (no-change) build:
| Configuration |
Wall time |
_BuildApkEmbed |
App with ProjectReference, EmbedAssembliesIntoApk=true |
32.5s |
29.7s |
App without any ProjectReference |
1.0s |
2ms |
App with ProjectReference, EmbedAssembliesIntoApk=false (fast deploy) |
3.2s |
_BuildApkFastDev 202ms |
That last row is likely why this isn't already widely reported: the default Debug deploy path uses fast deployment, where the same redundant recompile still happens but packaging is cheap. It only becomes expensive on the embed path — which is what you need when the APK is installed directly (adb install, or an Appium-driven test run) rather than deployed through the SDK.
Link to public reproduction project repository
https://github.com/Qythyx/bug_repros/tree/android-apk-repack-every-build
Root cause
The referenced library is compiled twice per app build, by two MSBuild project configurations. Diffing the two csc command lines: 362 arguments each, differing in exactly one — /define: — carrying the same 75 constants in a different order:
compile 1: /define:TRACE;DEBUG;__XAMARIN_ANDROID_v1_0__;__MOBILE__;__ANDROID__;...;NET;NET10_0;NETCOREAPP;ANDROID;...
compile 2: /define:TRACE;DEBUG;NET;NET10_0;NETCOREAPP;ANDROID;...;__XAMARIN_ANDROID_v1_0__;__MOBILE__;__ANDROID__;...
_GenerateCompileDependencyCache hashes $(DefineConstants) as a string, so the two configurations compute different hashes and overwrite each other's <Project>.csproj.CoreCompileInputs.cache on every build. The last write lands after csc wrote the compile outputs, so on the next build the cache is newer than the outputs and CoreCompile runs "completely" again. The cache content is byte-identical between builds — only the mtime moves — so it never converges.
Constant ordering has no effect on compilation, so this is pure build-system churn.
The resulting cascade, taken from an MSBuild -v d log of the repro (each line is MSBuild's own up-to-date reason):
Input file "obj/Debug/net10.0-android/ReproLib.csproj.CoreCompileInputs.cache"
is newer than output file "obj/Debug/net10.0-android/ReproLib.xml"
→ Input file ".../ReproLib/bin/Debug/net10.0-android/ReproLib.dll"
is newer than output file "obj/Debug/net10.0-android/stamp/_ResolveLibraryProjectImports.stamp"
→ Input file "obj/Debug/net10.0-android/android/assets/arm64-v8a/ReproLib.dll"
is newer than output file "obj/Debug/net10.0-android/stamp/_GenerateJavaStubs.stamp"
→ Input file "obj/Debug/net10.0-android/android/assets/arm64-v8a/ReproLib.dll"
is newer than output file "obj/Debug/net10.0-android/stamp/_GeneratePackageManagerJava.stamp"
→ Input file "obj/Debug/net10.0-android/android/assets/arm64-v8a/ReproLib.dll"
is newer than output file "obj/Debug/net10.0-android/android/bin/com.companyname.reproapp.apk"
→ _BuildApkEmbed runs completely (~30s): CreateAssemblyStore,
WrapAssembliesAsSharedLibraries, then re-zipalign and re-sign
Building the library on its own is perfectly incremental — CoreCompile is skipped, and the cache and the compile outputs share a timestamp. The second configuration only appears when the library is built as a reference of the Android app.
Did you find any workaround?
None that keeps the build both correct and incremental. Tried and ruled out, each verified by measurement:
- Pinning the RID (
-p:RuntimeIdentifier=android-arm64, so there is no outer/inner RID split) — no change, still ~48s.
- Removing a third-party build-time formatter that creates extra project configurations (CSharpier.MSBuild,
-p:CSharpier_Bypass=true) — no change, still ~41s.
GenerateDocumentationFile on or off — reproduces either way; it only changes which CoreCompile output the cache is compared against.
- Multi-targeting the library — not required; a single-TFM
net10.0-android library reproduces.
EmbedAssembliesIntoApk=false avoids the cost but not the underlying recompile, and it isn't usable when the APK must be installed directly.
The only practical mitigation we found is to avoid building the app at all when nothing changed — wrapping the app build in a hand-written MSBuild target with its own Inputs/Outputs staleness check, which duplicates work the SDK is already doing.
Android application type
.NET Android (net10.0-android), MAUI app
Affected platform version
.NET SDK 10.0.101
Workload version 10.0.107
android workload 36.1.53/10.0.100
maui workload 10.0.20/10.0.100
Microsoft.Android.Sdk.Darwin 36.1.53
Host macOS 26.6 (25G72), arm64
Is this a regression from previous behavior?
Unknown — not bisected against earlier SDK versions.
Description
With
EmbedAssembliesIntoApk=true, an Android app that has at least oneProjectReferencenever builds incrementally. Every build — including one run immediately after a successful build, with no file touched — re-runs_BuildApkEmbed, then re-zipaligns and re-signs the APK. That is ~30s in the hello-world repro below, and ~45s in our real app, on every build.The APK packaging targets themselves look correct:
_BuildApkEmbedhas properInputs/Outputs, and the input it names really is newer than the APK. The problem is upstream of it — the referenced library is recompiled on every build, which invalidates everything downstream.Removing the single
ProjectReferencedrops the no-change rebuild from 32.5s to 1.0s.Steps to Reproduce
The repro is nothing but
dotnet new maui+dotnet new classlibwith oneProjectReferenceadded. The only non-template properties inReproApp.csprojare:ReproLib.csprojis an unmodifieddotnet new classlibretargeted tonet10.0-android.Measured on the second (no-change) build:
_BuildApkEmbedProjectReference,EmbedAssembliesIntoApk=trueProjectReferenceProjectReference,EmbedAssembliesIntoApk=false(fast deploy)_BuildApkFastDev202msThat last row is likely why this isn't already widely reported: the default Debug deploy path uses fast deployment, where the same redundant recompile still happens but packaging is cheap. It only becomes expensive on the embed path — which is what you need when the APK is installed directly (
adb install, or an Appium-driven test run) rather than deployed through the SDK.Link to public reproduction project repository
https://github.com/Qythyx/bug_repros/tree/android-apk-repack-every-build
Root cause
The referenced library is compiled twice per app build, by two MSBuild project configurations. Diffing the two
csccommand lines: 362 arguments each, differing in exactly one —/define:— carrying the same 75 constants in a different order:_GenerateCompileDependencyCachehashes$(DefineConstants)as a string, so the two configurations compute different hashes and overwrite each other's<Project>.csproj.CoreCompileInputs.cacheon every build. The last write lands after csc wrote the compile outputs, so on the next build the cache is newer than the outputs andCoreCompileruns "completely" again. The cache content is byte-identical between builds — only the mtime moves — so it never converges.Constant ordering has no effect on compilation, so this is pure build-system churn.
The resulting cascade, taken from an MSBuild
-v dlog of the repro (each line is MSBuild's own up-to-date reason):Building the library on its own is perfectly incremental —
CoreCompileis skipped, and the cache and the compile outputs share a timestamp. The second configuration only appears when the library is built as a reference of the Android app.Did you find any workaround?
None that keeps the build both correct and incremental. Tried and ruled out, each verified by measurement:
-p:RuntimeIdentifier=android-arm64, so there is no outer/inner RID split) — no change, still ~48s.-p:CSharpier_Bypass=true) — no change, still ~41s.GenerateDocumentationFileon or off — reproduces either way; it only changes whichCoreCompileoutput the cache is compared against.net10.0-androidlibrary reproduces.EmbedAssembliesIntoApk=falseavoids the cost but not the underlying recompile, and it isn't usable when the APK must be installed directly.The only practical mitigation we found is to avoid building the app at all when nothing changed — wrapping the app build in a hand-written MSBuild target with its own
Inputs/Outputsstaleness check, which duplicates work the SDK is already doing.Android application type
.NET Android (
net10.0-android), MAUI appAffected platform version
Is this a regression from previous behavior?
Unknown — not bisected against earlier SDK versions.