Skip to content

No-change builds repack the APK when EmbedAssembliesIntoApk=true #12304

Description

@Qythyx

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    need-attentionA xamarin-android contributor needs to review

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions