|
| 1 | +#nullable enable |
| 2 | + |
| 3 | +using System; |
| 4 | +using System.Collections.Generic; |
| 5 | +using System.IO; |
| 6 | +using Microsoft.Android.Build.Tasks; |
| 7 | +using Microsoft.Build.Framework; |
| 8 | +using Microsoft.Build.Utilities; |
| 9 | +using Xamarin.Android.Tools; |
| 10 | + |
| 11 | +namespace Xamarin.Android.Tasks; |
| 12 | + |
| 13 | +/// <summary> |
| 14 | +/// Collects managed assemblies to be added to the final archive. |
| 15 | +/// </summary> |
| 16 | +public class CollectAssemblyFilesForArchive : AndroidTask |
| 17 | +{ |
| 18 | + const string ArchiveAssembliesPath = "lib"; |
| 19 | + const string ArchiveLibPath = "lib"; |
| 20 | + |
| 21 | + public override string TaskPrefix => "CAF"; |
| 22 | + |
| 23 | + [Required] |
| 24 | + public string AndroidBinUtilsDirectory { get; set; } = ""; |
| 25 | + |
| 26 | + [Required] |
| 27 | + public string ApkOutputPath { get; set; } = ""; |
| 28 | + |
| 29 | + [Required] |
| 30 | + public string AppSharedLibrariesDir { get; set; } = ""; |
| 31 | + |
| 32 | + public bool EmbedAssemblies { get; set; } |
| 33 | + |
| 34 | + [Required] |
| 35 | + public bool EnableCompression { get; set; } |
| 36 | + |
| 37 | + public bool IncludeDebugSymbols { get; set; } |
| 38 | + |
| 39 | + [Required] |
| 40 | + public string IntermediateOutputPath { get; set; } = ""; |
| 41 | + |
| 42 | + [Required] |
| 43 | + public string ProjectFullPath { get; set; } = ""; |
| 44 | + |
| 45 | + [Required] |
| 46 | + public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; |
| 47 | + |
| 48 | + [Required] |
| 49 | + public ITaskItem [] ResolvedUserAssemblies { get; set; } = []; |
| 50 | + |
| 51 | + [Required] |
| 52 | + public string [] SupportedAbis { get; set; } = []; |
| 53 | + |
| 54 | + public bool UseAssemblyStore { get; set; } |
| 55 | + |
| 56 | + [Output] |
| 57 | + public ITaskItem [] FilesToAddToArchive { get; set; } = []; |
| 58 | + |
| 59 | + public override bool RunTask () |
| 60 | + { |
| 61 | + // If we aren't embedding assemblies, we don't need to do anything |
| 62 | + if (!EmbedAssemblies) |
| 63 | + return !Log.HasLoggedErrors; |
| 64 | + |
| 65 | + var files = new PackageFileListBuilder (); |
| 66 | + |
| 67 | + DSOWrapperGenerator.Config dsoWrapperConfig = DSOWrapperGenerator.GetConfig (Log, AndroidBinUtilsDirectory, IntermediateOutputPath); |
| 68 | + bool compress = !IncludeDebugSymbols && EnableCompression; |
| 69 | + IDictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>>? compressedAssembliesInfo = null; |
| 70 | + |
| 71 | + if (compress) { |
| 72 | + string key = CompressedAssemblyInfo.GetKey (ProjectFullPath); |
| 73 | + Log.LogDebugMessage ($"Retrieving assembly compression info with key '{key}'"); |
| 74 | + compressedAssembliesInfo = BuildEngine4.UnregisterTaskObjectAssemblyLocal<IDictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>>> (key, RegisteredTaskObjectLifetime.Build); |
| 75 | + if (compressedAssembliesInfo == null) |
| 76 | + throw new InvalidOperationException ($"Assembly compression info not found for key '{key}'. Compression will not be performed."); |
| 77 | + } |
| 78 | + |
| 79 | + AddAssemblies (dsoWrapperConfig, files, IncludeDebugSymbols, compress, compressedAssembliesInfo, assemblyStoreApkName: null); |
| 80 | + |
| 81 | + FilesToAddToArchive = files.ToArray (); |
| 82 | + |
| 83 | + return !Log.HasLoggedErrors; |
| 84 | + } |
| 85 | + |
| 86 | + void AddAssemblies (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files, bool debug, bool compress, IDictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>>? compressedAssembliesInfo, string? assemblyStoreApkName) |
| 87 | + { |
| 88 | + string compressedOutputDir = Path.GetFullPath (Path.Combine (Path.GetDirectoryName (ApkOutputPath), "..", "lz4")); |
| 89 | + AssemblyStoreBuilder? storeBuilder = null; |
| 90 | + |
| 91 | + if (UseAssemblyStore) { |
| 92 | + storeBuilder = new AssemblyStoreBuilder (Log); |
| 93 | + } |
| 94 | + |
| 95 | + // Add user assemblies |
| 96 | + AssemblyPackagingHelper.AddAssembliesFromCollection (Log, SupportedAbis, ResolvedUserAssemblies, (TaskLoggingHelper log, AndroidTargetArch arch, ITaskItem assembly) => DoAddAssembliesFromArchCollection (log, arch, assembly, dsoWrapperConfig, files, debug, compress, compressedAssembliesInfo, compressedOutputDir, storeBuilder)); |
| 97 | + |
| 98 | + // Add framework assemblies |
| 99 | + AssemblyPackagingHelper.AddAssembliesFromCollection (Log, SupportedAbis, ResolvedFrameworkAssemblies, (TaskLoggingHelper log, AndroidTargetArch arch, ITaskItem assembly) => DoAddAssembliesFromArchCollection (log, arch, assembly, dsoWrapperConfig, files, debug, compress, compressedAssembliesInfo, compressedOutputDir, storeBuilder)); |
| 100 | + |
| 101 | + if (!UseAssemblyStore) { |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + Dictionary<AndroidTargetArch, string> assemblyStorePaths = storeBuilder!.Generate (AppSharedLibrariesDir); |
| 106 | + |
| 107 | + if (assemblyStorePaths.Count == 0) { |
| 108 | + throw new InvalidOperationException ("Assembly store generator did not generate any stores"); |
| 109 | + } |
| 110 | + |
| 111 | + if (assemblyStorePaths.Count != SupportedAbis.Length) { |
| 112 | + throw new InvalidOperationException ("Internal error: assembly store did not generate store for each supported ABI"); |
| 113 | + } |
| 114 | + |
| 115 | + string inArchivePath; |
| 116 | + foreach (var kvp in assemblyStorePaths) { |
| 117 | + string abi = MonoAndroidHelper.ArchToAbi (kvp.Key); |
| 118 | + inArchivePath = MakeArchiveLibPath (abi, "lib" + Path.GetFileName (kvp.Value)); |
| 119 | + string wrappedSourcePath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, kvp.Key, kvp.Value, Path.GetFileName (inArchivePath)); |
| 120 | + files.AddItem (wrappedSourcePath, inArchivePath); |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + void DoAddAssembliesFromArchCollection (TaskLoggingHelper log, AndroidTargetArch arch, ITaskItem assembly, DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files, bool debug, bool compress, IDictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>>? compressedAssembliesInfo, string compressedOutputDir, AssemblyStoreBuilder? storeBuilder) |
| 125 | + { |
| 126 | + // In the "all assemblies are per-RID" world, assemblies, pdb and config are disguised as shared libraries (that is, |
| 127 | + // their names end with the .so extension) so that Android allows us to put them in the `lib/{ARCH}` directory. |
| 128 | + // For this reason, they have to be treated just like other .so files, as far as compression rules are concerned. |
| 129 | + // Thus, we no longer just store them in the apk but we call the `GetCompressionMethod` method to find out whether |
| 130 | + // or not we're supposed to compress .so files. |
| 131 | + var sourcePath = CompressAssembly (assembly, compress, compressedAssembliesInfo, compressedOutputDir); |
| 132 | + if (UseAssemblyStore) { |
| 133 | + storeBuilder!.AddAssembly (sourcePath, assembly, includeDebugSymbols: debug); |
| 134 | + return; |
| 135 | + } |
| 136 | + |
| 137 | + // Add assembly |
| 138 | + (string assemblyPath, string assemblyDirectory) = GetInArchiveAssemblyPath (assembly); |
| 139 | + string wrappedSourcePath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, sourcePath, Path.GetFileName (assemblyPath)); |
| 140 | + files.AddItem (wrappedSourcePath, assemblyPath); |
| 141 | + |
| 142 | + // Try to add config if exists |
| 143 | + var config = Path.ChangeExtension (assembly.ItemSpec, "dll.config"); |
| 144 | + AddAssemblyConfigEntry (dsoWrapperConfig, files, arch, assemblyDirectory, config); |
| 145 | + |
| 146 | + // Try to add symbols if Debug |
| 147 | + if (!debug) { |
| 148 | + return; |
| 149 | + } |
| 150 | + |
| 151 | + string symbols = Path.ChangeExtension (assembly.ItemSpec, "pdb"); |
| 152 | + if (!File.Exists (symbols)) { |
| 153 | + return; |
| 154 | + } |
| 155 | + |
| 156 | + string archiveSymbolsPath = assemblyDirectory + MonoAndroidHelper.MakeDiscreteAssembliesEntryName (Path.GetFileName (symbols)); |
| 157 | + string wrappedSymbolsPath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, symbols, Path.GetFileName (archiveSymbolsPath)); |
| 158 | + files.AddItem (wrappedSymbolsPath, archiveSymbolsPath); |
| 159 | + } |
| 160 | + |
| 161 | + /// <summary> |
| 162 | + /// Returns the in-archive path for an assembly |
| 163 | + /// </summary> |
| 164 | + (string assemblyFilePath, string assemblyDirectoryPath) GetInArchiveAssemblyPath (ITaskItem assembly) |
| 165 | + { |
| 166 | + var parts = new List<string> (); |
| 167 | + |
| 168 | + // The PrepareSatelliteAssemblies task takes care of properly setting `DestinationSubDirectory`, so we can just use it here. |
| 169 | + string? subDirectory = assembly.GetMetadata ("DestinationSubDirectory")?.Replace ('\\', '/'); |
| 170 | + if (string.IsNullOrEmpty (subDirectory)) { |
| 171 | + throw new InvalidOperationException ($"Internal error: assembly '{assembly}' lacks the required `DestinationSubDirectory` metadata"); |
| 172 | + } |
| 173 | + |
| 174 | + string assemblyName = Path.GetFileName (assembly.ItemSpec); |
| 175 | + // For discrete assembly entries we need to treat assemblies specially. |
| 176 | + // All of the assemblies have their names mangled so that the possibility to clash with "real" shared |
| 177 | + // library names is minimized. All of the assembly entries will start with a special character: |
| 178 | + // |
| 179 | + // `_` - for regular assemblies (e.g. `_Mono.Android.dll.so`) |
| 180 | + // `-` - for satellite assemblies (e.g. `-es-Mono.Android.dll.so`) |
| 181 | + // |
| 182 | + // Second of all, we need to treat satellite assemblies with even more care. |
| 183 | + // If we encounter one of them, we will return the culture as part of the path transformed |
| 184 | + // so that it forms a `-culture-` assembly file name prefix, not a `culture/` subdirectory. |
| 185 | + // This is necessary because Android doesn't allow subdirectories in `lib/{ABI}/` |
| 186 | + // |
| 187 | + string [] subdirParts = subDirectory!.TrimEnd ('/').Split ('/'); |
| 188 | + if (subdirParts.Length == 1) { |
| 189 | + // Not a satellite assembly |
| 190 | + parts.Add (subDirectory); |
| 191 | + parts.Add (MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyName)); |
| 192 | + } else if (subdirParts.Length == 2) { |
| 193 | + parts.Add (subdirParts [0]); |
| 194 | + parts.Add (MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyName, subdirParts [1])); |
| 195 | + } else { |
| 196 | + throw new InvalidOperationException ($"Internal error: '{assembly}' `DestinationSubDirectory` metadata has too many components ({parts.Count} instead of 1 or 2)"); |
| 197 | + } |
| 198 | + |
| 199 | + string assemblyFilePath = MonoAndroidHelper.MakeZipArchivePath (ArchiveAssembliesPath, parts); |
| 200 | + return (assemblyFilePath, Path.GetDirectoryName (assemblyFilePath) + "/"); |
| 201 | + } |
| 202 | + |
| 203 | + void AddAssemblyConfigEntry (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files, AndroidTargetArch arch, string assemblyPath, string configFile) |
| 204 | + { |
| 205 | + string inArchivePath = MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyPath + Path.GetFileName (configFile)); |
| 206 | + |
| 207 | + if (!File.Exists (configFile)) { |
| 208 | + return; |
| 209 | + } |
| 210 | + |
| 211 | + string wrappedConfigFile = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, configFile, Path.GetFileName (inArchivePath)); |
| 212 | + |
| 213 | + files.AddItem (wrappedConfigFile, inArchivePath); |
| 214 | + } |
| 215 | + |
| 216 | + string CompressAssembly (ITaskItem assembly, bool compress, IDictionary<AndroidTargetArch, Dictionary<string, CompressedAssemblyInfo>>? compressedAssembliesInfo, string compressedOutputDir) |
| 217 | + { |
| 218 | + if (!compress) { |
| 219 | + return assembly.ItemSpec; |
| 220 | + } |
| 221 | + |
| 222 | + // NRT: compressedAssembliesInfo is guaranteed to be non-null if compress is true |
| 223 | + return AssemblyCompression.Compress (Log, assembly, compressedAssembliesInfo!, compressedOutputDir); |
| 224 | + } |
| 225 | + |
| 226 | + static string MakeArchiveLibPath (string abi, string fileName) => MonoAndroidHelper.MakeZipArchivePath (ArchiveLibPath, abi, fileName); |
| 227 | +} |
0 commit comments