Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,21 @@ public void Generate (IReadOnlyList<JavaPeerInfo> peers, Stream stream, string a
var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion);
emitter.Emit (model, stream, useSharedTypemapUniverse);
}

/// <summary>
/// Emits an empty typemap assembly (containing no type map entries) with the given
/// <paramref name="assemblyName"/>, writing it to <paramref name="stream"/>. Used to satisfy
/// <c>[assembly: TypeMapAssemblyTarget&lt;T&gt;("name")]</c> references to per-assembly typemaps
/// that the trimmer removed (their target Java binding was unused): the runtime can still
/// <c>Assembly.Load</c> the stub, which contributes no mappings, instead of throwing
/// <see cref="System.IO.FileNotFoundException"/>.
/// </summary>
/// <param name="stream">Stream to write the output PE assembly to.</param>
/// <param name="assemblyName">Assembly name for the generated stub.</param>
public void GenerateEmpty (Stream stream, string assemblyName)
{
var builder = new PEAssemblyBuilder (_systemRuntimeVersion);
builder.EmitPreamble (assemblyName, assemblyName + ".dll");
builder.WritePE (stream);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!-- Trimmable typemap: CoreCLR-specific (ILLink integration). -->
<Project>

<UsingTask TaskName="Xamarin.Android.Tasks.GenerateMissingTypeMapStubs" AssemblyFile="$(_XamarinAndroidBuildTasksAssembly)" />

<PropertyGroup>
<_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider</_TrimmableRuntimeProviderJavaName>
</PropertyGroup>
Expand Down Expand Up @@ -114,6 +116,27 @@
<_PostTrimDeletedCopiedJavaFiles Remove="@(_PostTrimDeletedCopiedJavaFiles)" />
</ItemGroup>
</Target>
<!-- After ILLink trims per-assembly _X.TypeMap assemblies whose target Java binding was unused,
the root _Microsoft.Android.TypeMaps assembly still carries
[assembly: TypeMapAssemblyTarget<T>("_X.TypeMap")] attributes pointing at the trimmed
assemblies. Those attributes reference the assembly by an opaque name string, so ILLink cannot
follow (or prune) them, and at startup CoreCLR's TypeMapping would Assembly.Load each one and
throw FileNotFoundException. Emit an empty stub for each trimmed typemap so Assembly.Load
succeeds (contributing no mappings) while leaving the linked root - and ILLink's reconciled
assembly references in it - untouched. Runs before the linked TypeMap DLLs are handed to the
publish/ReadyToRun pipeline. -->
<Target Name="_GenerateMissingTrimmableTypeMapStubs"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(PublishTrimmed)' == 'true' and '$(RuntimeIdentifier)' != '' "
AfterTargets="ILLink"
BeforeTargets="_AddTrimmableTypeMapToResolvedFileToPublish;CreateReadyToRunImages">
<GenerateMissingTypeMapStubs
Condition="Exists('$(IntermediateOutputPath)linked')"
TypeMapDirectory="$(_TypeMapOutputDirectory)"
LinkedAssembliesDirectory="$(IntermediateOutputPath)linked"
RootTypeMapAssemblyName="$(_TypeMapAssemblyName)"
TargetFrameworkVersion="$(TargetFrameworkVersion)" />
</Target>

<!-- Add linked TypeMap DLLs to the normal publish assembly pipeline. The SDK R2R
target only compiles ResolvedFileToPublish items with PostprocessAssembly=true. -->
<Target Name="_AddTrimmableTypeMapToResolvedFileToPublish"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Android.Build.Tasks;
using Microsoft.Android.Sdk.TrimmableTypeMap;

namespace Xamarin.Android.Tasks;

/// <summary>
/// Emits empty stub assemblies for per-assembly typemaps (<c>_X.TypeMap.dll</c>) that the root
/// <c>_Microsoft.Android.TypeMaps</c> assembly references via
/// <c>[assembly: TypeMapAssemblyTarget&lt;T&gt;("_X.TypeMap")]</c> but that ILLink trimmed away because
/// their target Java binding was unused.
///
/// Those attributes carry an opaque assembly-name string rather than a metadata reference, so ILLink
/// cannot follow (or prune) them, and at startup CoreCLR's <c>TypeMapping</c> enumerates every
/// attribute and calls <c>Assembly.Load</c> on the named assembly, throwing
/// <c>FileNotFoundException</c> for the trimmed ones. Emitting an empty, entry-free stub for each keeps
/// <c>Assembly.Load</c> succeeding (the stub contributes no type map entries) without editing the
/// linked root assembly, so ILLink's reconciled assembly references are preserved.
/// </summary>
public class GenerateMissingTypeMapStubs : AndroidTask
{
public override string TaskPrefix => "GMTS";

/// <summary>Directory holding every per-assembly typemap generated before trimming; defines the full referenced set.</summary>
[Required]
public string TypeMapDirectory { get; set; } = "";

/// <summary>Directory holding the surviving (post-trim) assemblies, e.g. the <c>linked/</c> output. Stubs are written here.</summary>
[Required]
public string LinkedAssembliesDirectory { get; set; } = "";

/// <summary>The root typemap assembly name to skip, e.g. <c>_Microsoft.Android.TypeMaps</c>.</summary>
[Required]
public string RootTypeMapAssemblyName { get; set; } = "";

/// <summary>Used to derive the emitted assembly's System.Runtime reference version.</summary>
[Required]
public string TargetFrameworkVersion { get; set; } = "";

[Output]
public ITaskItem [] GeneratedStubs { get; set; } = [];

public override bool RunTask ()
{
var stubs = new List<ITaskItem> ();
if (!Directory.Exists (TypeMapDirectory) || !Directory.Exists (LinkedAssembliesDirectory)) {
Log.LogDebugMessage ($"TypeMap directory '{TypeMapDirectory}' or linked directory '{LinkedAssembliesDirectory}' not found; skipping stub generation.");
GeneratedStubs = stubs.ToArray ();
return true;
}
Comment thread
simonrozsival marked this conversation as resolved.

var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion);
var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion);

foreach (var file in Directory.EnumerateFiles (TypeMapDirectory, "_*.TypeMap.dll")) {
var name = Path.GetFileNameWithoutExtension (file);
if (string.IsNullOrEmpty (name) || string.Equals (name, RootTypeMapAssemblyName, StringComparison.Ordinal))
continue;
var linkedPath = Path.Combine (LinkedAssembliesDirectory, name + ".dll");
if (File.Exists (linkedPath))
continue; // survived trimming; a real typemap already ships

using var stream = new MemoryStream ();
generator.GenerateEmpty (stream, name);
Files.CopyIfStreamChanged (stream, linkedPath);
Log.LogDebugMessage ($"Generated empty typemap stub for trimmed assembly '{name}'.");
stubs.Add (new TaskItem (linkedPath));
}

if (stubs.Count > 0)
Log.LogDebugMessage ($"Generated {stubs.Count} empty typemap stub(s) for trimmed per-assembly typemaps.");
GeneratedStubs = stubs.ToArray ();
return !Log.HasLoggedErrors;
}

static Version ParseTargetFrameworkVersion (string tfv)
{
if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) {
tfv = tfv.Substring (1);
}
if (Version.TryParse (tfv, out var version)) {
return version;
}
throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Microsoft.Build.Framework;
using NUnit.Framework;
using Xamarin.Android.Tasks;

namespace Xamarin.Android.Build.Tests {
[TestFixture]
[Parallelizable (ParallelScope.Children)]
public class GenerateMissingTypeMapStubsTests : BaseTest {

[Test]
public void Execute_TrimmedTypeMap_GeneratesLoadableStub ()
{
var path = Path.Combine (Root, "temp", TestName);
var typeMapDir = Path.Combine (path, "typemap");
var linkedDir = Path.Combine (path, "linked");
Directory.CreateDirectory (typeMapDir);
Directory.CreateDirectory (linkedDir);

// Pre-trim: two per-assembly typemaps plus the root were generated.
File.WriteAllText (Path.Combine (typeMapDir, "_Kept.TypeMap.dll"), "not-a-real-assembly");
File.WriteAllText (Path.Combine (typeMapDir, "_Trimmed.TypeMap.dll"), "not-a-real-assembly");
File.WriteAllText (Path.Combine (typeMapDir, "_Microsoft.Android.TypeMaps.dll"), "not-a-real-assembly");

// Post-trim (linked/): the root and one typemap survived; _Trimmed was trimmed away.
File.WriteAllText (Path.Combine (linkedDir, "_Kept.TypeMap.dll"), "survivor");
File.WriteAllText (Path.Combine (linkedDir, "_Microsoft.Android.TypeMaps.dll"), "survivor-root");

var task = new GenerateMissingTypeMapStubs {
BuildEngine = new MockBuildEngine (TestContext.Out),
TypeMapDirectory = typeMapDir,
LinkedAssembliesDirectory = linkedDir,
RootTypeMapAssemblyName = "_Microsoft.Android.TypeMaps",
TargetFrameworkVersion = "v11.0",
};

Assert.IsTrue (task.Execute (), "Task should succeed.");

// Only the trimmed-away typemap should get a stub; the survivor and the root are left alone.
var stubs = task.GeneratedStubs.Select (i => i.ItemSpec).ToArray ();
Assert.AreEqual (1, stubs.Length, "Exactly one stub should be generated.");
var stubPath = Path.Combine (linkedDir, "_Trimmed.TypeMap.dll");
CollectionAssert.Contains (stubs, stubPath);
FileAssert.Exists (stubPath);

Assert.AreEqual ("survivor", File.ReadAllText (Path.Combine (linkedDir, "_Kept.TypeMap.dll")),
"A surviving typemap must not be overwritten.");
Assert.AreEqual ("survivor-root", File.ReadAllText (Path.Combine (linkedDir, "_Microsoft.Android.TypeMaps.dll")),
"The root typemap must not be overwritten.");

// The stub must be a valid managed PE assembly named after the trimmed typemap.
using var stubStream = File.OpenRead (stubPath);
using var peReader = new PEReader (stubStream);
Assert.IsTrue (peReader.HasMetadata, "Stub should be a managed PE assembly.");
var reader = peReader.GetMetadataReader ();
var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name);
Assert.AreEqual ("_Trimmed.TypeMap", assemblyName, "Stub assembly name should match the trimmed typemap.");
}

[Test]
public void Execute_NothingTrimmed_GeneratesNoStubs ()
{
var path = Path.Combine (Root, "temp", TestName);
var typeMapDir = Path.Combine (path, "typemap");
var linkedDir = Path.Combine (path, "linked");
Directory.CreateDirectory (typeMapDir);
Directory.CreateDirectory (linkedDir);

File.WriteAllText (Path.Combine (typeMapDir, "_Kept.TypeMap.dll"), "not-a-real-assembly");
File.WriteAllText (Path.Combine (linkedDir, "_Kept.TypeMap.dll"), "survivor");

var task = new GenerateMissingTypeMapStubs {
BuildEngine = new MockBuildEngine (TestContext.Out),
TypeMapDirectory = typeMapDir,
LinkedAssembliesDirectory = linkedDir,
RootTypeMapAssemblyName = "_Microsoft.Android.TypeMaps",
TargetFrameworkVersion = "v11.0",
};

Assert.IsTrue (task.Execute (), "Task should succeed.");
Assert.IsEmpty (task.GeneratedStubs, "No stubs should be generated when nothing was trimmed.");
}
}
}
Loading