Skip to content

Commit cd79c47

Browse files
Remove all IO from TrimmableTypeMapGenerator
- Generator accepts (name, PEReader) pairs, returns in-memory content - MSBuild task owns all filesystem IO - Tests assert on in-memory content, no temp dirs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1183f9d commit cd79c47

5 files changed

Lines changed: 82 additions & 128 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,23 @@ public IReadOnlyList<string> Generate (IReadOnlyList<JavaPeerInfo> types, string
7575
return generatedFiles;
7676
}
7777

78-
/// <summary>
78+
public IReadOnlyList<GeneratedJavaSource> GenerateContent (IReadOnlyList<JavaPeerInfo> types)
79+
{
80+
if (types is null) throw new ArgumentNullException (nameof (types));
81+
var results = new List<GeneratedJavaSource> ();
82+
foreach (var type in types) {
83+
if (type.DoNotGenerateAcw || type.IsInterface) continue;
84+
using var writer = new StringWriter ();
85+
Generate (type, writer);
86+
results.Add (new GeneratedJavaSource (GetRelativePath (type), writer.ToString ()));
87+
}
88+
return results;
89+
}
90+
91+
/// <summary>
7992
/// Generates a single .java source file for the given type.
8093
/// </summary>
81-
internal void Generate (JavaPeerInfo type, TextWriter writer)
94+
public void Generate (JavaPeerInfo type, TextWriter writer)
8295
{
8396
writer.NewLine = "\n";
8497
WritePackageDeclaration (type, writer);
@@ -91,11 +104,15 @@ internal void Generate (JavaPeerInfo type, TextWriter writer)
91104
WriteClassClose (writer);
92105
}
93106

94-
static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
107+
static string GetRelativePath (JavaPeerInfo type)
95108
{
96109
JniSignatureHelper.ValidateJniName (type.JavaName);
97-
string relativePath = type.JavaName + ".java";
98-
return Path.Combine (outputDirectory, relativePath);
110+
return type.JavaName + ".java";
111+
}
112+
113+
static string GetOutputFilePath (JavaPeerInfo type, string outputDirectory)
114+
{
115+
return Path.Combine (outputDirectory, GetRelativePath (type));
99116
}
100117

101118
/// <summary>

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ sealed class AssemblyIndex : IDisposable
1818

1919
public MetadataReader Reader { get; }
2020
public string AssemblyName { get; }
21-
public string FilePath { get; }
2221

2322
/// <summary>
2423
/// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle.
@@ -35,21 +34,28 @@ sealed class AssemblyIndex : IDisposable
3534
/// </summary>
3635
public Dictionary<TypeDefinitionHandle, TypeAttributeInfo> AttributesByType { get; } = new ();
3736

38-
AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string filePath)
37+
AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName)
3938
{
4039
this.peReader = peReader;
4140
this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader);
4241
Reader = reader;
4342
AssemblyName = assemblyName;
44-
FilePath = filePath;
4543
}
4644

4745
public static AssemblyIndex Create (string filePath)
4846
{
4947
var peReader = new PEReader (File.OpenRead (filePath));
5048
var reader = peReader.GetMetadataReader ();
5149
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
52-
var index = new AssemblyIndex (peReader, reader, assemblyName, filePath);
50+
var index = new AssemblyIndex (peReader, reader, assemblyName);
51+
index.Build ();
52+
return index;
53+
}
54+
55+
public static AssemblyIndex Create (PEReader peReader, string assemblyName)
56+
{
57+
var reader = peReader.GetMetadataReader ();
58+
var index = new AssemblyIndex (peReader, reader, assemblyName);
5359
index.Build ();
5460
return index;
5561
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Reflection;
66
using System.Reflection.Metadata;
77
using System.Reflection.Metadata.Ecma335;
8+
using System.Reflection.PortableExecutable;
89

910
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
1011

@@ -81,22 +82,29 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan
8182
/// </summary>
8283
public List<JavaPeerInfo> Scan (IReadOnlyList<string> assemblyPaths)
8384
{
84-
// Phase 1: Build indices for all assemblies
8585
foreach (var path in assemblyPaths) {
8686
var index = AssemblyIndex.Create (path);
8787
assemblyCache [index.AssemblyName] = index;
8888
}
89+
return ScanCore ();
90+
}
8991

90-
// Phase 2: Analyze types using cached indices
91-
var resultsByManagedName = new Dictionary<string, JavaPeerInfo> (StringComparer.Ordinal);
92+
public List<JavaPeerInfo> Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
93+
{
94+
foreach (var (name, reader) in assemblies) {
95+
var index = AssemblyIndex.Create (reader, name);
96+
assemblyCache [index.AssemblyName] = index;
97+
}
98+
return ScanCore ();
99+
}
92100

101+
List<JavaPeerInfo> ScanCore ()
102+
{
103+
var resultsByManagedName = new Dictionary<string, JavaPeerInfo> (StringComparer.Ordinal);
93104
foreach (var index in assemblyCache.Values) {
94105
ScanAssembly (index, resultsByManagedName);
95106
}
96-
97-
// Phase 3: Force unconditional on types referenced by [Application] attributes
98107
ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache);
99-
100108
return new List<JavaPeerInfo> (resultsByManagedName.Values);
101109
}
102110

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 29 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,10 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5+
using System.Reflection.PortableExecutable;
56

67
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
78

8-
/// <summary>
9-
/// Core logic for generating trimmable TypeMap assemblies, JCW Java sources, and acw-map files.
10-
/// Extracted from the MSBuild task so it can be tested directly without MSBuild ceremony.
11-
/// </summary>
129
public class TrimmableTypeMapGenerator
1310
{
1411
readonly Action<string> log;
@@ -21,144 +18,68 @@ public TrimmableTypeMapGenerator (Action<string> log)
2118
this.log = log;
2219
}
2320

24-
/// <summary>
25-
/// Runs the full generation pipeline: scan assemblies, generate typemap
26-
/// assemblies, generate JCW Java sources, and write acw-map files.
27-
/// </summary>
2821
public TrimmableTypeMapResult Execute (
29-
IReadOnlyList<string> assemblyPaths,
30-
string outputDirectory,
31-
string javaSourceOutputDirectory,
22+
IReadOnlyList<(string Name, PEReader Reader)> assemblies,
3223
Version systemRuntimeVersion,
3324
HashSet<string> frameworkAssemblyNames)
3425
{
35-
if (assemblyPaths is null) {
36-
throw new ArgumentNullException (nameof (assemblyPaths));
37-
}
38-
if (outputDirectory is null) {
39-
throw new ArgumentNullException (nameof (outputDirectory));
40-
}
41-
if (javaSourceOutputDirectory is null) {
42-
throw new ArgumentNullException (nameof (javaSourceOutputDirectory));
43-
}
44-
if (systemRuntimeVersion is null) {
45-
throw new ArgumentNullException (nameof (systemRuntimeVersion));
46-
}
47-
if (frameworkAssemblyNames is null) {
48-
throw new ArgumentNullException (nameof (frameworkAssemblyNames));
49-
}
50-
51-
Directory.CreateDirectory (outputDirectory);
52-
Directory.CreateDirectory (javaSourceOutputDirectory);
53-
54-
var allPeers = ScanAssemblies (assemblyPaths);
26+
if (assemblies is null) throw new ArgumentNullException (nameof (assemblies));
27+
if (systemRuntimeVersion is null) throw new ArgumentNullException (nameof (systemRuntimeVersion));
28+
if (frameworkAssemblyNames is null) throw new ArgumentNullException (nameof (frameworkAssemblyNames));
5529

30+
var allPeers = ScanAssemblies (assemblies);
5631
if (allPeers.Count == 0) {
5732
log ("No Java peer types found, skipping typemap generation.");
5833
return new TrimmableTypeMapResult ([], [], allPeers);
5934
}
6035

61-
var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, assemblyPaths, outputDirectory);
62-
63-
// Generate JCW .java files for user assemblies + framework Implementor types.
64-
// Framework binding types already have compiled JCWs in the SDK but their constructors
65-
// use the legacy TypeManager.Activate() JNI native which isn't available in the
66-
// trimmable runtime. Implementor types (View_OnClickListenerImplementor, etc.) are
67-
// in the mono.* Java package so we use the mono/ prefix to identify them.
68-
// We generate fresh JCWs that use Runtime.registerNatives() for activation.
36+
var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion);
6937
var jcwPeers = allPeers.Where (p =>
70-
!frameworkAssemblyNames.Contains (p.AssemblyName)
7138
|| p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList ();
7239
log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total).");
73-
var generatedJavaFiles = GenerateJcwJavaSources (jcwPeers, javaSourceOutputDirectory);
74-
75-
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaFiles, allPeers);
40+
var generatedJavaSources = GenerateJcwJavaSources (jcwPeers);
41+
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers);
7642
}
7743

78-
// Future optimization: the scanner currently scans all assemblies on every run.
79-
// For incremental builds, we could:
80-
// 1. Add a Scan(allPaths, changedPaths) overload that only produces JavaPeerInfo
81-
// for changed assemblies while still indexing all assemblies for cross-assembly
82-
// resolution (base types, interfaces, activation ctors).
83-
// 2. Cache scan results per assembly to skip PE I/O entirely for unchanged assemblies.
84-
// Both require profiling to determine if they meaningfully improve build times.
85-
List<JavaPeerInfo> ScanAssemblies (IReadOnlyList<string> assemblyPaths)
44+
List<JavaPeerInfo> ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)
8645
{
8746
using var scanner = new JavaPeerScanner ();
88-
var peers = scanner.Scan (assemblyPaths);
89-
log ($"Scanned {assemblyPaths.Count} assemblies, found {peers.Count} Java peer types.");
47+
var peers = scanner.Scan (assemblies);
48+
log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types.");
9049
return peers;
9150
}
9251

93-
List<string> GenerateTypeMapAssemblies (List<JavaPeerInfo> allPeers, Version systemRuntimeVersion,
94-
IReadOnlyList<string> assemblyPaths, string outputDir)
52+
List<GeneratedAssembly> GenerateTypeMapAssemblies (List<JavaPeerInfo> allPeers, Version systemRuntimeVersion)
9553
{
96-
// Build a map from assembly name → source path for timestamp comparison
97-
var sourcePathByName = new Dictionary<string, string> (StringComparer.Ordinal);
98-
foreach (var path in assemblyPaths) {
99-
var name = Path.GetFileNameWithoutExtension (path);
100-
sourcePathByName [name] = path;
101-
}
102-
103-
var peersByAssembly = allPeers
104-
.GroupBy (p => p.AssemblyName, StringComparer.Ordinal)
105-
.OrderBy (g => g.Key, StringComparer.Ordinal);
106-
107-
var generatedAssemblies = new List<string> ();
54+
var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal);
55+
var generatedAssemblies = new List<GeneratedAssembly> ();
10856
var perAssemblyNames = new List<string> ();
10957
var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);
110-
bool anyRegenerated = false;
111-
11258
foreach (var group in peersByAssembly) {
11359
string assemblyName = $"_{group.Key}.TypeMap";
114-
string outputPath = Path.Combine (outputDir, assemblyName + ".dll");
11560
perAssemblyNames.Add (assemblyName);
116-
117-
if (IsUpToDate (outputPath, group.Key, sourcePathByName)) {
118-
log ($" {assemblyName}: up to date, skipping");
119-
generatedAssemblies.Add (outputPath);
120-
continue;
121-
}
122-
12361
var peers = group.ToList ();
124-
generator.Generate (peers, outputPath, assemblyName);
125-
generatedAssemblies.Add (outputPath);
126-
anyRegenerated = true;
127-
62+
var stream = new MemoryStream ();
63+
generator.Generate (peers, stream, assemblyName);
64+
stream.Position = 0;
65+
generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream));
12866
log ($" {assemblyName}: {peers.Count} types");
12967
}
130-
131-
// Root assembly references all per-assembly typemaps — regenerate if any changed
132-
string rootOutputPath = Path.Combine (outputDir, "_Microsoft.Android.TypeMaps.dll");
133-
if (anyRegenerated || !File.Exists (rootOutputPath)) {
134-
var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
135-
rootGenerator.Generate (perAssemblyNames, rootOutputPath);
136-
log ($" Root: {perAssemblyNames.Count} per-assembly refs");
137-
} else {
138-
log (" Root: up to date, skipping");
139-
}
140-
generatedAssemblies.Add (rootOutputPath);
141-
68+
var rootStream = new MemoryStream ();
69+
var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
70+
rootGenerator.Generate (perAssemblyNames, rootStream);
71+
rootStream.Position = 0;
72+
generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream));
73+
log ($" Root: {perAssemblyNames.Count} per-assembly refs");
14274
log ($"Generated {generatedAssemblies.Count} typemap assemblies.");
14375
return generatedAssemblies;
14476
}
14577

146-
internal static bool IsUpToDate (string outputPath, string assemblyName, Dictionary<string, string> sourcePathByName)
147-
{
148-
if (!File.Exists (outputPath)) {
149-
return false;
150-
}
151-
if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) {
152-
return false;
153-
}
154-
return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath);
155-
}
156-
157-
List<string> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers, string javaSourceOutputDirectory)
78+
List<GeneratedJavaSource> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers)
15879
{
15980
var jcwGenerator = new JcwJavaSourceGenerator ();
160-
var files = jcwGenerator.Generate (allPeers, javaSourceOutputDirectory);
161-
log ($"Generated {files.Count} JCW Java source files.");
162-
return files.ToList ();
81+
var sources = jcwGenerator.GenerateContent (allPeers);
82+
log ($"Generated {sources.Count} JCW Java source files.");
83+
return sources.ToList ();
16384
}
16485
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
using System.Collections.Generic;
2+
using System.IO;
23

34
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
45

5-
/// <summary>
6-
/// Result of the trimmable type map generation.
7-
/// </summary>
86
public record TrimmableTypeMapResult (
9-
IReadOnlyList<string> GeneratedAssemblies,
10-
IReadOnlyList<string> GeneratedJavaFiles,
7+
IReadOnlyList<GeneratedAssembly> GeneratedAssemblies,
8+
IReadOnlyList<GeneratedJavaSource> GeneratedJavaSources,
119
IReadOnlyList<JavaPeerInfo> AllPeers);
10+
11+
public record GeneratedAssembly (string Name, MemoryStream Content);
12+
13+
public record GeneratedJavaSource (string RelativePath, string Content);

0 commit comments

Comments
 (0)