Skip to content

Commit 9b8cbf6

Browse files
Move all IO from TrimmableTypeMapGenerator to MSBuild task
The previous PR (#11034) established the pattern that TTMG has no IO operations. The build pipeline PR regressed this by adding acw-map writing, manifest template loading, and manifest file saving into the generator. Move all IO back to GenerateTrimmableTypeMap (MSBuild task): - Load manifest template XDocument from file path - Write merged manifest to disk via XDocument.Save() - Write merged acw-map.txt via Files.CopyIfStreamChanged() Refactor ManifestGenerator.Generate to return (XDocument, IList<string>) instead of writing to disk. Add GeneratedManifest record to carry the in-memory result. Update ManifestGeneratorTests to work entirely in-memory (no temp dirs, no IDisposable). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ed8ea81 commit 9b8cbf6

File tree

5 files changed

+70
-93
lines changed

5 files changed

+70
-93
lines changed

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

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.IO;
43
using System.Linq;
54
using System.Xml.Linq;
65

@@ -38,11 +37,10 @@ class ManifestGenerator
3837
/// Generates the merged manifest from an optional pre-loaded template and writes it to <paramref name="outputPath"/>.
3938
/// Returns the list of additional content provider names (for ApplicationRegistration.java).
4039
/// </summary>
41-
public IList<string> Generate (
40+
public (XDocument Document, IList<string> ProviderNames) Generate (
4241
XDocument? manifestTemplate,
4342
IReadOnlyList<JavaPeerInfo> allPeers,
44-
AssemblyManifestInfo assemblyInfo,
45-
string outputPath)
43+
AssemblyManifestInfo assemblyInfo)
4644
{
4745
var doc = manifestTemplate ?? CreateDefaultManifest ();
4846
var manifest = doc.Root;
@@ -122,14 +120,7 @@ public IList<string> Generate (
122120
ApplyPlaceholders (doc, placeholders);
123121
}
124122

125-
// Write output
126-
var outputDir = Path.GetDirectoryName (outputPath);
127-
if (outputDir is not null) {
128-
Directory.CreateDirectory (outputDir);
129-
}
130-
doc.Save (outputPath);
131-
132-
return providerNames;
123+
return (doc, providerNames);
133124
}
134125

135126
XDocument CreateDefaultManifest ()

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

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,15 @@ public TrimmableTypeMapGenerator (Action<string> log)
1818

1919
/// <summary>
2020
/// Runs the full generation pipeline: scan assemblies, generate typemap
21-
/// assemblies, generate JCW Java sources, optionally generate manifest and acw-map.
21+
/// assemblies, generate JCW Java sources, and optionally generate a merged manifest.
22+
/// No file IO is performed — all results are returned in memory.
2223
/// </summary>
2324
public TrimmableTypeMapResult Execute (
2425
IReadOnlyList<(string Name, PEReader Reader)> assemblies,
2526
Version systemRuntimeVersion,
2627
HashSet<string> frameworkAssemblyNames,
2728
ManifestConfig? manifestConfig = null,
28-
string? manifestTemplatePath = null,
29-
string? mergedManifestOutputPath = null,
30-
string? acwMapOutputPath = null)
29+
XDocument? manifestTemplate = null)
3130
{
3231
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
3332
_ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
@@ -46,33 +45,18 @@ public TrimmableTypeMapResult Execute (
4645
log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total).");
4746
var generatedJavaSources = GenerateJcwJavaSources (jcwPeers);
4847

49-
string[]? additionalProviderSources = null;
48+
GeneratedManifest? manifest = null;
5049

5150
// Generate merged AndroidManifest.xml if requested
52-
if (!mergedManifestOutputPath.IsNullOrEmpty () && manifestConfig is not null) {
53-
var providerSources = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplatePath, mergedManifestOutputPath);
54-
if (providerSources.Count > 0) {
55-
additionalProviderSources = providerSources.ToArray ();
56-
}
51+
if (manifestConfig is not null) {
52+
manifest = GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate);
5753
}
5854

59-
// Write merged acw-map.txt if requested
60-
if (!acwMapOutputPath.IsNullOrEmpty ()) {
61-
var acwDirectory = Path.GetDirectoryName (acwMapOutputPath);
62-
if (!acwDirectory.IsNullOrEmpty ()) {
63-
Directory.CreateDirectory (acwDirectory);
64-
}
65-
using (var writer = new StreamWriter (acwMapOutputPath)) {
66-
AcwMapWriter.Write (writer, allPeers);
67-
}
68-
log ($"Wrote merged acw-map.txt with {allPeers.Count} types to {acwMapOutputPath}.");
69-
}
70-
71-
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, additionalProviderSources);
55+
return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest);
7256
}
7357

74-
IList<string> GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifestInfo assemblyManifestInfo,
75-
ManifestConfig config, string? manifestTemplatePath, string mergedManifestOutputPath)
58+
GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifestInfo assemblyManifestInfo,
59+
ManifestConfig config, XDocument? manifestTemplate)
7660
{
7761
string minSdk = "21";
7862
if (!config.SupportedOSPlatformVersion.IsNullOrEmpty () && Version.TryParse (config.SupportedOSPlatformVersion, out var sopv)) {
@@ -103,12 +87,8 @@ IList<string> GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifestInf
10387
ApplicationJavaClass = config.ApplicationJavaClass,
10488
};
10589

106-
XDocument? manifestTemplateDoc = null;
107-
if (!manifestTemplatePath.IsNullOrEmpty () && File.Exists (manifestTemplatePath)) {
108-
manifestTemplateDoc = XDocument.Load (manifestTemplatePath);
109-
}
110-
111-
return generator.Generate (manifestTemplateDoc, allPeers, assemblyManifestInfo, mergedManifestOutputPath);
90+
var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo);
91+
return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []);
11292
}
11393

11494
(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies)

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
using System.Xml.Linq;
34

45
namespace Microsoft.Android.Sdk.TrimmableTypeMap;
56

67
public record TrimmableTypeMapResult (
78
IReadOnlyList<GeneratedAssembly> GeneratedAssemblies,
89
IReadOnlyList<GeneratedJavaSource> GeneratedJavaSources,
910
IReadOnlyList<JavaPeerInfo> AllPeers,
10-
string[]? AdditionalProviderSources = null);
11+
GeneratedManifest? Manifest = null);
1112

1213
public record GeneratedAssembly (string Name, MemoryStream Content);
1314

1415
public record GeneratedJavaSource (string RelativePath, string Content);
1516

17+
/// <summary>
18+
/// The in-memory result of manifest generation: the merged document and
19+
/// any additional content provider class names for ApplicationRegistration.java.
20+
/// </summary>
21+
public record GeneratedManifest (XDocument Document, string[] AdditionalProviderSources);
22+
1623
/// <summary>
1724
/// Configuration values for manifest generation. Passed from MSBuild properties.
1825
/// </summary>

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

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,19 +96,46 @@ public override bool RunTask ()
9696
}
9797

9898
var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg));
99+
100+
XDocument? manifestTemplate = null;
101+
if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) {
102+
manifestTemplate = System.Xml.Linq.XDocument.Load (ManifestTemplate);
103+
}
104+
99105
result = generator.Execute (
100-
assemblies,
101-
systemRuntimeVersion,
102-
frameworkAssemblyNames,
103-
manifestConfig,
104-
ManifestTemplate,
105-
MergedAndroidManifestOutput,
106-
AcwMapOutputFile);
106+
assemblies,
107+
systemRuntimeVersion,
108+
frameworkAssemblyNames,
109+
manifestConfig,
110+
manifestTemplate);
107111

108112
GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths);
109113
GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources);
110114
PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers);
111-
AdditionalProviderSources = result.AdditionalProviderSources;
115+
116+
// Write manifest to disk if generated
117+
if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) {
118+
var manifestDir = Path.GetDirectoryName (MergedAndroidManifestOutput);
119+
if (!manifestDir.IsNullOrEmpty ()) {
120+
Directory.CreateDirectory (manifestDir);
121+
}
122+
result.Manifest.Document.Save (MergedAndroidManifestOutput);
123+
AdditionalProviderSources = result.Manifest.AdditionalProviderSources;
124+
}
125+
126+
// Write merged acw-map.txt if requested
127+
if (!AcwMapOutputFile.IsNullOrEmpty ()) {
128+
var acwDirectory = Path.GetDirectoryName (AcwMapOutputFile);
129+
if (!acwDirectory.IsNullOrEmpty ()) {
130+
Directory.CreateDirectory (acwDirectory);
131+
}
132+
using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
133+
AcwMapWriter.Write (sw, result.AllPeers);
134+
sw.Flush ();
135+
Files.CopyIfStreamChanged (sw.BaseStream, AcwMapOutputFile);
136+
}
137+
Log.LogDebugMessage ($"Wrote merged acw-map.txt with {result.AllPeers.Count} types to {AcwMapOutputFile}.");
138+
}
112139
} finally {
113140
if (result is not null) {
114141
foreach (var assembly in result.GeneratedAssemblies) {

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs

Lines changed: 13 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System;
21
using System.Collections.Generic;
3-
using System.IO;
42
using System.Linq;
53
using System.Xml.Linq;
64
using Microsoft.Android.Sdk.TrimmableTypeMap;
@@ -9,26 +7,11 @@
97

108
namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests;
119

12-
public class ManifestGeneratorTests : IDisposable
10+
public class ManifestGeneratorTests
1311
{
1412
static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android";
1513
static readonly XName AttName = AndroidNs + "name";
1614

17-
string tempDir;
18-
19-
public ManifestGeneratorTests ()
20-
{
21-
tempDir = Path.Combine (Path.GetTempPath (), "ManifestGeneratorTests_" + Guid.NewGuid ().ToString ("N"));
22-
Directory.CreateDirectory (tempDir);
23-
}
24-
25-
public void Dispose ()
26-
{
27-
if (Directory.Exists (tempDir)) {
28-
Directory.Delete (tempDir, recursive: true);
29-
}
30-
}
31-
3215
ManifestGenerator CreateDefaultGenerator () => new ManifestGenerator {
3316
PackageName = "com.example.app",
3417
ApplicationLabel = "My App",
@@ -39,14 +22,7 @@ public void Dispose ()
3922
AndroidRuntime = "coreclr",
4023
};
4124

42-
string OutputPath => Path.Combine (tempDir, "AndroidManifest.xml");
43-
44-
string WriteTemplate (string xml)
45-
{
46-
var path = Path.Combine (tempDir, "template.xml");
47-
File.WriteAllText (path, xml);
48-
return path;
49-
}
25+
static XDocument ParseTemplate (string xml) => XDocument.Parse (xml);
5026

5127
static JavaPeerInfo CreatePeer (
5228
string javaName,
@@ -70,16 +46,12 @@ XDocument GenerateAndLoad (
7046
ManifestGenerator gen,
7147
IReadOnlyList<JavaPeerInfo>? peers = null,
7248
AssemblyManifestInfo? assemblyInfo = null,
73-
string? templatePath = null)
49+
XDocument? template = null)
7450
{
7551
peers ??= [];
7652
assemblyInfo ??= new AssemblyManifestInfo ();
77-
XDocument? template = null;
78-
if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) {
79-
template = XDocument.Load (templatePath);
80-
}
81-
gen.Generate (template, peers, assemblyInfo, OutputPath);
82-
return XDocument.Load (OutputPath);
53+
var (doc, _) = gen.Generate (template, peers, assemblyInfo);
54+
return doc;
8355
}
8456

8557
[Fact]
@@ -305,7 +277,7 @@ public void RuntimeProvider_Added ()
305277
public void TemplateManifest_Preserved ()
306278
{
307279
var gen = CreateDefaultGenerator ();
308-
var template = WriteTemplate (
280+
var template = ParseTemplate (
309281
"""
310282
<?xml version="1.0" encoding="utf-8"?>
311283
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
@@ -314,7 +286,7 @@ public void TemplateManifest_Preserved ()
314286
</manifest>
315287
""");
316288

317-
var doc = GenerateAndLoad (gen, templatePath: template);
289+
var doc = GenerateAndLoad (gen, template: template);
318290
var app = doc.Root?.Element ("application");
319291

320292
Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup"));
@@ -384,7 +356,7 @@ public void ManifestPlaceholders_Replaced ()
384356
var gen = CreateDefaultGenerator ();
385357
gen.ManifestPlaceholders = "myAuthority=com.example.auth;myKey=12345";
386358

387-
var template = WriteTemplate (
359+
var template = ParseTemplate (
388360
"""
389361
<?xml version="1.0" encoding="utf-8"?>
390362
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
@@ -395,7 +367,7 @@ public void ManifestPlaceholders_Replaced ()
395367
</manifest>
396368
""");
397369

398-
var doc = GenerateAndLoad (gen, templatePath: template);
370+
var doc = GenerateAndLoad (gen, template: template);
399371
var provider = doc.Root?.Element ("application")?.Elements ("provider")
400372
.FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "com.example.MyProvider");
401373
Assert.Equal ("com.example.auth", (string?)provider?.Attribute (AndroidNs + "authorities"));
@@ -434,7 +406,7 @@ public void AbstractTypes_Skipped ()
434406
public void ExistingType_NotDuplicated ()
435407
{
436408
var gen = CreateDefaultGenerator ();
437-
var template = WriteTemplate (
409+
var template = ParseTemplate (
438410
"""
439411
<?xml version="1.0" encoding="utf-8"?>
440412
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
@@ -449,7 +421,7 @@ public void ExistingType_NotDuplicated ()
449421
Properties = new Dictionary<string, object?> { ["Label"] = "New Label" },
450422
});
451423

452-
var doc = GenerateAndLoad (gen, [peer], templatePath: template);
424+
var doc = GenerateAndLoad (gen, [peer], template: template);
453425
var activities = doc.Root?.Element ("application")?.Elements ("activity")
454426
.Where (a => (string?)a.Attribute (AttName) == "com.example.app.ExistingActivity")
455427
.ToList ();
@@ -556,7 +528,7 @@ public void AssemblyLevel_Application ()
556528
public void AssemblyLevel_Deduplication ()
557529
{
558530
var gen = CreateDefaultGenerator ();
559-
var template = WriteTemplate (
531+
var template = ParseTemplate (
560532
"""
561533
<?xml version="1.0" encoding="utf-8"?>
562534
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
@@ -573,7 +545,7 @@ public void AssemblyLevel_Deduplication ()
573545
info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy" });
574546
info.MetaData.Add (new MetaDataInfo { Name = "existing.key", Value = "new_value" });
575547

576-
var doc = GenerateAndLoad (gen, assemblyInfo: info, templatePath: template);
548+
var doc = GenerateAndLoad (gen, assemblyInfo: info, template: template);
577549

578550
var cameraPerms = doc.Root?.Elements ("uses-permission")
579551
.Where (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA")

0 commit comments

Comments
 (0)