Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
70f7f30
[illink] Move FixLegacyResourceDesignerStep to post-trim pipeline
sbomer Mar 30, 2026
77de75d
[targets] Fix NativeAOT designer assembly trimming
sbomer Apr 1, 2026
e9edefd
Merge remote-tracking branch 'origin/main' into fix-resource-designer
sbomer Apr 1, 2026
f0059fd
Fix merge conflict: remove FixAbstractMethodsStep from ILLink csproj
sbomer Apr 2, 2026
64d4ed1
Root designer assembly to prevent ILLink from trimming resource prope…
sbomer Apr 2, 2026
924649e
Use TrimmerRootDescriptor instead of TrimmerRootAssembly for designer…
sbomer Apr 3, 2026
8be3817
Update SkiaSharp test to expect XA8000 instead of IL8000 for release …
sbomer Apr 3, 2026
f12d747
Merge remote-tracking branch 'origin/main' into fix-resource-designer
sbomer Apr 3, 2026
83a8f64
Fix incremental build: use WriteOnlyWhenDifferent for linker descriptor
sbomer Apr 6, 2026
6e870ef
Add TODO to simplify TrimmerRootDescriptor once dotnet/runtime#126518…
sbomer Apr 6, 2026
4c4c091
Move FixLegacyResourceDesignerStep to run before ILLink trimming
sbomer Apr 7, 2026
5e3ba1f
Fix SkiaSharp NativeAOT test to expect XA8000 build failure
sbomer Apr 7, 2026
432b1b2
Filter framework/main assemblies in PreTrimmingFixLegacyDesigner for …
sbomer Apr 7, 2026
548ba35
Merge branch 'main' into dev/sbomer/fix-legacy-before-trim
sbomer Apr 10, 2026
7b72611
Filter pre/post trimming assemblies by PostprocessAssembly metadata
sbomer Apr 10, 2026
0dbbbee
Open assemblies read-only in PreTrimmingFixLegacyDesigner
sbomer Apr 10, 2026
6420522
Use InMemory=true to avoid file locking in PreTrimmingFixLegacyDesigner
sbomer Apr 13, 2026
3629ccf
Write modified assemblies to intermediate directory instead of in-place
sbomer Apr 13, 2026
db70715
Add FileWrites for incremental clean support
sbomer Apr 13, 2026
77e20ff
Add Inputs/Outputs for incremental build support
sbomer Apr 13, 2026
c1f3ceb
Scope Inputs to PostprocessAssembly assemblies only
sbomer Apr 13, 2026
bcac9af
Merge remote-tracking branch 'origin/main' into fix-legacy-before-trim
sbomer Apr 14, 2026
78fc39b
Fix target ordering: run pre-trimming before ILLink dependency chain
sbomer Apr 14, 2026
8e45623
Remove designer rooting and post-trimming FixLegacyResourceDesigner
sbomer Apr 15, 2026
1b8748b
Restore MonoDroid.Tuner using directive in PostTrimmingPipeline
sbomer Apr 15, 2026
122e9a4
Update apkdiff baselines for improved designer trimming
sbomer Apr 15, 2026
2f8b72e
Merge remote-tracking branch 'origin/main' into fix-legacy-before-trim
sbomer Apr 16, 2026
b28ccdf
Fix NativeAOT pre-trimming ordering
sbomer Apr 16, 2026
ac02d09
Create stamp directory and update NativeAOT apkdiff baselines
sbomer Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -227,42 +227,22 @@ Copyright (C) 2016 Xamarin. All rights reserved.
is processed by the ILLink step. If we do not do this then the reference to
`netstandard.dll` is not replaced with `System.Private.CoreLib` and the app crashes.

We use a TrimmerRootDescriptor (not TrimmerRootAssembly) to prevent ILLink from
trimming the designer's resource properties. FixLegacyResourceDesignerStep runs
after ILLink and needs all properties present when rewriting library assemblies.
A descriptor (-x) preserves types without making the assembly an entry point,
which avoids pulling netstandard.dll into the output.
See https://github.com/dotnet/runtime/issues/126518

TODO: Once dotnet/runtime#126518 is fixed and flows to dotnet/android,
simplify this to use TrimmerRootAssembly instead of the XML descriptor.
No TrimmerRootDescriptor is needed: PreTrimmingFixLegacyDesigner rewrites library
assemblies *before* ILLink, replacing designer field loads with calls to the designer
assembly's property getters. ILLink then naturally preserves only the referenced
properties through its mark step, allowing everything else to be trimmed.
-->
<Target Name="_AddResourceDesignerToPublishFiles"
Condition=" '$(AndroidUseDesignerAssembly)' == 'True' "
AfterTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="_SetupDesignerProperties">
<PropertyGroup>
<_DesignerLinkerDescriptor>$(IntermediateOutputPath)_Microsoft.Android.Resource.Designer.xml</_DesignerLinkerDescriptor>
</PropertyGroup>
<WriteLinesToFile
File="$(_DesignerLinkerDescriptor)"
Overwrite="true"
WriteOnlyWhenDifferent="true"
Lines="&lt;linker&gt;&lt;assembly fullname=&quot;$(_DesignerAssemblyName)&quot;&gt;&lt;type fullname=&quot;*&quot; preserve=&quot;all&quot; /&gt;&lt;/assembly&gt;&lt;/linker&gt;"
/>
<ItemGroup>
<ResolvedFileToPublish Include="$(_GenerateResourceDesignerAssemblyOutput)">
<RelativePath>$(_DesignerAssemblyName).dll</RelativePath>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<PostprocessAssembly>true</PostprocessAssembly>
<IsTrimmable>true</IsTrimmable>
</ResolvedFileToPublish>
<!-- Preserve all designer types/members via descriptor so ILLink keeps resource
properties. Using a descriptor (not TrimmerRootAssembly) avoids making the
assembly an entry point, which would pull netstandard.dll into the output.
See https://github.com/dotnet/runtime/issues/126518 -->
<TrimmerRootDescriptor Include="$(_DesignerLinkerDescriptor)" />
<FileWrites Include="$(_DesignerLinkerDescriptor)" />
</ItemGroup>
</Target>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android.
-->
<Target Name="_AndroidRunNativeCompile"
AfterTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="NativeCompile" />
DependsOnTargets="_PreTrimmingFixLegacyDesignerUpdateItems;NativeCompile" />

<Target Name="_AndroidFixNativeLibraryFileName" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<UsingTask TaskName="Xamarin.Android.Tasks.RemoveRegisterAttribute" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.GenerateProguardConfiguration" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.PostTrimmingPipeline" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
<UsingTask TaskName="Xamarin.Android.Tasks.PreTrimmingFixLegacyDesigner" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />

<PropertyGroup>
<_RemoveRegisterFlag>$(MonoAndroidIntermediateAssemblyDir)shrunk\shrunk.flag</_RemoveRegisterFlag>
Expand Down Expand Up @@ -223,6 +224,62 @@
</ItemGroup>
</Target>

<!--
Fix legacy resource designer references *before* ILLink runs. This rewrites
library assemblies so their resource field accesses (ldsfld) become calls to the
designer assembly's property getters, and clears the per-library designer classes.
Because the rewritten IL no longer references the old designer fields, ILLink can
freely trim unused designer types without needing a root descriptor.

Modified assemblies are written to an intermediate directory (not in-place) to
avoid mutating files in the shared NuGet cache or shared intermediate output paths.
-->
<Target Name="_CollectPreTrimmingAssemblies"
AfterTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="_ComputeAssembliesToPostprocessOnPublish"
Condition=" '$(PublishTrimmed)' == 'true' and '$(AndroidUseDesignerAssembly)' == 'True' ">
<ItemGroup>
<_PreTrimmingAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and '%(ResolvedFileToPublish.PostprocessAssembly)' == 'true' " />
</ItemGroup>
</Target>

<Target Name="_PreTrimmingFixLegacyDesigner"
AfterTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="_CollectPreTrimmingAssemblies"
Condition=" '$(PublishTrimmed)' == 'true' and '$(AndroidUseDesignerAssembly)' == 'True' "
Inputs="@(_PreTrimmingAssembly)"
Outputs="$(_AndroidStampDirectory)_PreTrimmingFixLegacyDesigner.stamp">
<MakeDir Directories="$(_AndroidStampDirectory)" />
<PreTrimmingFixLegacyDesigner
Assemblies="@(_PreTrimmingAssembly)"
TargetName="$(TargetName)"
OutputDirectory="$(IntermediateOutputPath)prelink"
Deterministic="$(Deterministic)">
<Output TaskParameter="ModifiedAssemblies" ItemName="_PreTrimmingModifiedAssembly" />
</PreTrimmingFixLegacyDesigner>
<Touch Files="$(_AndroidStampDirectory)_PreTrimmingFixLegacyDesigner.stamp" AlwaysCreate="true" />
<ItemGroup>
<FileWrites Include="@(_PreTrimmingModifiedAssembly)" />
</ItemGroup>
</Target>

<!--
Replace ResolvedFileToPublish entries with the prelink copies so ILLink picks them up.
This must run even when _PreTrimmingFixLegacyDesigner is skipped (up-to-date) because
ResolvedFileToPublish is populated fresh each build.
-->
<Target Name="_PreTrimmingFixLegacyDesignerUpdateItems"
AfterTargets="ComputeResolvedFilesToPublishList"
DependsOnTargets="_PreTrimmingFixLegacyDesigner"
Condition=" '$(PublishTrimmed)' == 'true' and '$(AndroidUseDesignerAssembly)' == 'True' ">
<ItemGroup>
<_PreTrimmingSwappableItem Include="@(ResolvedFileToPublish)"
Condition=" '%(Extension)' == '.dll' and Exists('$(IntermediateOutputPath)prelink/%(Filename)%(Extension)') " />
<ResolvedFileToPublish Remove="@(_PreTrimmingSwappableItem)" />
<ResolvedFileToPublish Include="@(_PreTrimmingSwappableItem->'$(IntermediateOutputPath)prelink/%(Filename)%(Extension)')" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Incremental builds — This swap uses an Exists() check on the filesystem to decide which ResolvedFileToPublish items to redirect to prelink/. If the stale prelink cleanup suggested in PreTrimmingFixLegacyDesigner.cs is not adopted, an alternative fix here would be to persist _PreTrimmingModifiedAssembly items to a file (or use a response-file pattern) and read that back, so only assemblies actually modified in the current (or most recent) task run get swapped.

Rule: Incremental build correctness

</ItemGroup>
</Target>

<Target Name="_TouchAndroidLinkFlag"
AfterTargets="ILLink"
Condition=" '$(PublishTrimmed)' == 'true' and '$(RunILLink)' != 'false' and Exists('$(_LinkSemaphore)') "
Expand All @@ -248,8 +305,7 @@
Assemblies="@(_PostTrimmingAssembly)"
AddKeepAlives="$(AndroidAddKeepAlives)"
AndroidLinkResources="$(AndroidLinkResources)"
Deterministic="$(Deterministic)"
UseDesignerAssembly="$(AndroidUseDesignerAssembly)" />
Deterministic="$(Deterministic)" />
</Target>

<!-- Inject _TypeMapKind into the property cache -->
Expand Down
35 changes: 1 addition & 34 deletions src/Xamarin.Android.Build.Tasks/Tasks/PostTrimmingPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Linker;
using MonoDroid.Tuner;

namespace Xamarin.Android.Tasks;
Expand All @@ -17,7 +16,7 @@ namespace Xamarin.Android.Tasks;
/// This opens each assembly once (via DirectoryAssemblyResolver with ReadWrite) and
/// runs all registered steps on it, then writes modified assemblies in-place. Currently
/// runs CheckForObsoletePreserveAttributeStep, StripEmbeddedLibrariesStep and
/// (optionally) AddKeepAlivesStep and FixLegacyResourceDesignerStep.
/// (optionally) AddKeepAlivesStep.
///
/// Runs in the inner build after ILLink but before ReadyToRun/crossgen2 compilation,
/// so that R2R images are generated from the already-modified assemblies.
Expand All @@ -35,8 +34,6 @@ public class PostTrimmingPipeline : AndroidTask

public bool Deterministic { get; set; }

public bool UseDesignerAssembly { get; set; }

public override bool RunTask ()
{
using var resolver = new DirectoryAssemblyResolver (
Expand Down Expand Up @@ -103,15 +100,6 @@ public override bool RunTask ()
},
(msg) => Log.LogDebugMessage (msg)));
}
if (UseDesignerAssembly) {
// Create an MSBuildLinkContext so FixLegacyResourceDesignerStep can resolve assemblies
// and log messages. The resolver is owned by the outer 'using' block, so we intentionally
// do not dispose this context (LinkContext.Dispose would double-dispose the resolver).
var linkContext = new MSBuildLinkContext (resolver, Log);
var fixLegacyStep = new FixLegacyResourceDesignerStep ();
fixLegacyStep.Initialize (linkContext);
steps.Add (new PostTrimmingFixLegacyResourceDesignerStep (fixLegacyStep));
}

foreach (var (item, assembly) in loadedAssemblies) {
var context = new StepContext (item, item);
Expand All @@ -130,24 +118,3 @@ public override bool RunTask ()
return !Log.HasLoggedErrors;
}
}

/// <summary>
/// Thin wrapper around <see cref="FixLegacyResourceDesignerStep"/> for the post-trimming pipeline.
/// Calls <see cref="FixLegacyResourceDesignerStep.ProcessAssemblyDesigner"/> directly, matching the
/// behavior of the former ILLink path which processed all assemblies without StepContext flag filtering.
/// Assemblies without a resource designer are skipped internally by ProcessAssemblyDesigner.
/// </summary>
class PostTrimmingFixLegacyResourceDesignerStep : IAssemblyModifierPipelineStep
{
readonly FixLegacyResourceDesignerStep _inner;

public PostTrimmingFixLegacyResourceDesignerStep (FixLegacyResourceDesignerStep inner)
{
_inner = inner;
}

public void ProcessAssembly (AssemblyDefinition assembly, StepContext context)
{
context.IsAssemblyModified |= _inner.ProcessAssemblyDesigner (assembly);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#nullable enable

using System.Collections.Generic;
using System.IO;
using Java.Interop.Tools.Cecil;
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Mono.Cecil;
using MonoDroid.Tuner;

namespace Xamarin.Android.Tasks;

/// <summary>
/// Runs <see cref="FixLegacyResourceDesignerStep"/> on assemblies that are about to be
/// trimmed by ILLink. This rewrites library assemblies so their resource field accesses
/// (ldsfld) become calls to the designer assembly's property getters.
///
/// Running this *before* ILLink means the trimmer sees the rewritten IL and can freely
/// trim unused designer types/fields. This avoids the need to root the entire designer
/// assembly during trimming (which causes an APK size regression).
///
/// Modified assemblies are written to <see cref="OutputDirectory"/> rather than in-place,
/// to avoid mutating files in the shared NuGet cache or shared intermediate output paths.
/// </summary>
public class PreTrimmingFixLegacyDesigner : AndroidTask
{
public override string TaskPrefix => "PTD";

[Required]
public ITaskItem [] Assemblies { get; set; } = [];

[Required]
public string TargetName { get; set; } = "";

[Required]
public string OutputDirectory { get; set; } = "";

public bool Deterministic { get; set; }

[Output]
public ITaskItem []? ModifiedAssemblies { get; set; }

public override bool RunTask ()
{
Directory.CreateDirectory (OutputDirectory);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 ⚠️ Incremental buildsDirectory.CreateDirectory doesn't clear old files. If a library assembly had a designer in a previous build but no longer does (e.g. NuGet update), the stale prelink/Foo.dll from the previous run persists. _PreTrimmingFixLegacyDesignerUpdateItems uses an Exists() check, so it would swap in the stale (old-version) assembly instead of the new one.

Consider cleaning the output directory at the start, or switching the swap target to use the _PreTrimmingModifiedAssembly output items instead of filesystem Exists():

// Clean stale prelink copies from previous runs
if (Directory.Exists (OutputDirectory)) {
	Directory.Delete (OutputDirectory, recursive: true);
}
Directory.CreateDirectory (OutputDirectory);

Rule: Incremental build correctness (Postmortem #53)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like FileWrites and IncrementalClean are supposed to solve this instead.


using var resolver = new DirectoryAssemblyResolver (
this.CreateTaskLogger (), loadDebugSymbols: true);

foreach (var assembly in Assemblies) {
var dir = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec) ?? "");
if (!resolver.SearchDirectories.Contains (dir)) {
resolver.SearchDirectories.Add (dir);
}
}

var linkContext = new MSBuildLinkContext (resolver, Log);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 Documentation — The removed code in PostTrimmingPipeline.cs had a helpful comment explaining why MSBuildLinkContext is intentionally not disposed (to avoid double-disposing the resolver). Consider adding a similar comment here:

// MSBuildLinkContext wraps the resolver; do not dispose it separately
// as LinkContext.Dispose would double-dispose the resolver.
var linkContext = new MSBuildLinkContext (resolver, Log);

Rule: Comments explain "why", not "what" (Postmortem #59)

var fixLegacyStep = new FixLegacyResourceDesignerStep ();
fixLegacyStep.Initialize (linkContext);

var modified = new List<ITaskItem> ();

foreach (var item in Assemblies) {
// Match the filtering in FixLegacyResourceDesignerStep.ProcessAssembly:
// skip the main assembly and framework/BCL assemblies.
if (Path.GetFileNameWithoutExtension (item.ItemSpec) == TargetName) {
continue;
}
if (MonoAndroidHelper.IsFrameworkAssembly (item)) {
continue;
}

var assembly = resolver.GetAssembly (item.ItemSpec);
if (fixLegacyStep.ProcessAssemblyDesigner (assembly)) {
var outputPath = Path.Combine (OutputDirectory, Path.GetFileName (item.ItemSpec));
Log.LogDebugMessage ($" Writing modified assembly: {outputPath}");
assembly.Write (outputPath, new WriterParameters {
WriteSymbols = assembly.MainModule.HasSymbols,
DeterministicMvid = Deterministic,
});

var outputItem = new TaskItem (outputPath);
item.CopyMetadataTo (outputItem);
outputItem.SetMetadata ("OriginalPath", item.ItemSpec);
modified.Add (outputItem);
}
}

ModifiedAssemblies = modified.ToArray ();

return !Log.HasLoggedErrors;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
"Size": 400044
},
"lib/arm64-v8a/libassembly-store.so": {
"Size": 3115312
"Size": 3107808
},
"lib/arm64-v8a/libclrjit.so": {
"Size": 3202072
"Size": 3207768
},
"lib/arm64-v8a/libcoreclr.so": {
"Size": 5766640
"Size": 5769760
},
"lib/arm64-v8a/libmonodroid.so": {
"Size": 1365104
"Size": 1365360
},
"lib/arm64-v8a/libmscordaccore.so": {
"Size": 2493552
"Size": 2485088
},
"lib/arm64-v8a/libmscordbi.so": {
"Size": 1902744
"Size": 1901544
},
"lib/arm64-v8a/libSystem.Globalization.Native.so": {
"Size": 71936
Expand All @@ -32,7 +32,7 @@
"Size": 1281696
},
"lib/arm64-v8a/libSystem.Native.so": {
"Size": 107904
"Size": 108424
},
"lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": {
"Size": 165536
Expand Down Expand Up @@ -74,5 +74,5 @@
"Size": 1904
}
},
"PackageSize": 9258778
"PackageSize": 9254682
}
Loading