From 33cee50c52c4a981d76f9007ac195f7791da61a8 Mon Sep 17 00:00:00 2001 From: Jonathan Pobst Date: Tue, 14 Jan 2025 11:34:17 -1000 Subject: [PATCH] Create separate 'CreateAssemblyStore' and 'WrapAssembliesAsSharedLibraries' tasks. --- .../Tasks/CollectAssemblyFilesForArchive.cs | 1 - .../Tasks/CompressAssemblies.cs | 5 +- .../Tasks/CreateAssemblyStore.cs | 86 +++++++++ .../Tasks/WrapAssembliesAsSharedLibraries.cs | 167 ++++++++++++++++++ .../Xamarin.Android.Common.targets | 28 ++- 5 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/CreateAssemblyStore.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CollectAssemblyFilesForArchive.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CollectAssemblyFilesForArchive.cs index 7914e4b7f3e..15f4d8351a6 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/CollectAssemblyFilesForArchive.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CollectAssemblyFilesForArchive.cs @@ -66,7 +66,6 @@ public override bool RunTask () void AddAssemblies (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files, string? assemblyStoreApkName) { - string compressedOutputDir = Path.GetFullPath (Path.Combine (Path.GetDirectoryName (ApkOutputPath), "..", "lz4")); AssemblyStoreBuilder? storeBuilder = null; if (UseAssemblyStore) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs index ccd6822fa00..e611e92186c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CompressAssemblies.cs @@ -6,7 +6,6 @@ using System.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; @@ -23,6 +22,8 @@ public class CompressAssemblies : AndroidTask [Required] public string ApkOutputPath { get; set; } = ""; + public bool EmbedAssemblies { get; set; } + [Required] public bool EnableCompression { get; set; } @@ -48,7 +49,7 @@ public class CompressAssemblies : AndroidTask public override bool RunTask () { - if (IncludeDebugSymbols || !EnableCompression) { + if (IncludeDebugSymbols || !EnableCompression || !EmbedAssemblies) { ResolvedFrameworkAssembliesOutput = ResolvedFrameworkAssemblies; ResolvedUserAssembliesOutput = ResolvedUserAssemblies; return true; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/CreateAssemblyStore.cs b/src/Xamarin.Android.Build.Tasks/Tasks/CreateAssemblyStore.cs new file mode 100644 index 00000000000..ed58c40481d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/CreateAssemblyStore.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// If using $(AndroidUseAssemblyStore), place all the assemblies in a single file. +/// +public class CreateAssemblyStore : AndroidTask +{ + public override string TaskPrefix => "CST"; + + [Required] + public string AppSharedLibrariesDir { get; set; } = ""; + + public bool IncludeDebugSymbols { get; set; } + + [Required] + public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; + + [Required] + public ITaskItem [] ResolvedUserAssemblies { get; set; } = []; + + [Required] + public string [] SupportedAbis { get; set; } = []; + + public bool UseAssemblyStore { get; set; } + + [Output] + public ITaskItem [] AssembliesToAddToArchive { get; set; } = []; + + public override bool RunTask () + { + // Get all the user and framework assemblies we may need to package + var assemblies = ResolvedFrameworkAssemblies.Concat (ResolvedUserAssemblies).Where (asm => !(ShouldSkipAssembly (asm))).ToArray (); + + if (!UseAssemblyStore) { + AssembliesToAddToArchive = assemblies; + return !Log.HasLoggedErrors; + } + + var store_builder = new AssemblyStoreBuilder (Log); + var per_arch_assemblies = MonoAndroidHelper.GetPerArchAssemblies (assemblies, SupportedAbis, true); + + foreach (var kvp in per_arch_assemblies) { + Log.LogDebugMessage ($"Adding assemblies for architecture '{kvp.Key}'"); + + foreach (var assembly in kvp.Value.Values) { + var sourcePath = assembly.GetMetadataOrDefault ("CompressedAssembly", assembly.ItemSpec); + store_builder.AddAssembly (sourcePath, assembly, includeDebugSymbols: IncludeDebugSymbols); + + Log.LogDebugMessage ($"Added '{sourcePath}' to assembly store."); + } + } + + var assembly_store_paths = store_builder.Generate (AppSharedLibrariesDir); + + if (assembly_store_paths.Count == 0) { + throw new InvalidOperationException ("Assembly store generator did not generate any stores"); + } + + if (assembly_store_paths.Count != SupportedAbis.Length) { + throw new InvalidOperationException ("Internal error: assembly store did not generate store for each supported ABI"); + } + + AssembliesToAddToArchive = assembly_store_paths.Select (kvp => new TaskItem (kvp.Value, new Dictionary { { "Abi", MonoAndroidHelper.ArchToAbi (kvp.Key) } })).ToArray (); + + return !Log.HasLoggedErrors; + } + + bool ShouldSkipAssembly (ITaskItem asm) + { + var should_skip = asm.GetMetadataOrDefault ("AndroidSkipAddToPackage", false); + + if (should_skip) + Log.LogDebugMessage ($"Skipping {asm.ItemSpec} due to 'AndroidSkipAddToPackage' == 'true' "); + + return should_skip; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs new file mode 100644 index 00000000000..0c38a1db32d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/WrapAssembliesAsSharedLibraries.cs @@ -0,0 +1,167 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Xamarin.Android.Tools; + +namespace Xamarin.Android.Tasks; + +/// +/// In the "all assemblies are per-RID" world, assembly stores, assemblies, pdb and config are disguised as shared libraries (that is, +/// their names end with the .so extension) so that Android allows us to put them in the `lib/{ARCH}` directory. +/// +public class WrapAssembliesAsSharedLibraries : AndroidTask +{ + const string ArchiveAssembliesPath = "lib"; + const string ArchiveLibPath = "lib"; + + public override string TaskPrefix => "WAS"; + + [Required] + public string AndroidBinUtilsDirectory { get; set; } = ""; + + public bool IncludeDebugSymbols { get; set; } + + [Required] + public string IntermediateOutputPath { get; set; } = ""; + + public bool UseAssemblyStore { get; set; } + + [Required] + public ITaskItem [] ResolvedAssemblies { get; set; } = []; + + [Required] + public string [] SupportedAbis { get; set; } = []; + + [Output] + public ITaskItem [] WrappedAssemblies { get; set; } = []; + + public override bool RunTask () + { + var dsoWrapperConfig = DSOWrapperGenerator.GetConfig (Log, AndroidBinUtilsDirectory, IntermediateOutputPath); + var files = new PackageFileListBuilder (); + + if (UseAssemblyStore) + WrapAssemblyStores (dsoWrapperConfig, files); + else + AssemblyPackagingHelper.AddAssembliesFromCollection (Log, SupportedAbis, ResolvedAssemblies, (TaskLoggingHelper log, AndroidTargetArch arch, ITaskItem assembly) => WrapAssembly (log, arch, assembly, dsoWrapperConfig, files)); + + WrappedAssemblies = files.ToArray (); + + return !Log.HasLoggedErrors; + } + + bool WrapAssemblyStores (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files) + { + foreach (var store in ResolvedAssemblies) { + var store_path = store.ItemSpec; + var abi = store.GetRequiredMetadata ("ResolvedAssemblies", "Abi", Log); + + if (abi is null) + return false; + + var arch = MonoAndroidHelper.AbiToTargetArch (abi); + var inArchivePath = MakeArchiveLibPath (abi, "lib" + Path.GetFileName (store_path)); + string wrappedSourcePath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, store_path, Path.GetFileName (inArchivePath)); + + files.AddItem (wrappedSourcePath, inArchivePath); + } + + return true; + } + + void WrapAssembly (TaskLoggingHelper log, AndroidTargetArch arch, ITaskItem assembly, DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files) + { + // In the "all assemblies are per-RID" world, assemblies, pdb and config are disguised as shared libraries (that is, + // their names end with the .so extension) so that Android allows us to put them in the `lib/{ARCH}` directory. + // For this reason, they have to be treated just like other .so files, as far as compression rules are concerned. + // Thus, we no longer just store them in the apk but we call the `GetCompressionMethod` method to find out whether + // or not we're supposed to compress .so files. + var sourcePath = assembly.GetMetadataOrDefault ("CompressedAssembly", assembly.ItemSpec); + + // Add assembly + (string assemblyPath, string assemblyDirectory) = GetInArchiveAssemblyPath (assembly); + string wrappedSourcePath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, sourcePath, Path.GetFileName (assemblyPath)); + files.AddItem (wrappedSourcePath, assemblyPath); + + // Try to add config if exists + var config = Path.ChangeExtension (assembly.ItemSpec, "dll.config"); + AddAssemblyConfigEntry (dsoWrapperConfig, files, arch, assemblyDirectory, config); + + // Try to add symbols if Debug + if (!IncludeDebugSymbols) { + return; + } + + string symbols = Path.ChangeExtension (assembly.ItemSpec, "pdb"); + if (!File.Exists (symbols)) { + return; + } + + string archiveSymbolsPath = assemblyDirectory + MonoAndroidHelper.MakeDiscreteAssembliesEntryName (Path.GetFileName (symbols)); + string wrappedSymbolsPath = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, symbols, Path.GetFileName (archiveSymbolsPath)); + files.AddItem (wrappedSymbolsPath, archiveSymbolsPath); + } + + static string MakeArchiveLibPath (string abi, string fileName) => MonoAndroidHelper.MakeZipArchivePath (ArchiveLibPath, abi, fileName); + + /// + /// Returns the in-archive path for an assembly + /// + (string assemblyFilePath, string assemblyDirectoryPath) GetInArchiveAssemblyPath (ITaskItem assembly) + { + var parts = new List (); + + // The PrepareSatelliteAssemblies task takes care of properly setting `DestinationSubDirectory`, so we can just use it here. + string? subDirectory = assembly.GetMetadata ("DestinationSubDirectory")?.Replace ('\\', '/'); + if (string.IsNullOrEmpty (subDirectory)) { + throw new InvalidOperationException ($"Internal error: assembly '{assembly}' lacks the required `DestinationSubDirectory` metadata"); + } + + string assemblyName = Path.GetFileName (assembly.ItemSpec); + // For discrete assembly entries we need to treat assemblies specially. + // All of the assemblies have their names mangled so that the possibility to clash with "real" shared + // library names is minimized. All of the assembly entries will start with a special character: + // + // `_` - for regular assemblies (e.g. `_Mono.Android.dll.so`) + // `-` - for satellite assemblies (e.g. `-es-Mono.Android.dll.so`) + // + // Second of all, we need to treat satellite assemblies with even more care. + // If we encounter one of them, we will return the culture as part of the path transformed + // so that it forms a `-culture-` assembly file name prefix, not a `culture/` subdirectory. + // This is necessary because Android doesn't allow subdirectories in `lib/{ABI}/` + // + string [] subdirParts = subDirectory!.TrimEnd ('/').Split ('/'); + if (subdirParts.Length == 1) { + // Not a satellite assembly + parts.Add (subDirectory); + parts.Add (MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyName)); + } else if (subdirParts.Length == 2) { + parts.Add (subdirParts [0]); + parts.Add (MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyName, subdirParts [1])); + } else { + throw new InvalidOperationException ($"Internal error: '{assembly}' `DestinationSubDirectory` metadata has too many components ({parts.Count} instead of 1 or 2)"); + } + + string assemblyFilePath = MonoAndroidHelper.MakeZipArchivePath (ArchiveAssembliesPath, parts); + return (assemblyFilePath, Path.GetDirectoryName (assemblyFilePath) + "/"); + } + + void AddAssemblyConfigEntry (DSOWrapperGenerator.Config dsoWrapperConfig, PackageFileListBuilder files, AndroidTargetArch arch, string assemblyPath, string configFile) + { + string inArchivePath = MonoAndroidHelper.MakeDiscreteAssembliesEntryName (assemblyPath + Path.GetFileName (configFile)); + + if (!File.Exists (configFile)) { + return; + } + + string wrappedConfigFile = DSOWrapperGenerator.WrapIt (Log, dsoWrapperConfig, arch, configFile, Path.GetFileName (inArchivePath)); + + files.AddItem (wrappedConfigFile, inArchivePath); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index fb68fbb8810..59cb0e5a344 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -55,6 +55,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -97,6 +98,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -2100,6 +2102,7 @@ because xbuild doesn't support framework reference assemblies. --> + + + + + + + + @@ -2126,7 +2149,7 @@ because xbuild doesn't support framework reference assemblies. - - + -->