From 882fc1f092adcacdb71f7734745a17c9388d0b52 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 19 Aug 2026 12:40:18 -0500 Subject: [PATCH 1/3] [tests] Reproduce NuGet package cache modification Add a red regression test for #11022 using Humanizer satellite assemblies from dotnet-public. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d50a004c-7088-4372-a35a-a824c74118be --- .../Xamarin.Android.Build.Tests/BuildTest.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 63a575f42a2..a3161e7e4f7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2300,6 +2300,54 @@ public void CheckLintResourceFileReferencesAreFixed ([Values (AndroidRuntime.Cor } } + [Test] + public void BuildDoesNotModifyNuGetPackageCache () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + GlobalPackagesFolder = Path.Combine (Root, TestName, "packages"), + TargetFramework = "net10.0-android", + PackageReferences = { + new Package { Id = "Humanizer.Core", Version = "2.14.1" }, + new Package { Id = "Humanizer.Core.es", Version = "2.14.1" }, + }, + }; + proj.SetRuntime (AndroidRuntime.MonoVM); + proj.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); + proj.SetProperty (KnownProperties.PublishTrimmed, true.ToString ()); + proj.MainActivity = proj.DefaultMainActivity + .Replace ("//${USINGS}", "using Humanizer;") + .Replace ("//${AFTER_ONCREATE}", "System.Console.WriteLine (System.DateTime.UtcNow.Humanize ());"); + + using var builder = CreateApkBuilder (); + var buildParameters = new [] { $"RestorePackagesPath={proj.GlobalPackagesFolder}" }; + Assert.IsTrue (builder.Restore (proj, parameters: buildParameters), "Package restore should have succeeded."); + + var satelliteAssemblies = Directory.GetFiles (proj.GlobalPackagesFolder, "*.resources.dll", SearchOption.AllDirectories); + Assert.IsNotEmpty (satelliteAssemblies, "The NuGet package should contain satellite assemblies."); + + var originalWriteTimes = new Dictionary (); + foreach (string assembly in satelliteAssemblies) { + File.SetLastWriteTimeUtc (assembly, new DateTime (2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + originalWriteTimes.Add (assembly, File.GetLastWriteTimeUtc (assembly)); + } + + // A package cache is immutable: deny write sharing and verify timestamps remain unchanged. + var packageLocks = satelliteAssemblies + .Select (assembly => File.Open (assembly, FileMode.Open, FileAccess.Read, FileShare.Read)) + .ToList (); + try { + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, parameters: buildParameters), "Build should have succeeded."); + foreach (string assembly in satelliteAssemblies) { + Assert.AreEqual (originalWriteTimes [assembly], File.GetLastWriteTimeUtc (assembly), $"Build should not modify '{assembly}'."); + } + } finally { + foreach (var packageLock in packageLocks) { + packageLock.Dispose (); + } + } + } + [Test] [TestCase (true, AndroidRuntime.CoreCLR)] [TestCase (false, AndroidRuntime.CoreCLR)] From e92d21f1313d7b5b31f4bcb15840b5ef08eb9192 Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 19 Aug 2026 14:44:15 -0500 Subject: [PATCH 2/3] [build] Avoid modifying NuGet satellite assemblies Skip satellite resource assemblies when the post-link pipeline operates in place, while retaining them in the packaging item flow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d50a004c-7088-4372-a35a-a824c74118be --- .../Xamarin.Android.Build.Tests/BuildTest.cs | 26 ++++++++++++++++--- .../Xamarin.Android.Common.targets | 9 ++++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index a3161e7e4f7..31c007c4ae1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2306,13 +2306,29 @@ public void BuildDoesNotModifyNuGetPackageCache () var proj = new XamarinAndroidApplicationProject { IsRelease = true, GlobalPackagesFolder = Path.Combine (Root, TestName, "packages"), - TargetFramework = "net10.0-android", + Imports = { + new Import (() => "EnableMarshalMethodsForPostLink.targets") { + TextContent = () => +""" + + + + + <_AndroidUseMarshalMethods>true + + + +""" + }, + }, PackageReferences = { new Package { Id = "Humanizer.Core", Version = "2.14.1" }, new Package { Id = "Humanizer.Core.es", Version = "2.14.1" }, }, }; - proj.SetRuntime (AndroidRuntime.MonoVM); + proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("AndroidTypeMapImplementation", "llvm-ir"); proj.SetProperty (KnownProperties.PublishTrimmed, true.ToString ()); proj.MainActivity = proj.DefaultMainActivity @@ -2322,6 +2338,8 @@ public void BuildDoesNotModifyNuGetPackageCache () using var builder = CreateApkBuilder (); var buildParameters = new [] { $"RestorePackagesPath={proj.GlobalPackagesFolder}" }; Assert.IsTrue (builder.Restore (proj, parameters: buildParameters), "Package restore should have succeeded."); + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, parameters: buildParameters), + "Initial build should have succeeded."); var satelliteAssemblies = Directory.GetFiles (proj.GlobalPackagesFolder, "*.resources.dll", SearchOption.AllDirectories); Assert.IsNotEmpty (satelliteAssemblies, "The NuGet package should contain satellite assemblies."); @@ -2337,7 +2355,9 @@ public void BuildDoesNotModifyNuGetPackageCache () .Select (assembly => File.Open (assembly, FileMode.Open, FileAccess.Read, FileShare.Read)) .ToList (); try { - Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false, parameters: buildParameters), "Build should have succeeded."); + var postLinkParameters = buildParameters.Append ("TestEnableMarshalMethodsForPostLink=true").ToArray (); + Assert.IsTrue (builder.RunTarget (proj, "_PrepareAssemblies", doNotCleanupOnUpdate: true, saveProject: false, parameters: postLinkParameters), + "Preparing assemblies should have succeeded."); foreach (string assembly in satelliteAssemblies) { Assert.AreEqual (originalWriteTimes [assembly], File.GetLastWriteTimeUtc (assembly), $"Build should not modify '{assembly}'."); } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 57fa1a57afc..82c799eed84 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1472,8 +1472,10 @@ because xbuild doesn't support framework reference assemblies. Outputs="$(_AdditionalPostLinkerStepsFlag)"> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' != 'True' " Include="@(ResolvedAssemblies->'$(_AfterILLinkOutputDir)%(DestinationSubPath)')" /> - <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' == 'True' " Include="@(ResolvedAssemblies)" /> + <_AfterILLinkSourceFiles Include="@(ResolvedAssemblies)" + Condition=" '$(_AndroidUseMarshalMethods)' != 'True' or !$([System.String]::Copy('%(ResolvedAssemblies.Filename)').ToLowerInvariant().EndsWith('.resources')) " /> + <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' != 'True' " Include="@(_AfterILLinkSourceFiles->'$(_AfterILLinkOutputDir)%(DestinationSubPath)')" /> + <_AfterILLinkDestFiles Condition=" '$(_AndroidUseMarshalMethods)' == 'True' " Include="@(_AfterILLinkSourceFiles)" /> + <_AfterILLinkSourceFiles Remove="@(_AfterILLinkSourceFiles)" /> <_AfterILLinkDestFiles Remove="@(_AfterILLinkDestFiles)" /> From 4894e2ce3002ad6bfe19b670220ab40ebea5d71b Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Wed, 19 Aug 2026 15:00:42 -0500 Subject: [PATCH 3/3] [tests] Force in-place post-link pipeline to rerun Delete the incremental post-link stamp before exercising the marshal-method path and verify that the target recreates it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d50a004c-7088-4372-a35a-a824c74118be --- .../Tests/Xamarin.Android.Build.Tests/BuildTest.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 31c007c4ae1..acb3f9c1713 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -2350,6 +2350,10 @@ public void BuildDoesNotModifyNuGetPackageCache () originalWriteTimes.Add (assembly, File.GetLastWriteTimeUtc (assembly)); } + var postLinkStamp = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "stamp", "_AdditionalPostLinkerSteps.stamp"); + FileAssert.Exists (postLinkStamp); + File.Delete (postLinkStamp); + // A package cache is immutable: deny write sharing and verify timestamps remain unchanged. var packageLocks = satelliteAssemblies .Select (assembly => File.Open (assembly, FileMode.Open, FileAccess.Read, FileShare.Read)) @@ -2358,6 +2362,7 @@ public void BuildDoesNotModifyNuGetPackageCache () var postLinkParameters = buildParameters.Append ("TestEnableMarshalMethodsForPostLink=true").ToArray (); Assert.IsTrue (builder.RunTarget (proj, "_PrepareAssemblies", doNotCleanupOnUpdate: true, saveProject: false, parameters: postLinkParameters), "Preparing assemblies should have succeeded."); + FileAssert.Exists (postLinkStamp); foreach (string assembly in satelliteAssemblies) { Assert.AreEqual (originalWriteTimes [assembly], File.GetLastWriteTimeUtc (assembly), $"Build should not modify '{assembly}'."); }