Android framework version
net10.0-android
Affected platform version
.NET SDK 10.0.400 · workload set 10.0.401 · Microsoft.Android.Sdk.Windows 36.1.69 · R8 8.11.18 · Windows 11
Description
Up front, so it is weighed correctly: this is derived from reading the shipped targets, not from an end-to-end reproduction. I have quoted the whole mechanism below and would be glad to be told I have misread it. (I have filed the reproduced one as #12965.)
In tools/Xamarin.Android.Aapt2.targets, both the ProguardConfiguration entry and the FileWrites registration for aapt_rules.txt are inside _CreateBaseApkWithAapt2:
<Target Name="_CreateBaseApkWithAapt2">
<PropertyGroup>
<_Aapt2ProguardRules Condition=" '$(AndroidLinkTool)' != '' ">$(IntermediateOutputPath)aapt_rules.txt</_Aapt2ProguardRules>
...
</PropertyGroup>
<Aapt2Link ... ProguardRuleOutput="$(_Aapt2ProguardRules)" />
<ItemGroup Condition=" '$(_Aapt2ProguardRules)' != '' And Exists('$(_Aapt2ProguardRules)') ">
<ProguardConfiguration Include="$(_Aapt2ProguardRules)" />
<FileWrites Include="$(_Aapt2ProguardRules)" />
<FileWrites Include="$(IntermediateOutputPath)android\*\aapt_rules.txt" />
</ItemGroup>
</Target>
When the base APK is up to date and this target is skipped, aapt_rules.txt is claimed by nobody for that build. IncrementalClean in Microsoft.Common.CurrentVersion.targets then computes orphans as the prior build's writes minus the current build's writes, and deletes those under the intermediate path:
<_CleanOrphanFileWrites Include="@(_CleanPriorFileWrites)" Exclude="@(_CleanCurrentFileWrites)"/>
...
<FindUnderPath Path="$(IntermediateOutputPath)" Files="@(_CleanOrphanFileWrites)">
<Output TaskParameter="InPath" ItemName="_CleanOrphanFileWritesInIntermediate"/>
</FindUnderPath>
<Delete Files="@(_CleanOrphanFileWritesInIntermediate);@(_CleanOrphanFileWritesInOutput)" ... />
aapt_rules.txt lives under $(IntermediateOutputPath), so it matches and is deleted.
Why this matters: that file holds the keeps aapt2 generates for every type named in AndroidManifest.xml — activities, services, receivers and providers, i.e. precisely the classes the Android runtime instantiates by name. If they are lost, R8 may rename or remove an entry point, and the application fails at launch or when the affected component is first reached.
The same skip also removes the file from ProguardConfiguration, so even before any deletion R8 does not receive it on that build.
Steps to Reproduce
- A
net10.0-android application, Configuration=Release, AndroidLinkTool=r8.
- Build once, so that
obj/<configuration>/<tfm>/aapt_rules.txt exists and is recorded in the write log.
- Build again under conditions where
_CreateBaseApkWithAapt2 is skipped as up to date, with any target that runs IncrementalClean.
- Observe
aapt_rules.txt.
Expected: the file survives, or it is registered in FileWrites from a scope that runs on every build. A file the build depends on should not become deletable by the clean step purely because the target that produced it was skipped.
Did you find any workaround?
We rebuild R8's configuration list ourselves rather than relying on the items the SDK contributes, and we added a build check that fails when aapt_rules.txt is missing from R8's list — so the condition is caught at build time rather than shipping. That addresses the symptom in our build only.
A possible fix, if the reading above is right: move the two aapt_rules.txt registrations out of _CreateBaseApkWithAapt2 into a target that runs unconditionally when $(AndroidLinkTool) is set. The existing Exists() guard already makes it safe to register a file produced by an earlier build, so both the FileWrites claim and the ProguardConfiguration entry would survive a skipped build.
Relevant log output
# No log is attached, deliberately: this report is a reading of the shipped targets
# rather than a captured failure. The two files quoted in the description are:
#
# <SDK>/tools/Xamarin.Android.Aapt2.targets _CreateBaseApkWithAapt2
# <dotnet>/sdk/10.0.401/Microsoft.Common.CurrentVersion.targets IncrementalClean
#
# Microsoft.Android.Sdk.Windows 36.1.69, .NET SDK 10.0.401.
One thing I noticed and am deliberately not reporting as a defect, in case it looks like an omission: the sibling <FileWrites Include="$(IntermediateOutputPath)android\*\aapt_rules.txt" /> points at a different path from $(_Aapt2ProguardRules) and appears to cover per-ABI copies. It matches nothing in a non-per-ABI build, which is harmless.
Possibly related but distinct: #7447 also mentions a missing aapt_rules.txt, but it is Classic Xamarin.Android on VS 17.3.5 and the symptom there is a build failure during AOT, not a silent loss of manifest keeps.
Android framework version
net10.0-android
Affected platform version
.NET SDK 10.0.400 · workload set 10.0.401 ·
Microsoft.Android.Sdk.Windows36.1.69 · R8 8.11.18 · Windows 11Description
Up front, so it is weighed correctly: this is derived from reading the shipped targets, not from an end-to-end reproduction. I have quoted the whole mechanism below and would be glad to be told I have misread it. (I have filed the reproduced one as #12965.)
In
tools/Xamarin.Android.Aapt2.targets, both theProguardConfigurationentry and theFileWritesregistration foraapt_rules.txtare inside_CreateBaseApkWithAapt2:When the base APK is up to date and this target is skipped,
aapt_rules.txtis claimed by nobody for that build.IncrementalCleaninMicrosoft.Common.CurrentVersion.targetsthen computes orphans as the prior build's writes minus the current build's writes, and deletes those under the intermediate path:aapt_rules.txtlives under$(IntermediateOutputPath), so it matches and is deleted.Why this matters: that file holds the keeps aapt2 generates for every type named in
AndroidManifest.xml— activities, services, receivers and providers, i.e. precisely the classes the Android runtime instantiates by name. If they are lost, R8 may rename or remove an entry point, and the application fails at launch or when the affected component is first reached.The same skip also removes the file from
ProguardConfiguration, so even before any deletion R8 does not receive it on that build.Steps to Reproduce
net10.0-androidapplication,Configuration=Release,AndroidLinkTool=r8.obj/<configuration>/<tfm>/aapt_rules.txtexists and is recorded in the write log._CreateBaseApkWithAapt2is skipped as up to date, with any target that runsIncrementalClean.aapt_rules.txt.Expected: the file survives, or it is registered in
FileWritesfrom a scope that runs on every build. A file the build depends on should not become deletable by the clean step purely because the target that produced it was skipped.Did you find any workaround?
We rebuild R8's configuration list ourselves rather than relying on the items the SDK contributes, and we added a build check that fails when
aapt_rules.txtis missing from R8's list — so the condition is caught at build time rather than shipping. That addresses the symptom in our build only.A possible fix, if the reading above is right: move the two
aapt_rules.txtregistrations out of_CreateBaseApkWithAapt2into a target that runs unconditionally when$(AndroidLinkTool)is set. The existingExists()guard already makes it safe to register a file produced by an earlier build, so both theFileWritesclaim and theProguardConfigurationentry would survive a skipped build.Relevant log output
One thing I noticed and am deliberately not reporting as a defect, in case it looks like an omission: the sibling
<FileWrites Include="$(IntermediateOutputPath)android\*\aapt_rules.txt" />points at a different path from$(_Aapt2ProguardRules)and appears to cover per-ABI copies. It matches nothing in a non-per-ABI build, which is harmless.Possibly related but distinct: #7447 also mentions a missing
aapt_rules.txt, but it is Classic Xamarin.Android on VS 17.3.5 and the symptom there is a build failure during AOT, not a silent loss of manifest keeps.