Skip to content

Commit b54ec05

Browse files
authored
[XABT] Refactor <LinkAssembliesNoShrink>/<AssemblyModifierPipeline> tasks to use an actual pipeline. (#10024)
In order to continue moving forward with moving tasks from the `<GenerateJavaStubs>` task to our "additional linker steps", we need a more flexible assembly scanning/modification pipeline. Specifically, we will need to run "RewriteMarshalMethods" in the `<AssemblyModifierPipeline>` task so that it happens for both linked and non-linked builds. However, we want to run it: - After the `FindJavaObjectsStep` step in the `<AssemblyModifierPipeline>` pipeline. - In the same loop as the other assembly modification steps in the `<LinkAssembliesNoShrink>` task (which is before `<AssemblyModifierPipeline>` steps run) so that we do not need to write the assemblies twice. Although we could achieve these goals with enough hacks, instead refactor the steps into a proper, flexible "pipeline". This pipeline provides the features we need: - Steps from both tasks can be run in any order - Steps from both tasks can modify assemblies, and only 1 write call will happen Additionally, encapsulate the "should this step run on this assembly" logic to each step, so that the pipeline doesn't need to know about each step's requirements.
1 parent ac5d8ff commit b54ec05

10 files changed

+247
-190
lines changed

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/AddKeepAlivesStep.cs

+14
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
namespace MonoDroid.Tuner
1212
{
1313
public class AddKeepAlivesStep : BaseStep
14+
#if !ILLINK
15+
, IAssemblyModifierPipelineStep
16+
#endif // !ILLINK
1417
{
1518

1619
protected override void ProcessAssembly (AssemblyDefinition assembly)
@@ -25,6 +28,17 @@ protected override void ProcessAssembly (AssemblyDefinition assembly)
2528
}
2629
}
2730

31+
#if !ILLINK
32+
public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context)
33+
{
34+
// Only run this step on user Android assemblies
35+
if (!context.IsAndroidUserAssembly)
36+
return false;
37+
38+
return AddKeepAlives (assembly);
39+
}
40+
#endif // !ILLINK
41+
2842
internal bool AddKeepAlives (AssemblyDefinition assembly)
2943
{
3044
if (!assembly.MainModule.HasTypeReference ("Java.Lang.Object"))

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs

+39-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace MonoDroid.Tuner;
1717
/// <summary>
1818
/// Scans an assembly for JLOs that need JCWs generated and writes them to an XML file.
1919
/// </summary>
20-
public class FindJavaObjectsStep : BaseStep
20+
public class FindJavaObjectsStep : BaseStep, IAssemblyModifierPipelineStep
2121
{
2222
public string ApplicationJavaClass { get; set; } = "";
2323

@@ -29,8 +29,26 @@ public class FindJavaObjectsStep : BaseStep
2929

3030
public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log;
3131

32-
public bool ProcessAssembly (AssemblyDefinition assembly, string destinationJLOXml)
32+
public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context)
3333
{
34+
var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (context.Destination.ItemSpec);
35+
var scanned = ScanAssembly (assembly, context, destinationJLOXml);
36+
37+
if (!scanned) {
38+
// We didn't scan for Java objects, so write an empty .xml file for later steps
39+
JavaObjectsXmlFile.WriteEmptyFile (destinationJLOXml, Log);
40+
return false;
41+
}
42+
43+
// This step does not change the assembly
44+
return false;
45+
}
46+
47+
public bool ScanAssembly (AssemblyDefinition assembly, StepContext context, string destinationJLOXml)
48+
{
49+
if (!ShouldScan (context))
50+
return false;
51+
3452
var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip;
3553

3654
if (action == AssemblyAction.Delete)
@@ -56,6 +74,25 @@ public bool ProcessAssembly (AssemblyDefinition assembly, string destinationJLOX
5674
return true;
5775
}
5876

77+
bool ShouldScan (StepContext context)
78+
{
79+
if (!context.IsAndroidAssembly)
80+
return false;
81+
82+
// 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
83+
// application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android
84+
// build and stored in a jar file.
85+
var useMarshalMethods = !context.IsDebug && context.EnableMarshalMethods;
86+
var shouldSkipNonUserAssemblies = !useMarshalMethods && context.CodeGenerationTarget == JavaPeerStyle.XAJavaInterop1;
87+
88+
if (shouldSkipNonUserAssemblies && !context.IsUserAssembly) {
89+
Log.LogDebugMessage ($"Skipping assembly '{context.Source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies");
90+
return false;
91+
}
92+
93+
return true;
94+
}
95+
5996
List<TypeDefinition> ScanForJavaTypes (AssemblyDefinition assembly)
6097
{
6198
var types = new List<TypeDefinition> ();

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixAbstractMethodsStep.cs

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ namespace MonoDroid.Tuner
2121
/// NOTE: this step is subclassed so it can be called directly from Xamarin.Android.Build.Tasks
2222
/// </summary>
2323
public class FixAbstractMethodsStep : BaseMarkHandler
24+
#if !ILLINK
25+
, IAssemblyModifierPipelineStep
26+
#endif // !ILLINK
2427
{
2528
public override void Initialize (LinkContext context, MarkContext markContext)
2629
{
@@ -79,6 +82,17 @@ internal bool FixAbstractMethods (AssemblyDefinition assembly)
7982
return changed;
8083
}
8184

85+
#if !ILLINK
86+
public bool ProcessAssembly (AssemblyDefinition assembly, StepContext context)
87+
{
88+
// Only run this step on non-main user Android assemblies
89+
if (context.IsMainAssembly || !context.IsAndroidUserAssembly)
90+
return false;
91+
92+
return FixAbstractMethods (assembly);
93+
}
94+
#endif // !ILLINK
95+
8296
readonly HashSet<string> warnedAssemblies = new (StringComparer.Ordinal);
8397

8498
internal void CheckAppDomainUsage (AssemblyDefinition assembly, Action<string> warn)

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs

+14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
namespace MonoDroid.Tuner
2222
{
2323
public class FixLegacyResourceDesignerStep : LinkDesignerBase
24+
#if !ILLINK
25+
, Xamarin.Android.Tasks.IAssemblyModifierPipelineStep
26+
#endif // !ILLINK
2427
{
2528
internal const string DesignerAssemblyName = "_Microsoft.Android.Resource.Designer";
2629
internal const string DesignerAssemblyNamespace = "_Microsoft.Android.Resource.Designer";
@@ -67,6 +70,17 @@ protected override void LoadDesigner ()
6770
}
6871
}
6972

73+
#if !ILLINK
74+
public bool ProcessAssembly (AssemblyDefinition assembly, Xamarin.Android.Tasks.StepContext context)
75+
{
76+
// Only run this step on non-main user Android assemblies
77+
if (context.IsMainAssembly || !context.IsAndroidUserAssembly)
78+
return false;
79+
80+
return ProcessAssemblyDesigner (assembly);
81+
}
82+
#endif // !ILLINK
83+
7084
internal override bool ProcessAssemblyDesigner (AssemblyDefinition assembly)
7185
{
7286
if (!FindResourceDesigner (assembly, mainApplication: false, out TypeDefinition designer, out CustomAttribute designerAttribute)) {

src/Xamarin.Android.Build.Tasks/Tasks/AssemblyModifierPipeline.cs

+48-112
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,6 @@ namespace Xamarin.Android.Tasks;
2222
/// </summary>
2323
public class AssemblyModifierPipeline : AndroidTask
2424
{
25-
// Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must
26-
// be scanned for Java types.
27-
static readonly HashSet<string> SpecialAssemblies = new HashSet<string> (StringComparer.OrdinalIgnoreCase) {
28-
"Java.Interop.dll",
29-
"Mono.Android.dll",
30-
"Mono.Android.Runtime.dll",
31-
};
32-
3325
public override string TaskPrefix => "AMP";
3426

3527
public string ApplicationJavaClass { get; set; } = "";
@@ -66,6 +58,12 @@ public class AssemblyModifierPipeline : AndroidTask
6658
[Required]
6759
public ITaskItem [] SourceFiles { get; set; } = [];
6860

61+
/// <summary>
62+
/// $(TargetName) would be "AndroidApp1" with no extension
63+
/// </summary>
64+
[Required]
65+
public string TargetName { get; set; } = "";
66+
6967
protected JavaPeerStyle codeGenerationTarget;
7068

7169
public override bool RunTask ()
@@ -86,14 +84,14 @@ public override bool RunTask ()
8684

8785
Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty<string> (), validate: false);
8886

89-
RunState? runState = null;
87+
AssemblyPipeline? pipeline = null;
9088
var currentArch = AndroidTargetArch.None;
9189

9290
for (int i = 0; i < SourceFiles.Length; i++) {
9391
ITaskItem source = SourceFiles [i];
94-
AndroidTargetArch sourceArch = GetValidArchitecture (source);
92+
AndroidTargetArch sourceArch = MonoAndroidHelper.GetRequiredValidArchitecture (source);
9593
ITaskItem destination = DestinationFiles [i];
96-
AndroidTargetArch destinationArch = GetValidArchitecture (destination);
94+
AndroidTargetArch destinationArch = MonoAndroidHelper.GetRequiredValidArchitecture (destination);
9795

9896
if (sourceArch != destinationArch) {
9997
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 ()
102100
// Each architecture must have a different set of context classes, or otherwise only the first instance of the assembly may be rewritten.
103101
if (currentArch != sourceArch) {
104102
currentArch = sourceArch;
105-
runState?.Dispose ();
103+
pipeline?.Dispose ();
106104

107105
var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: ReadSymbols, loadReaderParameters: readerParameters);
108-
runState = new RunState (resolver);
109106

110107
// Add SearchDirectories for the current architecture's ResolvedAssemblies
111108
foreach (var kvp in perArchAssemblies [sourceArch]) {
112109
ITaskItem assembly = kvp.Value;
113110
var path = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec));
114-
if (!runState.resolver.SearchDirectories.Contains (path)) {
115-
runState.resolver.SearchDirectories.Add (path);
111+
if (!resolver.SearchDirectories.Contains (path)) {
112+
resolver.SearchDirectories.Add (path);
116113
}
117114
}
118115

119116
// Set up the FixAbstractMethodsStep and AddKeepAlivesStep
120-
var context = new MSBuildLinkContext (runState.resolver, Log);
117+
var context = new MSBuildLinkContext (resolver, Log);
118+
pipeline = new AssemblyPipeline (resolver);
121119

122-
CreateRunState (runState, context);
120+
BuildPipeline (pipeline, context);
123121
}
124122

125123
Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec));
126124

127-
RunPipeline (source, destination, runState!, writerParameters);
125+
RunPipeline (pipeline!, source, destination, writerParameters);
128126
}
129127

130-
runState?.Dispose ();
128+
pipeline?.Dispose ();
131129

132130
return !Log.HasLoggedErrors;
133131
}
134132

135-
protected virtual void CreateRunState (RunState runState, MSBuildLinkContext context)
133+
protected virtual void BuildPipeline (AssemblyPipeline pipeline, MSBuildLinkContext context)
136134
{
135+
// FindJavaObjectsStep
137136
var findJavaObjectsStep = new FindJavaObjectsStep (Log) {
138137
ApplicationJavaClass = ApplicationJavaClass,
139138
ErrorOnCustomJavaObject = ErrorOnCustomJavaObject,
140139
UseMarshalMethods = EnableMarshalMethods,
141140
};
142141

143142
findJavaObjectsStep.Initialize (context);
144-
145-
runState.findJavaObjectsStep = findJavaObjectsStep;
143+
pipeline.Steps.Add (findJavaObjectsStep);
146144
}
147145

148-
protected virtual void RunPipeline (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters)
146+
void RunPipeline (AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination, WriterParameters writerParameters)
149147
{
150-
var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (destination.ItemSpec);
151-
152-
if (!TryScanForJavaObjects (source, destination, runState, writerParameters)) {
153-
// Even if we didn't scan for Java objects, we still write an empty .xml file for later steps
154-
JavaObjectsXmlFile.WriteEmptyFile (destinationJLOXml, Log);
155-
}
156-
}
157-
158-
bool TryScanForJavaObjects (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters)
159-
{
160-
if (!ShouldScanAssembly (source))
161-
return false;
162-
163-
var destinationJLOXml = JavaObjectsXmlFile.GetJavaObjectsXmlFilePath (destination.ItemSpec);
164-
var assemblyDefinition = runState.resolver!.GetAssembly (source.ItemSpec);
165-
166-
var scanned = runState.findJavaObjectsStep!.ProcessAssembly (assemblyDefinition, destinationJLOXml);
167-
168-
return scanned;
169-
}
170-
171-
bool ShouldScanAssembly (ITaskItem source)
172-
{
173-
// Skip this assembly if it is not an Android assembly
174-
if (!IsAndroidAssembly (source)) {
175-
Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not an Android assembly");
176-
return false;
177-
}
178-
179-
// 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
180-
// application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android
181-
// build and stored in a jar file.
182-
var useMarshalMethods = !Debug && EnableMarshalMethods;
183-
var shouldSkipNonUserAssemblies = !useMarshalMethods && codeGenerationTarget == JavaPeerStyle.XAJavaInterop1;
184-
185-
if (shouldSkipNonUserAssemblies && !ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec)) {
186-
Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies");
187-
return false;
188-
}
189-
190-
return true;
191-
}
192-
193-
bool IsAndroidAssembly (ITaskItem source)
194-
{
195-
string name = Path.GetFileName (source.ItemSpec);
196-
197-
if (SpecialAssemblies.Contains (name))
198-
return true;
148+
var assembly = pipeline.Resolver.GetAssembly (source.ItemSpec);
149+
150+
var context = new StepContext (source, destination) {
151+
CodeGenerationTarget = codeGenerationTarget,
152+
EnableMarshalMethods = EnableMarshalMethods,
153+
IsAndroidAssembly = MonoAndroidHelper.IsAndroidAssembly (source),
154+
IsDebug = Debug,
155+
IsFrameworkAssembly = MonoAndroidHelper.IsFrameworkAssembly (source),
156+
IsMainAssembly = Path.GetFileNameWithoutExtension (source.ItemSpec) == TargetName,
157+
IsUserAssembly = ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec),
158+
};
199159

200-
return MonoAndroidHelper.IsMonoAndroidAssembly (source);
201-
}
160+
var changed = pipeline.Run (assembly, context);
202161

203-
AndroidTargetArch GetValidArchitecture (ITaskItem item)
204-
{
205-
AndroidTargetArch ret = MonoAndroidHelper.GetTargetArch (item);
206-
if (ret == AndroidTargetArch.None) {
207-
throw new InvalidOperationException ($"Internal error: assembly '{item}' doesn't target any architecture.");
162+
if (changed) {
163+
Log.LogDebugMessage ($"Saving modified assembly: {destination.ItemSpec}");
164+
Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec));
165+
writerParameters.WriteSymbols = assembly.MainModule.HasSymbols;
166+
assembly.Write (destination.ItemSpec, writerParameters);
167+
} else {
168+
// If we didn't write a modified file, copy the original to the destination
169+
CopyIfChanged (source, destination);
208170
}
209-
210-
return ret;
211171
}
212172

213-
protected sealed class RunState : IDisposable
173+
void CopyIfChanged (ITaskItem source, ITaskItem destination)
214174
{
215-
public DirectoryAssemblyResolver resolver;
216-
public FixAbstractMethodsStep? fixAbstractMethodsStep = null;
217-
public AddKeepAlivesStep? addKeepAliveStep = null;
218-
public FixLegacyResourceDesignerStep? fixLegacyResourceDesignerStep = null;
219-
public FindJavaObjectsStep? findJavaObjectsStep = null;
220-
bool disposed_value;
221-
222-
public RunState (DirectoryAssemblyResolver resolver)
223-
{
224-
this.resolver = resolver;
225-
}
226-
227-
private void Dispose (bool disposing)
228-
{
229-
if (!disposed_value) {
230-
if (disposing) {
231-
resolver?.Dispose ();
232-
fixAbstractMethodsStep = null;
233-
fixLegacyResourceDesignerStep = null;
234-
addKeepAliveStep = null;
235-
findJavaObjectsStep = null;
236-
}
237-
disposed_value = true;
238-
}
239-
}
175+
if (MonoAndroidHelper.CopyAssemblyAndSymbols (source.ItemSpec, destination.ItemSpec)) {
176+
Log.LogDebugMessage ($"Copied: {destination.ItemSpec}");
177+
} else {
178+
Log.LogDebugMessage ($"Skipped unchanged file: {destination.ItemSpec}");
240179

241-
public void Dispose ()
242-
{
243-
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
244-
Dispose (disposing: true);
245-
GC.SuppressFinalize (this);
180+
// NOTE: We still need to update the timestamp on this file, or this target would run again
181+
File.SetLastWriteTimeUtc (destination.ItemSpec, DateTime.UtcNow);
246182
}
247183
}
248184
}

0 commit comments

Comments
 (0)