Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
be31f95
[TrimmableTypeMap] Root manifest-referenced types as unconditional
simonrozsival Mar 27, 2026
c0126d7
Address PR review: fix manifest name matching and null guard
simonrozsival Mar 31, 2026
023a765
Document IsUnconditional mutation contract: only set to true
simonrozsival Apr 1, 2026
431f59e
Fix RootManifestReferencedTypes: remove IO, add relative name resolution
simonrozsival Apr 1, 2026
ade15c9
[TrimmableTypeMap] Root Application and Instrumentation types
simonrozsival Apr 7, 2026
6ec6832
[TrimmableTypeMap] Match compat names in manifest rooting
simonrozsival Apr 7, 2026
4568e1a
[TrimmableTypeMap] Add coded warning for unresolved types
simonrozsival Apr 7, 2026
e9d0850
[TrimmableTypeMap] Track manifest-related target inputs
simonrozsival Apr 7, 2026
483dc54
[TrimmableTypeMap] Use ILogger for generator diagnostics
simonrozsival Apr 7, 2026
ff76bf5
[TrimmableTypeMap] Use switch expression for manifest names
simonrozsival Apr 7, 2026
f26a757
Revert "[TrimmableTypeMap] Use ILogger for generator diagnostics"
simonrozsival Apr 7, 2026
1fcdb5b
Revert "[TrimmableTypeMap] Track manifest-related target inputs"
simonrozsival Apr 7, 2026
b9398e9
[TrimmableTypeMap] Reuse existing manifest name helpers
simonrozsival Apr 7, 2026
9b668cf
[TrimmableTypeMap] Merge manifest matching tests into theory
simonrozsival Apr 7, 2026
b89d96f
[TrimmableTypeMap] Match LogCodedWarning callback shape
simonrozsival Apr 7, 2026
cbe4b30
[TrimmableTypeMap] Fix theory package name
simonrozsival Apr 7, 2026
9449cf6
[TrimmableTypeMap] Add typed logger interface
simonrozsival Apr 7, 2026
1940373
Drop generic log callback from type map generator
simonrozsival Apr 7, 2026
3bd5710
Restore typed typemap info logging
simonrozsival Apr 7, 2026
860901d
Fix manifest-rooting resolution and deferred registration
simonrozsival Apr 7, 2026
a10afa8
Add host test for placeholder-based typemap rooting
simonrozsival Apr 7, 2026
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
1 change: 1 addition & 0 deletions Documentation/docs-mobile/messages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
+ [XA4247](xa4247.md): Could not resolve POM file for artifact '{artifact}'.
+ [XA4248](xa4248.md): Could not find NuGet package '{nugetId}' version '{version}' in lock file. Ensure NuGet Restore has run since this `<PackageReference>` was added.
+ [XA4235](xa4249.md): Maven artifact specification '{artifact}' is invalid. The correct format is 'group_id:artifact_id:version'.
+ [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type.
+ XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI.
+ [XA4301](xa4301.md): Apk already contains the item `xxx`.
+ [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex}
Expand Down
33 changes: 33 additions & 0 deletions Documentation/docs-mobile/messages/xa4250.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: .NET for Android warning XA4250
description: XA4250 warning code
ms.date: 04/07/2026
f1_keywords:
- "XA4250"
---

# .NET for Android warning XA4250

## Example message

Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.

```text
warning XA4250: Manifest-referenced type 'com.example.MainActivity' was not found in any scanned assembly. It may be a framework type.
```

## Issue

The build found a type name in `AndroidManifest.xml`, but it could not match that name to any Java peer discovered in the app's managed assemblies.

This can be expected for framework-provided types, but it can also indicate that the manifest entry does not match the name generated for a managed Android component.

## Solution

If the manifest entry refers to an Android framework type, this warning can usually be ignored.

Otherwise:

1. Verify the `android:name` value in the manifest.
2. Ensure the managed type is included in the app build.
3. Check for namespace, `[Register]`, or nested-type naming mismatches between the manifest and the managed type.
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,7 @@ class ManifestGenerator
}

// Apply manifest placeholders
string? placeholders = ManifestPlaceholders;
if (placeholders is not null && placeholders.Length > 0) {
ApplyPlaceholders (doc, placeholders);
}
ApplyPlaceholders (doc, ManifestPlaceholders);

return (doc, providerNames);
}
Expand Down Expand Up @@ -250,8 +247,12 @@ XElement CreateRuntimeProvider (string name, string? processName, int initOrder)
/// Replaces ${key} placeholders in all attribute values throughout the document.
/// Placeholder format: "key1=value1;key2=value2"
/// </summary>
static void ApplyPlaceholders (XDocument doc, string placeholders)
internal static void ApplyPlaceholders (XDocument doc, string? placeholders)
{
if (placeholders.IsNullOrEmpty ()) {
return;
}

var replacements = new Dictionary<string, string> (StringComparer.Ordinal);
foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) {
var eqIndex = entry.IndexOf ('=');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Microsoft.Android.Sdk.TrimmableTypeMap;

public interface ITrimmableTypeMapLogger
{
void LogNoJavaPeerTypesFound ();

void LogJavaPeerScanInfo (int assemblyCount, int peerCount);

void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount);

void LogDeferredRegistrationTypesInfo (int typeCount);

void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount);

void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount);

void LogGeneratedTypeMapAssembliesInfo (int assemblyCount);

void LogGeneratedJcwFilesInfo (int sourceCount);

void LogUnresolvedTypeWarning (string name);

void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,22 @@ public sealed record JavaPeerInfo
/// Types with component attributes ([Activity], [Service], etc.),
/// custom views from layout XML, or manifest-declared components
/// are unconditionally preserved (not trimmable).
/// May be set to <c>true</c> after scanning when the manifest references a type
/// that the scanner did not mark as unconditional. Should only ever be set
/// to <c>true</c>, never back to <c>false</c>.
/// </summary>
public bool IsUnconditional { get; init; }
public bool IsUnconditional { get; set; }

/// <summary>
/// True for Application and Instrumentation types. These types cannot call
/// <c>registerNatives</c> in their static initializer because the native library
/// (<c>libmonodroid.so</c>) is not loaded until after the Application class is instantiated.
/// Registration is deferred to <c>ApplicationRegistration.registerApplications()</c>.
/// This may also be set after scanning when a type is only discovered from
/// manifest <c>android:name</c> usage on <c>&lt;application&gt;</c> or
/// <c>&lt;instrumentation&gt;</c>.
/// </summary>
public bool CannotRegisterInStaticConstructor { get; init; }
public bool CannotRegisterInStaticConstructor { get; set; }

/// <summary>
/// Marshal methods: methods with [Register(name, sig, connector)], [Export], or
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;

public class TrimmableTypeMapGenerator
{
readonly Action<string> log;
readonly ITrimmableTypeMapLogger? logger;

public TrimmableTypeMapGenerator (Action<string> log)
public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger? logger = null)
{
this.log = log ?? throw new ArgumentNullException (nameof (log));
this.logger = logger;
}

/// <summary>
Expand All @@ -34,15 +34,17 @@ public TrimmableTypeMapResult Execute (

var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies);
if (allPeers.Count == 0) {
log ("No Java peer types found, skipping typemap generation.");
logger?.LogNoJavaPeerTypesFound ();
return new TrimmableTypeMapResult ([], [], allPeers);
}

RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig));

var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion);
var jcwPeers = allPeers.Where (p =>
!frameworkAssemblyNames.Contains (p.AssemblyName)
|| p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList ();
log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total).");
logger?.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count);
var generatedJavaSources = GenerateJcwJavaSources (jcwPeers);

// Collect Application/Instrumentation types that need deferred registerNatives
Expand All @@ -51,7 +53,7 @@ public TrimmableTypeMapResult Execute (
.Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName))
.ToList ();
if (appRegTypes.Count > 0) {
log ($"Found {appRegTypes.Count} Application/Instrumentation types for deferred registration.");
logger?.LogDeferredRegistrationTypesInfo (appRegTypes.Count);
}

var manifest = manifestConfig is not null
Expand Down Expand Up @@ -102,7 +104,7 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
using var scanner = new JavaPeerScanner ();
var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types.");
logger?.LogJavaPeerScanInfo (assemblies.Count, peers.Count);
return (peers, manifestInfo);
}

Expand All @@ -120,23 +122,172 @@ List<GeneratedAssembly> GenerateTypeMapAssemblies (List<JavaPeerInfo> allPeers,
generator.Generate (peers, stream, assemblyName);
stream.Position = 0;
generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream));
log ($" {assemblyName}: {peers.Count} types");
logger?.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count);
}
var rootStream = new MemoryStream ();
var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion);
rootGenerator.Generate (perAssemblyNames, rootStream);
rootStream.Position = 0;
generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream));
log ($" Root: {perAssemblyNames.Count} per-assembly refs");
log ($"Generated {generatedAssemblies.Count} typemap assemblies.");
logger?.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count);
logger?.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count);
return generatedAssemblies;
}

List<GeneratedJavaSource> GenerateJcwJavaSources (List<JavaPeerInfo> allPeers)
{
var jcwGenerator = new JcwJavaSourceGenerator ();
var sources = jcwGenerator.GenerateContent (allPeers);
log ($"Generated {sources.Count} JCW Java source files.");
logger?.LogGeneratedJcwFilesInfo (sources.Count);
return sources.ToList ();
}

internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocument? doc)
{
if (doc?.Root is not { } root) {
return;
}

XNamespace androidNs = "http://schemas.android.com/apk/res/android";
XName attName = androidNs + "name";
var packageName = (string?) root.Attribute ("package") ?? "";

var componentNames = new HashSet<string> (StringComparer.Ordinal);
var deferredRegistrationNames = new HashSet<string> (StringComparer.Ordinal);
foreach (var element in root.Descendants ()) {
switch (element.Name.LocalName) {
case "application":
case "activity":
case "instrumentation":
case "service":
case "receiver":
case "provider":
var name = (string?) element.Attribute (attName);
if (name is not null) {
var resolvedName = ResolveManifestClassName (name, packageName);
componentNames.Add (resolvedName);

if (element.Name.LocalName is "application" or "instrumentation") {
deferredRegistrationNames.Add (resolvedName);
}
}
break;
}
}

if (componentNames.Count == 0) {
return;
}

// Build lookup by both Java and compat dot-names. Keep '$' for nested types,
// because manifests commonly use '$', but also include the Java source form.
var peersByDotName = new Dictionary<string, List<JavaPeerInfo>> (StringComparer.Ordinal);
foreach (var peer in allPeers) {
AddJniLookupNames (peersByDotName, peer.JavaName, peer);

if (peer.CompatJniName != peer.JavaName) {
AddJniLookupNames (peersByDotName, peer.CompatJniName, peer);
}
}

foreach (var name in componentNames) {
if (peersByDotName.TryGetValue (name, out var peers)) {
foreach (var peer in peers) {
if (deferredRegistrationNames.Contains (name)) {
peer.CannotRegisterInStaticConstructor = true;
}

if (!peer.IsUnconditional) {
peer.IsUnconditional = true;
logger?.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName);
}
}
} else {
logger?.LogUnresolvedTypeWarning (name);
}
}
}

static void AddPeerByDotName (Dictionary<string, List<JavaPeerInfo>> peersByDotName, string dotName, JavaPeerInfo peer)
{
if (!peersByDotName.TryGetValue (dotName, out var list)) {
list = [];
peersByDotName [dotName] = list;
}

list.Add (peer);
}

static XDocument? PrepareManifestForRooting (XDocument? manifestTemplate, ManifestConfig? manifestConfig)
{
if (manifestTemplate is null && manifestConfig is null) {
return null;
}

var doc = manifestTemplate is not null
? new XDocument (manifestTemplate)
: new XDocument (
new XElement (
"manifest",
new XAttribute (XNamespace.Xmlns + "android", ManifestConstants.AndroidNs.NamespaceName)));

if (doc.Root is not { } root) {
return doc;
}

if (manifestConfig is null) {
return doc;
}

if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) {
root.SetAttributeValue ("package", manifestConfig.PackageName);
}

ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders);

if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) {
var app = root.Element ("application");
if (app is null) {
app = new XElement ("application");
root.Add (app);
}

if (app.Attribute (ManifestConstants.AttName) is null) {
app.SetAttributeValue (ManifestConstants.AttName, manifestConfig.ApplicationJavaClass);
}
}

return doc;
}

static void AddJniLookupNames (Dictionary<string, List<JavaPeerInfo>> peersByDotName, string jniName, JavaPeerInfo peer)
{
var simpleName = JniSignatureHelper.GetJavaSimpleName (jniName);
var packageName = JniSignatureHelper.GetJavaPackageName (jniName);
var manifestName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName;
AddPeerByDotName (peersByDotName, manifestName, peer);

var javaSourceName = JniSignatureHelper.JniNameToJavaName (jniName);
if (javaSourceName != manifestName) {
AddPeerByDotName (peersByDotName, javaSourceName, peer);
}
}

/// <summary>
/// Resolves an android:name value to a fully-qualified class name.
/// Names starting with '.' are relative to the package. Names with no '.' at all
/// are also treated as relative (Android tooling convention).
/// </summary>
static string ResolveManifestClassName (string name, string packageName)
{
if (name.StartsWith (".", StringComparison.Ordinal)) {
return packageName + name;
}

if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) {
return packageName + "." + name;
}

return name;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,11 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS
<value>Maven artifact specification '{0}' is invalid. The correct format is 'group_id:artifact_id:version'.</value>
<comment>The following are literal names and should not be translated: Maven, group_id, artifact_id
{0} - A Maven artifact specification</comment>
</data>
<data name="XA4250" xml:space="preserve">
<value>Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.</value>
<comment>The following are literal names and should not be translated: Manifest, framework.
{0} - Java type name from AndroidManifest.xml</comment>
</data>
<data name="XA0142" xml:space="preserve">
<value>Command '{0}' failed.\n{1}</value>
Expand Down
Loading
Loading