|
| 1 | +#nullable enable |
| 2 | +using System; |
| 3 | +using System.Collections.Generic; |
| 4 | +using System.IO; |
| 5 | +using System.Linq; |
| 6 | +using Java.Interop.Tools.Cecil; |
| 7 | +using Java.Interop.Tools.JavaCallableWrappers; |
| 8 | +using Java.Interop.Tools.TypeNameMappings; |
| 9 | +using Microsoft.Android.Build.Tasks; |
| 10 | +using Microsoft.Build.Framework; |
| 11 | +using Mono.Cecil; |
| 12 | +using MonoDroid.Tuner; |
| 13 | +using Xamarin.Android.Tools; |
| 14 | +using PackageNamingPolicyEnum = Java.Interop.Tools.TypeNameMappings.PackageNamingPolicy; |
| 15 | + |
| 16 | +namespace Xamarin.Android.Tasks; |
| 17 | + |
| 18 | +/// <summary> |
| 19 | +/// This task runs additional "linker steps" that are not part of ILLink. These steps |
| 20 | +/// are run *after* the linker has run. Additionally, this task is run by |
| 21 | +/// LinkAssembliesNoShrink to modify assemblies when ILLink is not used. |
| 22 | +/// </summary> |
| 23 | +public class AssemblyModifierPipeline : AndroidTask |
| 24 | +{ |
| 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 | + |
| 33 | + public override string TaskPrefix => "AMP"; |
| 34 | + |
| 35 | + public string ApplicationJavaClass { get; set; } = ""; |
| 36 | + |
| 37 | + public string CodeGenerationTarget { get; set; } = ""; |
| 38 | + |
| 39 | + public bool Debug { get; set; } |
| 40 | + |
| 41 | + [Required] |
| 42 | + public ITaskItem [] DestinationFiles { get; set; } = []; |
| 43 | + |
| 44 | + public bool Deterministic { get; set; } |
| 45 | + |
| 46 | + public bool EnableMarshalMethods { get; set; } |
| 47 | + |
| 48 | + public bool ErrorOnCustomJavaObject { get; set; } |
| 49 | + |
| 50 | + public string? PackageNamingPolicy { get; set; } |
| 51 | + |
| 52 | + /// <summary> |
| 53 | + /// Defaults to false, enables Mono.Cecil to load symbols |
| 54 | + /// </summary> |
| 55 | + public bool ReadSymbols { get; set; } |
| 56 | + |
| 57 | + /// <summary> |
| 58 | + /// These are used so we have the full list of SearchDirectories |
| 59 | + /// </summary> |
| 60 | + [Required] |
| 61 | + public ITaskItem [] ResolvedAssemblies { get; set; } = []; |
| 62 | + |
| 63 | + [Required] |
| 64 | + public ITaskItem [] ResolvedUserAssemblies { get; set; } = []; |
| 65 | + |
| 66 | + [Required] |
| 67 | + public ITaskItem [] SourceFiles { get; set; } = []; |
| 68 | + |
| 69 | + protected JavaPeerStyle codeGenerationTarget; |
| 70 | + |
| 71 | + public override bool RunTask () |
| 72 | + { |
| 73 | + codeGenerationTarget = MonoAndroidHelper.ParseCodeGenerationTarget (CodeGenerationTarget); |
| 74 | + JavaNativeTypeManager.PackageNamingPolicy = Enum.TryParse (PackageNamingPolicy, out PackageNamingPolicyEnum pnp) ? pnp : PackageNamingPolicyEnum.LowercaseCrc64; |
| 75 | + |
| 76 | + if (SourceFiles.Length != DestinationFiles.Length) |
| 77 | + throw new ArgumentException ("source and destination count mismatch"); |
| 78 | + |
| 79 | + var readerParameters = new ReaderParameters { |
| 80 | + ReadSymbols = ReadSymbols, |
| 81 | + }; |
| 82 | + |
| 83 | + var writerParameters = new WriterParameters { |
| 84 | + DeterministicMvid = Deterministic, |
| 85 | + }; |
| 86 | + |
| 87 | + Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty<string> (), validate: false); |
| 88 | + |
| 89 | + RunState? runState = null; |
| 90 | + var currentArch = AndroidTargetArch.None; |
| 91 | + |
| 92 | + for (int i = 0; i < SourceFiles.Length; i++) { |
| 93 | + ITaskItem source = SourceFiles [i]; |
| 94 | + AndroidTargetArch sourceArch = GetValidArchitecture (source); |
| 95 | + ITaskItem destination = DestinationFiles [i]; |
| 96 | + AndroidTargetArch destinationArch = GetValidArchitecture (destination); |
| 97 | + |
| 98 | + if (sourceArch != destinationArch) { |
| 99 | + throw new InvalidOperationException ($"Internal error: assembly '{sourceArch}' targets architecture '{sourceArch}', while destination assembly '{destination}' targets '{destinationArch}' instead"); |
| 100 | + } |
| 101 | + |
| 102 | + // Each architecture must have a different set of context classes, or otherwise only the first instance of the assembly may be rewritten. |
| 103 | + if (currentArch != sourceArch) { |
| 104 | + currentArch = sourceArch; |
| 105 | + runState?.Dispose (); |
| 106 | + |
| 107 | + var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: ReadSymbols, loadReaderParameters: readerParameters); |
| 108 | + runState = new RunState (resolver); |
| 109 | + |
| 110 | + // Add SearchDirectories for the current architecture's ResolvedAssemblies |
| 111 | + foreach (var kvp in perArchAssemblies [sourceArch]) { |
| 112 | + ITaskItem assembly = kvp.Value; |
| 113 | + var path = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec)); |
| 114 | + if (!runState.resolver.SearchDirectories.Contains (path)) { |
| 115 | + runState.resolver.SearchDirectories.Add (path); |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + // Set up the FixAbstractMethodsStep and AddKeepAlivesStep |
| 120 | + var context = new MSBuildLinkContext (runState.resolver, Log); |
| 121 | + |
| 122 | + CreateRunState (runState, context); |
| 123 | + } |
| 124 | + |
| 125 | + Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec)); |
| 126 | + |
| 127 | + RunPipeline (source, destination, runState!, writerParameters); |
| 128 | + } |
| 129 | + |
| 130 | + runState?.Dispose (); |
| 131 | + |
| 132 | + return !Log.HasLoggedErrors; |
| 133 | + } |
| 134 | + |
| 135 | + protected virtual void CreateRunState (RunState runState, MSBuildLinkContext context) |
| 136 | + { |
| 137 | + var findJavaObjectsStep = new FindJavaObjectsStep (Log) { |
| 138 | + ApplicationJavaClass = ApplicationJavaClass, |
| 139 | + ErrorOnCustomJavaObject = ErrorOnCustomJavaObject, |
| 140 | + UseMarshalMethods = EnableMarshalMethods, |
| 141 | + }; |
| 142 | + |
| 143 | + findJavaObjectsStep.Initialize (context); |
| 144 | + |
| 145 | + runState.findJavaObjectsStep = findJavaObjectsStep; |
| 146 | + } |
| 147 | + |
| 148 | + protected virtual void RunPipeline (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters) |
| 149 | + { |
| 150 | + var destinationJLOXml = Path.ChangeExtension (destination.ItemSpec, ".jlo.xml"); |
| 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 | + FindJavaObjectsStep.WriteEmptyXmlFile (destinationJLOXml); |
| 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 = Path.ChangeExtension (destination.ItemSpec, ".jlo.xml"); |
| 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; |
| 199 | + |
| 200 | + return MonoAndroidHelper.IsMonoAndroidAssembly (source); |
| 201 | + } |
| 202 | + |
| 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."); |
| 208 | + } |
| 209 | + |
| 210 | + return ret; |
| 211 | + } |
| 212 | + |
| 213 | + protected sealed class RunState : IDisposable |
| 214 | + { |
| 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 | + } |
| 240 | + |
| 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); |
| 246 | + } |
| 247 | + } |
| 248 | +} |
0 commit comments