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 @@ -47,7 +47,7 @@ steps:
configuration: ${{ parameters.buildConfiguration }}
xaSourcePath: ${{ parameters.xaSourcePath }}
project: ${{ parameters.project }}
arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore
arguments: -t:Clean -c ${{ parameters.configuration }} --no-restore ${{ parameters.extraBuildArgs }}
displayName: Clean ${{ parameters.testName }}
condition: ${{ parameters.condition }}
continueOnError: false
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer)
PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings);
}

internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer)
internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer, string packageName)
{
string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName);
var element = new XElement ("instrumentation",
Expand All @@ -177,6 +177,11 @@ internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer)
}
PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings);

// Default targetPackage to the app package name, matching legacy ManifestDocument behavior
if (element.Attribute (AndroidNs + "targetPackage") is null) {
element.SetAttributeValue (AndroidNs + "targetPackage", packageName);
}

manifest.Add (element);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ class ManifestGenerator
static readonly XNamespace AndroidNs = ManifestConstants.AndroidNs;
static readonly XName AttName = ManifestConstants.AttName;
static readonly char [] PlaceholderSeparators = [';'];
static readonly HashSet<string> ComponentElementNames = new (StringComparer.Ordinal) {
"application",
"activity",
"instrumentation",
"service",
"receiver",
"provider",
};

int appInitOrder = 2000000000;

Expand Down Expand Up @@ -52,13 +60,21 @@ class ManifestGenerator
EnsureManifestAttributes (manifest);
var app = EnsureApplicationElement (manifest);

// Rewrite compat JNI names in the template to CRC names BEFORE collecting
// existing types, so the duplicate check works correctly.
RewriteCompatNames (manifest, allPeers);

// Apply assembly-level [Application] properties
if (assemblyInfo.ApplicationProperties is not null) {
AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties, allPeers, Warn);
}

var existingTypes = new HashSet<string> (
app.Descendants ().Select (a => (string?)a.Attribute (AttName)).OfType<string> ());
app.Descendants ()
.Where (IsComponentElement)
.Select (a => (string?) a.Attribute (AttName))
.OfType<string> (),
StringComparer.Ordinal);

// Add components from scanned types
foreach (var peer in allPeers) {
Expand All @@ -73,7 +89,7 @@ class ManifestGenerator
}

if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) {
ComponentElementBuilder.AddInstrumentation (manifest, peer);
ComponentElementBuilder.AddInstrumentation (manifest, peer, PackageName);
continue;
}

Expand Down Expand Up @@ -130,6 +146,58 @@ XDocument CreateDefaultManifest ()
new XAttribute ("package", PackageName)));
}

/// <summary>
/// Manifest templates may use compat JNI names (e.g., "android.apptests.App")
/// but the trimmable path generates JCWs with CRC-based names (e.g., "crc64.../App").
/// This method rewrites any compat name references to the actual JCW name so the
/// Android runtime can find the class.
/// </summary>
void RewriteCompatNames (XElement manifest, IReadOnlyList<JavaPeerInfo> allPeers)
{
// Build mapping: fully-qualified compat Java name → CRC Java name
var compatToCrc = new Dictionary<string, string> (allPeers.Count, StringComparer.Ordinal);
foreach (var peer in allPeers) {
string javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName);
string compatName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName);
if (javaName != compatName) {
compatToCrc [compatName] = javaName;
}
}

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

// Rewrite android:name attributes throughout the manifest. Android allows
// android:name to be specified as:
// - fully qualified ("com.example.app.MainActivity")
// - relative to the manifest package, starting with '.' (".MainActivity")
// - bare, with no '.' at all ("MainActivity"), also relative to the package
// Resolve to the fully-qualified form before the lookup, then write the CRC
// name back so duplicate detection later in the pipeline works correctly.
var packageName = (string?) manifest.Attribute ("package") ?? "";

foreach (var element in manifest.DescendantsAndSelf ()) {
if (!IsComponentElement (element)) {
continue;
}

var nameAttr = element.Attribute (AttName);
if (nameAttr is null) {
continue;
}
var resolved = ManifestNameResolver.Resolve (nameAttr.Value, packageName);
if (compatToCrc.TryGetValue (resolved, out var crcName)) {
nameAttr.Value = crcName;
}
}
Comment on lines +180 to +193
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 💡 CorrectnessDescendantsAndSelf() iterates all manifest elements, so this rewrites android:name on <meta-data>, <action>, <category>, <permission>, <uses-permission>, <uses-feature>, etc. — not just component elements where android:name denotes a Java class.

While a collision is unlikely in practice (the compatToCrc dictionary only contains actual peer class names), scoping to component element types would be more defensive:

static readonly HashSet<string> ComponentElements = new (StringComparer.Ordinal) {
	"application", "activity", "service", "receiver", "provider", "instrumentation",
};

// ...
foreach (var element in manifest.DescendantsAndSelf ()) {
	if (!ComponentElements.Contains (element.Name.LocalName)) {
		continue;
	}
	// ...
}

This matches the scope that RootManifestReferencedTypes uses in TrimmableTypeMapGenerator.

}
Comment thread
simonrozsival marked this conversation as resolved.

static bool IsComponentElement (XElement element)
{
return element.Name.NamespaceName.Length == 0 && ComponentElementNames.Contains (element.Name.LocalName);
}

void EnsureManifestAttributes (XElement manifest)
{
manifest.SetAttributeValue (XNamespace.Xmlns + "android", AndroidNs.NamespaceName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;

namespace Microsoft.Android.Sdk.TrimmableTypeMap;

static class ManifestNameResolver
{
/// <summary>
/// Resolves an <c>android:name</c> 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>
public static string Resolve (string name, string packageName)
{
return name switch {
_ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name,
_ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name,
_ => name,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ internal void RootManifestReferencedTypes (List<JavaPeerInfo> allPeers, XDocumen
case "provider":
var name = (string?) element.Attribute (attName);
if (name is not null) {
var resolvedName = ResolveManifestClassName (name, packageName);
var resolvedName = ManifestNameResolver.Resolve (name, packageName);
componentNames.Add (resolvedName);

if (element.Name.LocalName is "application" or "instrumentation") {
Expand Down Expand Up @@ -309,17 +309,4 @@ static void AddJniLookupNames (Dictionary<string, List<JavaPeerInfo>> peersByDot
}
}

/// <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)
{
return name switch {
_ when name.StartsWith (".", StringComparison.Ordinal) => packageName + name,
_ when name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty () => packageName + "." + name,
_ => name,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@

<PropertyGroup>
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps</_TypeMapAssemblyName>
<_TypeMapBaseOutputDir>$(IntermediateOutputPath)</_TypeMapBaseOutputDir>
<!-- Use the outer IntermediateOutputPath when available (inner per-RID builds set
_OuterIntermediateOutputPath) so that generated manifests/JCWs are written
where the packaging phase expects them. -->
<_TypeMapBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(_OuterIntermediateOutputPath)</_TypeMapBaseOutputDir>
<_TypeMapBaseOutputDir Condition=" '$(_TypeMapBaseOutputDir)' == '' ">$(IntermediateOutputPath)</_TypeMapBaseOutputDir>
<_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/'))</_TypeMapBaseOutputDir>
<_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/</_TypeMapOutputDirectory>
<_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java</_TypeMapJavaOutputDirectory>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,7 @@ because xbuild doesn't support framework reference assemblies.
<_PropertyCacheItems Include="AndroidManifestPlaceholders=$(AndroidManifestPlaceholders)" />
<_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" />
<_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" />
<_PropertyCacheItems Include="_AndroidTypeMapImplementation=$(_AndroidTypeMapImplementation)" />
</ItemGroup>
<WriteLinesToFile
File="$(_AndroidBuildPropertiesCache)"
Expand Down
Loading