Skip to content
Open
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 @@ -86,6 +86,36 @@ _ResolveAssemblies MSBuild target.
</ItemGroup>
</Target>

<!--
Resolve-only variant used by the trimmable typemap generator to discover runtime-only assemblies.

The trimmable typemap generator runs in the RID-independent OUTER build over @(ReferencePath),
the compile closure, which omits runtime-only assemblies (e.g. the NativeAOT runtime host) that
have no ref-pack counterpart and are only pulled from the RID-specific runtime pack. Runtime-pack
resolution (ResolveRuntimePackAssets, reached via ResolveReferences) requires a single
RuntimeIdentifier, so the outer build invokes this target through the MSBuild task for one RID.

It stops after ResolveReferences - it does NOT run ComputeFilesToPublish/ILLink/ILC/AOT - and
returns the Android runtime-pack managed assemblies that are NOT already in @(ReferencePath)
(matched on filename). Those are exactly the runtime-only assemblies the outer generator would
otherwise miss; everything with a compile reference is already scanned via @(ReferencePath). Only
the Android runtime packs (Microsoft.Android.Runtime.*) can carry Java peers, so the .NET/BCL
runtime pack (Microsoft.NETCore.App.Runtime.*) is excluded to avoid scanning large BCL assemblies
that have none. The managed metadata is RID-independent, so resolving one RID suffices for all. -->
<Target Name="_ResolveRuntimeOnlyAssembliesForTypeMap"
DependsOnTargets="BuildOnlySettings;_FixupIntermediateAssembly;_PatchNuGetReferenceMetadata;ResolveReferences"
Returns="@(_TypeMapRuntimeOnlyAssembly)">
<ItemGroup>
<_TypeMapRuntimeOnlyAssembly Include="@(RuntimePackAsset)"
Condition=" '%(RuntimePackAsset.Extension)' == '.dll' and '%(RuntimePackAsset.AssetType)' == 'runtime' and $([System.String]::Copy('%(RuntimePackAsset.NuGetPackageId)').StartsWith('Microsoft.Android.Runtime')) " />
<!-- Drop any runtime-pack assembly that also has a compile reference (e.g. Mono.Android,
Java.Interop): the outer build already scans those via @(ReferencePath), and scanning both
the ref and runtime-pack copies would double-process the same assembly (GenerateTrimmableTypeMap
groups inputs by path). Match on filename to keep only the runtime-only assemblies. -->
<_TypeMapRuntimeOnlyAssembly Remove="@(ReferencePath)" MatchOnMetadata="Filename" />
</ItemGroup>
</Target>

<Target Name="_FixupIntermediateAssembly" Condition=" '$(_OuterIntermediateAssembly)' != '' ">
<ItemGroup>
<IntermediateAssembly Remove="@(IntermediateAssembly)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(PrivateSdkAssemblies->'_%(Filename).TypeMap')" />
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(ReferencePath->'_%(Filename).TypeMap')"
Condition=" '%(ReferencePath.FrameworkAssembly)' == 'true' " />
<!-- Runtime-only assemblies (e.g. the NativeAOT runtime host Microsoft.Android.Runtime.NativeAOT)
come from the runtime pack and have no @(ReferencePath) counterpart, so their per-assembly
typemap DLLs are not caught by the rules above. Every runtime-pack managed assembly is a
framework/runtime assembly: classify its typemap DLL as framework so it stays an IlcReference
(ILC reads its conditional TypeMap attributes) but is removed from the unmanaged-entrypoint
roots, matching Mono.Android/Java.Interop. Their JCW native methods register via the runtime
registerNatives path. -->
<_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(RuntimePackAsset->'_%(Filename).TypeMap')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 MSBuild targets — This includes every runtime-pack managed DLL, including the .NET/BCL runtime pack (Microsoft.NETCore.App.Runtime.*), whereas the sibling _ResolveRuntimeOnlyAssembliesForTypeMap deliberately filters to Microsoft.Android.Runtime and excludes the BCL. It is harmless today — these names only feed the Remove on _TrimmableTypeMapUnmanagedEntryPointAssemblyNames, and BCL assemblies have no generated _*.TypeMap DLL to remove — but the asymmetry between the two targets is a maintenance trap and inflates the item group with hundreds of no-op BCL names. Consider matching the resolve target's StartsWith('Microsoft.Android.Runtime') filter here, or add a note explaining why the broader set is intentional.

Rule: Consistency with sibling targets

Condition=" '%(RuntimePackAsset.Extension)' == '.dll' and '%(RuntimePackAsset.AssetType)' == 'runtime' " />
<!-- The root assembly (_Microsoft.Android.TypeMaps) only contains TypeMapLoader.Initialize()
and assembly-level attributes — no types that need vtable generation.
Framework per-assembly typemap DLLs must remain IlcReference inputs so ILC can read
Expand Down Expand Up @@ -66,6 +75,70 @@
BeforeTargets="_ResolveAssemblies"
DependsOnTargets="_GenerateTrimmableTypeMap" />

<!--
The trimmable typemap generator runs in the OUTER build over @(ReferencePath), which contains only
the compile closure (app + its references + the ref-pack framework assemblies). Runtime-only
assemblies - e.g. the NativeAOT runtime host Microsoft.Android.Runtime.NativeAOT - come from the
RID-specific runtime pack and have no ref-pack counterpart, so they are resolved only in the per-RID
inner build and are never in @(ReferencePath). Their Java Callable Wrapper types (e.g.
UncaughtExceptionMarshaler) would then be missing from the typemap and JCWs -> R8 has nothing to
keep -> on-device startup crash in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler).

Runtime-pack resolution (ResolveRuntimePackAssets) requires a single RuntimeIdentifier and must
happen before ILC. So, before _ResolveAssemblies spawns the per-RID ILC builds, run a lightweight
resolve-only pass for the first RID (the managed metadata is RID-independent) via
_ResolveRuntimeOnlyAssembliesForTypeMap, and cache the discovered runtime-only assemblies to a file.
This auto-detects every runtime-only Java-peer assembly - no assembly is hard-coded.

The nested resolve is gated on Inputs/Outputs so it only re-runs when the reference graph could have
changed (restore output or the cached build properties), not on every incremental build. The
always-run loader below reads the cached list into the item the generator consumes, keeping the
generator's Inputs stable across builds.
-->
<Target Name="_ResolveRuntimeOnlyAssembliesForTrimmableTypeMap"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(_OuterIntermediateOutputPath)' == '' and '$(DesignTimeBuild)' != 'true' "
Inputs="$(ProjectAssetsFile);$(_AndroidBuildPropertiesCache)"
Outputs="$(_TypeMapOutputDirectory)runtime-only-assemblies.txt">
<PropertyGroup>
<_TrimmableTypeMapResolveRid Condition=" '$(RuntimeIdentifiers)' != '' ">$([System.String]::Copy('$(RuntimeIdentifiers)').Split(';')[0])</_TrimmableTypeMapResolveRid>
<_TrimmableTypeMapResolveRid Condition=" '$(_TrimmableTypeMapResolveRid)' == '' ">$(RuntimeIdentifier)</_TrimmableTypeMapResolveRid>
<!-- Item lists (e.g. @(IntermediateAssembly)) cannot be embedded directly in the MSBuild task's
Properties string, so flatten them into this property and pass it via AdditionalProperties
item metadata, mirroring _ResolveAssemblies. -->
<_TrimmableTypeMapResolveProperties>_ComputeFilesToPublishForRuntimeIdentifiers=true;SelfContained=true;DesignTimeBuild=$(DesignTimeBuild);AppendRuntimeIdentifierToOutputPath=true;ResolveAssemblyReferencesFindRelatedSatellites=false;SkipCompilerExecution=true;_OuterIntermediateAssembly=@(IntermediateAssembly);_OuterOutputPath=$(OutputPath);_OuterIntermediateOutputPath=$(IntermediateOutputPath);_AndroidNdkDirectory=$(_AndroidNdkDirectory)</_TrimmableTypeMapResolveProperties>
</PropertyGroup>
<ItemGroup Condition=" '$(_TrimmableTypeMapResolveRid)' != '' ">
<_TrimmableTypeMapResolveProject Include="$(MSBuildProjectFile)"
AdditionalProperties="RuntimeIdentifier=$(_TrimmableTypeMapResolveRid);$(_TrimmableTypeMapResolveProperties)" />
</ItemGroup>
<MSBuild
Condition=" '$(_TrimmableTypeMapResolveRid)' != '' "
Projects="@(_TrimmableTypeMapResolveProject)"
Targets="_ResolveRuntimeOnlyAssembliesForTypeMap">
<Output TaskParameter="TargetOutputs" ItemName="_ResolvedRuntimeOnlyAssemblyForTypeMap" />
</MSBuild>
<MakeDir Directories="$(_TypeMapOutputDirectory)" />
<WriteLinesToFile
File="$(_TypeMapOutputDirectory)runtime-only-assemblies.txt"
Lines="@(_ResolvedRuntimeOnlyAssemblyForTypeMap)"
Overwrite="true"
WriteOnlyWhenDifferent="true" />
<ItemGroup>
<FileWrites Include="$(_TypeMapOutputDirectory)runtime-only-assemblies.txt" />
</ItemGroup>
</Target>

<Target Name="_AddRuntimeOnlyAssembliesToTrimmableTypeMap"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(_OuterIntermediateOutputPath)' == '' and '$(DesignTimeBuild)' != 'true' "
DependsOnTargets="_ResolveRuntimeOnlyAssembliesForTrimmableTypeMap"
BeforeTargets="_GenerateTrimmableTypeMap">
<ReadLinesFromFile
File="$(_TypeMapOutputDirectory)runtime-only-assemblies.txt"
Condition=" Exists('$(_TypeMapOutputDirectory)runtime-only-assemblies.txt') ">
<Output TaskParameter="Lines" ItemName="_AndroidTrimmableTypeMapExtraFrameworkAssembly" />
</ReadLinesFromFile>
</Target>

<Target Name="_CollectTrimmableNativeAotDgmlFiles"
Condition=" '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' ">
<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(DesignTimeBuild)' != 'true' and '@(ReferencePath->Count())' != '0' and '$(_OuterIntermediateOutputPath)' == '' "
AfterTargets="CoreCompile"
DependsOnTargets="_GetLibraryImports"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(ExtractedManifestDocuments);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(ExtractedManifestDocuments);@(_AndroidTrimmableTypeMapExtraFrameworkAssembly);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
Outputs="$(_TrimmableTypeMapOutputStamp)">

<ItemGroup>
Expand All @@ -108,9 +108,14 @@
<_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" />
<_TypeMapInputAssemblies Include="@(PrivateSdkAssemblies)" />
<_TypeMapInputAssemblies Include="@(FrameworkAssemblies)" />
<!-- Runtime-only framework assemblies that have no counterpart in @(ReferencePath) (e.g. the
NativeAOT runtime host Microsoft.Android.Runtime.NativeAOT.dll, injected by the trimmable
NativeAOT targets). They carry Java Callable Wrapper types that must be in the typemap. -->
<_TypeMapInputAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" />
<_TypeMapFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" />
<_TypeMapFrameworkAssemblies Include="@(PrivateSdkAssemblies)" />
<_TypeMapFrameworkAssemblies Include="@(FrameworkAssemblies)" />
<_TypeMapFrameworkAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" />
<_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)"
Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" />
<!-- Library (.aar) manifests to merge into the app manifest. Only on the legacy manifest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease
}
}

[Test]
public void Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws ()
{
const bool isRelease = true;
if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) {
return;
}

var proj = new XamarinAndroidApplicationProject {
IsRelease = isRelease,
LinkTool = "r8",
};
proj.SetRuntime (AndroidRuntime.NativeAOT);
proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable");

using var builder = CreateApkBuilder ();
Assert.IsTrue (builder.Build (proj), "Build should have succeeded.");

var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex"));
FileAssert.Exists (dexFile);

// Regression test: the NativeAOT runtime host assembly (Microsoft.Android.Runtime.NativeAOT) is
// resolved only in the per-RID inner build, so the RID-independent outer-build trimmable typemap
// generator never scanned it. Its only Java Callable Wrapper type, UncaughtExceptionMarshaler,
// therefore had no JCW and no typemap entry -> the runtime ACW is absent from classes.dex and the
// app crashes at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler). A
// reference assembly for the host is now shipped in the SDK pack and fed to the generator. The JCW

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Documentation — This comment claims a "reference assembly for the host is now shipped in the SDK pack and fed to the generator," but that is not what the PR does. The final implementation auto-detects runtime-only assemblies at build time via the nested _ResolveRuntimeOnlyAssembliesForTypeMap resolve pass — the PR description itself states "No hard-coded assembly names, no per-assembly reference-assembly plumbing." This stale wording describes the abandoned ref-assembly approach (cf. the ...-refasm branch name) and will mislead anyone debugging this area later. Please reword it to describe the resolve-and-cache mechanism that the test actually exercises.

Rule: Remove stale comments (Postmortem #59)

// name is CRC-hashed (e.g. `scrc64...UncaughtExceptionMarshaler`), so match on the type name suffix.
Assert.IsTrue (DexUtils.ContainsClass ("UncaughtExceptionMarshaler;", dexFile, AndroidSdkPath),
$"`{dexFile}` should include the UncaughtExceptionMarshaler runtime ACW.");
}

[Test]
public void Build_WithTrimmableTypeMap_DeletesStaleGeneratedJavaSourcesAndCopies ()
{
Expand Down
Loading