diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets new file mode 100644 index 00000000000..3a3432bf141 --- /dev/null +++ b/build-tools/create-packs/Microsoft.Android.Sdk.TrimmableTypeMap.targets @@ -0,0 +1,166 @@ + + + + + + + <_PreBuiltTypeMapPackSubDir>data\prebuilt-typemap + <_PreBuiltTypeMapOutputDir>$(MicrosoftAndroidSdkPackDir)$(_PreBuiltTypeMapPackSubDir)\ + + $(NoWarn);NU5100 + + <_PreBuiltTypeMapIntDir>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)', 'obj', 'prebuilt-typemap')) + <_PreBuiltTypeMapJavaDir>$(_PreBuiltTypeMapIntDir)java\ + <_PreBuiltTypeMapAssembly>$(_PreBuiltTypeMapOutputDir)_Mono.Android.TypeMap.dll + <_PreBuiltTypeMapJavaInteropAssembly>$(_PreBuiltTypeMapOutputDir)_Java.Interop.TypeMap.dll + <_PreBuiltTypeMapAcwMap>$(_PreBuiltTypeMapOutputDir)mono.android-acw-map.txt + <_PreBuiltTypeMapProperties>$(_PreBuiltTypeMapOutputDir)framework.props + <_PreBuiltTypeMapJavaFilesList>$(_PreBuiltTypeMapIntDir)java-files.txt + <_PreBuiltTypeMapGenerationStamp>$(_PreBuiltTypeMapIntDir)_GeneratePreBuiltMonoAndroidTypeMap.stamp + + + + + <_PreBuiltTypeMapInput Remove="@(_PreBuiltTypeMapInput)" /> + <_PreBuiltTypeMapFramework Remove="@(_PreBuiltTypeMapFramework)" /> + <_PreBuiltTypeMapInput Include="$(_MonoAndroidNETDefaultOutDir)Mono.Android.dll" /> + <_PreBuiltTypeMapInput Include="$(_MonoAndroidNETDefaultOutDir)Java.Interop.dll" /> + <_PreBuiltTypeMapFramework Include="$(_MonoAndroidNETDefaultOutDir)*.dll" + Exclude="@(_PreBuiltTypeMapInput)" /> + + + + + + + <_ExpectedPreBuiltTypeMapJavaFile Remove="@(_ExpectedPreBuiltTypeMapJavaFile)" /> + <_ActualPreBuiltTypeMapJavaFile Remove="@(_ActualPreBuiltTypeMapJavaFile)" /> + <_MissingPreBuiltTypeMapJavaFile Remove="@(_MissingPreBuiltTypeMapJavaFile)" /> + <_UnexpectedPreBuiltTypeMapJavaFile Remove="@(_UnexpectedPreBuiltTypeMapJavaFile)" /> + + + + + + <_ActualPreBuiltTypeMapJavaFile Include="$(_PreBuiltTypeMapJavaDir)**\*.java" /> + <_MissingPreBuiltTypeMapJavaFile Include="@(_ExpectedPreBuiltTypeMapJavaFile)" + Condition="!Exists('%(_ExpectedPreBuiltTypeMapJavaFile.Identity)')" /> + <_UnexpectedPreBuiltTypeMapJavaFile Include="@(_ActualPreBuiltTypeMapJavaFile->'%(FullPath)')" + Exclude="@(_ExpectedPreBuiltTypeMapJavaFile)" /> + + + + <_ExpectedPreBuiltTypeMapJavaFile Remove="@(_ExpectedPreBuiltTypeMapJavaFile)" /> + <_ActualPreBuiltTypeMapJavaFile Remove="@(_ActualPreBuiltTypeMapJavaFile)" /> + <_MissingPreBuiltTypeMapJavaFile Remove="@(_MissingPreBuiltTypeMapJavaFile)" /> + <_UnexpectedPreBuiltTypeMapJavaFile Remove="@(_UnexpectedPreBuiltTypeMapJavaFile)" /> + + + + + + + + + + + + + + + + + + + + <_PreBuiltTypeMapJcwJar>$(_PreBuiltTypeMapOutputDir)mono.android-typemaps.jar + <_PreBuiltTypeMapJcwClassesDir>$(_PreBuiltTypeMapIntDir)jcw-classes\ + <_PreBuiltTypeMapJcwSourceList>$(_PreBuiltTypeMapIntDir)jcw-sources.txt + <_PreBuiltTypeMapAndroidJar>$(AndroidSdkDirectory)\platforms\android-$(AndroidPlatformId)\android.jar + <_PreBuiltTypeMapMonoAndroidJar>$(_MonoAndroidNETDefaultOutDir)mono.android.jar + <_PreBuiltTypeMapRuntimeJar>$(MicrosoftAndroidSdkOutDir)java_runtime.jar + + + + + + <_PreBuiltTypeMapJavaFile Remove="@(_PreBuiltTypeMapJavaFile)" /> + + + + + + + + + + + <_PreBuiltTypeMapJavaFile> + <_JavacResponseFilePath>$([System.String]::Copy('%(Identity)').Replace('\', '\\')) + + + + + + + + + + <_PreBuiltTypeMapAllAssemblies Include="$(_PreBuiltTypeMapOutputDir)*.TypeMap.dll" /> + + + + + + + + diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.proj b/build-tools/create-packs/Microsoft.Android.Sdk.proj index 6f00fb8f438..b1514ce65bd 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.proj +++ b/build-tools/create-packs/Microsoft.Android.Sdk.proj @@ -12,6 +12,8 @@ core workload SDK packs imported by WorkloadManifest.targets. + + $(DotNetStableTargetFramework) diff --git a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 9ca1b1e0dd5..8211a2fbb50 100644 --- a/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Microsoft.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.IO.Compression; using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; @@ -131,6 +132,38 @@ public void LogCustomJavaObjectWarning (string managedTypeName) => public string? ApplicationJavaClass { get; set; } public bool GenerateTypeMapAssemblies { get; set; } = true; public bool IncludeBuiltInValueTypeUniverses { get; set; } + + // When false, the per-assembly typemap DLLs (and JCWs) are emitted but the root + // _Microsoft.Android.TypeMaps assembly is not. Used for SDK-build-time pre-generation of + // framework typemaps (e.g. Mono.Android, issue #10792); the app build emits the root, which + // references the pre-generated per-assembly typemaps alongside the app's own. + public bool GenerateRootAssembly { get; set; } = true; + + // Framework assemblies whose typemap is pre-generated at SDK build time (issue #10792), e.g. + // Mono.Android. On the app build these are indexed for base-type resolution but NOT scanned for + // peers; instead their pre-generated per-assembly typemap (_.TypeMap) is referenced by the + // generated root assembly through its private __TypeMapAnchor. + public ITaskItem [] PreGeneratedTypeMapAssemblies { get; set; } = []; + + // Assemblies that are indexed for type resolution but are not scanned for Java peers. + public ITaskItem [] ReferenceOnlyAssemblies { get; set; } = []; + + // Pre-compiled framework JCWs whose Java names must not collide with app-generated JCWs. + public string? PreGeneratedJcwJar { get; set; } + + // ACW map for pre-generated framework peers. Linked builds use this lightweight index to root + // framework types referenced only by application manifests or layouts without rescanning them. + public string? PreGeneratedFrameworkAcwMap { get; set; } + + // Linker descriptor containing application-specific roots selected from PreGeneratedFrameworkAcwMap. + public string? PreGeneratedFrameworkRootDescriptorFile { get; set; } + + /// + /// When false, application-specific manifest and layout roots do not mutate framework peers, + /// allowing maps generated without app inputs to match the SDK's pre-generated framework maps. + /// + public bool RootFrameworkPeersFromApplication { get; set; } = true; + public bool CleanJavaSourceOutputDirectory { get; set; } /// @@ -155,11 +188,25 @@ public override bool RunTask () var frameworkAssemblyPaths = new HashSet ( ResolvedFrameworkAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), StringComparer.OrdinalIgnoreCase); + // Assemblies whose typemap is pre-generated: index them for resolution but don't scan for + // peers, and reference their pre-generated _.TypeMap through its private anchor. + var preGeneratedAssemblyPaths = new HashSet ( + PreGeneratedTypeMapAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), + StringComparer.OrdinalIgnoreCase); + var referenceOnlyAssemblyPaths = new HashSet ( + ReferenceOnlyAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), + StringComparer.OrdinalIgnoreCase); + referenceOnlyAssemblyPaths.UnionWith (preGeneratedAssemblyPaths); + var preGeneratedTypeMapNames = PreGeneratedTypeMapAssemblies + .Select (i => $"_{Path.GetFileNameWithoutExtension (i.ItemSpec)}.TypeMap") + .Distinct (StringComparer.Ordinal) + .ToList (); var assemblyInputs = ResolvedAssemblies .GroupBy (i => Path.GetFullPath (i.ItemSpec), StringComparer.OrdinalIgnoreCase) .Select (g => ( Path: g.Key, - IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem))) + IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem), + ScanForPeers: !referenceOnlyAssemblyPaths.Contains (g.Key))) .ToList (); var frameworkAssemblyNames = new HashSet (DefaultFrameworkAssemblyNames, StringComparer.OrdinalIgnoreCase); foreach (var assemblyName in FrameworkAssemblyNames) { @@ -197,7 +244,7 @@ public override bool RunTask () var priorTypeMapFingerprints = ReadTypeMapFingerprints (); TrimmableTypeMapResult? result = null; try { - foreach (var (path, isFrameworkAssembly) in assemblyInputs) { + foreach (var (path, isFrameworkAssembly, scanForPeers) in assemblyInputs) { var peReader = new PEReader (File.OpenRead (path)); peReaders.Add (peReader); if (!peReader.HasMetadata) { @@ -208,7 +255,7 @@ public override bool RunTask () continue; } var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - assemblies.Add (new AssemblyInput (assemblyName, path, peReader)); + assemblies.Add (new AssemblyInput (assemblyName, path, peReader, scanForPeers)); if (isFrameworkAssembly) { frameworkAssemblyNames.Add (assemblyName); } @@ -241,6 +288,8 @@ public override bool RunTask () IReadOnlyCollection? customViewTypeNames = CustomViewMapFile.IsNullOrEmpty () ? null : LoadCustomViewTypeNames (CustomViewMapFile); + IReadOnlyCollection? preGeneratedJcwNames = LoadPreGeneratedJcwNames (); + IReadOnlyList? preGeneratedFrameworkPeers = LoadPreGeneratedFrameworkPeers (); result = generator.Execute ( assemblies, @@ -251,8 +300,14 @@ public override bool RunTask () manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, generateTypeMapAssemblies: GenerateTypeMapAssemblies, + generateRootAssembly: GenerateRootAssembly, + preGeneratedTypeMapNames: preGeneratedTypeMapNames, + preGeneratedFrameworkPeers: preGeneratedFrameworkPeers, errorOnCustomJavaObject: ErrorOnCustomJavaObject, customViewTypeNames: customViewTypeNames, + preGeneratedJcwNames: preGeneratedJcwNames, + preGeneratedJcwSource: PreGeneratedJcwJar, + rootFrameworkPeersFromApplication: RootFrameworkPeersFromApplication, collectMarshalMethodsForNonAcw: false, includeBuiltInValueTypeUniverses: IncludeBuiltInValueTypeUniverses, shouldGenerateTypeMapAssembly: TypeMapFingerprintsFile.IsNullOrEmpty () ? null : ShouldGenerateTypeMapAssembly); @@ -311,6 +366,8 @@ public override bool RunTask () Files.CopyIfStringChanged (GenerateApplicationRegistrationJava (result.ApplicationRegistrationTypes), ApplicationRegistrationOutputFile); Log.LogDebugMessage ($"Generated ApplicationRegistration.java with {result.ApplicationRegistrationTypes.Count} deferred registration(s)."); } + + WritePreGeneratedFrameworkRootDescriptor (preGeneratedFrameworkPeers); } finally { if (result is not null) { foreach (var assembly in result.GeneratedAssemblies) { @@ -561,4 +618,99 @@ static string GenerateApplicationRegistrationJava (IReadOnlyList registr sb.AppendLine ("}"); return sb.ToString (); } + + IReadOnlyCollection? LoadPreGeneratedJcwNames () + { + if (PreGeneratedTypeMapAssemblies.Length == 0 || PreGeneratedJcwJar.IsNullOrEmpty ()) { + return null; + } + + var names = new HashSet (StringComparer.Ordinal); + using var stream = File.OpenRead (PreGeneratedJcwJar); + using var jar = new ZipArchive (stream, ZipArchiveMode.Read); + foreach (var entry in jar.Entries) { + if (entry.Name.Length > 0 && entry.FullName.EndsWith (".class", StringComparison.Ordinal)) { + names.Add (entry.FullName.Substring (0, entry.FullName.Length - ".class".Length)); + } + } + return names; + } + + IReadOnlyList? LoadPreGeneratedFrameworkPeers () + { + if (!RootFrameworkPeersFromApplication || PreGeneratedFrameworkAcwMap.IsNullOrEmpty ()) { + return null; + } + + var peers = new List (); + int lineNumber = 0; + foreach (var line in File.ReadLines (PreGeneratedFrameworkAcwMap)) { + lineNumber++; + if (line.Length == 0) { + continue; + } + int separator = line.IndexOf (';'); + if (separator <= 0 || separator == line.Length - 1) { + throw new InvalidDataException ($"Invalid pre-generated framework ACW map entry in '{PreGeneratedFrameworkAcwMap}' at line {lineNumber}: '{line}'."); + } + + string managedName = line.Substring (0, separator); + int assemblySeparator = managedName.LastIndexOf (", ", StringComparison.Ordinal); + // Each type also has managed-name and Java-name alias lines without an assembly name. + if (assemblySeparator <= 0) { + continue; + } + + string managedTypeName = managedName.Substring (0, assemblySeparator); + string assemblyName = managedName.Substring (assemblySeparator + 2); + string javaName = line.Substring (separator + 1).Replace ('.', '/'); + int namespaceSeparator = managedTypeName.LastIndexOf ('.'); + peers.Add (new JavaPeerInfo { + JavaName = javaName, + // The ACW map does not carry a distinct compatibility JNI name. Framework manifest + // and layout references use the actual Java binary name represented here. + CompatJniName = javaName, + ManagedTypeName = managedTypeName, + ManagedTypeNamespace = namespaceSeparator < 0 ? "" : managedTypeName.Substring (0, namespaceSeparator), + ManagedTypeShortName = managedTypeName.Substring (namespaceSeparator + 1), + AssemblyName = assemblyName, + IsFrameworkAssembly = true, + DoNotGenerateAcw = true, + }); + } + return peers; + } + + void WritePreGeneratedFrameworkRootDescriptor (IReadOnlyList? frameworkPeers) + { + if (PreGeneratedFrameworkRootDescriptorFile.IsNullOrEmpty ()) { + return; + } + + var roots = frameworkPeers? + .Where (peer => peer.IsUnconditional) + .Select (peer => (peer.AssemblyName, peer.ManagedTypeName)) + .Distinct () + .OrderBy (root => root.AssemblyName, StringComparer.Ordinal) + .ThenBy (root => root.ManagedTypeName, StringComparer.Ordinal) + .ToList () ?? []; + var directory = Path.GetDirectoryName (PreGeneratedFrameworkRootDescriptorFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + + var document = new XDocument ( + new XElement ("linker", + roots.GroupBy (root => root.AssemblyName, StringComparer.Ordinal) + .Select (assembly => new XElement ("assembly", + new XAttribute ("fullname", assembly.Key), + assembly.Select (root => new XElement ("type", + new XAttribute ("fullname", root.ManagedTypeName.Replace ('+', '/')), + new XAttribute ("preserve", "nothing"))))))); + using var stream = new MemoryStream (); + document.Save (stream); + stream.Position = 0; + Files.CopyIfStreamChanged (stream, PreGeneratedFrameworkRootDescriptorFile); + Log.LogDebugMessage ($"Generated linker roots for {roots.Count} pre-generated framework type(s)."); + } } diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs index 283dc2cf301..84c09248b5e 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/GenerateTrimmableTypeMapTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Compression; using System.Linq; using Microsoft.Android.Tasks; using Microsoft.Build.Framework; @@ -198,6 +199,88 @@ public void ReadTypeMapFingerprints_InvalidCache_Regenerates () Assert.IsEmpty (task.ReadTypeMapFingerprints (), "An invalid incremental cache should regenerate every typemap assembly."); } + [Test] + public void Execute_SwitchToPreGeneratedFrameworkTypeMap_PreservesRoot () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var assemblies = new [] { monoAndroidItem }; + var firstTask = CreateTask (assemblies, outputDir, javaDir); + firstTask.Debug = true; + Assert.IsTrue (firstTask.Execute (), "Initial local typemap generation should succeed."); + + var rootPath = Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll"); + var initialRoot = File.ReadAllBytes (rootPath); + + var secondTask = CreateTask (assemblies, outputDir, javaDir); + secondTask.Debug = true; + secondTask.PreGeneratedTypeMapAssemblies = assemblies; + Assert.IsTrue (secondTask.Execute (), "Switching to the pre-generated framework typemap should succeed."); + + var preGeneratedRoot = File.ReadAllBytes (rootPath); + CollectionAssert.AreEqual ( + initialRoot, + preGeneratedRoot, + "The root should reference a per-assembly map identically whether that map was generated locally or pre-generated."); + CollectionAssert.AreEqual ( + new [] { rootPath }, + secondTask.GeneratedAssemblies.Select (item => item.ItemSpec).ToArray (), + "Only the existing root should be reported when every scanned assembly uses a pre-generated typemap."); + } + + [Test] + public void Execute_ReadsPreGeneratedJcwNamesFromStandardZipArchive () + { + var path = Path.Combine (Root, "temp", TestName); + var outputDir = Path.Combine (path, "typemap"); + var javaDir = Path.Combine (path, "java"); + var jcwJar = Path.Combine (path, "framework-jcws.jar"); + Directory.CreateDirectory (path); + using (var stream = File.Create (jcwJar)) + using (var archive = new ZipArchive (stream, ZipArchiveMode.Create)) { + archive.CreateEntry ("android/app/Activity.class"); + archive.CreateEntry ("META-INF/"); + } + + var task = CreateTask ([], outputDir, javaDir); + task.Debug = true; + task.PreGeneratedTypeMapAssemblies = [new TaskItem ("Mono.Android.dll")]; + task.PreGeneratedJcwJar = jcwJar; + + Assert.IsTrue (task.Execute (), "The task should read pre-generated JCW names without external ZIP dependencies."); + CollectionAssert.AreEqual ( + new [] { Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll") }, + task.GeneratedAssemblies.Select (item => item.ItemSpec).ToArray ()); + } + + [Test] + public void Execute_InvalidPreGeneratedFrameworkAcwMap_ReportsFileAndLine () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var acwMap = Path.Combine (Root, path, "framework-acw-map.txt"); + Directory.CreateDirectory (Path.GetDirectoryName (acwMap)); + File.WriteAllText (acwMap, $"{Environment.NewLine}invalid"); + + var errors = new List (); + var task = CreateTask ([], outputDir, javaDir, errors: errors); + task.PreGeneratedFrameworkAcwMap = acwMap; + + Assert.IsFalse (task.Execute (), "A malformed pre-generated framework ACW map should fail the task."); + Assert.IsTrue ( + errors.Any (error => error.Message?.Contains ($"'{acwMap}' at line 2: 'invalid'", StringComparison.Ordinal) == true), + "The error should identify the malformed ACW map and line number."); + } + [Test] public void Execute_MissingJavaSource_DoesNotPruneExistingOutput () { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs index d742d120d8c..c924746abf6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs @@ -2,4 +2,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; -public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader); +/// +/// When true (default) the assembly is scanned and its Java peers are emitted into a typemap. +/// When false the assembly is only indexed for base-type resolution (e.g. Mono.Android on the app +/// build, whose typemap is pre-generated at SDK build time, issue #10792) and no peers are emitted. +/// +public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader, bool ScanForPeers = true); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 428b3e989e4..259976f734a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -252,6 +252,9 @@ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> as public List Scan (IEnumerable assemblies) { + // Reference-only assemblies (ScanForPeers == false) are indexed so base-type resolution + // works, but are not scanned for peer emission (their typemap is pre-generated elsewhere). + var referenceOnlyAssemblyNames = new HashSet (StringComparer.Ordinal); foreach (var assembly in assemblies) { var index = AssemblyIndex.Create (assembly.Reader, assembly.Name, assembly.Path); if (assemblyCache.TryGetValue (index.AssemblyName, out var existingIndex)) { @@ -278,6 +281,9 @@ public List Scan (IEnumerable assemblies) assemblyCache [index.AssemblyName] = index; resolvabilityCache [index.AssemblyName] = new ResolvabilityResult? [index.Reader.TypeDefinitions.Count + 1]; AnyUnmanagedCallersOnlyAssembly |= index.UsesUnmanagedCallersOnlyCallbacks; + if (!assembly.ScanForPeers) { + referenceOnlyAssemblyNames.Add (index.AssemblyName); + } } // Key by (managedTypeName, assemblyName) to avoid collisions when two assemblies @@ -285,6 +291,9 @@ public List Scan (IEnumerable assemblies) // Java.Interop and Mono.Android). var resultsByQualifiedName = new Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> (); foreach (var index in assemblyCache.Values) { + if (referenceOnlyAssemblyNames.Contains (index.AssemblyName)) { + continue; + } ScanAssembly (index, resultsByQualifiedName); } ForceUnconditionalCrossReferences (resultsByQualifiedName, assemblyCache); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 2a7309ed4df..c964860a7a3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -45,11 +45,17 @@ public TrimmableTypeMapResult Execute ( XDocument? manifestTemplate = null, string? packageNamingPolicy = null, bool generateTypeMapAssemblies = true, + bool generateRootAssembly = true, bool errorOnCustomJavaObject = true, + IReadOnlyList? preGeneratedTypeMapNames = null, IReadOnlyCollection? customViewTypeNames = null, + IReadOnlyCollection? preGeneratedJcwNames = null, + string? preGeneratedJcwSource = null, + bool rootFrameworkPeersFromApplication = true, bool collectMarshalMethodsForNonAcw = true, bool includeBuiltInValueTypeUniverses = false, - Func? shouldGenerateTypeMapAssembly = null) + Func? shouldGenerateTypeMapAssembly = null, + IReadOnlyList? preGeneratedFrameworkPeers = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -60,32 +66,45 @@ public TrimmableTypeMapResult Execute ( frameworkAssemblyNames, errorOnCustomJavaObject, collectMarshalMethodsForNonAcw); - var manifestForRooting = PrepareManifestForRooting (manifestTemplate, manifestConfig); - if (allPeers.Count == 0) { - if (ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass, manifestForRooting)) { - logger.LogNoJavaPeerTypesFound (); - } - return new TrimmableTypeMapResult ([], [], allPeers); - } + bool hasPeers = allPeers.Count > 0; MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); bool validConstructors = ValidateConstructors (allPeers); + var manifestForRooting = PrepareManifestForRooting (manifestTemplate, manifestConfig); - RootCustomViewTypes (allPeers, customViewTypeNames); - RootManifestReferencedTypes (allPeers, manifestForRooting, manifestConfig?.ApplicationJavaClass); + var peersForApplicationRooting = allPeers; + if (preGeneratedFrameworkPeers is { Count: > 0 }) { + peersForApplicationRooting = new List (allPeers.Count + preGeneratedFrameworkPeers.Count); + peersForApplicationRooting.AddRange (allPeers); + peersForApplicationRooting.AddRange (preGeneratedFrameworkPeers); + } + RootCustomViewTypes (peersForApplicationRooting, customViewTypeNames, rootFrameworkPeersFromApplication); + RootManifestReferencedTypes ( + peersForApplicationRooting, + manifestForRooting, + manifestConfig?.ApplicationJavaClass, + rootFrameworkPeersFromApplication); PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - bool validJavaNames = ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass, manifestForRooting); + bool validJavaNames = ValidateJavaNames (allPeers, manifestConfig?.ApplicationJavaClass, manifestForRooting, preGeneratedJcwNames, preGeneratedJcwSource); if (!validConstructors || !validJavaNames) { return new TrimmableTypeMapResult ([], [], allPeers); } + if (!hasPeers) { + logger.LogNoJavaPeerTypesFound (); + } - var generatedAssemblies = generateTypeMapAssemblies + bool needsRootForPreGeneratedTypeMaps = + generateRootAssembly && + preGeneratedTypeMapNames is { Count: > 0 }; + var generatedAssemblies = generateTypeMapAssemblies && (hasPeers || needsRootForPreGeneratedTypeMaps) ? GenerateTypeMapAssemblies ( allPeers, systemRuntimeVersion, useSharedTypemapUniverse, shouldGenerateTypeMapAssembly, - includeBuiltInValueTypeUniverses) + includeBuiltInValueTypeUniverses, + generateRootAssembly, + preGeneratedTypeMapNames) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -103,7 +122,10 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } - internal static void RootCustomViewTypes (List allPeers, IReadOnlyCollection? customViewTypeNames) + internal static void RootCustomViewTypes ( + List allPeers, + IReadOnlyCollection? customViewTypeNames, + bool rootFrameworkPeers = true) { if (customViewTypeNames is null || customViewTypeNames.Count == 0) { return; @@ -111,6 +133,9 @@ internal static void RootCustomViewTypes (List allPeers, IReadOnly var names = new HashSet (customViewTypeNames, StringComparer.Ordinal); foreach (var peer in allPeers) { + if (!rootFrameworkPeers && peer.IsFrameworkAssembly) { + continue; + } if (names.Contains (peer.ManagedTypeName) || names.Contains (JniSignatureHelper.JniNameToJavaBinaryName (peer.JavaName)) || names.Contains (JniSignatureHelper.JniNameToJavaBinaryName (peer.CompatJniName))) { @@ -152,7 +177,9 @@ internal bool ValidateConstructors (IReadOnlyList peers) internal bool ValidateJavaNames ( IReadOnlyList peers, string? applicationJavaClass = null, - XDocument? manifest = null) + XDocument? manifest = null, + IReadOnlyCollection? preGeneratedJcwNames = null, + string? preGeneratedJcwSource = null) { bool valid = true; var reportedNames = new HashSet (StringComparer.Ordinal); @@ -161,7 +188,10 @@ internal bool ValidateJavaNames ( .GroupBy (peer => peer.JavaName, StringComparer.Ordinal) .OrderBy (group => group.Key, StringComparer.Ordinal)) { var firstAssemblyName = group.First ().AssemblyName; - if (group.All (peer => string.Equals (peer.AssemblyName, firstAssemblyName, StringComparison.Ordinal))) { + bool generatedByMultipleAssemblies = !group.All ( + peer => string.Equals (peer.AssemblyName, firstAssemblyName, StringComparison.Ordinal)); + bool conflictsWithPreGeneratedJcw = preGeneratedJcwNames?.Contains (group.Key) == true; + if (!generatedByMultipleAssemblies && !conflictsWithPreGeneratedJcw) { continue; } @@ -172,6 +202,9 @@ internal bool ValidateJavaNames ( .ThenBy (peer => peer.AssemblyName, StringComparer.Ordinal)) { logger.LogDuplicateJavaTypeDetailsError (javaName, $"{peer.ManagedTypeName}, {peer.AssemblyName}"); } + if (conflictsWithPreGeneratedJcw && preGeneratedJcwSource is { Length: > 0 }) { + logger.LogDuplicateJavaTypeDetailsError (javaName, preGeneratedJcwSource); + } valid = false; } if (applicationJavaClass is not null && @@ -413,8 +446,13 @@ internal List GenerateTypeMapAssemblies ( Version systemRuntimeVersion, bool useSharedTypemapUniverse, Func? shouldGenerateTypeMapAssembly = null, - bool includeBuiltInValueTypeUniverses = false) + bool includeBuiltInValueTypeUniverses = false, + bool generateRootAssembly = true, + IReadOnlyList? preGeneratedTypeMapNames = null) { + if (useSharedTypemapUniverse && preGeneratedTypeMapNames is { Count: > 0 }) { + throw new ArgumentException ("Pre-generated per-assembly type maps cannot be used in a shared universe.", nameof (preGeneratedTypeMapNames)); + } List<(string AssemblyName, List Peers)> peersByAssembly; if (useSharedTypemapUniverse) { @@ -456,24 +494,37 @@ internal List GenerateTypeMapAssemblies ( generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } - const string rootAssemblyName = "_Microsoft.Android.TypeMaps"; - bool generateRoot = true; - if (shouldGenerateTypeMapAssembly is not null) { - var rootFingerprint = MetadataHelper.ComputeRootIncrementalFingerprint ( - perAssemblyNames, - systemRuntimeVersion, - useSharedTypemapUniverse, - includeBuiltInValueTypeUniverses); - generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); - } - if (generateRoot) { - var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - var rootStream = rootGenerator.GenerateToStream ( - perAssemblyNames, - useSharedTypemapUniverse, - includeBuiltInValueTypeUniverses); - generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); - logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + // The root assembly (_Microsoft.Android.TypeMaps) carries the + // [assembly: TypeMapAssemblyTarget] attributes and TypeMapLoader.Initialize() that + // bind the per-assembly typemaps into universes at runtime. When pre-generating a + // framework typemap (e.g. Mono.Android) at SDK build time, the root is intentionally + // skipped: it is emitted by the app build, which references the pre-generated per-assembly + // typemap alongside the app's own. + if (generateRootAssembly) { + var rootTypeMapNames = new List (perAssemblyNames); + if (preGeneratedTypeMapNames is not null) { + rootTypeMapNames.AddRange (preGeneratedTypeMapNames); + } + rootTypeMapNames.Sort (StringComparer.Ordinal); + const string rootAssemblyName = "_Microsoft.Android.TypeMaps"; + bool generateRoot = true; + if (shouldGenerateTypeMapAssembly is not null) { + var rootFingerprint = MetadataHelper.ComputeRootIncrementalFingerprint ( + rootTypeMapNames, + systemRuntimeVersion, + useSharedTypemapUniverse, + includeBuiltInValueTypeUniverses); + generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); + } + if (generateRoot) { + var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + var rootStream = rootGenerator.GenerateToStream ( + rootTypeMapNames, + useSharedTypemapUniverse, + includeBuiltInValueTypeUniverses); + generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); + logger.LogGeneratedRootTypeMapInfo (rootTypeMapNames.Count); + } } logger.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; @@ -564,7 +615,11 @@ List GenerateJcwJavaSources (List allPeers, s return sources.ToList (); } - internal void RootManifestReferencedTypes (List allPeers, XDocument? doc, string? applicationJavaClass = null) + internal void RootManifestReferencedTypes ( + List allPeers, + XDocument? doc, + string? applicationJavaClass = null, + bool rootFrameworkPeers = true) { if (doc?.Root is not { } root) { return; @@ -620,6 +675,9 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen foreach (var name in componentNames) { if (peersByDotName.TryGetValue (name, out var peers)) { foreach (var peer in peers) { + if (!rootFrameworkPeers && peer.IsFrameworkAssembly) { + continue; + } if (deferredRegistrationNames.Contains (name)) { peer.CannotRegisterInStaticConstructor = true; } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index d10125e8890..db2ed8f5a2e 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -221,6 +221,7 @@ _ResolveAssemblies MSBuild target. ;_OuterOutputPath=$(OutputPath) ;_OuterIntermediateOutputPath=$(IntermediateOutputPath) ;_OuterCustomViewMapFile=$(_CustomViewMapFile) + ;_OuterPreGeneratedFrameworkTypeMapRootDescriptor=$(_PreGeneratedFrameworkTypeMapRootDescriptor) ;_AndroidNdkDirectory=$(_AndroidNdkDirectory) <_AndroidBuildRuntimeIdentifiersInParallel Condition=" '$(_AndroidBuildRuntimeIdentifiersInParallel)' == '' ">true diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index c10ae389cea..b0c4f409101 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -202,6 +202,10 @@ out-of-date and run it on every build. --> <_PostTrimTrimmableTypeMapInputAssemblies Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' and Exists('%(FullPath)') and ('%(RuntimeIdentifier)' == '' or '$(_PostTrimTypeMapFirstRuntimeIdentifier)' == '' or '%(RuntimeIdentifier)' == '$(_PostTrimTypeMapFirstRuntimeIdentifier)') " /> + <_PostTrimPreGeneratedTypeMapAssemblyInput Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' " + Include="@(_PostTrimTrimmableTypeMapInputAssemblies)" KeepDuplicates="false" /> + <_PostTrimPreGeneratedTypeMapAssemblyInput Remove="@(_PostTrimPreGeneratedTypeMapAssemblyInput)" + Condition=" '%(Filename)' != 'Mono.Android' and '%(Filename)' != 'Java.Interop' " /> @@ -243,11 +247,13 @@ DependsOnTargets="_ComputePostTrimTrimmableTypeMapInputs;_InvalidatePostTrimTrimmableTypeMapStampIfOutputsMissing" AfterTargets="_ResolveAssemblies" BeforeTargets="_GenerateJavaStubs;_CompileJava;_CompileToDalvik" - Inputs="@(_PostTrimTrimmableTypeMapInputAssemblies);$(_ResolvedUserAssembliesHashFile);$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects)" + Inputs="@(_PostTrimTrimmableTypeMapInputAssemblies);$(_ResolvedUserAssembliesHashFile);$(_AndroidBuildPropertiesCache);@(_AndroidMSBuildAllProjects);$(_PreGeneratedJcwValidationInput)" Outputs="$(_PostTrimTrimmableTypeMapJavaStamp)"> <_PostTrimTrimmableTypeMapInputAssemblies Remove="@(_PostTrimTrimmableTypeMapInputAssemblies)" /> + <_PostTrimPreGeneratedTypeMapAssemblyInput Remove="@(_PostTrimPreGeneratedTypeMapAssemblyInput)" /> <_PostTrimGeneratedJavaFiles Remove="@(_PostTrimGeneratedJavaFiles)" /> <_PostTrimDeletedJavaFiles Remove="@(_PostTrimDeletedJavaFiles)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 1117cd1dcc9..877d055fba6 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -84,6 +84,51 @@ <_AdditionalJavaStubDirectory Include="$([MSBuild]::EnsureTrailingSlash('$(_TypeMapJavaStubsSourceDirectory)'))" /> + + + <_PreGeneratedTypeMapDir>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)..\data\prebuilt-typemap')) + <_PreGeneratedMonoAndroidTypeMapAssembly>$(_PreGeneratedTypeMapDir)_Mono.Android.TypeMap.dll + <_PreGeneratedJavaInteropTypeMapAssembly>$(_PreGeneratedTypeMapDir)_Java.Interop.TypeMap.dll + <_PreGeneratedMonoAndroidTypeMapJar>$(_PreGeneratedTypeMapDir)mono.android-typemaps.jar + <_PreGeneratedMonoAndroidTypeMapAcwMap>$(_PreGeneratedTypeMapDir)mono.android-acw-map.txt + + + + + <_AndroidPreGeneratedTypeMapMatchesApi>false + <_AndroidPreGeneratedTypeMapMatchesApi Condition=" '$(_AndroidPreGeneratedTypeMapApiLevel)' != '' and '$(TargetPlatformVersion)' != '' ">$([MSBuild]::VersionEquals('$(_AndroidPreGeneratedTypeMapApiLevel)', '$(TargetPlatformVersion)')) + <_AndroidPreGeneratedTypeMapEnabled>false + <_AndroidPreGeneratedTypeMapEnabled Condition=" '$(_AndroidUsePreGeneratedMonoAndroidTypeMap)' != 'false' and '$(AndroidIncludeDebugSymbols)' == 'true' and '$(_AndroidPreGeneratedTypeMapMatchesApi)' == 'true' and Exists('$(_PreGeneratedMonoAndroidTypeMapAssembly)') and Exists('$(_PreGeneratedJavaInteropTypeMapAssembly)') and Exists('$(_PreGeneratedMonoAndroidTypeMapJar)') and Exists('$(_PreGeneratedMonoAndroidTypeMapAcwMap)') ">true + <_PreGeneratedJcwValidationInput Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' ">$(_PreGeneratedMonoAndroidTypeMapJar) + <_RootFrameworkTypeMapPeersFromApplication Condition=" '$(AndroidLinkMode)' != 'None' or '$(PublishTrimmed)' == 'true' or '$(PublishAot)' == 'true' ">true + <_RootFrameworkTypeMapPeersFromApplication Condition=" '$(_RootFrameworkTypeMapPeersFromApplication)' == '' ">false + <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_ResolveTrimmableTypeMapKeepRulesAcwMap;_MergePreGeneratedMonoAndroidAcwMapForKeepRules + + + + + <_PreGeneratedFrameworkTypeMapRootDescriptor + Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' and '$(_RootFrameworkTypeMapPeersFromApplication)' == 'true' ">$(IntermediateOutputPath)prebuilt-typemap-roots.xml + <_PreGeneratedFrameworkTypeMapAcwInput + Condition=" '$(_PreGeneratedFrameworkTypeMapRootDescriptor)' != '' ">$(_PreGeneratedMonoAndroidTypeMapAcwMap) + + + + + + @@ -99,6 +144,8 @@ Condition=" '$(_AndroidRuntime)' == 'CoreCLR' " /> + <_PropertyCacheItems Include="TrimmableTypeMapRuntimeIdentifiers=$(_TrimmableTypeMapEffectiveRuntimeIdentifiers)" /> + <_PropertyCacheItems Include="UsePreGeneratedFrameworkTypeMaps=$(_AndroidPreGeneratedTypeMapEnabled)" /> @@ -195,8 +243,8 @@ @@ -205,9 +253,10 @@ + <_TypeMapInputAssemblies Include="@(ReferencePath)" /> - <_TypeMapInputAssemblies Include="@(ResolvedAssemblies)" /> - <_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" /> <_TypeMapInputAssemblies Include="@(PrivateSdkAssemblies)" /> <_TypeMapInputAssemblies Include="@(FrameworkAssemblies)" /> + <_PreGeneratedTypeMapAssemblyInput Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' " + Include="@(_TypeMapInputAssemblies)" KeepDuplicates="false" /> + <_PreGeneratedTypeMapAssemblyInput Remove="@(_PreGeneratedTypeMapAssemblyInput)" + Condition=" '%(Filename)' != 'Mono.Android' and '%(Filename)' != 'Java.Interop' " /> + + @@ -310,6 +373,51 @@ + + + <_GeneratedTypeMapAssembliesFromList Include="$(_PreGeneratedTypeMapDir)*.TypeMap.dll" /> + + + + + + + + + + + <_TrimmableTypeMapKeepRulesAcwMapFile>$(_AcwMapFile) + <_TrimmableTypeMapKeepRulesAcwMapFile Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' ">$(IntermediateOutputPath)acw-map.prebuilt-merged.txt + + + + + + + <_MergedKeepRulesAcwMapLines Remove="@(_MergedKeepRulesAcwMapLines)" /> + + + + + + + + + + + + Condition=" '$(_OuterIntermediateOutputPath)' == '' and Exists('$(_TypeMapAssembliesListFile)') " + DependsOnTargets="_ResolveTrimmableTypeMapKeepRulesAcwMap;_ResolvePreGeneratedFrameworkTypeMapRoots"> <_GeneratedTypeMapAssembliesForFileWrites Remove="@(_GeneratedTypeMapAssembliesForFileWrites)" /> <_TypeMapJavaFilesForFileWrites Remove="@(_TypeMapJavaFilesForFileWrites)" /> @@ -364,7 +473,9 @@ - + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 9b6dfff193f..40bd477eb03 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; using System.Security; using System.Security.Cryptography; using System.Text; @@ -115,13 +117,240 @@ public void Build_WithTrimmableTypeMap_Succeeds ( Assert.IsTrue (builder.Build (proj, parameters: parameters), "Build should use the trimmable type map regardless of the obsolete property."); var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); - AssertTrimmableTypeMapOutputs (intermediateDir); + AssertTrimmableTypeMapOutputs (intermediateDir, usePreGeneratedFrameworkTypeMaps: !isRelease); if (!isRelease) { - var frameworkJcw = Path.Combine (intermediateDir, "java", "android", "app", "ActivityTracker.java"); - FileAssert.Exists (frameworkJcw); - var source = File.ReadAllText (frameworkJcw); - StringAssert.Contains ("mono.android.Runtime.registerNatives (ActivityTracker.class)", source); - StringAssert.DoesNotContain ("mono.android.TypeManager.Activate", source); + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + FileAssert.Exists (dexFile); + Assert.IsTrue ( + DexUtils.ContainsClassWithMethod ("Lmono/android/view/View_OnClickListenerImplementor;", "", "()V", dexFile, AndroidSdkPath), + $"`{dexFile}` should include the pre-generated framework listener implementors."); + } + } + + [TestCase (AndroidRuntime.CoreCLR)] + [TestCase (AndroidRuntime.NativeAOT)] + public void R8WithPreGeneratedTypeMapKeepsFrameworkJcws (AndroidRuntime runtime) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetProperty (KnownProperties.AndroidLinkTool, "r8"); + if (runtime == AndroidRuntime.CoreCLR) { + proj.SetProperty ("PublishTrimmed", "false"); + } else { + proj.SetProperty ("DebugSymbols", "true"); + proj.SetProperty ("DebugType", "portable"); + proj.SetProperty ("EmbedAssembliesIntoApk", "false"); + proj.SetProperty ("_AndroidEnableTypemapR8Trimming", "false"); + } + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var mergedAcwMap = builder.Output.GetIntermediaryPath ("acw-map.prebuilt-merged.txt"); + var intermediateDir = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + var proguardConfigurations = runtime == AndroidRuntime.NativeAOT + ? Directory.GetFiles (intermediateDir, "proguard_project_references.cfg", SearchOption.AllDirectories) + : [builder.Output.GetIntermediaryPath (Path.Combine ("proguard", "proguard_project_primary.cfg"))]; + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + const string frameworkJcw = "mono.android.view.View_OnClickListenerImplementor"; + + AssertFileContains (mergedAcwMap, frameworkJcw, expected: true, "merged framework ACW map"); + Assert.IsNotEmpty (proguardConfigurations, "R8 configuration should exist."); + foreach (var proguardConfiguration in proguardConfigurations) { + AssertFileContains (proguardConfiguration, frameworkJcw, expected: true, "R8 configuration"); + } + Assert.IsTrue ( + DexUtils.ContainsClassWithMethod ($"L{frameworkJcw.Replace ('.', '/')};", "", "()V", dexFile, AndroidSdkPath), + $"`{dexFile}` should retain the pre-generated framework listener implementor when R8 shrinking is enabled."); + + Assert.IsTrue (builder.Build (proj), "Incremental build should have succeeded."); + Assert.IsTrue (builder.Output.IsTargetSkipped ("_CompileToDalvik"), "_CompileToDalvik should be up to date."); + FileAssert.Exists (mergedAcwMap, "IncrementalClean must preserve the merged framework ACW map."); + } + + [Test] + public void LowercaseCrc64UsesPreGeneratedFrameworkTypeMaps () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("AndroidPackageNamingPolicy", "LowercaseCrc64"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var typemapDir = builder.Output.GetIntermediaryPath ("typemap"); + FileAssert.DoesNotExist ( + Path.Combine (typemapDir, "_Mono.Android.TypeMap.dll"), + "Mono.Android's pre-generated map has no package-policy-dependent peer names."); + FileAssert.DoesNotExist ( + Path.Combine (typemapDir, "_Java.Interop.TypeMap.dll"), + "Java.Interop's pre-generated map has no package-policy-dependent peer names."); + } + + [TestCase ("Crc64")] + [TestCase ("LowercaseCrc64")] + public void PreGeneratedFrameworkTypeMapsMatchAppGeneratedMaps (string packageNamingPolicy) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("AndroidPackageNamingPolicy", packageNamingPolicy); + proj.SetProperty ("_AndroidUsePreGeneratedMonoAndroidTypeMap", "false"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var generatedTypeMapDir = builder.Output.GetIntermediaryPath ("typemap"); + var frameworkJcw = Path.Combine (generatedTypeMapDir, "java", "android", "app", "ActivityTracker.java"); + FileAssert.Exists (frameworkJcw); + var source = File.ReadAllText (frameworkJcw); + StringAssert.Contains ("mono.android.Runtime.registerNatives (ActivityTracker.class)", source); + StringAssert.DoesNotContain ("mono.android.TypeManager.Activate", source); + var preGeneratedTypeMapDir = Path.Combine ( + TestEnvironment.DotNetPreviewAndroidSdkDirectory, + "data", + "prebuilt-typemap"); + foreach (var fileName in new [] { + "_Mono.Android.TypeMap.dll", + "_Java.Interop.TypeMap.dll", + }) { + var preGenerated = Path.Combine (preGeneratedTypeMapDir, fileName); + var generated = Path.Combine (generatedTypeMapDir, fileName); + FileAssert.Exists (preGenerated); + FileAssert.Exists (generated); + AssertTypeMapContentsEqual (generated, preGenerated, fileName); + } + } + + [TestCase ("37.1")] + [TestCase ("37.2")] + public void DifferentApiPackGeneratesFrameworkTypeMaps (string apiLevel) + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + TargetFramework = $"{XABuildConfig.LatestDotNetTargetFramework}-android{apiLevel}", + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "A different API pack should fall back to framework typemap generation."); + AssertTrimmableTypeMapOutputs (builder.Output.GetIntermediaryPath ("typemap"), usePreGeneratedFrameworkTypeMaps: false); + FileAssert.DoesNotExist (builder.Output.GetIntermediaryPath ("acw-map.prebuilt-merged.txt"), + "An incompatible API pack must not consume the SDK's framework wrappers."); + } + + [Test] + public void SwitchingPreGeneratedFrameworkTypeMapsInvalidatesGeneration () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidApplicationProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Initial prebuilt typemap build should succeed."); + var typemapDir = builder.Output.GetIntermediaryPath ("typemap"); + AssertTrimmableTypeMapOutputs (typemapDir, usePreGeneratedFrameworkTypeMaps: true); + + proj.SetProperty ("_AndroidUsePreGeneratedMonoAndroidTypeMap", "false"); + Assert.IsTrue (builder.Build (proj), "Switching to app-generated framework maps should succeed."); + Assert.IsFalse (builder.Output.IsTargetSkipped ("_GenerateTrimmableTypeMap"), + "Changing reuse eligibility must invalidate the typemap generation stamp."); + AssertTrimmableTypeMapOutputs (typemapDir, usePreGeneratedFrameworkTypeMaps: false); + } + + [Test] + public void TrimmedBuildWithDebugSymbolsUsesPreGeneratedTypeMap () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("DebugSymbols", "true"); + proj.SetProperty ("DebugType", "portable"); + proj.SetProperty ("Optimize", "false"); + proj.SetProperty ("AndroidLinkMode", "Full"); + proj.SetProperty ("PublishTrimmed", "true"); + proj.AndroidManifest = proj.AndroidManifest.Replace ( + "", + ""); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var typemapDir = builder.Output.GetIntermediaryPath ("typemap"); + AssertTrimmableTypeMapOutputs (typemapDir, usePreGeneratedFrameworkTypeMaps: true); + FileAssert.Exists ( + builder.Output.GetIntermediaryPath ("acw-map.prebuilt-merged.txt"), + "Per-assembly universes should consume the pre-generated framework artifacts even when trimming."); + using var monoAndroid = AssemblyDefinition.ReadAssembly (BuildTest.GetLinkedPath (builder, true, "Mono.Android.dll")); + Assert.IsNotNull ( + monoAndroid.MainModule.GetType ("Android.App.NativeActivity"), + "Framework types referenced only from AndroidManifest.xml must remain available to conditional pre-generated typemap entries."); + using var monoAndroidTypeMap = AssemblyDefinition.ReadAssembly (BuildTest.GetLinkedPath (builder, true, "_Mono.Android.TypeMap.dll")); + Assert.IsTrue ( + monoAndroidTypeMap.CustomAttributes.Any (attribute => + attribute.AttributeType.Namespace == "System.Runtime.InteropServices" && + attribute.AttributeType.Name.StartsWith ("TypeMapAttribute", StringComparison.Ordinal) && + attribute.ConstructorArguments.Count > 0 && + attribute.ConstructorArguments [0].Value as string == "android/app/NativeActivity"), + "The conditional pre-generated typemap entry for the manifest-rooted framework type must survive trimming."); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "No-op build should have succeeded."); + Assert.IsTrue ( + builder.Output.IsTargetSkipped ("_GenerateTrimmableTypeMap"), + "Per-RID inner builds must not invalidate the outer typemap generation stamp."); + } + + [Test] + public void DebugTrimmableTypeMapLibrary_DoesNotPackageFrameworkJavaWrappers () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: false)) { + return; + } + + var proj = new XamarinAndroidLibraryProject (); + proj.SetRuntime (AndroidRuntime.CoreCLR); + + using var builder = CreateDllBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var aarPath = Path.Combine (Root, builder.ProjectDirectory, proj.OutputPath, $"{proj.ProjectName}.aar"); + FileAssert.Exists (aarPath); + using var aar = ZipHelper.OpenZip (aarPath); + foreach (var entry in aar.Entries.Where (entry => entry.FullName.StartsWith ("libs/", StringComparison.Ordinal) && entry.FullName.EndsWith (".jar", StringComparison.Ordinal))) { + using var stream = new MemoryStream (); + using (var source = entry.Open ()) { + source.CopyTo (stream); + } + stream.Position = 0; + using var jar = new System.IO.Compression.ZipArchive (stream, System.IO.Compression.ZipArchiveMode.Read); + Assert.IsNull ( + jar.GetEntry ("android/runtime/JavaProxyThrowable.class"), + $"{aarPath}!/{entry.FullName} should not redistribute the SDK's pre-generated framework wrappers."); } } @@ -152,7 +381,7 @@ public class CollectionExport : Java.Lang.Object Assert.IsTrue (builder.Build (proj), "Build with an IList export should have succeeded."); var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); - AssertTrimmableTypeMapOutputs (intermediateDir); + AssertTrimmableTypeMapOutputs (intermediateDir, usePreGeneratedFrameworkTypeMaps: !isRelease); } [Test] @@ -1543,12 +1772,19 @@ public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () }; proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty (KnownProperties.PublishAot, "true"); + proj.AndroidManifest = proj.AndroidManifest.Replace ( + "", + ""); using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); - AssertTrimmableTypeMapOutputs (intermediateDir); + AssertTrimmableTypeMapOutputs (intermediateDir, usePreGeneratedFrameworkTypeMaps: true); + StringAssert.Contains ( + "Android.App.NativeActivity", + File.ReadAllText (builder.Output.GetIntermediaryPath ("prebuilt-typemap-roots.xml")), + "PublishAot must root manifest-referenced framework types even when AndroidLinkMode is None."); } [Test] @@ -1570,7 +1806,7 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([ValueSource (typeof (B Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); - AssertTrimmableTypeMapOutputs (intermediateDir); + AssertTrimmableTypeMapOutputs (intermediateDir, usePreGeneratedFrameworkTypeMaps: !isRelease); var typemapDlls = Directory.GetFiles (intermediateDir, "*.dll"); Assert.IsNotEmpty (typemapDlls, "First build should have generated typemap DLL(s)."); var typemapFingerprints = Path.Combine (intermediateDir, "typemap-fingerprints.txt"); @@ -2198,16 +2434,17 @@ public void NativeAotTrimmableTypeMap_DoesNotExportFrameworkTypeMaps () StringAssert.Contains ($"--generateunmanagedentrypoints:_{proj.ProjectName}.TypeMap", rspText); } - [Test] - public void TrimmableTypeMap_PackagesAndroidJavaProxyThrowable ( - [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) + [TestCase (false, AndroidRuntime.CoreCLR)] + [TestCase (true, AndroidRuntime.CoreCLR)] + [TestCase (true, AndroidRuntime.NativeAOT)] + public void TrimmableTypeMap_PackagesAndroidJavaProxyThrowable (bool isRelease, AndroidRuntime runtime) { - if (IgnoreUnsupportedConfiguration (runtime, release: true)) { + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } var proj = new XamarinAndroidApplicationProject { - IsRelease = true, + IsRelease = isRelease, }; proj.SetRuntime (runtime); @@ -3071,11 +3308,29 @@ static void AssertNoUnicodeOutputs (ProjectBuilder builder, string memberName) ); } - static void AssertTrimmableTypeMapOutputs (string typemapDir) + static void AssertTrimmableTypeMapOutputs (string typemapDir, bool usePreGeneratedFrameworkTypeMaps) { DirectoryAssert.Exists (typemapDir); FileAssert.Exists (Path.Combine (typemapDir, "_Microsoft.Android.TypeMaps.dll")); - FileAssert.Exists (Path.Combine (typemapDir, "_Mono.Android.TypeMap.dll")); + + var generatedAssemblies = File.ReadAllLines (Path.Combine (typemapDir, "typemap-assemblies.txt")) + .Select (Path.GetFileName) + .ToArray (); + if (!usePreGeneratedFrameworkTypeMaps) { + FileAssert.Exists (Path.Combine (typemapDir, "_Mono.Android.TypeMap.dll"), + "Builds that cannot use the pre-generated framework maps must generate Mono.Android's typemap."); + FileAssert.Exists (Path.Combine (typemapDir, "_Java.Interop.TypeMap.dll"), + "Builds that cannot use the pre-generated framework maps must generate Java.Interop's typemap."); + CollectionAssert.Contains (generatedAssemblies, "_Mono.Android.TypeMap.dll"); + CollectionAssert.Contains (generatedAssemblies, "_Java.Interop.TypeMap.dll"); + } else { + FileAssert.DoesNotExist (Path.Combine (typemapDir, "_Mono.Android.TypeMap.dll"), + "Per-assembly universe builds should use the typemap pre-generated in the SDK pack."); + FileAssert.DoesNotExist (Path.Combine (typemapDir, "_Java.Interop.TypeMap.dll"), + "Per-assembly universe builds should use the typemap pre-generated in the SDK pack."); + CollectionAssert.DoesNotContain (generatedAssemblies, "_Mono.Android.TypeMap.dll"); + CollectionAssert.DoesNotContain (generatedAssemblies, "_Java.Interop.TypeMap.dll"); + } var javaDir = Path.Combine (typemapDir, "java"); DirectoryAssert.Exists (javaDir, "Trimmable JCW Java output directory should exist."); @@ -3094,6 +3349,48 @@ static bool AssemblyContainsType (string assemblyPath, string typeFullName) return ContainsType (assembly.MainModule.Types, typeFullName); } + static byte [] NormalizeModuleIdentity (string assemblyPath) + { + var content = File.ReadAllBytes (assemblyPath); + using var pe = new PEReader (new MemoryStream (content, writable: false)); + var reader = System.Reflection.Metadata.PEReaderExtensions.GetMetadataReader (pe); + var mvidIndex = MetadataTokens.GetHeapOffset (reader.GetModuleDefinition ().Mvid); + var mvidOffset = pe.PEHeaders.MetadataStartOffset + + reader.GetHeapMetadataOffset (HeapIndex.Guid) + + (mvidIndex - 1) * 16; + Array.Clear (content, mvidOffset, 16); + Array.Clear (content, pe.PEHeaders.CoffHeaderStartOffset + 4, 4); + return content; + } + + static void AssertTypeMapContentsEqual (string generatedPath, string preGeneratedPath, string fileName) + { + var generated = NormalizeModuleIdentity (generatedPath); + var preGenerated = NormalizeModuleIdentity (preGeneratedPath); + var commonLength = Math.Min (generated.Length, preGenerated.Length); + int firstDifference = 0; + while (firstDifference < commonLength && generated [firstDifference] == preGenerated [firstDifference]) { + firstDifference++; + } + if (firstDifference == generated.Length && firstDifference == preGenerated.Length) { + return; + } + + Assert.Fail ( + $"{fileName} should have identical metadata and IL whether generated by the SDK or the app build. " + + $"First difference at 0x{firstDifference:X}; generated length: {generated.Length}, " + + $"pre-generated length: {preGenerated.Length}; generated: {FormatHexWindow (generated, firstDifference)}; " + + $"pre-generated: {FormatHexWindow (preGenerated, firstDifference)}."); + } + + static string FormatHexWindow (byte [] content, int offset) + { + const int radius = 8; + var start = Math.Max (0, offset - radius); + var count = Math.Min (content.Length - start, radius * 2 + 1); + return count == 0 ? "" : $"{start:X}: {BitConverter.ToString (content, start, count)}"; + } + static bool ContainsType (IEnumerable types, string typeFullName) { foreach (var type in types) { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets index ed2846846a1..33b8ebec834 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.D8.targets @@ -17,9 +17,17 @@ Copyright (C) 2018 Xamarin. All rights reserved. + + + <_R8AcwMapFile>$(_AcwMapFile) + <_R8AcwMapFile Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' ">$(_TrimmableTypeMapKeepRulesAcwMapFile) + <_R8AcwMapInput Condition=" '$(_AndroidPreGeneratedTypeMapEnabled)' == 'true' ">$(_R8AcwMapFile) + + + @@ -72,7 +80,7 @@ Copyright (C) 2018 Xamarin. All rights reserved. JavaLibrariesToReference="@(AndroidExternalJavaLibrary)" EnableDesugar="$(AndroidEnableDesugar)" AndroidSdkBuildToolsPath="$(AndroidSdkBuildToolsPath)" - AcwMapFile="$(_AcwMapFile)" + AcwMapFile="$(_R8AcwMapFile)" JavaSourceFiles="@(_R8KeepJavaSource)" ProguardCommonXamarinConfiguration="$([System.IO.Path]::Combine('$(IntermediateOutputPath)', 'proguard', 'proguard_xamarin.cfg'))" ProguardGeneratedReferenceConfiguration="$(_ProguardProjectConfiguration)" diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 82eca1db65f..a53fbabaf70 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -65,7 +65,18 @@ public void DotNetRun (bool isRelease, AndroidRuntime runtime) var dotnet = new DotNetCLI (Path.Combine (Root, builder.ProjectDirectory, proj.ProjectFilePath)); Assert.IsTrue (dotnet.Build (), "`dotnet build` should succeed"); - Assert.IsTrue (dotnet.Run (), "`dotnet run --no-build` should succeed"); + var manifestPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "android", "AndroidManifest.xml"); + var manifest = File.ReadAllText (manifestPath); + string [] runParameters = []; + var device = Environment.GetEnvironmentVariable ("ANDROID_SERIAL"); + if (!string.IsNullOrEmpty (device)) { + runParameters = ["--device", device]; + } + Assert.IsTrue (dotnet.Run (parameters: runParameters), "`dotnet run --no-build` should succeed"); + Assert.AreEqual (manifest, File.ReadAllText (manifestPath), + "`dotnet run --no-build` must preserve the generated manifest, including the launchable activity."); + Assert.IsFalse (File.ReadLines (dotnet.ProcessLogFile).Any (line => line.Contains ("XA4257")), + "`dotnet run --no-build` must not skip Java peers by scanning trimmed assemblies as pre-trim inputs."); bool didLaunch = WaitForActivityToStart (proj.PackageName, "MainActivity", Path.Combine (Root, builder.ProjectDirectory, "logcat.log"), ActivityStartTimeoutInSeconds);