diff --git a/build-tools/automation/yaml-templates/apk-instrumentation.yaml b/build-tools/automation/yaml-templates/apk-instrumentation.yaml index 81468c6c681..3ab581056f5 100644 --- a/build-tools/automation/yaml-templates/apk-instrumentation.yaml +++ b/build-tools/automation/yaml-templates/apk-instrumentation.yaml @@ -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 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index e382556c748..5d4957fc212 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -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", @@ -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); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 5b2d6204eb5..e83e7120ff6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -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 ComponentElementNames = new (StringComparer.Ordinal) { + "application", + "activity", + "instrumentation", + "service", + "receiver", + "provider", + }; int appInitOrder = 2000000000; @@ -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 ( - app.Descendants ().Select (a => (string?)a.Attribute (AttName)).OfType ()); + app.Descendants () + .Where (IsComponentElement) + .Select (a => (string?) a.Attribute (AttName)) + .OfType (), + StringComparer.Ordinal); // Add components from scanned types foreach (var peer in allPeers) { @@ -73,7 +89,7 @@ class ManifestGenerator } if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) { - ComponentElementBuilder.AddInstrumentation (manifest, peer); + ComponentElementBuilder.AddInstrumentation (manifest, peer, PackageName); continue; } @@ -130,6 +146,58 @@ XDocument CreateDefaultManifest () new XAttribute ("package", PackageName))); } + /// + /// 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. + /// + void RewriteCompatNames (XElement manifest, IReadOnlyList allPeers) + { + // Build mapping: fully-qualified compat Java name → CRC Java name + var compatToCrc = new Dictionary (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; + } + } + } + + 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); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs new file mode 100644 index 00000000000..d44f750075f --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestNameResolver.cs @@ -0,0 +1,20 @@ +using System; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +static class ManifestNameResolver +{ + /// + /// 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). + /// + 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, + }; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 78ae91c7cfb..5dc065f59b7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -165,7 +165,7 @@ internal void RootManifestReferencedTypes (List 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") { @@ -309,17 +309,4 @@ static void AddJniLookupNames (Dictionary> peersByDot } } - /// - /// 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). - /// - 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, - }; - } } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index d38aad44fb9..f84d03d1edd 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -11,7 +11,11 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps - <_TypeMapBaseOutputDir>$(IntermediateOutputPath) + + <_TypeMapBaseOutputDir Condition=" '$(_OuterIntermediateOutputPath)' != '' ">$(_OuterIntermediateOutputPath) + <_TypeMapBaseOutputDir Condition=" '$(_TypeMapBaseOutputDir)' == '' ">$(IntermediateOutputPath) <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 520e85fab23..f7c7b08b239 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -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)" /> { + ["Label"] = "My Test", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + + var instrumentation = doc.Root?.Element ("instrumentation"); + Assert.NotNull (instrumentation); + + // targetPackage should default to the app's PackageName + Assert.Equal ("com.example.app", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); + } + + [Fact] + public void CompatNames_RewrittenToCrc () + { + var gen = CreateDefaultGenerator (); + + // Template uses compat names + var template = ParseTemplate (""" + + + + + + """); + + // Peer has CRC JavaName but compat CompatJniName + var appPeer = new JavaPeerInfo { + JavaName = "crc64abc123/MyApp", + CompatJniName = "com/example/app/MyApp", + ManagedTypeName = "Com.Example.App.MyApp", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MyApp", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Application, + Properties = new Dictionary (), + }, + }; + var activityPeer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [appPeer, activityPeer], template: template); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("crc64abc123.MyApp", (string?)app?.Attribute (AttName)); + + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + Assert.Equal ("crc64def456.MainActivity", (string?)activity?.Attribute (AttName)); + } + + [Fact] + public void CompatNames_RewrittenToCrc_RelativeDotForm () + { + var gen = CreateDefaultGenerator (); + + // Template uses the ".Type" relative form that Android resolves against the + // manifest package. RewriteCompatNames must resolve it before the compat lookup. + var template = ParseTemplate (""" + + + + + + """); + + var appPeer = new JavaPeerInfo { + JavaName = "crc64abc123/MyApp", + CompatJniName = "com/example/app/MyApp", + ManagedTypeName = "Com.Example.App.MyApp", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MyApp", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Application, + Properties = new Dictionary (), + }, + }; + var activityPeer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [appPeer, activityPeer], template: template); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("crc64abc123.MyApp", (string?)app?.Attribute (AttName)); + + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + Assert.Equal ("crc64def456.MainActivity", (string?)activity?.Attribute (AttName)); + } + + [Fact] + public void CompatNames_RewrittenToCrc_UsesManifestPackageAttribute () + { + var gen = CreateDefaultGenerator (); + gen.PackageName = "com.other.app"; + + var template = ParseTemplate (""" + + + + + + """); + + var appPeer = new JavaPeerInfo { + JavaName = "crc64abc123/MyApp", + CompatJniName = "com/example/app/MyApp", + ManagedTypeName = "Com.Example.App.MyApp", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MyApp", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Application, + Properties = new Dictionary (), + }, + }; + var activityPeer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [appPeer, activityPeer], template: template); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("crc64abc123.MyApp", (string?)app?.Attribute (AttName)); + + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + Assert.Equal ("crc64def456.MainActivity", (string?)activity?.Attribute (AttName)); + } + + [Fact] + public void CompatNames_RewrittenToCrc_UnqualifiedForm () + { + var gen = CreateDefaultGenerator (); + + // Template uses the bare "Type" form (no dot) that Android also resolves + // against the manifest package. + var template = ParseTemplate (""" + + + + + + """); + + var appPeer = new JavaPeerInfo { + JavaName = "crc64abc123/MyApp", + CompatJniName = "com/example/app/MyApp", + ManagedTypeName = "Com.Example.App.MyApp", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MyApp", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Application, + Properties = new Dictionary (), + }, + }; + var activityPeer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [appPeer, activityPeer], template: template); + + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("crc64abc123.MyApp", (string?)app?.Attribute (AttName)); + + var activity = app?.Element ("activity"); + Assert.NotNull (activity); + Assert.Equal ("crc64def456.MainActivity", (string?)activity?.Attribute (AttName)); + } + + [Fact] + public void CompatNames_RelativeForm_NotDuplicated () + { + // Regression: when a template uses a relative name (".MainActivity"), the + // rewrite must resolve it to the fully-qualified compat name before looking + // up the CRC mapping. If the relative name slipped through unchanged, the + // duplicate-detection in ManifestGenerator would miss it and emit a second + // CRC-named alongside the existing relative-named entry. + var gen = CreateDefaultGenerator (); + var template = ParseTemplate (""" + + + + + + """); + + var peer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary { ["Label"] = "New Label" }, + }, + }; + + var doc = GenerateAndLoad (gen, [peer], template: template); + var activities = doc.Root?.Element ("application")?.Elements ("activity").ToList (); + + Assert.NotNull (activities); + Assert.Single (activities!); + Assert.Equal ("crc64def456.MainActivity", (string?)activities [0].Attribute (AttName)); + // Existing android:label from the template is preserved (duplicate detection worked). + Assert.Equal ("Existing", (string?)activities [0].Attribute (AndroidNs + "label")); + } + + [Fact] + public void CompatNames_MetadataName_NotRewritten_AndDoesNotSuppressActivity () + { + var gen = CreateDefaultGenerator (); + var template = ParseTemplate (""" + + + + + + """); + + var peer = new JavaPeerInfo { + JavaName = "crc64def456/MainActivity", + CompatJniName = "com/example/app/MainActivity", + ManagedTypeName = "Com.Example.App.MainActivity", + ManagedTypeNamespace = "Com.Example.App", + ManagedTypeShortName = "MainActivity", + AssemblyName = "TestApp", + ComponentAttribute = new ComponentInfo { + Kind = ComponentKind.Activity, + Properties = new Dictionary (), + }, + }; + + var doc = GenerateAndLoad (gen, [peer], template: template); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + + var metadata = app?.Element ("meta-data"); + Assert.NotNull (metadata); + Assert.Equal (".MainActivity", (string?)metadata?.Attribute (AttName)); + + var activities = app?.Elements ("activity").ToList (); + Assert.NotNull (activities); + Assert.Single (activities!); + Assert.Equal ("crc64def456.MainActivity", (string?)activities [0].Attribute (AttName)); + } + [Fact] public void RuntimeProvider_Added () {