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 @@ -176,7 +176,7 @@ internal static void ApplyApplicationProperties (
XElement app,
Dictionary<string, object?> properties,
IReadOnlyList<JavaPeerInfo> allPeers,
Action<string>? warn = null)
Action<int, string>? warn = null)
{
PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true);

Expand All @@ -191,7 +191,7 @@ static void ApplyTypeProperty (
IReadOnlyList<JavaPeerInfo> allPeers,
string propertyName,
string xmlAttrName,
Action<string>? warn)
Action<int, string>? warn)
{
if (app.Attribute (AndroidNs + xmlAttrName) is not null) {
return;
Expand All @@ -213,7 +213,8 @@ static void ApplyTypeProperty (
}
}

warn?.Invoke ($"Could not resolve {propertyName} type '{managedName}' to a Java peer for android:{xmlAttrName}.");
// Code 0 = no XA code assigned; the caller may silently ignore it.
warn?.Invoke (0, $"Could not resolve {propertyName} type '{managedName}' to a Java peer for android:{xmlAttrName}.");
}

internal static void AddInternetPermission (XElement manifest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class ManifestGenerator
"provider",
};

/// <summary>Warning code for library-manifest merge failures (maps to XA4302).</summary>
internal const int LibraryManifestMergeWarningCode = 4302;

int appInitOrder = 2000000000;

public string PackageName { get; set; } = "";
Expand All @@ -41,7 +44,7 @@ class ManifestGenerator
public bool ForceExtractNativeLibs { get; set; }
public string? ManifestPlaceholders { get; set; }
public string? ApplicationJavaClass { get; set; }
public Action<string>? Warn { get; set; }
public Action<int, string>? Warn { get; set; }
public Action<string>? WarnInvalidPlaceholder { get; set; }

/// <summary>
Expand Down Expand Up @@ -194,7 +197,7 @@ void MergeLibraryManifests (XElement manifest)
try {
libDoc = XDocument.Load (path);
} catch (Exception ex) {
Warn?.Invoke ($"Unable to merge library manifest '{path}': {ex.Message}");
Warn?.Invoke (LibraryManifestMergeWarningCode, $"Unable to merge library manifest '{path}': {ex.Message}");
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public interface ITrimmableTypeMapLogger
void LogGeneratedJcwFilesInfo (int sourceCount);
void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName);
void LogManifestReferencedTypeNotFoundWarning (string javaTypeName);
void LogLibraryManifestMergeWarning (string message);
void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
ForceExtractNativeLibs = forceDebuggable,
ManifestPlaceholders = config.ManifestPlaceholders,
ApplicationJavaClass = config.ApplicationJavaClass,
Warn = (code, message) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 💡 PatternsWarn was widened to Action<int, string>? and now multiplexes two unrelated diagnostics through magic codes (0 = ignore, 4302 = surface as XA4302). ManifestGenerator already models exactly this case with a dedicated Action<string>? WarnInvalidPlaceholder; a parallel Action<string>? WarnLibraryManifestMerge would drop the 0 sentinel and this if (code == ...) routing, and keep the two warnings independently wireable.

Separately: the code == 0 path (the AssemblyLevelElementBuilder "Could not resolve ... type" warning) stays silently swallowed on the trimmable path. It was already dropped before this PR, so this is not a regression — but if that suppression is deliberate, a one-line note on why would save the next reader a spelunk.

(Consistency with the existing WarnInvalidPlaceholder callback)

if (code == ManifestGenerator.LibraryManifestMergeWarningCode)
logger.LogLibraryManifestMergeWarning (message);
// Other codes (e.g. unresolvable type properties) are not yet assigned XA codes
// and are intentionally not surfaced here.
},
LibraryManifests = config.LibraryManifests ?? [],
};

var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ public record ManifestConfig (
bool EmbedAssemblies = false,
string? ManifestPlaceholders = null,
string? CheckedBuild = null,
string? ApplicationJavaClass = null);
string? ApplicationJavaClass = null,
IReadOnlyList<string>? LibraryManifests = null);
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@
<Target Name="_GenerateTrimmableTypeMap"
Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(DesignTimeBuild)' != 'true' and '@(ReferencePath->Count())' != '0' and '$(_OuterIntermediateOutputPath)' == '' "
AfterTargets="CoreCompile"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
DependsOnTargets="_GetLibraryImports"
Inputs="@(ReferencePath);@(PrivateSdkAssemblies);@(FrameworkAssemblies);@(ExtractedManifestDocuments);$(IntermediateOutputPath)$(TargetFileName);$(_AndroidManifestAbs);$(_AndroidBuildPropertiesCache)"
Outputs="$(_TrimmableTypeMapOutputStamp)">

<ItemGroup>
Expand All @@ -112,6 +113,9 @@
<_TypeMapFrameworkAssemblies Include="@(FrameworkAssemblies)" />
<_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)"
Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" />
<!-- Library (.aar) manifests to merge into the app manifest. Only on the legacy manifest
merger path; manifestmerger.jar merges these downstream in the _ManifestMerger target. -->
<_MergedManifestDocuments Condition=" '$(AndroidManifestMerger)' == 'legacy' " Include="@(ExtractedManifestDocuments)" />
</ItemGroup>

<GenerateTrimmableTypeMap
Expand All @@ -123,6 +127,7 @@
TargetFrameworkVersion="$(TargetFrameworkVersion)"
ManifestTemplate="$(_AndroidManifestAbs)"
MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml"
MergedManifestDocuments="@(_MergedManifestDocuments)"
PackageName="$(_AndroidPackage)"
ApplicationLabel="$(_ApplicationLabel)"
VersionCode="$(_AndroidVersionCode)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName);
public void LogLibraryManifestMergeWarning (string message) =>
log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message);
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down Expand Up @@ -79,6 +81,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN

public string? MergedAndroidManifestOutput { get; set; }

/// <summary>
/// Absolute paths to extracted library (.aar) <c>AndroidManifest.xml</c> documents that must be
/// merged into the application manifest. Only populated on the legacy manifest-merger path;
/// <c>manifestmerger.jar</c> handles this downstream in the <c>_ManifestMerger</c> target.
/// </summary>
public string []? MergedManifestDocuments { get; set; }

public string? PackageName { get; set; }
public string? ApplicationLabel { get; set; }
public string? VersionCode { get; set; }
Expand Down Expand Up @@ -184,7 +193,8 @@ public override bool RunTask ()
EmbedAssemblies: EmbedAssemblies,
ManifestPlaceholders: ManifestPlaceholders,
CheckedBuild: CheckedBuild,
ApplicationJavaClass: ApplicationJavaClass);
ApplicationJavaClass: ApplicationJavaClass,
LibraryManifests: MergedManifestDocuments);
}

var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1436,4 +1436,89 @@ public void AssemblyLevel_ApplicationManageSpaceActivity ()
Assert.NotNull (app);
Assert.Equal ("com.example.app.ManageActivity", (string?)app?.Attribute (AndroidNs + "manageSpaceActivity"));
}

[Fact]
public void LibraryManifests_MergedWithApplicationIdAndRelativeNamesResolved ()
{
// Mirrors ManifestTest.MergeLibraryManifest: a library (.aar) manifest is merged into the
// app manifest, ${applicationId} resolves to the app package, and relative component names
// (".Type") are qualified with the library's own package attribute.
var libManifest = Path.Combine (Path.GetTempPath (), $"lib-manifest-{Path.GetRandomFileName ()}.xml");
File.WriteAllText (libManifest, """
<?xml version='1.0'?>
<manifest xmlns:android='http://schemas.android.com/apk/res/android' package='com.xamarin.test'>
<uses-sdk android:minSdkVersion='16'/>
<permission android:name='${applicationId}.permission.C2D_MESSAGE' android:protectionLevel='signature' />
<application>
<activity android:name='.signin.internal.SignInHubActivity' />
<provider
android:authorities='${applicationId}.FacebookInitProvider'
android:name='.internal.FacebookInitProvider'
android:exported='false' />
<meta-data android:name='android.support.VERSION' android:value='25.4.0' />
<meta-data android:name='android.support.VERSION' android:value='25.4.0' />
</application>
</manifest>
""");
try {
var gen = CreateDefaultGenerator ();
gen.PackageName = "com.xamarin.manifest";
gen.LibraryManifests = [libManifest];
var template = ParseTemplate ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.xamarin.manifest">
<uses-sdk />
<application android:label="App" />
</manifest>
""");

var doc = GenerateAndLoad (gen, template: template);

// ${applicationId} resolves to the app package on the merged permission.
var permission = doc.Root?.Elements ("permission")
.FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.manifest.permission.C2D_MESSAGE");
Assert.NotNull (permission);

var app = doc.Root?.Element ("application");
Assert.NotNull (app);

// Relative ".Type" names are qualified with the library package (com.xamarin.test).
var activity = app?.Elements ("activity")
.FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.test.signin.internal.SignInHubActivity");
Assert.NotNull (activity);

var provider = app?.Elements ("provider")
.FirstOrDefault (e => (string?) e.Attribute (AttName) == "com.xamarin.test.internal.FacebookInitProvider");
Assert.NotNull (provider);
// authorities uses ${applicationId} -> app package.
Assert.Equal ("com.xamarin.manifest.FacebookInitProvider", (string?) provider?.Attribute (AndroidNs + "authorities"));

// The two identical meta-data elements collapse to a single element.
var versionMeta = app?.Elements ("meta-data")
.Where (e => (string?) e.Attribute (AttName) == "android.support.VERSION")
.ToList ();
Assert.NotNull (versionMeta);
Assert.Single (versionMeta);
} finally {
File.Delete (libManifest);
}
}

[Fact]
public void LibraryManifests_MissingFileIgnored ()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 💡 Testing — Good coverage of the successful merge and the missing-file skip. The remaining gap is the merge-failure branch: a malformed library manifest that makes XDocument.Load throw and routes through Warn (LibraryManifestMergeWarningCode, ...)XA4302. Surfacing that warning is the entire reason for the Action<int, string> refactor in this PR, so a test that points LibraryManifests at an invalid .xml, sets gen.Warn to capture the callback, and asserts the merge-failure message fires would lock in the new routing.

(Rule: Test error paths, not just the happy path)

{
// A non-existent library manifest path is skipped without throwing.
var gen = CreateDefaultGenerator ();
gen.LibraryManifests = [Path.Combine (Path.GetTempPath (), $"does-not-exist-{Path.GetRandomFileName ()}.xml")];
var template = ParseTemplate ("""
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.app">
<uses-sdk />
<application android:label="App" />
</manifest>
""");

var doc = GenerateAndLoad (gen, template: template);
Assert.NotNull (doc.Root?.Element ("application"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string ma
logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional.");
public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) =>
warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type.");
public void LogLibraryManifestMergeWarning (string message) =>
warnings?.Add (message);
public void LogUnresolvableJavaPeerSkippedWarning (
string managedTypeName,
string assemblyName,
Expand Down
Loading