-
Notifications
You must be signed in to change notification settings - Fork 564
[XABT] Refactor <LinkAssembliesNoShrink>/<AssemblyModifierPipeline> tasks to use an actual pipeline.
#10024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[XABT] Refactor <LinkAssembliesNoShrink>/<AssemblyModifierPipeline> tasks to use an actual pipeline.
#10024
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,6 +11,9 @@ | |||||||||||||||||||||||||||||||||||||||||
| namespace MonoDroid.Tuner | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| public class AddKeepAlivesStep : BaseStep | ||||||||||||||||||||||||||||||||||||||||||
| #if !ILLINK | ||||||||||||||||||||||||||||||||||||||||||
| , IAssemblyModifierPipelineStep | ||||||||||||||||||||||||||||||||||||||||||
| #endif // !ILLINK | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| protected override void ProcessAssembly (AssemblyDefinition assembly) | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,6 +28,17 @@ protected override void ProcessAssembly (AssemblyDefinition assembly) | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| #if !ILLINK | ||||||||||||||||||||||||||||||||||||||||||
| public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context) | ||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||
| // Only run this step on user Android assemblies | ||||||||||||||||||||||||||||||||||||||||||
| if (context.IsFrameworkAssembly || !context.IsAndroidAssembly) | ||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||
| public static bool IsFrameworkAssembly (string assembly) => | |
| KnownAssemblyNames.Contains (Path.GetFileNameWithoutExtension (assembly)); |
android/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs
Lines 381 to 398 in 9ad492a
| public static bool IsFrameworkAssembly (ITaskItem assembly) | |
| { | |
| // Known assembly names: Mono.Android, Java.Interop, etc. | |
| if (IsFrameworkAssembly (assembly.ItemSpec)) | |
| return true; | |
| // Known %(FrameworkReferenceName) | |
| var frameworkReferenceName = assembly.GetMetadata ("FrameworkReferenceName") ?? ""; | |
| if (frameworkReferenceName == "Microsoft.Android") { | |
| return true; // Microsoft.Android assemblies | |
| } | |
| if (frameworkReferenceName.StartsWith ("Microsoft.NETCore.", StringComparison.OrdinalIgnoreCase)) { | |
| return true; // BCL assemblies | |
| } | |
| // Known %(NuGetPackageId) runtime pack names | |
| return IsFromAKnownRuntimePack (assembly); | |
| } |
which looks like it should be true for Mono.Android.dll, and IsAndroidAssembly is set from MonoAndroidHelper.IsAndroidAssembly(), which would also be true for Mono.Andorid.dll.
So I read this as true || !true? And just get confused.
I think our naming needs an improvement. Relatedly: what is "Framework" supposed to mean in "IsFrameworkAssembly"? As per MonoAndroidHelper.Linker.cs, it means "assembly name is one of Mono.Android, Mono.Android.Export, or Java.Interop", but in general "framework" could also mean the entire .NET BCL. I think "Framework" may be causing me more confusion than enlightenment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we could add a StepContext.ShouldScanAssemblyForJCWs property, and let AssemblyModifierPipeline.cs sort it out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think our naming needs an improvement. Relatedly: what is "Framework" supposed to mean in "IsFrameworkAssembly"? As per MonoAndroidHelper.Linker.cs, it means "assembly name is one of Mono.Android, Mono.Android.Export, or Java.Interop", but in general "framework" could also mean the entire .NET BCL. I think "Framework" may be causing me more confusion than enlightenment.
Agreed that this is confusing (this is what led to the temporary "WasScanned" tracking just so I could verify I translated the logic correctly). Here, there are two dimensions that influence whether this step runs:
IsFrameworkAssembly/IsUserAssembly- Framework: "dotnet" ships it, ex:
System.Collections.dll,Mono.Android.dll - User: an assembly the user added, ex:
MyApp.dll,Xamarin.AndroidX.Core.dll
- Framework: "dotnet" ships it, ex:
IsAndroidAssembly/!IsAndroidAssembly- In this context, this is essentially: can it contain JLO objects or not
So I read this as true || !true? And just get confused.
Correct, the key is the word "user" in the comment. The AddKeepAlives step is only run on user assemblies that can contain JLO objects. We assume that everything we ship "in box" these days has keep alives built in, so we don't need to check things like Mono.Android.
Maybe we could add a StepContext.ShouldScanAssemblyForJCWs property, and let AssemblyModifierPipeline.cs sort it out?
The issue is that different steps have different criteria for whether they need to run.
- JavaCallableWrappers: only scans user assemblies for JLOs in
Debugmode, scans user and framework assemblies inReleasemode - Typemaps: scans both user and framework assemblies for JLOs in both
Debug/Releasemodes
Given this, I am happy with where the logic is, but maybe we can address naming so that it's less confusing. If we have better naming than Framework/User or Android we can certainly go with that. Another way to make it clearer might be to add some compound properties in StepContext like:
public class StepContext {
public bool IsAndroidFrameworkAssembly => IsAndroidAssembly && IsFrameworkAssembly;
public bool IsAndroidUserAssembly => IsAndroidAssembly && IsUserAssembly;
}Then our check becomes the much clearer:
// Only run this step on user Android assemblies
if (!context.IsAndroidUserAssembly)
return false;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decision was to use StepContext.IsAndroidUserAssembly for better clarity here. PR updated.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,16 +20,8 @@ namespace Xamarin.Android.Tasks; | |
| /// are run *after* the linker has run. Additionally, this task is run by | ||
| /// LinkAssembliesNoShrink to modify assemblies when ILLink is not used. | ||
| /// </summary> | ||
| public class AssemblyModifierPipeline : AndroidTask | ||
| public partial class AssemblyModifierPipeline : AndroidTask | ||
| { | ||
| // Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must | ||
| // be scanned for Java types. | ||
| static readonly HashSet<string> SpecialAssemblies = new HashSet<string> (StringComparer.OrdinalIgnoreCase) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How many "copies" of this list do we have?! See also
The ones that "don't match" are
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The unsatisfying answer is I simply moved the existing logic.😉
Maybe? We would likely need to audit all users of both lists to see what their intentions are.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unified the lists and CI isn't complaining, so I guess that's good enough! 🤷♂️ |
||
| "Java.Interop.dll", | ||
| "Mono.Android.dll", | ||
| "Mono.Android.Runtime.dll", | ||
| }; | ||
|
|
||
| public override string TaskPrefix => "AMP"; | ||
|
|
||
| public string ApplicationJavaClass { get; set; } = ""; | ||
|
|
@@ -66,6 +58,12 @@ public class AssemblyModifierPipeline : AndroidTask | |
| [Required] | ||
| public ITaskItem [] SourceFiles { get; set; } = []; | ||
|
|
||
| /// <summary> | ||
| /// $(TargetName) would be "AndroidApp1" with no extension | ||
| /// </summary> | ||
| [Required] | ||
| public string TargetName { get; set; } = ""; | ||
|
|
||
| protected JavaPeerStyle codeGenerationTarget; | ||
|
|
||
| public override bool RunTask () | ||
|
|
@@ -86,14 +84,14 @@ public override bool RunTask () | |
|
|
||
| Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty<string> (), validate: false); | ||
|
|
||
| RunState? runState = null; | ||
| AssemblyPipeline? pipeline = null; | ||
| var currentArch = AndroidTargetArch.None; | ||
|
|
||
| for (int i = 0; i < SourceFiles.Length; i++) { | ||
| ITaskItem source = SourceFiles [i]; | ||
| AndroidTargetArch sourceArch = GetValidArchitecture (source); | ||
| AndroidTargetArch sourceArch = MonoAndroidHelper.GetRequiredValidArchitecture (source); | ||
| ITaskItem destination = DestinationFiles [i]; | ||
| AndroidTargetArch destinationArch = GetValidArchitecture (destination); | ||
| AndroidTargetArch destinationArch = MonoAndroidHelper.GetRequiredValidArchitecture (destination); | ||
|
|
||
| if (sourceArch != destinationArch) { | ||
| throw new InvalidOperationException ($"Internal error: assembly '{sourceArch}' targets architecture '{sourceArch}', while destination assembly '{destination}' targets '{destinationArch}' instead"); | ||
|
|
@@ -102,147 +100,85 @@ public override bool RunTask () | |
| // 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; | ||
| runState?.Dispose (); | ||
| pipeline?.Dispose (); | ||
|
|
||
| var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: ReadSymbols, loadReaderParameters: readerParameters); | ||
| runState = new RunState (resolver); | ||
|
|
||
| // Add SearchDirectories for the current architecture's ResolvedAssemblies | ||
| foreach (var kvp in perArchAssemblies [sourceArch]) { | ||
| ITaskItem assembly = kvp.Value; | ||
| var path = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec)); | ||
| if (!runState.resolver.SearchDirectories.Contains (path)) { | ||
| runState.resolver.SearchDirectories.Add (path); | ||
| if (!resolver.SearchDirectories.Contains (path)) { | ||
| resolver.SearchDirectories.Add (path); | ||
| } | ||
| } | ||
|
|
||
| // Set up the FixAbstractMethodsStep and AddKeepAlivesStep | ||
| var context = new MSBuildLinkContext (runState.resolver, Log); | ||
| var context = new MSBuildLinkContext (resolver, Log); | ||
| pipeline = new AssemblyPipeline (resolver); | ||
|
|
||
| CreateRunState (runState, context); | ||
| BuildPipeline (pipeline, context); | ||
| } | ||
|
|
||
| Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); | ||
|
|
||
| RunPipeline (source, destination, runState!, writerParameters); | ||
| RunPipeline (pipeline!, source, destination, writerParameters); | ||
| } | ||
|
|
||
| runState?.Dispose (); | ||
| pipeline?.Dispose (); | ||
|
|
||
| return !Log.HasLoggedErrors; | ||
| } | ||
|
|
||
| protected virtual void CreateRunState (RunState runState, MSBuildLinkContext context) | ||
| protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context) | ||
| { | ||
| // FindJavaObjectsStep | ||
| var findJavaObjectsStep = new FindJavaObjectsStep (Log) { | ||
| ApplicationJavaClass = ApplicationJavaClass, | ||
| ErrorOnCustomJavaObject = ErrorOnCustomJavaObject, | ||
| UseMarshalMethods = EnableMarshalMethods, | ||
| }; | ||
|
|
||
| findJavaObjectsStep.Initialize (context); | ||
|
|
||
| runState.findJavaObjectsStep = findJavaObjectsStep; | ||
| pipeline.Steps.Add (findJavaObjectsStep); | ||
| } | ||
|
|
||
| protected virtual void RunPipeline (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters) | ||
| void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination, WriterParameters writerParameters) | ||
| { | ||
| var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (destination.ItemSpec); | ||
|
|
||
| if (!TryScanForJavaObjects (source, destination, runState, writerParameters)) { | ||
| // Even if we didn't scan for Java objects, we still write an empty .xml file for later steps | ||
| JavaObjectsXmlFile.WriteEmptyFile (destinationJLOXml, Log); | ||
| } | ||
| } | ||
|
|
||
| bool TryScanForJavaObjects (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters) | ||
| { | ||
| if (!ShouldScanAssembly (source)) | ||
| return false; | ||
|
|
||
| var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (destination.ItemSpec); | ||
| var assemblyDefinition = runState.resolver!.GetAssembly (source.ItemSpec); | ||
|
|
||
| var scanned = runState.findJavaObjectsStep!.ProcessAssembly (assemblyDefinition, destinationJLOXml); | ||
|
|
||
| return scanned; | ||
| } | ||
|
|
||
| bool ShouldScanAssembly (ITaskItem source) | ||
| { | ||
| // Skip this assembly if it is not an Android assembly | ||
| if (!IsAndroidAssembly (source)) { | ||
| Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not an Android assembly"); | ||
| return false; | ||
| } | ||
|
|
||
| // When marshal methods or non-JavaPeerStyle.XAJavaInterop1 are in use we do not want to skip non-user assemblies (such as Mono.Android) - we need to generate JCWs for them during | ||
| // application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android | ||
| // build and stored in a jar file. | ||
| var useMarshalMethods = !Debug && EnableMarshalMethods; | ||
| var shouldSkipNonUserAssemblies = !useMarshalMethods && codeGenerationTarget == JavaPeerStyle.XAJavaInterop1; | ||
|
|
||
| if (shouldSkipNonUserAssemblies && !ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec)) { | ||
| Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies"); | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool IsAndroidAssembly (ITaskItem source) | ||
| { | ||
| string name = Path.GetFileName (source.ItemSpec); | ||
|
|
||
| if (SpecialAssemblies.Contains (name)) | ||
| return true; | ||
| var assembly = pipeline.Resolver.GetAssembly (source.ItemSpec); | ||
|
|
||
| var context = new StepContext (source, destination) { | ||
| CodeGenerationTarget = codeGenerationTarget, | ||
| EnableMarshalMethods = EnableMarshalMethods, | ||
| IsAndroidAssembly = MonoAndroidHelper.IsAndroidAssembly (source), | ||
| IsDebug = Debug, | ||
| IsFrameworkAssembly = MonoAndroidHelper.IsFrameworkAssembly (source), | ||
| IsMainAssembly = Path.GetFileNameWithoutExtension (source.ItemSpec) == TargetName, | ||
| IsUserAssembly = ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec), | ||
| }; | ||
|
|
||
| return MonoAndroidHelper.IsMonoAndroidAssembly (source); | ||
| } | ||
| var changed = pipeline.Run (assembly, context); | ||
|
|
||
| AndroidTargetArch GetValidArchitecture (ITaskItem item) | ||
| { | ||
| AndroidTargetArch ret = MonoAndroidHelper.GetTargetArch (item); | ||
| if (ret == AndroidTargetArch.None) { | ||
| throw new InvalidOperationException ($"Internal error: assembly '{item}' doesn't target any architecture."); | ||
| if (changed) { | ||
| Log.LogDebugMessage ($"Saving modified assembly: {destination.ItemSpec}"); | ||
| Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); | ||
| writerParameters.WriteSymbols = assembly.MainModule.HasSymbols; | ||
| assembly.Write (destination.ItemSpec, writerParameters); | ||
| } else { | ||
| // If we didn't write a modified file, copy the original to the destination | ||
| CopyIfChanged (source, destination); | ||
| } | ||
|
|
||
| return ret; | ||
| } | ||
|
|
||
| protected sealed class RunState : IDisposable | ||
| void CopyIfChanged (ITaskItem source, ITaskItem destination) | ||
| { | ||
| public DirectoryAssemblyResolver resolver; | ||
| public FixAbstractMethodsStep? fixAbstractMethodsStep = null; | ||
| public AddKeepAlivesStep? addKeepAliveStep = null; | ||
| public FixLegacyResourceDesignerStep? fixLegacyResourceDesignerStep = null; | ||
| public FindJavaObjectsStep? findJavaObjectsStep = null; | ||
| bool disposed_value; | ||
|
|
||
| public RunState (DirectoryAssemblyResolver resolver) | ||
| { | ||
| this.resolver = resolver; | ||
| } | ||
|
|
||
| private void Dispose (bool disposing) | ||
| { | ||
| if (!disposed_value) { | ||
| if (disposing) { | ||
| resolver?.Dispose (); | ||
| fixAbstractMethodsStep = null; | ||
| fixLegacyResourceDesignerStep = null; | ||
| addKeepAliveStep = null; | ||
| findJavaObjectsStep = null; | ||
| } | ||
| disposed_value = true; | ||
| } | ||
| } | ||
| if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) { | ||
| Log.LogDebugMessage ($"Copied: {destination.ItemSpec}"); | ||
| } else { | ||
| Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}"); | ||
|
|
||
| public void Dispose () | ||
| { | ||
| // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method | ||
| Dispose (disposing: true); | ||
| GC.SuppressFinalize (this); | ||
| // NOTE: We still need to update the timestamp on this file, or this target would run again | ||
| File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
src\Microsoft.Android.Sdk.ILLink\Microsoft.Android.Sdk.ILLink.csprojhadIAssemblyModifierPipelineStep.cswe could drop the#if?