22using System . Collections . Generic ;
33using System . IO ;
44using System . Linq ;
5+ using System . Reflection . PortableExecutable ;
56
67namespace 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>
129public 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}
0 commit comments