Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -264,10 +264,13 @@ _ResolveAssemblies MSBuild target.
<Target Name="_PrepareAssemblies"
DependsOnTargets="$(_PrepareAssembliesDependsOnTargets)">
<ItemGroup Condition=" '$(PublishTrimmed)' != 'true' ">
<_ResolvedAssemblies Include="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedUserAssemblies Include="@(ResolvedUserAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedSymbols Include="@(ResolvedSymbols->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedAssemblies Include="@(_AndroidAssembliesToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedAssemblies Include="@(_AndroidAssembliesToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedUserAssemblies Include="@(ResolvedUserAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedFrameworkAssemblies Include="@(_AndroidAssembliesToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(FrameworkAssembly)' == 'True' And '%(DestinationSubPath)' != '' " />
<_ResolvedFrameworkAssemblies Include="@(_AndroidAssembliesToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedSymbols Include="@(_AndroidSymbolsToLinkNoShrink->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" Condition=" '%(DestinationSubPath)' != '' " />
<_ResolvedSymbols Include="@(_AndroidSymbolsToPackageWithoutLinkNoShrink)" Condition=" '%(DestinationSubPath)' != '' " />
<_ShrunkAssemblies Include="@(_ResolvedAssemblies)" />
<_ShrunkUserAssemblies Include="@(_ResolvedUserAssemblies)" />
<_ShrunkFrameworkAssemblies Include="@(_ResolvedFrameworkAssemblies)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public override bool RunTask ()
throw new InvalidOperationException ($"Internal error: assembly '{sourceArch}' targets architecture '{sourceArch}', while destination assembly '{destination}' targets '{destinationArch}' instead");
}

if (TryProcessWithoutPipeline (source, destination))
continue;

// Each architecture must have a different set of context classes, or otherwise only the first instance of the assembly may be rewritten.
if (currentArch != sourceArch) {
currentArch = sourceArch;
Expand Down Expand Up @@ -128,6 +131,8 @@ public override bool RunTask ()
return !Log.HasLoggedErrors;
}

protected virtual bool TryProcessWithoutPipeline (ITaskItem source, ITaskItem destination) => false;

protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context)
{
// FindJavaObjectsStep
Expand Down
2 changes: 1 addition & 1 deletion src/Xamarin.Android.Build.Tasks/Tasks/GenerateACWMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void GenerateMap ()
var entries = new List<ACWMapEntry> ();

foreach (var assembly in singleArchAssemblies) {
var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly.ItemSpec);
var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly);

if (!File.Exists (wrappersPath)) {
Log.LogError ($"'{wrappersPath}' not found.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ void GenerateWrappers (List<ITaskItem> assemblies)
var sw = Stopwatch.StartNew ();

foreach (var assembly in assemblies) {
var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly.ItemSpec);
var wrappersPath = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (assembly);

if (!File.Exists (wrappersPath)) {
Log.LogError ($"'{wrappersPath}' not found.");
Expand Down
17 changes: 17 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#nullable enable

using System;
using System.IO;
using Microsoft.Build.Framework;
using MonoDroid.Tuner;

namespace Xamarin.Android.Tasks
Expand All @@ -17,6 +19,21 @@ public class LinkAssembliesNoShrink : AssemblyModifierPipeline

public bool UseDesignerAssembly { get; set; }

protected override bool TryProcessWithoutPipeline (ITaskItem source, ITaskItem destination)
{
if (!bool.TryParse (source.GetMetadata ("AndroidSkipAssemblyModification"), out bool skipAssemblyModification) || !skipAssemblyModification)
return false;

// Downstream scanners treat a zero-byte file as an assembly that did not need scanning.
var marker = Path.ChangeExtension (destination.ItemSpec, ".scan.empty");
var markerDirectory = Path.GetDirectoryName (marker);
if (markerDirectory.IsNullOrEmpty ())
throw new InvalidOperationException ($"Could not determine the output directory for '{marker}'.");
Directory.CreateDirectory (markerDirectory);
JavaObjectsXmlFile.WriteEmptyFile (marker, Log);
return true;
}

protected override void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context)
{
// FixAbstractMethodsStep
Expand Down
13 changes: 11 additions & 2 deletions src/Xamarin.Android.Build.Tasks/Tasks/ProcessAssemblies.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ namespace Xamarin.Android.Tasks
/// Also sets some metadata:
/// * %(FrameworkAssembly)=True to determine if framework or user assembly
/// * %(HasMonoAndroidReference)=True for incremental build performance
/// * %(AndroidSkipAssemblyModification)=True for framework assemblies with no Android types
/// * Modify %(DestinationSubDirectory) and %(DestinationSubPath) if an assembly has an architecture-specific version
/// </summary>
public class ProcessAssemblies : AndroidTask
Expand Down Expand Up @@ -138,11 +139,19 @@ void SetMetadataForAssemblies (List<ITaskItem> output, Dictionary<string, ITaskI
ITaskItem? symbol = GetOrCreateSymbolItem (symbols, assembly);
SetAssemblyAbiMetadata (assembly, symbol);
SetDestinationSubDirectory (assembly, symbol);
assembly.SetMetadata ("FrameworkAssembly", MonoAndroidHelper.IsFrameworkAssembly (assembly).ToString ());
bool isFrameworkAssembly = MonoAndroidHelper.IsFrameworkAssembly (assembly);
assembly.SetMetadata ("FrameworkAssembly", isFrameworkAssembly.ToString ());
symbol?.SetMetadata ("FrameworkAssembly", isFrameworkAssembly.ToString ());

if (!DesignTimeBuild) {
// Designer builds don't produce assemblies, the HasMonoAndroidReference call would throw an exception in that case
assembly.SetMetadata ("HasMonoAndroidReference", MonoAndroidHelper.HasMonoAndroidReference (assembly).ToString ());
string hasMonoAndroidReference = MonoAndroidHelper.HasMonoAndroidReference (assembly).ToString ();
assembly.SetMetadata ("HasMonoAndroidReference", hasMonoAndroidReference);
symbol?.SetMetadata ("HasMonoAndroidReference", hasMonoAndroidReference);

string skipAssemblyModification = (isFrameworkAssembly && !MonoAndroidHelper.IsAndroidAssembly (assembly)).ToString ();
assembly.SetMetadata ("AndroidSkipAssemblyModification", skipAssemblyModification);
symbol?.SetMetadata ("AndroidSkipAssemblyModification", skipAssemblyModification);
}
output.Add (assembly);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,90 @@ namespace Xamarin.Android.Build.Tests
[Parallelizable (ParallelScope.Children)]
public class IncrementalBuildTest : BaseTest
{
[Test]
public void FrameworkAssembliesArePackagedWithoutStaging ()
{
const string runtimeIdentifier = "android-arm64";
const string abi = "arm64-v8a";

if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) {
return;
}

var captureFrameworkAssemblies = new Import (() => "CaptureFrameworkAssemblies.targets") {
TextContent = () =>
"""
<Project>
<Target Name="_CaptureFrameworkAssemblies" AfterTargets="_PrepareAssemblies">
<Message
Condition=" '%(_ResolvedFrameworkAssemblies.Filename)' == 'Microsoft.CSharp' Or '%(_ResolvedFrameworkAssemblies.Filename)' == 'Mono.Android' "
Importance="high"
Text="FrameworkAssembly=%(_ResolvedFrameworkAssemblies.Filename)|%(_ResolvedFrameworkAssemblies.Identity)|%(_ResolvedFrameworkAssemblies.NuGetPackageId)|%(_ResolvedFrameworkAssemblies.JavaObjectsXmlFile)|%(_ResolvedFrameworkAssemblies.TypeMapObjectsXmlFile)" />
</Target>
</Project>
"""
};
var proj = new XamarinAndroidApplicationProject {
EmbedAssembliesIntoApk = true,
Imports = { captureFrameworkAssemblies },
};
proj.SetRuntime (AndroidRuntime.CoreCLR);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 ⚠️ Testing — This regression test only exercises CoreCLR's mandatory assembly-store path, but the target changes also affect MonoVM debug builds, where assembly stores are disabled and WrapAssembliesAsSharedLibraries packages each DLL/PDB directly. Please parameterize this coverage for MonoVM (including the discrete assembly and symbol archive entries) so the new runtime-pack source paths are verified on both packaging paths.

Rule: Test changed behavior across configurations

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

MonoVM Android builds are no longer supported on .NET 11. Attempting to parameterize this test for AndroidRuntime.MonoVM fails with NETSDK1242 before reaching _LinkAssembliesNoShrink or the discrete packaging path. MonoVM build-test variants were intentionally removed in d115ab9cd and 72788a388 for this reason. Also, the runtime pack does not ship Microsoft.CSharp.pdb, so there is no skipped-framework symbol entry to assert. The existing CoreCLR case covers the supported Debug/no-shrink path affected by this PR.

proj.SetProperty (KnownProperties.RuntimeIdentifier, runtimeIdentifier);

using var builder = CreateApkBuilder ();
Assert.IsTrue (builder.Build (proj), "first build should succeed");
builder.Output.AssertTargetIsNotSkipped ("_LinkAssembliesNoShrink");

string [] GetFrameworkAssemblyMetadata (string assemblyName)
{
string prefix = $"FrameworkAssembly={assemblyName}|";
string line = builder.LastBuildOutput.Single (line => line.Contains (prefix, StringComparison.Ordinal));
int start = line.IndexOf (prefix, StringComparison.Ordinal);
var metadata = line.Substring (start + "FrameworkAssembly=".Length).Split ('|');
Assert.AreEqual (5, metadata.Length, $"Unexpected framework assembly metadata: {line}");
return metadata;
}

var projectDirectory = Path.Combine (Root, builder.ProjectDirectory);
string ResolveProjectPath (string path) => Path.GetFullPath (Path.Combine (projectDirectory, path));

var assemblyDirectory = Path.Combine (projectDirectory, proj.IntermediateOutputPath, runtimeIdentifier, "android", "assets", abi);
var stagedMicrosoftCSharp = Path.Combine (assemblyDirectory, "Microsoft.CSharp.dll");
var marker = Path.Combine (assemblyDirectory, "Microsoft.CSharp.scan.empty");
var microsoftCSharp = GetFrameworkAssemblyMetadata ("Microsoft.CSharp");

FileAssert.Exists (ResolveProjectPath (microsoftCSharp [1]));
Assert.That (microsoftCSharp [2], Does.StartWith ("Microsoft.NETCore.App.Runtime."),
$"Microsoft.CSharp.dll should come from a runtime pack, but its package ID was '{microsoftCSharp [2]}'.");
Assert.AreNotEqual (Path.GetFullPath (stagedMicrosoftCSharp), ResolveProjectPath (microsoftCSharp [1]),
"Microsoft.CSharp.dll should be packaged directly from its runtime pack.");
FileAssert.DoesNotExist (stagedMicrosoftCSharp);
FileAssert.Exists (marker);
Assert.AreEqual (0, new FileInfo (marker).Length, $"{marker} should be empty.");
Assert.AreEqual (Path.GetFullPath (marker), ResolveProjectPath (microsoftCSharp [3]),
"JavaObjectsXmlFile should point to the shared scan marker.");
Assert.AreEqual (Path.GetFullPath (marker), ResolveProjectPath (microsoftCSharp [4]),
"TypeMapObjectsXmlFile should point to the shared scan marker.");

var monoAndroid = GetFrameworkAssemblyMetadata ("Mono.Android");
var stagedMonoAndroid = Path.Combine (assemblyDirectory, "Mono.Android.dll");
Assert.AreEqual (Path.GetFullPath (stagedMonoAndroid), ResolveProjectPath (monoAndroid [1]),
"Mono.Android.dll should continue to use the staged assembly.");
FileAssert.Exists (stagedMonoAndroid);
FileAssert.Exists (Path.ChangeExtension (stagedMonoAndroid, ".jlo.xml"));
FileAssert.Exists (Path.ChangeExtension (stagedMonoAndroid, ".typemap.xml"));
FileAssert.DoesNotExist (Path.Combine (assemblyDirectory, "Mono.Android.scan.empty"));

var outputDirectory = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath);
var apk = Directory.GetFiles (outputDirectory, "*-Signed.apk", SearchOption.AllDirectories).Single ();
var archive = new ArchiveAssemblyHelper (apk, useAssemblyStores: true);
Assert.IsTrue (archive.Exists ($"assemblies/{abi}/Microsoft.CSharp.dll"),
$"Microsoft.CSharp.dll should be packaged in {apk}.");

Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "second build should succeed");
builder.Output.AssertTargetIsSkipped ("_LinkAssembliesNoShrink");
}

[Test]
[Ignore ("Flaky timing-based test. Disabled while investigating incremental build regressions. See: https://github.com/dotnet/android/issues/11792")]
public void BasicApplicationRepetitiveBuild ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Java.Interop.Tools.JavaCallableWrappers.Adapters;
using Java.Interop.Tools.JavaCallableWrappers.CallableWrapperMembers;
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Xamarin.Android.Tasks;
Expand Down Expand Up @@ -91,6 +92,12 @@ void ExportACWMappingTypes (XmlWriter xml)
public static string GetJavaObjectsXmlFilePath (string assemblyPath)
=> Path.ChangeExtension (assemblyPath, ".jlo.xml");

public static string GetJavaObjectsXmlFilePath (ITaskItem assembly)
{
var path = assembly.GetMetadata ("JavaObjectsXmlFile");
return path.IsNullOrEmpty () ? GetJavaObjectsXmlFilePath (assembly.ItemSpec) : path;
}

public static JavaObjectsXmlFile Import (string filename, JavaObjectsXmlFileReadType readType)
{
// If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ public ReleaseGenerationState GetReleaseGenerationState ()
var adapter = new TypeMapObjectsFileAdapter (targetArch);

foreach (var assembly in assemblies) {
var typeMapPath = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (assembly.ItemSpec);
var typeMapPath = TypeMapObjectsXmlFile.GetTypeMapObjectsXmlFilePath (assembly);

if (!File.Exists (typeMapPath)) {
log.LogError ($"'{typeMapPath}' not found.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.IO;
using System.Xml;
using Microsoft.Android.Build.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

using ModuleReleaseData = Xamarin.Android.Tasks.TypeMapGenerator.ModuleReleaseData;
Expand Down Expand Up @@ -165,6 +166,12 @@ void ExportTypeMapReleaseEntry (XmlWriter xml, TypeMapReleaseEntry entry, string
public static string GetTypeMapObjectsXmlFilePath (string assemblyPath)
=> Path.ChangeExtension (assemblyPath, ".typemap.xml");

public static string GetTypeMapObjectsXmlFilePath (ITaskItem assembly)
{
var path = assembly.GetMetadata ("TypeMapObjectsXmlFile");
return path.IsNullOrEmpty () ? GetTypeMapObjectsXmlFilePath (assembly.ItemSpec) : path;
}

public static TypeMapObjectsXmlFile Import (string filename)
{
// If the file has zero length, then the assembly wasn't scanned because it couldn't contain JLOs.
Expand Down
27 changes: 26 additions & 1 deletion src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets
Original file line number Diff line number Diff line change
Expand Up @@ -1434,14 +1434,39 @@ because xbuild doesn't support framework reference assemblies.
<ItemGroup>
<!-- We need this in its own item group so it isn't lost during a partial build -->
<_AllResolvedAssemblies Include="@(ResolvedAssemblies)" />
<!-- Non-Android framework assemblies are packaged directly from their runtime packs.
A zero-byte marker replaces both scanner sidecars without staging the DLL. -->
<ResolvedAssemblies>
<_AndroidLinkNoShrinkOutput
Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' == 'True' "
>$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty</_AndroidLinkNoShrinkOutput>
<_AndroidLinkNoShrinkOutput
Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' != 'True' "
>$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)</_AndroidLinkNoShrinkOutput>
</ResolvedAssemblies>
<_AndroidAssembliesToLinkNoShrink
Include="@(ResolvedAssemblies)"
Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' != 'True' " />
<_AndroidAssembliesToPackageWithoutLinkNoShrink
Include="@(ResolvedAssemblies)"
Condition=" '%(ResolvedAssemblies.AndroidSkipAssemblyModification)' == 'True' ">
Comment thread
simonrozsival marked this conversation as resolved.
<JavaObjectsXmlFile>$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty</JavaObjectsXmlFile>
<TypeMapObjectsXmlFile>$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubDirectory)%(Filename).scan.empty</TypeMapObjectsXmlFile>
</_AndroidAssembliesToPackageWithoutLinkNoShrink>
<_AndroidSymbolsToLinkNoShrink
Include="@(ResolvedSymbols)"
Condition=" '%(ResolvedSymbols.AndroidSkipAssemblyModification)' != 'True' " />
<_AndroidSymbolsToPackageWithoutLinkNoShrink
Include="@(ResolvedSymbols)"
Condition=" '%(ResolvedSymbols.AndroidSkipAssemblyModification)' == 'True' " />
</ItemGroup>
</Target>

<Target Name="_LinkAssembliesNoShrink"
DependsOnTargets="_LinkAssembliesNoShrinkInputs"
Condition="'$(PublishTrimmed)' != 'true'"
Inputs="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)"
Outputs="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')">
Outputs="@(ResolvedAssemblies->'%(_AndroidLinkNoShrinkOutput)')">
<LinkAssembliesNoShrink
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
CodeGenerationTarget="$(AndroidCodegenTarget)"
Expand Down