diff --git a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs index 9528a5097e8..8c45435a8ce 100644 --- a/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs +++ b/src/Mono.Android/Android.Runtime/ResourceDesignerAttribute.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Reflection; namespace Android.Runtime { @@ -17,5 +18,34 @@ public ResourceDesignerAttribute ( public string FullName { get; set; } public bool IsApplication { get; set; } + + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + internal Type? GetResourceTypeFromAssembly (Assembly assembly) + { + // Primary scenario: FullName is an assembly-qualified name + if (FullName.IndexOf (',') > 0) { + var resourceType = Type.GetType (FullName); + if (resourceType is not null) { + if (resourceType.Assembly == assembly) { + return resourceType; + } else { + return null; // no need to fallback to the assembly lookup if the type is found but in a different assembly + } + } + } + + // Fallback for when the type name is not an assembly-qualified name. If a non-AQN is passed to the constructor, + // the trimmer will report the following warning: + // + // warning IL2122: Type 'XYZ' is not assembly qualified. Type name strings used for dynamically accessing a type should be assembly qualified + // + // Since there is already a build warning, we can suppress the fallback warning. + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")] + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Fallback for non-assembly-qualified type names. Warning is already emitted for non-AQN type names used in the constructor.")] + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] + static Type? FallbackAssemblyGetType (Assembly a, string name) => a.GetType (name); + + return FallbackAssemblyGetType (assembly, FullName); + } } } diff --git a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs index 5ef1e6255df..0275fbc6195 100644 --- a/src/Mono.Android/Android.Runtime/ResourceIdManager.cs +++ b/src/Mono.Android/Android.Runtime/ResourceIdManager.cs @@ -34,16 +34,9 @@ public static void UpdateIdValues () [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] static Type? GetResourceTypeFromAssembly (Assembly assembly) { - const string rootAssembly = "Resources.UpdateIdValues() methods are trimmed away by the LinkResourceDesigner trimmer step. This codepath is not called unless $(AndroidUseDesignerAssembly) is disabled."; - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = rootAssembly)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = rootAssembly)] - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods)] - static Type AssemblyGetType (Assembly a, string name) => a.GetType (name); - foreach (var customAttribute in assembly.GetCustomAttributes (typeof (ResourceDesignerAttribute), true)) { if (customAttribute is ResourceDesignerAttribute resourceDesignerAttribute && resourceDesignerAttribute.IsApplication) { - var type = AssemblyGetType (assembly, resourceDesignerAttribute.FullName); + var type = resourceDesignerAttribute.GetResourceTypeFromAssembly (assembly); if (type != null) return type; } @@ -52,4 +45,3 @@ public static void UpdateIdValues () } } } - diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs index 6e50b10d7d7..515da6d5515 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs @@ -48,7 +48,7 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli { if (p.Name == "IsApplication" && (bool)p.Argument.Value == (mainApplication ? mainApplication : (bool)p.Argument.Value)) { - designerFullName = attribute.ConstructorArguments[0].Value.ToString (); + designerFullName = ResourceDesignerImportGenerator.GetTypeFullNameFromAssemblyQualifiedName (attribute.ConstructorArguments[0].Value.ToString ()); break; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index 4fb35a3c26b..a2161081ac7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -28,6 +28,9 @@ public class GenerateResourceDesigner : AndroidTask public string? Namespace { get; set; } + [Required] + public string AssemblyName { get; set; } = ""; + [Required] public string ProjectDir { get; set; } = ""; @@ -201,7 +204,9 @@ private void WriteFile (string file, CodeTypeDeclaration resources, string langu unit.Namespaces.Add (ns); var resgenatt = new CodeAttributeDeclaration (new CodeTypeReference ("Android.Runtime.ResourceDesignerAttribute", CodeTypeReferenceOptions.GlobalReference)); - resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource"))); + var resourceTypeName = namespaceName.Length > 0 ? namespaceName + ".Resource" : "Resource"; + var resourceAssemblyQualifiedName = $"{resourceTypeName}, {AssemblyName}"; + resgenatt.Arguments.Add (new CodeAttributeArgument (new CodePrimitiveExpression (resourceAssemblyQualifiedName))); resgenatt.Arguments.Add (new CodeAttributeArgument ("IsApplication", new CodePrimitiveExpression (IsApplication))); unit.AssemblyCustomAttributes.Add (resgenatt); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs index 2b55531d7e7..f24b67f3599 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/CodeBehindTests.cs @@ -540,6 +540,9 @@ string[] GetBuildProperties (LocalBuilder builder, AndroidRuntime runtime, bool Console.WriteLine ("CodeBehindTests: using NativeAOT and running on CI, disabling warnings."); noWarn.Add ("IL2091"); noWarn.Add ("IL2104"); + // Transitive AndroidX bindings can still reference ResourceDesignerAttribute(string) + // with non-AQN type names; this is expected until those libraries rebuild. + noWarn.Add ("IL2122"); noWarn.Add ("IL3053"); noWarn.Add ("XA1040"); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs index 6fd85d7ee77..b9eb64c5946 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs index 3b3fd726277..51c9a1b307c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithElevenStyleableAttributesExpected.cs @@ -9,7 +9,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs index 70688414260..e8d55e29777 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Expected/GenerateDesignerFileWithLibraryReferenceExpected.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource", IsApplication=true)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Foo.Foo.Resource, Foo", IsApplication=true)] namespace Foo.Foo { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs index 21edf2f634d..aaff26bd489 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/ManagedResourceParserTests.cs @@ -15,6 +15,17 @@ namespace Xamarin.Android.Build.Tests { [TestFixture] [Parallelizable (ParallelScope.Children)] public class ManagedResourceParserTests : BaseTest { + class ResourceDesignerAttributeLibraryProject : DotNetXamarinProject + { + public override string ProjectTypeGuid => ""; + + public ResourceDesignerAttributeLibraryProject () + { + Language = XamarinAndroidProjectLanguage.CSharp; + TargetFramework = "net10.0"; + } + } + const string ValuesXml = @" false @@ -332,6 +343,49 @@ void BuildLibraryWithResources (string path, AndroidRuntime runtime) } } + void BuildLibraryWithResourceDesignerAttribute (string path, string resourceDesignerTypeName) + { + var library = new ResourceDesignerAttributeLibraryProject () { + ProjectName = "Library", + }; + library.Sources.Add (new BuildItem.Source ("Resource.cs") { + TextContent = () => $$""" + [assembly: Android.Runtime.ResourceDesignerAttribute ("{{resourceDesignerTypeName}}", IsApplication=false)] + + namespace Android.Runtime + { + [System.AttributeUsage (System.AttributeTargets.Assembly)] + public class ResourceDesignerAttribute : System.Attribute + { + public ResourceDesignerAttribute (string fullName) + { + FullName = fullName; + } + + public string FullName { get; set; } + + public bool IsApplication { get; set; } + } + } + + namespace Library + { + public partial class Resource + { + public partial class Animator + { + public static int slide_in_bottom; + } + } + } + """ + }); + + using (ProjectBuilder builder = CreateDllBuilder (Path.Combine (Root, path))) { + Assert.IsTrue (builder.Build (library), "Build should have succeeded"); + } + } + void CompareFilesIgnoreRuntimeInfoString (string file1, string file2) { FileAssert.Exists (file1); @@ -384,6 +438,7 @@ GenerateResourceDesigner CreateTask (string path) task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.DesignTimeOutputFile = Path.Combine (Root, path, "designtime", "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); @@ -460,6 +515,33 @@ public void GenerateDesignerFileFromRtxt ([Values] bool withLibraryReference, [V Directory.Delete (Path.Combine (Root, path), recursive: true); } + [TestCase ("Library.Resource, Library")] + [TestCase ("Library.Resource")] + public void ResourceDesignerImportGeneratorHandlesResourceDesignerAttributeFormats (string resourceDesignerTypeName) + { + var path = Path.Combine ("temp", TestName + " Some Space"); + CreateResourceDirectory (path); + var mapTask = CreateCaseMapTask (path); + Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully."); + + var libraryPath = Path.Combine (path, "Library"); + BuildLibraryWithResourceDesignerAttribute (libraryPath, resourceDesignerTypeName); + var libraryAssemblyPath = Path.Combine (Root, libraryPath, "bin", "Debug", "Library.dll"); + + var task = CreateTask (path); + task.RTxtFile = Path.Combine (Root, path, "R.txt"); + File.WriteAllText (task.RTxtFile, Rtxt); + task.References = new TaskItem [] { + new TaskItem (libraryAssemblyPath) + }; + + Assert.IsTrue (task.Execute (), "Task should have executed successfully."); + var designer = File.ReadAllText (task.NetResgenOutputFile); + // The import generator can only emit this assignment if it found "Library.Resource". + StringAssert.Contains ("global::Library.Resource.Animator.slide_in_bottom = global::Foo.Foo.Resource.Animator.slide_in_bottom;", designer); + Directory.Delete (Path.Combine (Root, path), recursive: true); + } + [Test] public void GenerateDesignerFileFromEmptyRtxt () { @@ -489,6 +571,7 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); task.ResourceDirectory = Path.Combine (Root, path, "res") + Path.DirectorySeparatorChar; @@ -607,6 +690,7 @@ public void CompareAapt2AndManagedParserOutput () task.UseManagedResourceGenerator = true; task.DesignTimeBuild = false; task.Namespace = "MonoAndroidApplication4.MonoAndroidApplication4"; + task.AssemblyName = "MonoAndroidApplication4"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.aapt2.cs"); task.ProjectDir = Path.Combine (Root, path); task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); @@ -726,6 +810,7 @@ int styleable ElevenAttributes_attr09 9 task.UseManagedResourceGenerator = true; task.DesignTimeBuild = true; task.Namespace = "Foo.Foo"; + task.AssemblyName = "Foo"; task.NetResgenOutputFile = Path.Combine (Root, path, "Resource.designer.cs"); task.ProjectDir = Path.Combine (Root, path); task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 9a5a0c1df29..84bdd57fac6 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -83,8 +83,9 @@ public void CreateImportMethods (IEnumerable libraries) string? GetResourceDesignerClass (MetadataReader reader) { - // Looking for: - // [assembly: Android.Runtime.ResourceDesignerAttribute("MyApp.Resource", IsApplication=true)] + // Looking for library assemblies: + // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource, MyLibrary", IsApplication=false)] + // [assembly: Android.Runtime.ResourceDesignerAttribute("MyLibrary.Resource", IsApplication=false)] var assembly = reader.GetAssemblyDefinition (); foreach (var handle in assembly.GetCustomAttributes ()) { @@ -98,12 +99,25 @@ public void CreateImportMethods (IEnumerable libraries) return null; } } - return (string?) values.FixedArguments.First ().Value; + var typeName = (string?) values.FixedArguments.First ().Value; + return GetTypeFullNameFromAssemblyQualifiedName (typeName); } } return null; } + internal static string? GetTypeFullNameFromAssemblyQualifiedName (string? typeName) + { + if (typeName is null) + return typeName; + + int assemblySeparator = typeName.IndexOf (','); + if (assemblySeparator < 0) + return typeName; + + return typeName.Substring (0, assemblySeparator).Trim (); + } + void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMemberMethod method, MetadataReader reader, bool hasAlias) { var typeName = reader.GetString (type.Name); @@ -127,4 +141,3 @@ void CreateImportFor (string declaringTypeFullName, TypeDefinition type, CodeMem } } } - diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 42b4601125c..299888f7b78 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1045,6 +1045,7 @@ because xbuild doesn't support framework reference assemblies. JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)" RTxtFile="$(IntermediateOutputPath)R.txt" Namespace="$(AndroidResgenNamespace)" + AssemblyName="$(AssemblyName)" ProjectDir="$(ProjectDir)" Resources="@(AndroidResource);@(AndroidBoundLayout)" ResourceDirectory="$(MonoAndroidResourcePrefix)" @@ -1301,6 +1302,7 @@ because xbuild doesn't support framework reference assemblies. JavaResgenInputFile="$(_GeneratedPrimaryJavaResgenFile)" RTxtFile="$(IntermediateOutputPath)R.txt" Namespace="$(AndroidResgenNamespace)" + AssemblyName="$(AssemblyName)" ProjectDir="$(ProjectDir)" Resources="@(_AndroidResourceDest)" ResourceDirectory="$(MonoAndroidResDirIntermediate)"