Skip to content

Commit 39fc4ab

Browse files
[TrimmableTypeMap] Generate per-assembly acw-map.txt from scanner results
Part of #10807. Stacked on #10924. The trimmable typemap path needs acw-map.txt for _ConvertCustomView to fix up custom view names in layout XMLs. This adds acw-map generation as a side-output of GenerateTrimmableTypeMap — the task already has all JavaPeerInfo records from scanning, so no extra scan is needed. Changes: - AcwMapWriter: converts JavaPeerInfo → acw-map.txt format (3 lines per type: partial-asm-qualified, managed, compat-jni — matching legacy format) - GenerateTrimmableTypeMap: new AcwMapDirectory input + PerAssemblyAcwMapFiles output, writes per-assembly acw-map.{AssemblyName}.txt during generation - Trimmable.targets: _MergeAcwMaps target concatenates per-assembly files into the single acw-map.txt consumed by _ConvertCustomView and R8 - 8 unit tests for AcwMapWriter (263 total tests pass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 72eb604 commit 39fc4ab

4 files changed

Lines changed: 281 additions & 4 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
7+
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
8+
9+
/// <summary>
10+
/// Generates per-assembly acw-map.txt content from <see cref="JavaPeerInfo"/> records.
11+
/// The acw-map.txt file maps managed type names to Java/ACW type names, consumed by
12+
/// <c>_ConvertCustomView</c> to fix up custom view names in layout XMLs.
13+
///
14+
/// Format per type (3 lines):
15+
/// Line 1: PartialAssemblyQualifiedName;JavaKey (always written)
16+
/// Line 2: ManagedKey;JavaKey
17+
/// Line 3: CompatJniName;JavaKey
18+
///
19+
/// Java keys use dots (not slashes): e.g., "android.app.Activity"
20+
/// </summary>
21+
public static class AcwMapWriter
22+
{
23+
/// <summary>
24+
/// Writes acw-map lines for the given <paramref name="peers"/> to the <paramref name="writer"/>.
25+
/// Conflict detection is NOT performed here — it happens at merge time when all per-assembly
26+
/// maps are combined. Per-assembly maps write all 3 line variants unconditionally.
27+
/// </summary>
28+
public static void Write (TextWriter writer, IEnumerable<JavaPeerInfo> peers)
29+
{
30+
foreach (var peer in peers.OrderBy (p => p.ManagedTypeName, StringComparer.Ordinal)) {
31+
string javaKey = peer.JavaName.Replace ('/', '.');
32+
string managedKey = peer.ManagedTypeName;
33+
string partialAsmQualifiedName = $"{managedKey}, {peer.AssemblyName}";
34+
string compatJniName = peer.CompatJniName.Replace ('/', '.');
35+
36+
// Line 1: PartialAssemblyQualifiedName;JavaKey
37+
writer.Write (partialAsmQualifiedName);
38+
writer.Write (';');
39+
writer.WriteLine (javaKey);
40+
41+
// Line 2: ManagedKey;JavaKey
42+
writer.Write (managedKey);
43+
writer.Write (';');
44+
writer.WriteLine (javaKey);
45+
46+
// Line 3: CompatJniName;JavaKey
47+
writer.Write (compatJniName);
48+
writer.Write (';');
49+
writer.WriteLine (javaKey);
50+
}
51+
}
52+
53+
/// <summary>
54+
/// Writes acw-map lines to a file, only updating the file if the content has changed.
55+
/// Returns true if the file was written (content changed or file didn't exist).
56+
/// </summary>
57+
public static bool WriteToFile (string filePath, IEnumerable<JavaPeerInfo> peers)
58+
{
59+
using var memoryStream = new MemoryStream ();
60+
using (var writer = new StreamWriter (memoryStream, new System.Text.UTF8Encoding (encoderShouldEmitUTF8Identifier: false), bufferSize: 1024, leaveOpen: true)) {
61+
Write (writer, peers);
62+
}
63+
64+
memoryStream.Position = 0;
65+
byte[] newContent = memoryStream.ToArray ();
66+
67+
if (File.Exists (filePath)) {
68+
byte[] existingContent = File.ReadAllBytes (filePath);
69+
if (existingContent.AsSpan ().SequenceEqual (newContent)) {
70+
return false;
71+
}
72+
}
73+
74+
string? directory = Path.GetDirectoryName (filePath);
75+
if (directory != null) {
76+
Directory.CreateDirectory (directory);
77+
}
78+
79+
File.WriteAllBytes (filePath, newContent);
80+
return true;
81+
}
82+
}

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- Trimmable typemap: managed type mapping instead of native binary typemaps.
22
Generates per-assembly TypeMap .dll assemblies, a root _Microsoft.Android.TypeMaps.dll,
3-
and JCW .java source files with registerNatives. -->
3+
JCW .java source files with registerNatives, and per-assembly acw-map files. -->
44
<Project>
55

66
<UsingTask TaskName="Xamarin.Android.Tasks.GenerateTrimmableTypeMap" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />
@@ -14,6 +14,7 @@
1414
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps</_TypeMapAssemblyName>
1515
<_TypeMapOutputDirectory>$(IntermediateOutputPath)typemap\</_TypeMapOutputDirectory>
1616
<_TypeMapJavaOutputDirectory>$(IntermediateOutputPath)typemap\java</_TypeMapJavaOutputDirectory>
17+
<_PerAssemblyAcwMapDirectory>$(IntermediateOutputPath)acw-maps\</_PerAssemblyAcwMapDirectory>
1718
</PropertyGroup>
1819

1920
<!-- Tell the runtime which assembly contains the TypeMap attributes -->
@@ -41,17 +42,53 @@
4142
ResolvedAssemblies="@(_ResolvedAssemblies)"
4243
OutputDirectory="$(_TypeMapOutputDirectory)"
4344
JavaSourceOutputDirectory="$(_TypeMapJavaOutputDirectory)"
45+
AcwMapDirectory="$(_PerAssemblyAcwMapDirectory)"
4446
TargetFrameworkVersion="$(TargetFrameworkVersion)">
4547
<Output TaskParameter="GeneratedAssemblies" ItemName="_GeneratedTypeMapAssemblies" />
4648
<Output TaskParameter="GeneratedJavaFiles" ItemName="_GeneratedJavaFiles" />
49+
<Output TaskParameter="PerAssemblyAcwMapFiles" ItemName="_PerAssemblyAcwMapFiles" />
4750
</GenerateTrimmableTypeMap>
4851

4952
<ItemGroup>
5053
<FileWrites Include="@(_GeneratedTypeMapAssemblies)" />
5154
<FileWrites Include="@(_GeneratedJavaFiles)" />
55+
<FileWrites Include="@(_PerAssemblyAcwMapFiles)" />
5256
</ItemGroup>
5357

5458
<Touch Files="$(_AndroidStampDirectory)_GenerateJavaStubs.stamp" AlwaysCreate="True" />
5559
</Target>
5660

61+
<!--
62+
Merge per-assembly acw-map files into a single acw-map.txt.
63+
Consumed by _ConvertCustomView (layout XML custom view fixups) and R8.
64+
Runs after _GenerateJavaStubs produces the per-assembly files;
65+
skipped on incremental builds when no *.txt is newer than acw-map.txt.
66+
-->
67+
<Target Name="_CollectPerAssemblyAcwMaps"
68+
AfterTargets="_GenerateJavaStubs">
69+
<ItemGroup>
70+
<_PerAssemblyAcwMapFiles Include="$(_PerAssemblyAcwMapDirectory)*.txt" />
71+
</ItemGroup>
72+
</Target>
73+
74+
<Target Name="_MergeAcwMaps"
75+
AfterTargets="_CollectPerAssemblyAcwMaps"
76+
Inputs="@(_PerAssemblyAcwMapFiles)"
77+
Outputs="$(_AcwMapFile)">
78+
79+
<ReadLinesFromFile File="%(_PerAssemblyAcwMapFiles.Identity)">
80+
<Output TaskParameter="Lines" ItemName="_AcwMapLines" />
81+
</ReadLinesFromFile>
82+
83+
<WriteLinesToFile
84+
File="$(_AcwMapFile)"
85+
Lines="@(_AcwMapLines)"
86+
Overwrite="true"
87+
WriteOnlyWhenDifferent="true" />
88+
89+
<ItemGroup>
90+
<FileWrites Include="$(_AcwMapFile)" />
91+
</ItemGroup>
92+
</Target>
93+
5794
</Project>

src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
namespace Xamarin.Android.Tasks;
1313

1414
/// <summary>
15-
/// Generates trimmable TypeMap assemblies and JCW Java source files from resolved assemblies.
16-
/// Runs before the trimmer to produce per-assembly typemap .dll files and a root
17-
/// _Microsoft.Android.TypeMaps.dll, plus .java files for ACW types with registerNatives.
15+
/// Generates trimmable TypeMap assemblies, JCW Java source files, and per-assembly
16+
/// acw-map files from resolved assemblies. The acw-map files are later merged into
17+
/// a single acw-map.txt consumed by _ConvertCustomView for layout XML fixups.
1818
/// </summary>
1919
public class GenerateTrimmableTypeMap : AndroidTask
2020
{
@@ -29,6 +29,12 @@ public class GenerateTrimmableTypeMap : AndroidTask
2929
[Required]
3030
public string JavaSourceOutputDirectory { get; set; } = "";
3131

32+
/// <summary>
33+
/// Directory for per-assembly acw-map.{AssemblyName}.txt files.
34+
/// </summary>
35+
[Required]
36+
public string AcwMapDirectory { get; set; } = "";
37+
3238
/// <summary>
3339
/// The .NET target framework version (e.g., "v11.0"). Used to set the System.Runtime
3440
/// assembly reference version in generated typemap assemblies.
@@ -42,13 +48,21 @@ public class GenerateTrimmableTypeMap : AndroidTask
4248
[Output]
4349
public ITaskItem []? GeneratedJavaFiles { get; set; }
4450

51+
/// <summary>
52+
/// Per-assembly acw-map files produced during scanning. Each file contains
53+
/// ManagedName;JavaName lines for types in that assembly.
54+
/// </summary>
55+
[Output]
56+
public ITaskItem []? PerAssemblyAcwMapFiles { get; set; }
57+
4558
public override bool RunTask ()
4659
{
4760
var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion);
4861
var assemblyPaths = GetJavaInteropAssemblyPaths (ResolvedAssemblies);
4962

5063
Directory.CreateDirectory (OutputDirectory);
5164
Directory.CreateDirectory (JavaSourceOutputDirectory);
65+
Directory.CreateDirectory (AcwMapDirectory);
5266

5367
var allPeers = ScanAssemblies (assemblyPaths);
5468
if (allPeers.Count == 0) {
@@ -58,6 +72,7 @@ public override bool RunTask ()
5872

5973
GeneratedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths);
6074
GeneratedJavaFiles = GenerateJcwJavaSources (allPeers);
75+
PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (allPeers);
6176

6277
return !Log.HasLoggedErrors;
6378
}
@@ -153,6 +168,32 @@ ITaskItem [] GenerateJcwJavaSources (List<JavaPeerInfo> allPeers)
153168
return items;
154169
}
155170

171+
ITaskItem [] GeneratePerAssemblyAcwMaps (List<JavaPeerInfo> allPeers)
172+
{
173+
var peersByAssembly = allPeers
174+
.GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
175+
.OrderBy (g => g.Key, StringComparer.Ordinal);
176+
177+
var outputFiles = new List<ITaskItem> ();
178+
179+
foreach (var group in peersByAssembly) {
180+
var peers = group.ToList ();
181+
string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt");
182+
bool written = AcwMapWriter.WriteToFile (outputFile, peers);
183+
184+
Log.LogDebugMessage (written
185+
? $" acw-map.{group.Key}.txt: {peers.Count} types"
186+
: $" acw-map.{group.Key}.txt: unchanged");
187+
188+
var item = new TaskItem (outputFile);
189+
item.SetMetadata ("AssemblyName", group.Key);
190+
outputFiles.Add (item);
191+
}
192+
193+
Log.LogDebugMessage ($"Generated {outputFiles.Count} per-assembly ACW map files.");
194+
return outputFiles.ToArray ();
195+
}
196+
156197
static Version ParseTargetFrameworkVersion (string tfv)
157198
{
158199
if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using Xunit;
5+
6+
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
7+
8+
public class AcwMapWriterTests : FixtureTestBase
9+
{
10+
static string WriteToString (IEnumerable<JavaPeerInfo> peers)
11+
{
12+
using var writer = new StringWriter ();
13+
AcwMapWriter.Write (writer, peers);
14+
return writer.ToString ();
15+
}
16+
17+
[Fact]
18+
public void Write_SingleMcwType_ProducesThreeLines ()
19+
{
20+
var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android");
21+
22+
var output = WriteToString (new [] { peer });
23+
var lines = output.TrimEnd ().Split (new [] { '\n' }, StringSplitOptions.None);
24+
25+
Assert.Equal (3, lines.Length);
26+
// Line 1: PartialAssemblyQualifiedName;JavaKey
27+
Assert.Equal ("Android.App.Activity, Mono.Android;android.app.Activity", lines [0]);
28+
// Line 2: ManagedKey;JavaKey
29+
Assert.Equal ("Android.App.Activity;android.app.Activity", lines [1]);
30+
// Line 3: CompatJniName;JavaKey
31+
Assert.Equal ("android.app.Activity;android.app.Activity", lines [2]);
32+
}
33+
34+
[Fact]
35+
public void Write_UserType_SlashesConvertedToDots ()
36+
{
37+
var peer = new JavaPeerInfo {
38+
JavaName = "crc64abcdef/MyActivity",
39+
CompatJniName = "my.namespace/MyActivity",
40+
ManagedTypeName = "My.Namespace.MyActivity",
41+
ManagedTypeNamespace = "My.Namespace",
42+
ManagedTypeShortName = "MyActivity",
43+
AssemblyName = "MyApp",
44+
};
45+
46+
var output = WriteToString (new [] { peer });
47+
var lines = output.TrimEnd ().Split (new [] { '\n' }, StringSplitOptions.None);
48+
49+
Assert.Equal (3, lines.Length);
50+
Assert.Equal ("My.Namespace.MyActivity, MyApp;crc64abcdef.MyActivity", lines [0]);
51+
Assert.Equal ("My.Namespace.MyActivity;crc64abcdef.MyActivity", lines [1]);
52+
Assert.Equal ("my.namespace.MyActivity;crc64abcdef.MyActivity", lines [2]);
53+
}
54+
55+
[Fact]
56+
public void Write_MultipleTypes_OrderedByManagedName ()
57+
{
58+
var peers = new [] {
59+
MakeMcwPeer ("android/widget/TextView", "Android.Widget.TextView", "Mono.Android"),
60+
MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android"),
61+
MakeMcwPeer ("android/content/Context", "Android.Content.Context", "Mono.Android"),
62+
};
63+
64+
var output = WriteToString (peers);
65+
var lines = output.TrimEnd ().Split (new [] { '\n' }, StringSplitOptions.None);
66+
67+
// 3 types × 3 lines each = 9 lines
68+
Assert.Equal (9, lines.Length);
69+
70+
// First type alphabetically: Android.App.Activity
71+
Assert.StartsWith ("Android.App.Activity, Mono.Android;", lines [0]);
72+
// Second: Android.Content.Context
73+
Assert.StartsWith ("Android.Content.Context, Mono.Android;", lines [3]);
74+
// Third: Android.Widget.TextView
75+
Assert.StartsWith ("Android.Widget.TextView, Mono.Android;", lines [6]);
76+
}
77+
78+
[Fact]
79+
public void Write_EmptyList_ProducesEmptyOutput ()
80+
{
81+
var output = WriteToString (Array.Empty<JavaPeerInfo> ());
82+
Assert.Equal ("", output);
83+
}
84+
85+
[Fact]
86+
public void Write_MatchesExpectedAcwMapFormat ()
87+
{
88+
// Verify the format matches what LoadMapFile expects:
89+
// each line is "key;value" where LoadMapFile splits on ';'
90+
var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android");
91+
92+
var output = WriteToString (new [] { peer });
93+
94+
foreach (var line in output.TrimEnd ().Split (new [] { '\n' }, StringSplitOptions.None)) {
95+
var parts = line.Split (new [] { ';' }, count: 2);
96+
Assert.Equal (2, parts.Length);
97+
Assert.False (string.IsNullOrWhiteSpace (parts [0]), "Key should not be empty");
98+
Assert.False (string.IsNullOrWhiteSpace (parts [1]), "Value should not be empty");
99+
}
100+
}
101+
102+
[Fact]
103+
public void Write_FromScannedFixtures_ProducesValidOutput ()
104+
{
105+
var peers = ScanFixtures ();
106+
Assert.NotEmpty (peers);
107+
108+
var output = WriteToString (peers);
109+
110+
foreach (var line in output.TrimEnd ().Split (new [] { '\n' }, StringSplitOptions.None)) {
111+
var parts = line.Split (new [] { ';' }, count: 2);
112+
Assert.Equal (2, parts.Length);
113+
// No slashes in the output — they should all be converted to dots
114+
Assert.DoesNotContain ("/", parts [1]);
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)