Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 578
[TrimmableTypeMap] Generate per-assembly acw-map.txt from scanner results#10959
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
ef9bcb8c0fb36c0d2d9086a1d6da27c0186File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| #nullable enable | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using System.Linq; | ||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap; | ||
| /// <summary> | ||
| /// Generates per-assembly acw-map.txt content from <see cref="JavaPeerInfo"/> records. | ||
| /// The acw-map.txt file maps managed type names to Java/ACW type names, consumed by | ||
| /// <c>_ConvertCustomView</c> to fix up custom view names in layout XMLs. | ||
| /// | ||
| /// Format per type (3 lines): | ||
| /// Line 1: PartialAssemblyQualifiedName;JavaKey (always written) | ||
| /// Line 2: ManagedKey;JavaKey | ||
| /// Line 3: CompatJniName;JavaKey | ||
| /// | ||
| /// Java keys use dots (not slashes): e.g., "android.app.Activity" | ||
| /// </summary> | ||
| public static class AcwMapWriter | ||
| { | ||
| /// <summary> | ||
| /// Writes acw-map lines for the given <paramref name="peers"/> to the <paramref name="writer"/>. | ||
| /// Per-assembly maps write all 3 line variants unconditionally. No conflict detection | ||
| /// is performed — the merged acw-map.txt is a simple concatenation consumed by | ||
| /// LoadMapFile which uses first-entry-wins semantics for duplicate keys. | ||
| /// </summary> | ||
| public static void Write (TextWriter writer, IEnumerable<JavaPeerInfo> peers) | ||
| { | ||
| foreach (var peer in peers.OrderBy (p => p.ManagedTypeName, StringComparer.Ordinal)) { | ||
| string javaKey = peer.JavaName.Replace ('/', '.'); | ||
| string managedKey = peer.ManagedTypeName; | ||
| string partialAsmQualifiedName = $"{managedKey}, {peer.AssemblyName}"; | ||
| string compatJniName = peer.CompatJniName.Replace ('/', '.'); | ||
| // Line 1: PartialAssemblyQualifiedName;JavaKey | ||
| writer.Write (partialAsmQualifiedName); | ||
| writer.Write (';'); | ||
| writer.WriteLine (javaKey); | ||
| // Line 2: ManagedKey;JavaKey | ||
| writer.Write (managedKey); | ||
| writer.Write (';'); | ||
| writer.WriteLine (javaKey); | ||
| // Line 3: CompatJniName;JavaKey | ||
| writer.Write (compatJniName); | ||
| writer.Write (';'); | ||
| writer.WriteLine (javaKey); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -12,9 +12,9 @@ | ||
| namespace Xamarin.Android.Tasks; | ||
| /// <summary> | ||
| /// Generates trimmable TypeMap assemblies and JCW Java source files from resolved assemblies. | ||
| /// Runs before the trimmer to produce per-assembly typemap .dll files and a root | ||
| /// _Microsoft.Android.TypeMaps.dll, plus .java files for ACW types with registerNatives. | ||
| /// Generates trimmable TypeMap assemblies, JCW Java source files, and per-assembly | ||
| /// acw-map files from resolved assemblies. The acw-map files are later merged into | ||
| /// a single acw-map.txt consumed by _ConvertCustomView for layout XML fixups. | ||
| /// </summary> | ||
| public class GenerateTrimmableTypeMap : AndroidTask | ||
| { | ||
| @@ -29,6 +29,12 @@ public class GenerateTrimmableTypeMap : AndroidTask | ||
| [Required] | ||
| public string JavaSourceOutputDirectory { get; set; } = ""; | ||
| /// <summary> | ||
| /// Directory for per-assembly acw-map.{AssemblyName}.txt files. | ||
| /// </summary> | ||
| [Required] | ||
simonrozsival marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| public string AcwMapDirectory { get; set; } = ""; | ||
| /// <summary> | ||
| /// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime | ||
| /// assembly reference version in generated typemap assemblies. | ||
| @@ -42,13 +48,22 @@ public class GenerateTrimmableTypeMap : AndroidTask | ||
| [Output] | ||
| public ITaskItem []? GeneratedJavaFiles { get; set; } | ||
| /// <summary> | ||
| /// Per-assembly acw-map files produced during scanning. Each file contains | ||
| /// three lines per type: PartialAssemblyQualifiedName;JavaKey, | ||
| /// ManagedKey;JavaKey, and CompatJniName;JavaKey. | ||
| /// </summary> | ||
| [Output] | ||
| public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } | ||
| public override bool RunTask () | ||
| { | ||
| var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); | ||
| var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies); | ||
| Directory.CreateDirectory (OutputDirectory); | ||
| Directory.CreateDirectory (JavaSourceOutputDirectory); | ||
| Directory.CreateDirectory (AcwMapDirectory); | ||
| var allPeers = ScanAssemblies (assemblyPaths); | ||
| if (allPeers.Count == 0) { | ||
| @@ -58,6 +73,7 @@ public override bool RunTask () | ||
| GeneratedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths); | ||
| GeneratedJavaFiles = GenerateJcwJavaSources (allPeers); | ||
| PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (allPeers); | ||
| return !Log.HasLoggedErrors; | ||
| } | ||
| @@ -153,6 +169,38 @@ ITaskItem [] GenerateJcwJavaSources (List<JavaPeerInfo> allPeers) | ||
| return items; | ||
| } | ||
| ITaskItem [] GeneratePerAssemblyAcwMaps (List<JavaPeerInfo> allPeers) | ||
| { | ||
| var peersByAssembly = allPeers | ||
| .GroupBy (p => p.AssemblyName, StringComparer.Ordinal) | ||
| .OrderBy (g => g.Key, StringComparer.Ordinal); | ||
| var outputFiles = new List<ITaskItem> (); | ||
| foreach (var group in peersByAssembly) { | ||
| var peers = group.ToList (); | ||
| string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt"); | ||
| bool written; | ||
| using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { | ||
| AcwMapWriter.Write (sw, peers); | ||
| sw.Flush (); | ||
| written = Files.CopyIfStreamChanged (sw.BaseStream, outputFile); | ||
| } | ||
| Log.LogDebugMessage (written | ||
| ? $" acw-map.{group.Key}.txt: {peers.Count} types" | ||
| : $" acw-map.{group.Key}.txt: unchanged"); | ||
| var item = new TaskItem (outputFile); | ||
| item.SetMetadata ("AssemblyName", group.Key); | ||
| outputFiles.Add (item); | ||
| } | ||
| Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files."); | ||
| return outputFiles.ToArray (); | ||
| } | ||
| static Version ParseTargetFrameworkVersion (string tfv) | ||
| { | ||
| if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| using System; | ||
| using System.Collections.Generic; | ||
| using System.IO; | ||
| using Xunit; | ||
| namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; | ||
| public class AcwMapWriterTests : FixtureTestBase | ||
| { | ||
| static string [] WriteLines (IEnumerable<JavaPeerInfo> peers) | ||
| { | ||
| using var writer = new StringWriter (); | ||
| AcwMapWriter.Write (writer, peers); | ||
| var output = writer.ToString ().TrimEnd (); | ||
| if (output.Length == 0) { | ||
| return Array.Empty<string> (); | ||
MemberAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 💡 Formatting — Use Rule: Use | ||
| } | ||
| return output.Split (new [] { Environment.NewLine }, StringSplitOptions.None); | ||
| } | ||
| [Fact] | ||
| public void Write_SingleMcwType_ProducesThreeLines () | ||
| { | ||
| var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"); | ||
| var lines = WriteLines (new [] { peer }); | ||
| Assert.Equal (3, lines.Length); | ||
| // Line 1: PartialAssemblyQualifiedName;JavaKey | ||
| Assert.Equal ("Android.App.Activity, Mono.Android;android.app.Activity", lines [0]); | ||
| // Line 2: ManagedKey;JavaKey | ||
| Assert.Equal ("Android.App.Activity;android.app.Activity", lines [1]); | ||
| // Line 3: CompatJniName;JavaKey | ||
| Assert.Equal ("android.app.Activity;android.app.Activity", lines [2]); | ||
| } | ||
| [Fact] | ||
| public void Write_UserType_SlashesConvertedToDots () | ||
| { | ||
| var peer = new JavaPeerInfo { | ||
| JavaName = "crc64abcdef/MyActivity", | ||
| CompatJniName = "my.namespace/MyActivity", | ||
| ManagedTypeName = "My.Namespace.MyActivity", | ||
| ManagedTypeNamespace = "My.Namespace", | ||
| ManagedTypeShortName = "MyActivity", | ||
| AssemblyName = "MyApp", | ||
| }; | ||
| var lines = WriteLines (new [] { peer }); | ||
| Assert.Equal (3, lines.Length); | ||
| Assert.Equal ("My.Namespace.MyActivity, MyApp;crc64abcdef.MyActivity", lines [0]); | ||
| Assert.Equal ("My.Namespace.MyActivity;crc64abcdef.MyActivity", lines [1]); | ||
| Assert.Equal ("my.namespace.MyActivity;crc64abcdef.MyActivity", lines [2]); | ||
| } | ||
| [Fact] | ||
| public void Write_MultipleTypes_OrderedByManagedName () | ||
| { | ||
| var peers = new [] { | ||
| MakeMcwPeer ("android/widget/TextView", "Android.Widget.TextView", "Mono.Android"), | ||
| MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"), | ||
| MakeMcwPeer ("android/content/Context", "Android.Content.Context", "Mono.Android"), | ||
| }; | ||
| var lines = WriteLines (peers); | ||
| // 3 types × 3 lines each = 9 lines | ||
| Assert.Equal (9, lines.Length); | ||
| // First type alphabetically: Android.App.Activity | ||
| Assert.StartsWith ("Android.App.Activity, Mono.Android;", lines [0]); | ||
| // Second: Android.Content.Context | ||
| Assert.StartsWith ("Android.Content.Context, Mono.Android;", lines [3]); | ||
| // Third: Android.Widget.TextView | ||
| Assert.StartsWith ("Android.Widget.TextView, Mono.Android;", lines [6]); | ||
| } | ||
| [Fact] | ||
| public void Write_EmptyList_ProducesEmptyOutput () | ||
| { | ||
| var lines = WriteLines (Array.Empty<JavaPeerInfo> ()); | ||
| Assert.Empty (lines); | ||
| } | ||
| [Fact] | ||
| public void Write_MatchesExpectedAcwMapFormat () | ||
| { | ||
| // Verify the format matches what LoadMapFile expects: | ||
| // each line is "key;value" where LoadMapFile splits on ';' | ||
| var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"); | ||
| var lines = WriteLines (new [] { peer }); | ||
| foreach (var line in lines) { | ||
| var parts = line.Split (new [] { ';' }, count: 2); | ||
| Assert.Equal (2, parts.Length); | ||
| Assert.False (string.IsNullOrWhiteSpace (parts [0]), "Key should not be empty"); | ||
| Assert.False (string.IsNullOrWhiteSpace (parts [1]), "Value should not be empty"); | ||
| } | ||
| } | ||
| [Fact] | ||
| public void Write_FromScannedFixtures_ProducesValidOutput () | ||
| { | ||
| var peers = ScanFixtures (); | ||
| Assert.NotEmpty (peers); | ||
| var lines = WriteLines (peers); | ||
| foreach (var line in lines) { | ||
| var parts = line.Split (new [] { ';' }, count: 2); | ||
| Assert.Equal (2, parts.Length); | ||
| // No slashes in the output — they should all be converted to dots | ||
| Assert.DoesNotContain ("/", parts [1]); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.