From dc3ccf28cdbe9f8c0a705400b83c11a85c81a980 Mon Sep 17 00:00:00 2001 From: Dean Ellis Date: Thu, 5 Jan 2023 16:05:18 +0000 Subject: [PATCH] [Xamarin.Android.Build.Tasks] _Microsoft.Android.Resource.Designer (#6427) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: https://github.com/xamarin/xamarin-android/issues/6310 Context: https://github.com/dotnet/runtime/commit/60d9b98938a9003d937efdaa53dfe6f0033de9bb Context: https://github.com/dotnet/fsharp/issues/12640 Context: 103b5a755c048c6eaedb43c139cac32b920372cd Optimize ResourceIdManager.UpdateIdValues() invocations Context: 9e6ce03ca2d72f5415a2a1650185ba46113cc3dd Adds $(AndroidLinkResource) Context: 522d7fb61f3669d85d077ba4f2889dbe8c9c8ac3 Context: 9c0437866c7308794283b76125483adecdea9067 (AndroidEnablePreloadAssemblies crash) Context: d521ac0280c0ad165570077a860cb1846025010b (Styleables array values) Replace the existing `Resource.designer.cs` generation code with a new system that relies on Reference Assemblies. This results in smaller apps and faster startup. ~~ Bind `@(AndroidResource)` values as fields ~~ The original approach to binding `@(AndroidResource)` values was to Do What Java Does™: there are two "styles" of `Resource.designer.cs` files, one for Library projects, and one for App projects. `Resource.designer.cs` for Library projects involves mutable read/write fields: [assembly: Android.Runtime.ResourceDesignerAttribute ("ExampleLib.Resource", IsApplication=false)] namespace ExampleLib; partial class Resource { partial class String { public static int app_name = 2130771968; static String() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); } } partial class Styleable { public static int[] MyLibraryWidget = new int[]{…}; static Styleable() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); } } } `Resource.designer.cs` for App projects involves *`const`* fields: [assembly: Android.Runtime.ResourceDesignerAttribute ("App.Resource", IsApplication=true)] namespace App; partial class Resource { partial class String { public const int app_name = 2130968576; static String() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); } } partial class Styleable { public static int[] MyLibraryWidget = new int[]{…}; // still read+write, not const static Styleable() { global::Android.Runtime.ResourceIdManager.UpdateIdValues(); } } } There is a field each Android `resource` in the project *and* any `resource`s declared in a referenced assembly or `.aar` files. This can result in 1000's of fields ending up in each `Resource` class. Because we only know the final `Id` values at app packaging time, library projects could not know those values at build time. This meant that we needed to update those library values at startup with the ones that were compiled into the final application project. This is handled by the `Resource.UpdateIdValues()` method. This method is called by reflection on app startup and contains code to set the read/write fields for *all* `Resource` types from *all referenced assemblies*: partial class Resource { public static void UpdateIdValues() { global::ExampleLib.Resource.String.app_name = String.app_name; // plus all other resources } } **Pros**: * It's a "known good" construct, as it's what Java does! (Or *did*, circa 12 years ago…) **Cons**: * There is a semantic difference between the use of the `Resource` types between Library and App projects: in an App project, you can use Resource IDs in switch `case`s, e.g. `case Resource.String.app_name: …`. This is not possible in Library projects. * As the App `Resource.UpdateIdValues()` method references *all* fields from all referenced libraries, the linker is not able to remove any of the fields. This pattern is linker hostile. This results in larger `.apk` sizes, though this can be optimized via [`$(AndroidLinkResources)`][0] (9e6ce03c, d521ac02). * As the App `Resource.UpdateIdValues()` method references *all* fields from all referenced libraries, the method can be *huge*; it depends on how many resources the App and all dependencies pull in. We have seen cases where the size of `Resource.UpdateIdValues()` would cause the interpreter to crash, breaking certain Hot Reload scenarios. (Fixed in dotnet/runtime@60d9b989). * The `Resource.UpdateIdValues()` method needs to be invoked during process startup, *before* any assemblies try to use their `Resource.…` values, and the method is looked up via *Reflection*. This means System.Reflection is part of the app startup path, which has overheads. (This overhead is also removed via `$(AndroidLinkResources)`.) ~~ Bind `@(AndroidRoesource)` values as properties ~~ Replace the "bind resources as fields" approach with a new system with significant differences: 1. Android resource ids are bound as read-only *properties*, and 2. The `Resource` class is placed into a *separate assembly*, `_Microsoft.Android.Resource.Designer.dll`. The new `$(AndroidUseDesignerAssembly)` MSBuild property controls which Android resource approach is used; if True -- the default for .NET 8 -- then `_Microsoft.Android.Resource.Designer.dll` will be used. If False, then the previous "bind resource ids as fields" approach will be used. This property is only valid for Library projects; App projects must use the property-oriented approach. This new approach takes advantage of [Reference Assemblies][1]. Reference Assemblies are designed to be replaced at runtime, and are generally used to provide placeholder API's which can be swapped out later. Library projects will generate a Reference Assembly for `_Microsoft.Android.Resource.Designer.dll` which contains read-only properties for each `@(AndroidResource)` within the project and all dependencies. This is otherwise identical to the "fields" approach, *except* that the namespace is predefined, its a new assembly, and properties are used instead of fields, *as if* it contained: // _Microsoft.Android.Resource.Designer.dll for Library project [assembly: System.Runtime.CompilerServices.ReferenceAssemblyAttribute] namespace Microsoft.Android.Resource.Designer; public partial class Resource { public partial class String { public static int app_name => 0; } public partial class Styleable { public static int[] MyLibraryWidget => nullptr; } } Also note that `_Microsoft.Android.Resource.Designer.dll` is produced *with Mono.Cecil* as a pre-build action; no C# source is generated. The Library assembly references the generated `_Microsoft.Android.Resource.Designer.dll`. The generated `_Microsoft.Android.Resource.Designer.dll` should ***NOT*** be shipped with NuGet packages. App projects will generate the "real" `_Microsoft.Android.Resource.Designer.dll`, also as a pre-build step, and the "real" assembly will contain actual values for resource ids. The App-built `_Microsoft.Android.Resource.Designer.dll` will also have `[assembly:InternalsVisibleToAttribute]` to the App assembly: // _Microsoft.Android.Resource.Designer.dll for App project [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute ("App…")] namespace Microsoft.Android.Resource.Designer; public partial class Resource { public partial class String { public static int app_name => 2130968576; } public partial class Styleable { static int[] MyLibraryWidget = new[]{…}; public static int[] MyLibraryWidget => MyLibraryWidget; } } This approach has a number of benefits 1. All the property declarations are in one place and are not duplicated (-ish… more on that later). As a result the size of the app will be reduced. 2. Because we no longer need the `Resource.UpdateIdValues()` method, start up time will be reduced. 3. The linker can now do its job and properly link out unused properties. This further reduces application size. 4. F# is now fully supported. See also: dotnet/fsharp#12640. ~~ Styleable Arrays ~~ Styleable resources may be arrays; see e.g. d521ac02. Via the power of Cecil (and not using C# as an intermediate codegen), the binding of styleable arrays in the "Bind `@(AndroidRoesource)` values as properties" world order involves a static field containing the array data, and a public property which returns the private field, which has the same name: public partial class Resource { public partial class Styleable { static int[] MyLibraryWidget = new[]{…}; public static int[] MyLibraryWidget => MyLibraryWidget; } } CIL-wise, *yes*, the field and the property have the same name (?!), but because properties actually have `get_` method prefix, there will actually be a `MyLibraryWidget` field and a `get_MyLibraryWidget()` method, so there are no name collisions. *Note*: ***The styleable array is not copied***. This means it is global mutable data, i.e. one can do this: Microsoft.Android.Resource.Designer.Resource.Styleable.MyLibraryWidget[0] = 42; ***DO NOT DO THIS***. It will introduce runtime errors. The e.g. `Resource.Styleable.MyLibraryWidget` property must be an `int[]` in order to maintain compatibility, as these are often passed to methods which take `int[]` as the parameter type. We thus cannot instead use e.g. `IEnumeragble` as the property type. Additionally, the array isn't copied for performance reasons. We do not think that this will be a problem in practice, as the previous "Bind `@(AndroidRoesource)` values as fields" strategy *also* had mutable `int[]` fields, and suffers from the same safety concerns, and the world hasn't ended… ~~ Source Compatibility ~~ In the "bind resource ids as fields" approach, the `Resource` class was in the default namespace for the Library project, set via the [`$(RootNamespace)`][2] MSBuild property. In order to maintain source compatibility, Library projects will have a generated `__Microsoft.Android.Resource.Designer.cs` file which contains a new `Resource` declaration which *inherits* from the `Resource` type in `_Microsoft.Android.Resource.Designer.dll`: // Generated __Microsoft.Android.Resource.Designer.cs in Library projects namespace ExampleLib; public class Resource : Microsoft.Android.Resource.Designer.Resource { } This allows existing code such as `ExampleLib.Resource.String.app_name` to continue to compile. App projects also expect a `Resource` class in `$(RootNamespace)`, *and* expect the values to be `const`. To support this, the generated `_Microsoft.Android.Resource.Designer.dll` *actually* has two sets of `Resource` types, one with properties, and an *`internal`* `ResourceConstant` type: // _Microsoft.Android.Resource.Designer.dll for Library project [assembly: System.Runtime.CompilerServices.InternalsVisibleToAttribute ("App…")] namespace Microsoft.Android.Resource.Designer; internal partial class ResourceConstant { public partial class String { public const int app_name = 2130968576; } } public partial class Resource { public partial class String { public static int app_name => ResourceConstant.String.app_name; } } App projects *also* have a generated `__Microsoft.Android.Resource.Designer.cs`, which has a `Resource` type which inherits from `ResourceConstant`. This is why the App-built `_Microsoft.Android.Resource.Designer.dll` needs `[assembly: InternalsVisibleToAttribute]`: // Generated __Microsoft.Android.Resource.Designer.cs in App projects namespace App; public class Resource : Microsoft.Android.Resource.Designer.ResourceConstant { } This allows existing App code to use `App.Resource.String.app_name` in `case` statements. ~~ Binary Compatibility ~~ Binary compatibility is maintained via a new `MonoDroid.Tuner.FixLegacyResourceDesignerStep` linker step. `FixLegacyResourceDesignerStep` rewrites Library assemblies to replace `Resource.…` field access with property access to `Microsoft.Android.Resource.Designer.Resource.…` in `_Microsoft.Android.Resource.Designer.dll`. Much of this code overlaps with the existing logic of `$(AndroidLinkResources)`, and allows existing Library assemblies to participate in the property- oriented system. ~~ Internals ~~ The new build system introduces a number of new Tasks and Targets to bring this all together. It also unify's some code between the field- oriented and property-oriented approaches which would otherwise be duplicated. The field-oriented system will be maintained for now for backward compatibility, however the property-oriented system will be enabled by default for .net 8. The property-oriented system is mostly contained in `Xamarin.Android.Resource.Designer.targets`. The entry point for this set of targets is `_BuildResourceDesigner`, which will only be run if the `$(AndroidUseDesignerAssembly)` MSBuild property is `True`, as it will be for .NET 8+. New tasks are as follows. - `` is responsible for scanning the resource directory and generating an `aapt2`-compatible `R.txt` file. This will be used by ``. - `` is responsible for generating a `casemap.txt` file which will map the all lower case android resources to the casing required for the C# code. Android requires ALL resources be lower case, but our system allows the user to define the case using any system then want. This task handles generating this mapping between what the android system needs and what the user is expecting. Its output is used by the `` task when generating the IL in `_Microsoft.Android.Resource.Designer.dll`. It is also used by the old system to generate the same file. - `` is responsible for generating the `__Microsoft.Android.Resource.Designer.cs` file in `$(IntermediateOutputPath)`. - `` is the key to the whole property-oriented approach. This task will read the `R.xt` file and generate a `_Microsoft.Android.Resource.Designer.dll` assembly in `$(IntermediateOutputPath)`. This task is called in two places. The first is in `_GenerateResourceDesignerAssembly`, this is called as part of the build which happens just before `CoreCompile` and only for design time builds. It is also called in `_UpdateAndroidResgen` which happens as part of the build and runs just after `aapt2` is called. This ensures we always use the most up to date version of `R.txt` to generate the new assembly. Because we are using the `R.txt` file to drive the generation of the new assembly, we needed some way for that to work when `aapt2` was not being run. This usually happens on a first time design time build. The field-oriented approach has a `` task which is responsible for both scanning the resources and generating a design time `Resource.designer.cs` file. While we could have duplicated the code it made more sense to split out the resource scanner into its own class. We now have a new `` task which is responsible for scanning the resources and generating an `R.txt` file. This is only used when we are not doing a full build with `aapt2`. This new task lets us generate the needed `R.txt` which can then be used by both the old and new system to generate their respective outputs. As part of this we have two other classes: `RtxtReader` and `RtxtWriter`. The `RtxtReader` unify's the code which was used to read the values of the `R.txt` into one class which can be used by both approaches. The `RtxtWriter` is responsible for writing the `R.txt` file for design time builds. Again it will be used by both the old and new system. The `_AddResourceDesignerFiles` target is responsible for ensuring that the new assembly and `__Microsoft.Android.Resource.Designer.cs` get added to the correct item groups. These are `@(ReferencePath)` for the assembly and `@(Compile)` for the source file. In the case of F# the `__Microsoft.Android.Resource.Designer.fs` file which gets generated has to be added to the `@(CompileBefore)` ItemGroup, this is so that the types are resolved in the correct order. To ensure that the new assembly is added to the final application we have to introduce the `_AddResourceDesignerToPublishFiles` target. This target makes sure that the new assembly is added to the `@(ResolvedFileToPublish)` ItemGroup. It also adds the require MetaData items such as `%(IsTrimmable)` and `%(PostprocessAssembly)` which are required to get the assembly linked correctly. ~~ Results ~~ Results are most visible when lots of Android Resources are used. For a [Sample app][3] app which uses lots of resources, we see the following improvements to the **ActivityTaskManager: Displayed** time: | Before (ms) | After (ms) | Δ (%) | Notes | | ----------: | ----------: | --------: | ------------------------------------ | | 340.500 | 313.250 | -8.00% ✓ | defaults; 64-bit build | | 341.950 | 316.200 | -7.53% ✓ | defaults; profiled AOT; 64-bit build | | 345.950 | 324.600 | -6.17% ✓ | defaults; 32-bit build | | 341.000 | 323.050 | -5.26% ✓ | defaults; profiled AOT; 32-bit build | [0]: https://learn.microsoft.com/en-us/xamarin/android/deploy-test/building-apps/build-properties#androidlinkresources [1]: https://learn.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies [2]: https://learn.microsoft.com/en-us/visualstudio/msbuild/common-msbuild-project-properties?view=vs-2022 [3]: https://github.com/dellis1972/DotNetAndroidTest --- .../guides/building-apps/build-properties.md | 29 ++ Documentation/guides/messages/xa1034.md | 16 + .../installers/create-installers.targets | 1 + .../ThirdPartyNotices/StrongNameSigner.cs | 33 ++ .../CryptoConvert.cs | 338 +++++++++++++++ .../StrongNameSigner/SigningHelper.cs | 35 ++ .../FixLegacyResourceDesignerStep.cs | 163 ++++++++ .../MonoDroid.Tuner/LinkDesignerBase.cs | 20 +- .../Linker/MonoDroid.Tuner/Linker.cs | 2 + .../Linker/MonoDroid.Tuner/LinkerOptions.cs | 1 + .../Android/Xamarin.Android.Aapt2.targets | 2 +- .../Android/Xamarin.Android.Designer.targets | 2 +- .../Xamarin.Android.Resource.Designer.targets | 210 ++++++++++ ...oft.Android.Sdk.AssemblyResolution.targets | 8 + .../Microsoft.Android.Sdk.BuildOrder.targets | 14 +- ...soft.Android.Sdk.DefaultProperties.targets | 2 + .../Microsoft.Android.Sdk.ILLink.targets | 7 +- .../Microsoft.Android.Sdk.Publish.targets | 1 + .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 5 + .../Resources/Resource.Designer.snk | Bin 0 -> 596 bytes .../Tasks/Aapt2Link.cs | 7 +- .../Tasks/AppendCustomMetadataToItemGroup.cs | 5 + .../Tasks/ConvertCustomView.cs | 6 +- .../Tasks/FilterAssemblies.cs | 2 +- .../Tasks/GenerateLibraryResources.cs | 3 +- .../Tasks/GeneratePackageManagerJava.cs | 2 +- .../Tasks/GenerateResourceCaseMap.cs | 121 ++++++ .../Tasks/GenerateResourceDesigner.cs | 32 +- .../Tasks/GenerateResourceDesignerAssembly.cs | 350 ++++++++++++++++ ...nerateResourceDesignerIntermediateClass.cs | 53 +++ .../Tasks/GenerateRtxt.cs | 48 +++ .../Tasks/LinkAssemblies.cs | 3 + .../Tasks/LinkAssembliesNoShrink.cs | 40 +- .../Tasks/ResolveLibraryProjectImports.cs | 8 +- .../AndroidUpdateResourcesTest.cs | 112 +++-- .../Xamarin.Android.Build.Tests/AotTests.cs | 8 + .../Xamarin.Android.Build.Tests/BuildTest.cs | 30 +- .../DesignerTests.cs | 6 + .../IncrementalBuildTest.cs | 5 + .../PackagingTest.cs | 1 + .../Tasks/GenerateResourceCaseMapTests.cs | 51 +++ .../Tasks/ManagedResourceParserTests.cs | 62 ++- .../Utilities/BaseTest.cs | 27 ++ .../Xamarin.Android.Build.Tests/XASdkTests.cs | 82 +++- .../Xamarin.Android.Build.Tests.csproj | 1 + .../Android/KnownPackages.cs | 15 + .../Android/XamarinAndroidCommonProject.cs | 6 +- .../Android/XamarinAndroidProjectLanguage.cs | 2 +- .../Xamarin.ProjectTools/Common/DotNetCLI.cs | 4 +- .../BuildReleaseArm64SimpleDotNet.apkdesc | 25 +- .../BuildReleaseArm64SimpleLegacy.apkdesc | 16 +- .../BuildReleaseArm64XFormsDotNet.apkdesc | 27 +- .../BuildReleaseArm64XFormsLegacy.apkdesc | 78 ++-- .../Utilities/FileResourceParser.cs | 389 ++++++++++++++++++ .../Utilities/JavaResourceParser.cs | 6 +- .../Utilities/ManagedResourceParser.cs | 130 +----- .../Utilities/MonoAndroidHelper.cs | 44 +- .../ResourceDesignerImportGenerator.cs | 1 + .../Utilities/ResourceIdentifier.cs | 18 +- .../Utilities/ResourceParser.cs | 16 - .../Utilities/RtxtParser.cs | 158 +++++++ .../Utilities/RtxtWriter.cs | 25 ++ .../Xamarin.Android.Build.Tasks.csproj | 9 + .../Xamarin.Android.Build.Tasks.targets | 4 + .../Xamarin.Android.Common.targets | 70 +++- .../Xamarin.Android.DesignTime.targets | 1 + .../Xamarin.Android.Legacy.targets | 7 +- .../MSBuildDeviceIntegration.csproj | 1 + .../Tests/InstallAndRunTests.cs | 82 ++++ .../Tests/InstantRunTest.cs | 1 + .../Java.Interop-Tests.csproj | 1 + .../Mono.Android-Test.Shared.projitems | 2 +- .../Mono.Android-TestsMultiDex.csproj | 2 +- 74 files changed, 2772 insertions(+), 331 deletions(-) create mode 100644 Documentation/guides/messages/xa1034.md create mode 100644 build-tools/xaprepare/xaprepare/ThirdPartyNotices/StrongNameSigner.cs create mode 100644 src-ThirdParty/Mono.Security.Cryptography/CryptoConvert.cs create mode 100644 src-ThirdParty/StrongNameSigner/SigningHelper.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs create mode 100644 src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Resource.Designer.targets create mode 100644 src/Xamarin.Android.Build.Tasks/Resources/Resource.Designer.snk create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceCaseMap.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerIntermediateClass.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateRtxt.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateResourceCaseMapTests.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Utilities/RtxtWriter.cs diff --git a/Documentation/guides/building-apps/build-properties.md b/Documentation/guides/building-apps/build-properties.md index ee48b5653fe..113c87c3635 100644 --- a/Documentation/guides/building-apps/build-properties.md +++ b/Documentation/guides/building-apps/build-properties.md @@ -1364,6 +1364,35 @@ To suppress the default AOT profiles, set the property to `false`. Added in Xamarin.Android 10.1. +## AndroidUseDesignerAssembly + +A bool property which controls if the build system will generate an +`_Microsoft.Android.Resource.Designer.dll` as apposed to a `Resource.Designer.cs` file. The benefits of this are smaller applications and +faster startup time. + +The default value is `true` in .NET 8. + +This setting is not backward compatible with Classic Xamarin.Android. +As a Nuget Author it is recommended that you ship three versions of +the assembly if you want to maintain backward compatibility. +One for MonoAndroid, one for net6.0-android and +one for net8.0-android. You can do this by using [Xamarin.Legacy.Sdk](https://www.nuget.org/packages/Xamarin.Legacy.Sdk). This is only required if your Nuget Library +project makes use of `AndroidResource` items in the project or via a dependency. + +``` +monoandroid90;net6.0-android;net8.0-android +``` + +Alternatively turn this setting off until such time as both Classic and +net7.0-android have been deprecated. + +.NET 8 Projects which choose to turn this setting off will not be able to +consume references which do use it. If you try to use an assembly +which does have this feature enabled in a project that does not, you will +get a `XA1034` build error. + +Added in .NET 8. Unsupported in Classic Xamarin.Android. + ## AndroidUseInterpreter A boolean property that causes the `.apk` to contain the mono diff --git a/Documentation/guides/messages/xa1034.md b/Documentation/guides/messages/xa1034.md new file mode 100644 index 00000000000..be1bffd43e7 --- /dev/null +++ b/Documentation/guides/messages/xa1034.md @@ -0,0 +1,16 @@ +--- +title: Xamarin.Android error XA1034 +description: XA1034 error code +ms.date: 13/12/2022 +--- +# Xamarin.Android error XA1034 + +## Example messages + +``` +Your project references 'Foo.dll' which uses the `_Microsoft.Android.Resource.Designer` assembly, but you do not have this feature enabled. Please set the `AndroidUseDesignerAssembly` MSBuild property to `true` in your project file. +``` + +## Solution + +Edit your csproj directly and change the 'AndroidUseDesignerAssembly' to `True`. \ No newline at end of file diff --git a/build-tools/installers/create-installers.targets b/build-tools/installers/create-installers.targets index 51ff5bb9a1d..3e589147ceb 100644 --- a/build-tools/installers/create-installers.targets +++ b/build-tools/installers/create-installers.targets @@ -303,6 +303,7 @@ <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.CSharp.targets" ExcludeFromAndroidNETSdk="true" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.D8.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Designer.targets" /> + <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.Resource.Designer.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.DesignTime.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.EmbeddedResource.targets" /> <_MSBuildFiles Include="$(MicrosoftAndroidSdkOutDir)Xamarin.Android.FSharp.targets" ExcludeFromAndroidNETSdk="true" /> diff --git a/build-tools/xaprepare/xaprepare/ThirdPartyNotices/StrongNameSigner.cs b/build-tools/xaprepare/xaprepare/ThirdPartyNotices/StrongNameSigner.cs new file mode 100644 index 00000000000..45ab44b28b4 --- /dev/null +++ b/build-tools/xaprepare/xaprepare/ThirdPartyNotices/StrongNameSigner.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Xamarin.Android.Prepare +{ + [TPN] + class StrongNameSigner_TPN : ThirdPartyNotice + { + static readonly Uri url = new Uri ("https://github.com/brutaldev/StrongNameSigner/"); + + public override string LicenseFile => string.Empty; + public override string Name => "brutaldev/StrongNameSigner"; + public override Uri SourceUrl => url; + public override string LicenseText => @" +Copyright (c) Werner van Deventer (werner@brutaldev.com). All rights reserved. + +Licensed under the Apache License, Version 2.0 (the 'License'); you +may not use this file except in compliance with the License. You may +obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing permissions +and limitations under the License. +"; + + public override bool Include (bool includeExternalDeps, bool includeBuildDeps) => includeExternalDeps; + } +} diff --git a/src-ThirdParty/Mono.Security.Cryptography/CryptoConvert.cs b/src-ThirdParty/Mono.Security.Cryptography/CryptoConvert.cs new file mode 100644 index 00000000000..5a5eecc1468 --- /dev/null +++ b/src-ThirdParty/Mono.Security.Cryptography/CryptoConvert.cs @@ -0,0 +1,338 @@ +// +// CryptoConvert.cs - Crypto Conversion Routines +// +// Author: +// Sebastien Pouliot +// +// (C) 2003 Motus Technologies Inc. (http://www.motus.com) +// Copyright (C) 2004-2006 Novell Inc. (http://www.novell.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// Original source can be found at +// https://github.com/mono/mono/blob/e2c5f4b0ad1a6b21ca0735f0b35b8611d4ad87b3/mcs/class/Mono.Security/Mono.Security.Cryptography/CryptoConvert.cs#L4 + +using System; +using System.Security.Cryptography; + +namespace Mono.Security.Cryptography +{ + internal static class CryptoConvert + { + static private int ToInt32LE(byte[] bytes, int offset) + { + return (bytes[offset + 3] << 24) | (bytes[offset + 2] << 16) | (bytes[offset + 1] << 8) | bytes[offset]; + } + + static private uint ToUInt32LE(byte[] bytes, int offset) + { + return (uint)((bytes[offset + 3] << 24) | (bytes[offset + 2] << 16) | (bytes[offset + 1] << 8) | bytes[offset]); + } + + static private byte[] GetBytesLE(int val) + { + return new byte[] { + (byte) (val & 0xff), + (byte) ((val >> 8) & 0xff), + (byte) ((val >> 16) & 0xff), + (byte) ((val >> 24) & 0xff) + }; + } + + static private byte[] Trim(byte[] array) + { + for (int i = 0; i < array.Length; i++) + { + if (array[i] != 0x00) + { + byte[] result = new byte[array.Length - i]; + Buffer.BlockCopy(array, i, result, 0, result.Length); + return result; + } + } + +#pragma warning disable S1168 // Empty arrays and collections should be returned instead of null + return null; +#pragma warning restore S1168 // Empty arrays and collections should be returned instead of null + } + + private static RSA FromCapiPrivateKeyBlob(byte[] blob, int offset) + { + var rsap = new RSAParameters(); + + try + { + if ((blob[offset] != 0x07) || // PRIVATEKEYBLOB (0x07) + (blob[offset + 1] != 0x02) || // Version (0x02) + (blob[offset + 2] != 0x00) || // Reserved (word) + (blob[offset + 3] != 0x00) || + (ToUInt32LE(blob, offset + 8) != 0x32415352)) // DWORD magic = RSA2 + { + throw new CryptographicException("Invalid blob header"); + } + + //// ALGID (CALG_RSA_SIGN, CALG_RSA_KEYX, ...) + //// int algId = ToInt32LE (blob, offset+4); + + //// DWORD bitlen + int bitLen = ToInt32LE(blob, offset + 12); + + //// DWORD public exponent + byte[] exp = new byte[4]; + Buffer.BlockCopy(blob, offset + 16, exp, 0, 4); + Array.Reverse(exp); + rsap.Exponent = Trim(exp); + + int pos = offset + 20; + //// BYTE modulus[rsapubkey.bitlen/8]; + int byteLen = (bitLen >> 3); + rsap.Modulus = new byte[byteLen]; + Buffer.BlockCopy(blob, pos, rsap.Modulus, 0, byteLen); + Array.Reverse(rsap.Modulus); + pos += byteLen; + + //// BYTE prime1[rsapubkey.bitlen/16]; + int byteHalfLen = (byteLen >> 1); + rsap.P = new byte[byteHalfLen]; + Buffer.BlockCopy(blob, pos, rsap.P, 0, byteHalfLen); + Array.Reverse(rsap.P); + pos += byteHalfLen; + + //// BYTE prime2[rsapubkey.bitlen/16]; + rsap.Q = new byte[byteHalfLen]; + Buffer.BlockCopy(blob, pos, rsap.Q, 0, byteHalfLen); + Array.Reverse(rsap.Q); + pos += byteHalfLen; + + //// BYTE exponent1[rsapubkey.bitlen/16]; + rsap.DP = new byte[byteHalfLen]; + Buffer.BlockCopy(blob, pos, rsap.DP, 0, byteHalfLen); + Array.Reverse(rsap.DP); + pos += byteHalfLen; + + //// BYTE exponent2[rsapubkey.bitlen/16]; + rsap.DQ = new byte[byteHalfLen]; + Buffer.BlockCopy(blob, pos, rsap.DQ, 0, byteHalfLen); + Array.Reverse(rsap.DQ); + pos += byteHalfLen; + + //// BYTE coefficient[rsapubkey.bitlen/16]; + rsap.InverseQ = new byte[byteHalfLen]; + Buffer.BlockCopy(blob, pos, rsap.InverseQ, 0, byteHalfLen); + Array.Reverse(rsap.InverseQ); + pos += byteHalfLen; + + // ok, this is hackish but CryptoAPI support it so... + // note: only works because CRT is used by default + // http://bugzilla.ximian.com/show_bug.cgi?id=57941 + rsap.D = new byte[byteLen]; // must be allocated + if (pos + byteLen + offset <= blob.Length) + { + //// BYTE privateExponent[rsapubkey.bitlen/8]; + Buffer.BlockCopy(blob, pos, rsap.D, 0, byteLen); + Array.Reverse(rsap.D); + } + } + catch (Exception e) + { + throw new CryptographicException("Invalid blob.", e); + } + + RSA rsa = null; + try + { + rsa = RSA.Create(); + rsa.ImportParameters(rsap); + } + catch (CryptographicException) + { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + bool throws = false; + try + { + var csp = new CspParameters + { + Flags = CspProviderFlags.UseMachineKeyStore + }; + +#pragma warning disable S4426 // Cryptographic keys should be robust + rsa = new RSACryptoServiceProvider(csp); +#pragma warning restore S4426 // Cryptographic keys should be robust + rsa.ImportParameters(rsap); + } + catch + { + throws = true; + } + + if (throws) + { + // rethrow original, not the latter, exception if this fails + throw; + } + } + return rsa; + } + + private static RSA FromCapiPublicKeyBlob(byte[] blob, int offset) + { + try + { + if ((blob[offset] != 0x06) || // PUBLICKEYBLOB (0x06) + (blob[offset + 1] != 0x02) || // Version (0x02) + (blob[offset + 2] != 0x00) || // Reserved (word) + (blob[offset + 3] != 0x00) || + (ToUInt32LE(blob, offset + 8) != 0x31415352)) // DWORD magic = RSA1 + { + throw new CryptographicException("Invalid blob header"); + } + + //// ALGID (CALG_RSA_SIGN, CALG_RSA_KEYX, ...) + //// int algId = ToInt32LE (blob, offset+4); + + // DWORD bitlen + int bitLen = ToInt32LE(blob, offset + 12); + + //// DWORD public exponent + var rsap = new RSAParameters + { + Exponent = new byte[3] + }; + + rsap.Exponent[0] = blob[offset + 18]; + rsap.Exponent[1] = blob[offset + 17]; + rsap.Exponent[2] = blob[offset + 16]; + + int pos = offset + 20; + //// BYTE modulus[rsapubkey.bitlen/8]; + int byteLen = bitLen >> 3; + rsap.Modulus = new byte[byteLen]; + Buffer.BlockCopy(blob, pos, rsap.Modulus, 0, byteLen); + Array.Reverse(rsap.Modulus); + + RSA rsa = null; + try + { + rsa = RSA.Create(); + rsa.ImportParameters(rsap); + } + catch (CryptographicException) + { + // this may cause problem when this code is run under + // the SYSTEM identity on Windows (e.g. ASP.NET). See + // http://bugzilla.ximian.com/show_bug.cgi?id=77559 + var csp = new CspParameters + { + Flags = CspProviderFlags.UseMachineKeyStore + }; + +#pragma warning disable S4426 // Cryptographic keys should be robust + rsa = new RSACryptoServiceProvider(csp); +#pragma warning restore S4426 // Cryptographic keys should be robust + rsa.ImportParameters(rsap); + } + + return rsa; + } + catch (Exception e) + { + throw new CryptographicException("Invalid blob.", e); + } + } + + // PRIVATEKEYBLOB + // PUBLICKEYBLOB + static public RSA FromCapiKeyBlob(byte[] blob) + { + return FromCapiKeyBlob(blob, 0); + } + + static public RSA FromCapiKeyBlob(byte[] blob, int offset) + { + if (blob == null) + { + throw new ArgumentNullException(nameof(blob)); + } + + if (offset >= blob.Length) + { + throw new ArgumentException("blob is too small."); + } + + switch (blob[offset]) + { + case 0x00: + // this could be a public key inside an header + // like "sn -e" would produce + if (blob[offset + 12] == 0x06) + { + return FromCapiPublicKeyBlob(blob, offset + 12); + } + break; + case 0x06: + return FromCapiPublicKeyBlob(blob, offset); + case 0x07: + return FromCapiPrivateKeyBlob(blob, offset); + } + throw new CryptographicException("Unknown blob format."); + } + + static public byte[] ToCapiPublicKeyBlob(RSA rsa) + { + var p = rsa.ExportParameters(false); + int keyLength = p.Modulus.Length; // in bytes + byte[] blob = new byte[20 + keyLength]; + + blob[0] = 0x06; // Type - PUBLICKEYBLOB (0x06) + blob[1] = 0x02; // Version - Always CUR_BLOB_VERSION (0x02) + // [2], [3] // RESERVED - Always 0 + blob[5] = 0x24; // ALGID - Always 00 24 00 00 (for CALG_RSA_SIGN) + blob[8] = 0x52; // Magic - RSA1 (ASCII in hex) + blob[9] = 0x53; + blob[10] = 0x41; + blob[11] = 0x31; + + byte[] bitlen = GetBytesLE(keyLength << 3); + blob[12] = bitlen[0]; // bitlen + blob[13] = bitlen[1]; + blob[14] = bitlen[2]; + blob[15] = bitlen[3]; + + // public exponent (DWORD) + int pos = 16; + int n = p.Exponent.Length; + while (n > 0) + { + blob[pos++] = p.Exponent[--n]; + } + + // modulus + pos = 20; + byte[] part = p.Modulus; + int len = part.Length; + Array.Reverse(part, 0, len); + Buffer.BlockCopy(part, 0, blob, pos, len); + + return blob; + } + } +} diff --git a/src-ThirdParty/StrongNameSigner/SigningHelper.cs b/src-ThirdParty/StrongNameSigner/SigningHelper.cs new file mode 100644 index 00000000000..260e1c92147 --- /dev/null +++ b/src-ThirdParty/StrongNameSigner/SigningHelper.cs @@ -0,0 +1,35 @@ +// Original source can be found at +// https://github.com/brutaldev/StrongNameSigner/blob/c38d42ab8d1444504720a62736b310303236cd85/src/Brutal.Dev.StrongNameSigner/SigningHelper.cs#L437 +using System; +using Mono.Security.Cryptography; + + +namespace Brutal.Dev.StrongNameSigner { + /// + /// Static helper class for easily getting assembly information and strong-name signing .NET assemblies. + /// + public static class SigningHelper + { + internal static byte[] GetPublicKey (byte[] keyBlob) + { + using var rsa = CryptoConvert.FromCapiKeyBlob(keyBlob); + var cspBlob = CryptoConvert.ToCapiPublicKeyBlob(rsa); + var publicKey = new byte[12 + cspBlob.Length]; + Buffer.BlockCopy(cspBlob, 0, publicKey, 12, cspBlob.Length); + // The first 12 bytes are documented at: + // http://msdn.microsoft.com/library/en-us/cprefadd/html/grfungethashfromfile.asp + // ALG_ID - Signature + publicKey[1] = 36; + // ALG_ID - Hash + publicKey[4] = 4; + publicKey[5] = 128; + // Length of Public Key (in bytes) + publicKey[8] = (byte)(cspBlob.Length >> 0); + publicKey[9] = (byte)(cspBlob.Length >> 8); + publicKey[10] = (byte)(cspBlob.Length >> 16); + publicKey[11] = (byte)(cspBlob.Length >> 24); + + return publicKey; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs new file mode 100644 index 00000000000..8a88408c42d --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FixLegacyResourceDesignerStep.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +using Java.Interop.Tools.Cecil; + +using Mono.Linker; +using Mono.Linker.Steps; + +using Mono.Tuner; +#if ILLINK +using Microsoft.Android.Sdk.ILLink; +#endif // ILLINK + +namespace MonoDroid.Tuner +{ + public class FixLegacyResourceDesignerStep : LinkDesignerBase + { + internal const string DesignerAssemblyName = "_Microsoft.Android.Resource.Designer"; + internal const string DesignerAssemblyNamespace = "Microsoft.Android.Resource.Designer"; +#if ILLINK + protected override void Process () + { + cache = Context; + } +#else // ILLINK + public FixLegacyResourceDesignerStep (IMetadataResolver cache) + { + this.cache = cache; + } + + readonly +#endif // ILLINK + IMetadataResolver cache; + AssemblyDefinition designerAssembly = null; + TypeDefinition designerType = null; + Dictionary lookup; + + protected override void EndProcess () + { + if (designerAssembly != null) { + LogMessage ($" Setting Action on {designerAssembly.Name} to Link."); + Annotations.SetAction (designerAssembly, AssemblyAction.Link); + } + } + + protected override void LoadDesigner () + { + if (designerAssembly != null) + return; + var designerNameAssembly = AssemblyNameReference.Parse ($"{DesignerAssemblyName}, Version=1.0.0.0"); + try { + designerAssembly = Resolve (designerNameAssembly); + } catch (Mono.Cecil.AssemblyResolutionException) { + LogMessage ($" Could not resolve assembly {DesignerAssemblyName}."); + } catch (System.IO.FileNotFoundException) { + LogMessage ($" Assembly {DesignerAssemblyName} did not exist."); + } + if (designerAssembly == null) { + return; + } + designerType = designerAssembly.MainModule.GetTypes ().FirstOrDefault (x => x.FullName == $"{DesignerAssemblyNamespace}.Resource"); + if (designerType == null) { + LogMessage ($" Did not find {DesignerAssemblyNamespace}.Resource type. It was probably linked out."); + return; + } + lookup = BuildResourceDesignerPropertyLookup (designerType); + return; + } + + internal override bool ProcessAssemblyDesigner (AssemblyDefinition assembly) + { + if (designerAssembly == null || designerType == null) { + LogMessage ($" Not using {DesignerAssemblyName}"); + return false; + } + + if (!FindResourceDesigner (assembly, mainApplication: false, out TypeDefinition designer, out CustomAttribute designerAttribute)) { + LogMessage ($" {assembly.Name.Name} has no designer. "); + return false; + } + + LogMessage ($" {assembly.Name.Name} has a designer. "); + LogMessage ($" BaseType: {designer.BaseType.FullName}. "); + if (designer.BaseType.FullName == $"{DesignerAssemblyNamespace}.Resource") { + LogMessage ($" {assembly.Name.Name} has already been processed. "); + return false; + } + + LogMessage ($" Adding reference {designerAssembly.Name.Name}."); + assembly.MainModule.AssemblyReferences.Add (designerAssembly.Name); + var importedDesignerType = assembly.MainModule.ImportReference (designerType.Resolve ()); + + LogMessage ($" FixupAssemblyTypes {assembly.Name.Name}."); + // now replace all ldsfld with a call to the property get_ method. + FixupAssemblyTypes (assembly, designer); + + LogMessage ($" ClearDesignerClass {assembly.Name.Name}."); + // then clean out the designer. + ClearDesignerClass (designer, completely: true); + designer.BaseType = importedDesignerType; + return true; + } + + Dictionary BuildResourceDesignerPropertyLookup (TypeDefinition type) + { + LogMessage ($" Building Designer Lookups for {type.FullName}"); + var output = new Dictionary (StringComparer.Ordinal); + foreach (TypeDefinition definition in type.NestedTypes) + { + foreach (PropertyDefinition property in definition.Properties) + { + string key = $"{definition.Name}::{property.Name}"; + if (!output.ContainsKey (key)) { + LogMessage ($" Adding {key}"); + output.Add(key, property.GetMethod); + } + } + } + return output; + } + + protected override void FixBody (MethodBody body, TypeDefinition designer) + { + // replace + // IL_0068: ldsfld int32 Xamarin.Forms.Platform.Android.Resource/Layout::Toolbar + // with + // call int32 Xamarin.Forms.Platform.Android.Resource/Layout::get_Toolbar() + string designerFullName = $"{designer.FullName}/"; + var processor = body.GetILProcessor (); + Dictionary instructions = new Dictionary(); + foreach (var i in body.Instructions) + { + if (i.OpCode != OpCodes.Ldsfld) + continue; + string line = i.ToString (); + int idx = line.IndexOf (designerFullName, StringComparison.Ordinal); + if (idx >= 0) { + string key = line.Substring (idx + designerFullName.Length); + LogMessage ($"Looking for {key}."); + if (lookup.TryGetValue (key, out MethodDefinition method)) { + var importedMethod = designer.Module.ImportReference (method); + var newIn = Instruction.Create (OpCodes.Call, importedMethod); + instructions.Add (i, newIn); + } else { + LogMessage ($"DEBUG! Failed to find {key}!"); + } + } + } + if (instructions.Count > 0) + LogMessage ($" Fixing up {body.Method.FullName}"); + foreach (var i in instructions) + { + LogMessage ($" Replacing {i.Key}"); + LogMessage ($" With {i.Value}"); + processor.Replace(i.Key, i.Value); + } + } + } +} 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 928fdfd6e4f..f03fa12d0fc 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkDesignerBase.cs @@ -67,20 +67,24 @@ protected bool FindResourceDesigner (AssemblyDefinition assembly, bool mainAppli return false; } - protected void ClearDesignerClass (TypeDefinition designer) + protected void ClearDesignerClass (TypeDefinition designer, bool completely = false) { LogMessage ($" TryRemoving {designer.FullName}"); // for each of the nested types clear all but the // int[] fields. - for (int i = designer.NestedTypes.Count -1; i >= 0; i--) { - var nestedType = designer.NestedTypes [i]; - RemoveFieldsFromType (nestedType, designer.Module); - if (nestedType.Fields.Count == 0) { - // no fields we do not need this class at all. - designer.NestedTypes.RemoveAt (i); + if (!completely) { + for (int i = designer.NestedTypes.Count -1; i >= 0; i--) { + var nestedType = designer.NestedTypes [i]; + RemoveFieldsFromType (nestedType, designer.Module); + if (nestedType.Fields.Count == 0) { + // no fields we do not need this class at all. + designer.NestedTypes.RemoveAt (i); + } } + RemoveUpdateIdValues (designer); + } else { + designer.NestedTypes.Clear (); } - RemoveUpdateIdValues (designer); designer.Fields.Clear (); designer.Properties.Clear (); designer.CustomAttributes.Clear (); diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Linker.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Linker.cs index cca2056cf35..45c3a3feeb3 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Linker.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/Linker.cs @@ -111,6 +111,8 @@ static Pipeline CreatePipeline (LinkerOptions options) pipeline.AppendStep (new RemoveResources (options.I18nAssemblies)); // remove collation tables // end monodroid specific + if (options.UseDesignerAssembly) + pipeline.AppendStep (new FixLegacyResourceDesignerStep (cache)); pipeline.AppendStep (new FixAbstractMethodsStep (cache)); pipeline.AppendStep (new MonoDroidMarkStep (cache)); pipeline.AppendStep (new SweepStep ()); diff --git a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkerOptions.cs b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkerOptions.cs index 0cd81bb8641..1886d50915e 100644 --- a/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkerOptions.cs +++ b/src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/LinkerOptions.cs @@ -26,5 +26,6 @@ class LinkerOptions public bool PreserveJniMarshalMethods { get; set; } public bool DeterministicOutput { get; set; } public bool LinkResources { get; set; } + public bool UseDesignerAssembly { get; set; } } } diff --git a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets index 8827e1ceb51..d1bfde205de 100644 --- a/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets +++ b/src/Xamarin.Android.Build.Tasks/MSBuild/Xamarin/Android/Xamarin.Android.Aapt2.targets @@ -154,7 +154,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. --no-version-vectors $(AndroidAapt2LinkExtraArgs) + + + + + + + + + + False + + + + + <_DesignerAssemblyName>_Microsoft.Android.Resource.Designer + + <_DesignerIntermediateOutputPath Condition=" '$(_DesignerIntermediateOutputPath)' == '' And Exists ('$(IntermediateOutputPath)$(_DesignerAssemblyName).dll') ">$(IntermediateOutputPath) + <_DesignerIntermediateOutputPath Condition=" '$(_DesignerIntermediateOutputPath)' == '' And '$(_OuterIntermediateOutputPath)' != '' And Exists ('$(_OuterIntermediateOutputPath)$(_DesignerAssemblyName).dll') ">$(_OuterIntermediateOutputPath) + + <_DesignerIntermediateOutputPath Condition=" '$(_DesignerIntermediateOutputPath)' == '' And '$(_OuterIntermediateOutputPath)' != '' ">$(_OuterIntermediateOutputPath) + <_DesignerIntermediateOutputPath Condition=" '$(_DesignerIntermediateOutputPath)' == '' ">$(IntermediateOutputPath) + <_GenerateResourceDesignerAssemblyOutput>$(_DesignerIntermediateOutputPath)$(_DesignerAssemblyName).dll + <_GenerateResourceDesignerClassFile Condition=" '$(Language)' == 'F#' ">$(_DesignerIntermediateOutputPath)_$(_DesignerAssemblyName).fs + <_GenerateResourceDesignerClassFile Condition=" '$(_GenerateResourceDesignerClassFile)' == '' ">$(_DesignerIntermediateOutputPath)_$(_DesignerAssemblyName).cs + <_GenerateResourceCaseMapFile>$(_DesignerIntermediateOutputPath)case_map.txt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <_ErrorItems Include="@(_MonoAndroidReferencePath)" Condition=" '%(_MonoAndroidReferencePath.HasResourceDesignerAssemblyReference)' == 'True' "/> + <_ErrorItems Include="@(_MonoAndroidReferenceDependencyPaths)" Condition=" '%(_MonoAndroidReferenceDependencyPaths.HasResourceDesignerAssemblyReference)' == 'True' "/> + + + + + + <_BuildResourceDesignerDependsOn> + _SetupDesignerProperties; + _GenerateResourceCaseMap; + _GenerateRtxt; + _GenerateResourceDesignerIntermediateClass; + _GenerateResourceDesignerAssembly; + _AddResourceDesignerFiles; + + + + + + + PreserveNewest + + + + + + + + + + + + + $(_DesignerAssemblyName).dll + PreserveNewest + true + true + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index e15b10a264f..c4a7a91b861 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -67,6 +67,14 @@ _ResolveAssemblies MSBuild target. + + + True + PreserveNewest + false + android + + <_DebugSymbolsIntermediatePath Remove="@(_DebugSymbolsIntermediatePath)" /> <_DebugSymbolsIntermediatePath Include="$([System.IO.Path]::ChangeExtension ($(_OuterIntermediateAssembly), '.pdb'))" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets index b9305f91637..fa15917a912 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets @@ -107,6 +107,7 @@ projects, these properties are set in Xamarin.Android.Legacy.targets. + _CreatePropertiesCache; _SeparateAppExtensionReferences; $(ResolveReferencesDependsOn); _ConvertAndroidMamMappingFileToXml; @@ -116,17 +117,25 @@ projects, these properties are set in Xamarin.Android.Legacy.targets. $(CoreResolveReferencesDependsOn); - UpdateAndroidInterfaceProxies; UpdateAndroidResources; + _BuildResourceDesigner; + UpdateAndroidInterfaceProxies; _SetAndroidGenerateManagedBindings; AddBindingsToCompile; + _CheckForInvalidDesignerConfig; + + $(DesignTimeResolveAssemblyReferencesDependsOn); + _BuildResourceDesigner; + <_UpdateAndroidResourcesDependsOn> $(CoreResolveReferencesDependsOn); _CreatePropertiesCache; _CheckForDeletedResourceFile; _ComputeAndroidResourcePaths; _UpdateAndroidResgen; + _CreateAar; + _BuildResourceDesigner; _SetupMSBuildAllProjects; @@ -134,6 +143,8 @@ projects, these properties are set in Xamarin.Android.Legacy.targets. _AddAndroidDefines; _IncludeLayoutBindingSources; AddLibraryJarsToBind; + _BuildResourceDesigner; + _AddResourceDesignerFiles; $(CompileDependsOn); _CheckAndroidHttpClientHandlerType; @@ -170,6 +181,7 @@ projects, these properties are set in Xamarin.Android.Legacy.targets. _BeforeGetAndroidDependencies; _SetLatestTargetFrameworkVersion; _ResolveSdks; + _ResolveMonoAndroidSdks; _ResolveAndroidTooling; $(GetAndroidDependenciesDependsOn); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets index 687dc79e266..43ed246a334 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.DefaultProperties.targets @@ -10,6 +10,8 @@ true Xamarin.Android.Net.AndroidMessageHandler Xamarin.Android.Net.AndroidClientHandler + true + false true $(AndroidGenerateResourceDesigner) false diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets index f28c5b6d9d5..7400de192ff 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.ILLink.targets @@ -88,7 +88,12 @@ This file contains the .NET 5-specific targets to customize ILLink AfterStep="CleanStep" Type="MonoDroid.Tuner.GetAssembliesStep" /> - + <_TrimmerCustomSteps + Condition=" '$(AndroidUseDesignerAssembly)' == 'true' " + Include="$(_AndroidLinkerCustomStepAssembly)" + BeforeStep="MarkStep" + Type="MonoDroid.Tuner.FixLegacyResourceDesignerStep" + /> <_PreserveLists Include="$(MSBuildThisFileDirectory)..\PreserveLists\*.xml" /> <_AndroidFilesToPublish Include="$(OutputPath)*.%(_AndroidPackageFormats.Identity)" /> <_AndroidFilesToPublish Include="$(AndroidProguardMappingFile)" Condition="Exists ('$(AndroidProguardMappingFile)')" /> + <_AndroidFilesToPublish Include="$(_GenerateResourceDesignerAssemblyOutput)" Condition="Exists('$(_GenerateResourceDesignerAssemblyOutput)')" /> diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index b2a969da7cc..c87254b93ce 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1467,5 +1467,14 @@ public static string XA1033 { return ResourceManager.GetString("XA1033", resourceCulture); } } + + /// + /// Looks up a localized string similar to Your project references '{0}' which uses the `_Microsoft.Android.Resource.Designer` assembly, but you do not have this feature enabled. Please set the `AndroidUseDesignerAssembly` MSBuild property to `true` in your project file. + /// + public static string XA1034 { + get { + return ResourceManager.GetString("XA1034", resourceCulture); + } + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index ac48bbe6967..c36b272704b 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -487,6 +487,11 @@ Please change the value to an assembly-qualifed type name which inherits from '{ Could not resolve '{0}'. Please check your `AndroidHttpClientHandlerType` setting. The following are literal names and should not be translated: 'AndroidHttpClientHandlerType', {0} - The value of the property. + + + Your project references '{0}' which uses the `_Microsoft.Android.Resource.Designer` assembly, but you do not have this feature enabled. Please set the `AndroidUseDesignerAssembly` MSBuild property to `true` in your project file. + The following are literal names and should not be translated: '_Microsoft.Android.Resource.Desinger', 'AndroidUseDesignerAssembly', 'true' +{0} - The name of the assembly Use of AppDomain.CreateDomain() detected in assembly: {0}. .NET 6 and higher will only support a single AppDomain, so this API will no longer be available in Xamarin.Android once .NET 6 is released. diff --git a/src/Xamarin.Android.Build.Tasks/Resources/Resource.Designer.snk b/src/Xamarin.Android.Build.Tasks/Resources/Resource.Designer.snk new file mode 100644 index 0000000000000000000000000000000000000000..7f3d225c9d23ae363ae3378adb74e2b0c860d59e GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098026=^U^~-OCvXK3hrS`Mi9$2T05eInu z%u!T#dB{ERX+i@+0*f`VmqMtp0n(;Ep79;#Oid*rH=5Acn+qM0=?#T6V!VZwa=&_- zLLFfKy>c6dkp(~}mbQL`5AWqjP3S9QiCLe+&9xa)oph2omoO&;feli8Ts_@j62^nO z7Cl?z7W;b)ejklVInG->T>)KEqpH0%2f!3eQ8SuOv@$%+#Q-}zD_7L~QDXlK2qSOT z24oe49RNY9lAOTnYv0|oaEwef7a-~u-y!blIRFO1%#hwDDQ)E2T|t29h~-CuQt6Zn zR37YYxD;QuBAz+3qrUE%i@shj@FTCg_90|@0X!NF`MNrkkDQaf_*_)x?hGG}+9p3~ zV6K6qJcZk;15>|w)RygGHeO*t=4qbQghEyJmQ=n4;cJBNHLt~AmUha-*FoU+Zfv6^ z{$!G9kW=?cIA#qcn8R;@j?BdH(~!v{;ZWiZ+`})IHMoLSmH~KKlc6`)DZh+XL)tXE z8#zV{t#Ux;jU-=zTsAkE0MY9SbDI~@c0B2NR|MN1;U5y1uQpnS%oZDZZ=gUlT#dQX z_#Q1UGzz!bmMO36?&OQ??;+oTK_ tempFiles = new List (); SortedSet rulesFiles = new SortedSet (); Dictionary apks = new Dictionary (); + string resourceSymbolsTextFileTemp; protected override int GetRequiredDaemonInstances () { @@ -93,6 +94,8 @@ public async override System.Threading.Tasks.Task RunTaskAsync () try { assemblyMap.Load (Path.Combine (WorkingDirectory, AssemblyIdentityMapFile)); + resourceSymbolsTextFileTemp = GetTempFile (); + await this.WhenAll (ManifestFiles, ProcessManifest); ProcessOutput (); @@ -132,6 +135,8 @@ public async override System.Threading.Tasks.Task RunTaskAsync () } Files.CopyIfStringChanged (sb.ToString (), ProguardRuleOutput); } + if (!string.IsNullOrEmpty (ResourceSymbolsTextFile)) + Files.CopyIfChanged (resourceSymbolsTextFileTemp, GetFullPath (ResourceSymbolsTextFile)); } finally { lock (tempFiles) { foreach (var temp in tempFiles) { @@ -253,7 +258,7 @@ string [] GenerateCommandLineCommands (string ManifestFile, string currentAbi, s if (!string.IsNullOrEmpty (ResourceSymbolsTextFile)) { cmd.Add ("--output-text-symbols"); - cmd.Add (GetFullPath (ResourceSymbolsTextFile)); + cmd.Add (GetFullPath (resourceSymbolsTextFileTemp)); } if (ProtobufFormat) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/AppendCustomMetadataToItemGroup.cs b/src/Xamarin.Android.Build.Tasks/Tasks/AppendCustomMetadataToItemGroup.cs index ffd28e90201..9ab1d323596 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/AppendCustomMetadataToItemGroup.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/AppendCustomMetadataToItemGroup.cs @@ -35,6 +35,11 @@ public override bool RunTask () foreach (var item in Inputs) { var fn = Path.GetFileNameWithoutExtension (item.ItemSpec); output.Add (item); + var md = item.GetMetadata ("HasResourceDesignerAssemblyReference"); + if (string.IsNullOrEmpty (md)) { + var b = MonoAndroidHelper.HasResourceDesignerAssemblyReference (item); + item.SetMetadata ("HasResourceDesignerAssemblyReference", MonoAndroidHelper.HasResourceDesignerAssemblyReference (item).ToString ()); + } List metaDataList; if (!metaData.TryGetValue (fn, out metaDataList)) continue; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ConvertCustomView.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ConvertCustomView.cs index ea7a65b7a57..9e88ff78d2f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ConvertCustomView.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ConvertCustomView.cs @@ -31,7 +31,7 @@ public class ConvertCustomView : AndroidTask { public override bool RunTask () { - var acw_map = MonoAndroidHelper.LoadAcwMapFile (AcwMapFile); + var acw_map = MonoAndroidHelper.LoadMapFile (BuildEngine4, AcwMapFile, StringComparer.Ordinal); var customViewMap = MonoAndroidHelper.LoadCustomViewMapFile (BuildEngine4, CustomViewMapFile); var processed = new HashSet (); @@ -119,8 +119,8 @@ bool TryFixCustomClassAttribute (XAttribute attr, Dictionary acw bool TryFixFragment (XAttribute attr, Dictionary acwMap) { - // Looks for any: - // ParseFile (StreamReader reader) while (!reader.EndOfStream) { var line = reader.ReadLine (); var items = line.Split (Delimiter, 4); - yield return items; + if (items.Length == 4) + yield return items; } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs index e8638b29761..a87f24a5832 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs @@ -471,7 +471,7 @@ void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_ } if (android_runtime_jnienv_class_token == -1 || jnienv_initialize_method_token == -1 || jnienv_registerjninatives_method_token == -1) { - throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens"); + throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens for {assemblyFilePath}"); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceCaseMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceCaseMap.cs new file mode 100644 index 00000000000..9717be99f74 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceCaseMap.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2021 Microsoft, Inc. All rights reserved. +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tasks +{ + public class GenerateResourceCaseMap : AndroidTask + { + public override string TaskPrefix => "GRCM"; + public ITaskItem[] Resources { get; set; } + + [Required] + public string ResourceDirectory { get; set; } + + [Required] + public string ProjectDir { get; set; } + + public ITaskItem[] AdditionalResourceDirectories { get; set; } + + [Required] + public ITaskItem OutputFile { get; set; } + + private Dictionary resource_fixup = new Dictionary (StringComparer.OrdinalIgnoreCase); + + public override bool RunTask () + { + // ResourceDirectory may be a relative path, and + // we need to compare it to absolute paths + ResourceDirectory = Path.GetFullPath (ResourceDirectory); + + // Create our capitalization maps so we can support mixed case resources + foreach (var item in Resources) { + var path = Path.GetFullPath (item.ItemSpec); + if (!path.StartsWith (ResourceDirectory, StringComparison.OrdinalIgnoreCase)) { + Log.LogDebugMessage ($"Skipping {item}. Path is not include the '{ResourceDirectory}'"); + continue; + } + + var name = path.Substring (ResourceDirectory.Length).TrimStart ('/', '\\'); + var logical_name = item.GetMetadata ("LogicalName").Replace ('\\', '/'); + if (string.IsNullOrEmpty (logical_name)) + logical_name = Path.GetFileName (path); + + AddRename (name.Replace ('/', Path.DirectorySeparatorChar), logical_name.Replace ('/', Path.DirectorySeparatorChar)); + } + foreach (var additionalDir in AdditionalResourceDirectories ?? Array.Empty()) { + var dir = Path.Combine (ProjectDir, Path.GetDirectoryName (additionalDir.ItemSpec)); + var file = Path.Combine (dir, "__res_name_case_map.txt"); + if (!File.Exists (file)) { + // .NET 6 .aar files place the file in a sub-directory + file = Path.Combine (dir, ".net", "__res_name_case_map.txt"); + if (!File.Exists (file)) + continue; + } + foreach (var line in File.ReadLines (file)) { + if (string.IsNullOrEmpty (line)) + continue; + string [] tok = line.Split (';'); + AddRename (tok [1].Replace ('/', Path.DirectorySeparatorChar), tok [0].Replace ('/', Path.DirectorySeparatorChar)); + } + } + + if (MonoAndroidHelper.SaveMapFile (BuildEngine4, Path.GetFullPath (OutputFile.ItemSpec), resource_fixup)) { + Log.LogDebugMessage ($"Writing to: {OutputFile.ItemSpec}"); + } else { + Log.LogDebugMessage ($"Up to date: {OutputFile.ItemSpec}"); + } + + return !Log.HasLoggedErrors; + } + + private void AddRename (string android, string user) + { + var from = android; + var to = user; + + if (from.Contains ('.')) + from = from.Substring (0, from.LastIndexOf ('.')); + if (to.Contains ('.')) + to = to.Substring (0, to.LastIndexOf ('.')); + + from = NormalizeAlternative (from); + to = NormalizeAlternative (to); + + string curTo; + + if (resource_fixup.TryGetValue (from, out curTo)) { + if (string.Compare (to, curTo, StringComparison.OrdinalIgnoreCase) != 0) { + var ext = Path.GetExtension (android); + var dir = Path.GetDirectoryName (user); + + Log.LogDebugMessage ("Resource target names differ; got '{0}', expected '{1}'.", + Path.Combine (dir, Path.GetFileName (to) + ext), + Path.Combine (dir, Path.GetFileName (curTo) + ext)); + } + return; + } + Log.LogDebugMessage ($"Adding map from '{from}' to '{to}'."); + resource_fixup.Add (from, to); + } + + static string NormalizeAlternative (string value) + { + int s = value.IndexOfAny (new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }); + + if (s < 0) + return value; + + int a = value.IndexOf ('-'); + + return + ResourceParser.GetNestedTypeName (value.Substring (0, (a < 0 || a >= s) ? s : a)).ToLowerInvariant () + + value.Substring (s); + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs index ba7fbe053a3..efb8fa465f5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesigner.cs @@ -53,6 +53,8 @@ public class GenerateResourceDesigner : AndroidTask public string ResourceFlagFile { get; set; } + public string CaseMapFile { get; set; } + private Dictionary resource_fixup = new Dictionary (StringComparer.OrdinalIgnoreCase); public override bool RunTask () @@ -73,35 +75,7 @@ public override bool RunTask () var javaPlatformDirectory = Path.GetDirectoryName (JavaPlatformJarPath); - // Create our capitalization maps so we can support mixed case resources - foreach (var item in Resources) { - var path = Path.GetFullPath (item.ItemSpec); - if (!path.StartsWith (ResourceDirectory, StringComparison.OrdinalIgnoreCase)) - continue; - - var name = path.Substring (ResourceDirectory.Length).TrimStart ('/', '\\'); - var logical_name = item.GetMetadata ("LogicalName").Replace ('\\', '/'); - if (string.IsNullOrEmpty (logical_name)) - logical_name = Path.GetFileName (path); - - AddRename (name.Replace ('/', Path.DirectorySeparatorChar), logical_name.Replace ('/', Path.DirectorySeparatorChar)); - } - if (AdditionalResourceDirectories != null) { - foreach (var additionalDir in AdditionalResourceDirectories) { - var dir = Path.Combine (ProjectDir, Path.GetDirectoryName (additionalDir.ItemSpec)); - var file = Path.Combine (dir, "__res_name_case_map.txt"); - if (!File.Exists (file)) { - // .NET 6 .aar files place the file in a sub-directory - file = Path.Combine (dir, ".net", "__res_name_case_map.txt"); - if (!File.Exists (file)) - continue; - } - foreach (var line in File.ReadAllLines (file).Where (l => !string.IsNullOrEmpty (l))) { - string [] tok = line.Split (';'); - AddRename (tok [1].Replace ('/', Path.DirectorySeparatorChar), tok [0].Replace ('/', Path.DirectorySeparatorChar)); - } - } - } + resource_fixup = MonoAndroidHelper.LoadMapFile (BuildEngine4, CaseMapFile, StringComparer.OrdinalIgnoreCase); // Parse out the resources from the R.java file CodeTypeDeclaration resources; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs new file mode 100644 index 00000000000..ba009724f7b --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerAssembly.cs @@ -0,0 +1,350 @@ +// Copyright (C) 2011 Xamarin, Inc. All rights reserved. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; +using System.IO; +using System.Linq; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Android.Build.Tasks; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Java.Interop.Tools.Cecil; +using Brutal.Dev.StrongNameSigner; +using MonoDroid.Tuner; + +namespace Xamarin.Android.Tasks +{ + public class GenerateResourceDesignerAssembly : AndroidTask + { + public override string TaskPrefix => "GRDA"; + + [Required] + public ITaskItem RTxtFile { get; set; } + + public ITaskItem ResourceMap { get; set; } + + [Required] + public bool IsApplication { get; set; } + + [Required] + public bool DesignTimeBuild { get; set; } + + [Required] + public ITaskItem OutputFile { get; set; } + + [Required] + public string TargetFrameworkVersion { get; set; } + + [Required] + public string TargetFrameworkIdentifier { get; set; } + + [Required] + public string ProjectDir { get; set; } + + [Required] + public ITaskItem[] Resources { get; set; } + + [Required] + public string ResourceDirectory { get; set; } + public string CaseMapFile { get; set; } + public ITaskItem[] AdditionalResourceDirectories { get; set; } + public ITaskItem[] FrameworkDirectories { get; set; } + public bool Deterministic { get; set; } + public string AssemblyName { get; set; } + TypeReference intArray; + TypeReference intRef; + TypeReference objectRef; + Dictionary resource_fixup = new Dictionary (StringComparer.OrdinalIgnoreCase); + + public override bool RunTask () + { + using (var res = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: false)) { + Run(res); + } + return !Log.HasLoggedErrors; + } + + bool Run(DirectoryAssemblyResolver res) + { + foreach (var dir in FrameworkDirectories) { + if (Directory.Exists (dir.ItemSpec)) + res.SearchDirectories.Add (dir.ItemSpec); + } + // ResourceDirectory may be a relative path, and + // we need to compare it to absolute paths + ResourceDirectory = Path.GetFullPath (ResourceDirectory); + + string assemblyName = Path.GetFileNameWithoutExtension (OutputFile.ItemSpec); + + resource_fixup = MonoAndroidHelper.LoadMapFile (BuildEngine4, CaseMapFile, StringComparer.OrdinalIgnoreCase); + // Generate an assembly which contains all the values in the provided + // R.txt file. + var mp = new ModuleParameters (); + mp.AssemblyResolver = res; + mp.Kind = ModuleKind.Dll; + var assembly = AssemblyDefinition.CreateAssembly ( + new AssemblyNameDefinition (assemblyName, new Version (1, 0)), + assemblyName, + mp); + + var module = assembly.MainModule; + + module.AssemblyReferences.Clear (); + var netstandardAsm = AssemblyNameReference.Parse ("netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51"); + module.AssemblyReferences.Add(netstandardAsm); + var netstandardDef = module.AssemblyResolver.Resolve(netstandardAsm); + + if (!IsApplication) { + MethodReference referenceAssemblyConstructor = ImportCustomAttributeConstructor ("System.Runtime.CompilerServices.ReferenceAssemblyAttribute", module, netstandardDef.MainModule); + module.Assembly.CustomAttributes.Add (new CustomAttribute (referenceAssemblyConstructor)); + } else { + // Add the InternalsVisibleToAttribute so the app can access ResourceConstant + if (!string.IsNullOrEmpty (AssemblyName)) { + MethodReference internalsVisibleToAttributeConstructor = ImportCustomAttributeConstructor ("System.Runtime.CompilerServices.InternalsVisibleToAttribute", module, netstandardDef.MainModule); + var ar = new CustomAttribute (internalsVisibleToAttributeConstructor); + ar.ConstructorArguments.Add (new CustomAttributeArgument (module.TypeSystem.String, AssemblyName)); + module.Assembly.CustomAttributes.Add (ar); + } + } + + MethodReference targetFrameworkConstructor = ImportCustomAttributeConstructor ("System.Runtime.Versioning.TargetFrameworkAttribute", module, netstandardDef.MainModule); + + var attr = new CustomAttribute (targetFrameworkConstructor); + attr.ConstructorArguments.Add (new CustomAttributeArgument (module.TypeSystem.String, $".NETStandard,Version=v2.1")); + attr.Properties.Add (new CustomAttributeNamedArgument ("FrameworkDisplayName", new CustomAttributeArgument (module.TypeSystem.String, ""))); + module.Assembly.CustomAttributes.Add (attr); + + var att = TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.Public | TypeAttributes.BeforeFieldInit; + + intArray = new ArrayType (module.TypeSystem.Int32); + intRef = module.TypeSystem.Int32; + objectRef = module.TypeSystem.Object; + + // The Property Based class. + var resourceDesigner = new TypeDefinition ( + FixLegacyResourceDesignerStep.DesignerAssemblyNamespace, + "Resource", + att, + objectRef + ); + CreateCtor (resourceDesigner, module); + module.Types.Add (resourceDesigner); + TypeDefinition constDesigner = null; + if (IsApplication) { + // The Constant based class + TypeAttributes attrib = string.IsNullOrEmpty (AssemblyName) ? TypeAttributes.Public : TypeAttributes.Public; + constDesigner = new TypeDefinition ( + FixLegacyResourceDesignerStep.DesignerAssemblyNamespace, + "ResourceConstant", + attrib | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, + objectRef + ); + CreateCtor (constDesigner, module); + module.Types.Add (constDesigner); + } + + DateTime lastWriteTimeUtc = DateTime.MinValue; + if (File.Exists (OutputFile.ItemSpec)) + lastWriteTimeUtc = File.GetLastWriteTimeUtc (OutputFile.ItemSpec); + + if (File.Exists (RTxtFile.ItemSpec)) { + if (File.GetLastWriteTimeUtc (RTxtFile.ItemSpec) < lastWriteTimeUtc) { + Log.LogDebugMessage ($"{RTxtFile.ItemSpec} has not changed since {OutputFile.ItemSpec} was generated."); + return !Log.HasLoggedErrors; + } + var parser = new RtxtParser (); + var resources = parser.Parse (RTxtFile.ItemSpec, Log, resource_fixup); + foreach (var r in resources) { + switch (r.Type) { + case RType.Integer: + if (IsApplication) + CreateIntField (r.ResourceTypeName, r.Identifier, r.Id, constDesigner, module); + CreateIntProperty (r.ResourceTypeName, r.Identifier, r.Id, resourceDesigner, module); + break; + case RType.Array: + if (IsApplication) + CreateIntArrayField (r.ResourceTypeName, r.Identifier, r.Ids, constDesigner, module); + CreateIntArrayProperty (r.ResourceTypeName, r.Identifier, r.Ids, resourceDesigner, module); + break; + } + } + } + // Add a return to each of the static constructor + foreach(var c in staticConstructors) { + var il = c.Value.Body.GetILProcessor (); + il.Emit(OpCodes.Ret); + } + StrongNameAssembly (assembly.Name); + var wp = new WriterParameters () { + DeterministicMvid = Deterministic, + }; + var s = MemoryStreamPool.Shared.Rent (); + try { + assembly.Write (s, wp); + s.Position = 0; + if (Files.CopyIfStreamChanged (s, OutputFile.ItemSpec)) { + Log.LogDebugMessage ($"Updated '{OutputFile.ItemSpec}'."); + } else { + Log.LogDebugMessage ($"'{OutputFile.ItemSpec}' was up to date."); + } + } finally { + MemoryStreamPool.Shared.Return (s); + } + return !Log.HasLoggedErrors; + } + + MethodReference ImportCustomAttributeConstructor (string type, ModuleDefinition module, ModuleDefinition sourceModule = null) + { + var tr = module.ImportReference ((sourceModule ?? module).ExportedTypes.First(x => x.FullName == type).Resolve ()); + var tv = tr.Resolve(); + return module.ImportReference (tv.Methods.First(x => x.IsConstructor)); + } + + void CreateIntProperty (string resourceClass, string propertyName, int value, TypeDefinition resourceDesigner, ModuleDefinition module, + MethodAttributes attributes = MethodAttributes.Public, TypeAttributes typeAttributes = TypeAttributes.NestedPublic) + { + TypeDefinition nestedType = CreateResourceClass (resourceDesigner, resourceClass, module, typeAttributes); + PropertyDefinition p = CreateProperty (propertyName, value, module, attributes); + nestedType.Properties.Add (p); + nestedType.Methods.Insert (Math.Max(0, nestedType.Methods.Count () - 1), p.GetMethod); + } + + void CreateIntField (string resourceClass, string fieldName, int value, TypeDefinition resourceDesigner, ModuleDefinition module, + FieldAttributes attributes = FieldAttributes.Public, TypeAttributes typeAttributes = TypeAttributes.NestedPublic) + { + TypeDefinition nestedType = CreateResourceClass (resourceDesigner, resourceClass, module, typeAttributes); + FieldDefinition p = CreateField (fieldName, value, module, attributes); + nestedType.Fields.Add (p); + } + + void CreateIntArrayProperty (string resourceClass, string propertyName, int[] values, TypeDefinition resourceDesigner, ModuleDefinition module, + MethodAttributes attributes = MethodAttributes.Public, TypeAttributes typeAttributes = TypeAttributes.NestedPublic) + { + TypeDefinition nestedType = CreateResourceClass (resourceDesigner, resourceClass, module, typeAttributes); + PropertyDefinition p = CreateArrayProperty (propertyName, values, module, attributes); + nestedType.Properties.Add (p); + nestedType.Methods.Insert (Math.Max(0, nestedType.Methods.Count () - 1), p.GetMethod); + } + + void CreateIntArrayField (string resourceClass, string fieldName, int[] values, TypeDefinition resourceDesigner, ModuleDefinition module, + FieldAttributes attributes = FieldAttributes.Public, TypeAttributes typeAttributes = TypeAttributes.NestedPublic) + { + TypeDefinition nestedType = CreateResourceClass (resourceDesigner, resourceClass, module, typeAttributes); + FieldDefinition p = CreateArrayField (fieldName, values, module, attributes); + nestedType.Fields.Add (p); + MethodDefinition ctor = GetOrCreateStaticCtor (nestedType, module); + ILProcessor il = ctor.Body.GetILProcessor (); + il.Emit (OpCodes.Ldc_I4, values.Length); // store array size + il.Emit (OpCodes.Newarr, intRef); //create a new array + il.Emit (OpCodes.Stsfld, p); + int index = 0; + foreach (int value in values) { + il.Emit (OpCodes.Ldsfld, p); + il.Emit (OpCodes.Ldc_I4, index++); // index + il.Emit (OpCodes.Ldc_I4, value); // value + il.Emit (OpCodes.Stelem_I4); + } + } + + Dictionary resourceClasses = new Dictionary (StringComparer.OrdinalIgnoreCase); + Dictionary staticConstructors = new Dictionary (); + + void CreateCtor (TypeDefinition type, ModuleDefinition module) + { + var ctor = new MethodDefinition (".ctor", MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, module.TypeSystem.Void); + var ctoril = ctor.Body.GetILProcessor (); + ctoril.Emit (OpCodes.Ldarg_0); + var o = module.TypeSystem.Object.Resolve (); + ctoril.Emit (OpCodes.Call, module.ImportReference (o.Methods.First (x => x.IsConstructor))); + ctoril.Emit (OpCodes.Ret); + type.Methods.Add (ctor); + } + + MethodDefinition GetOrCreateStaticCtor (TypeDefinition type, ModuleDefinition module) + { + string key = type.FullName + ".cctor"; + if (staticConstructors.ContainsKey (key)) + return staticConstructors[key]; + var ctor = new MethodDefinition (".cctor", MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Static, module.TypeSystem.Void); + type.Methods.Add (ctor); + type.IsBeforeFieldInit = false; + staticConstructors.Add (key, ctor); + return ctor; + } + + TypeDefinition CreateResourceClass (TypeDefinition resourceDesigner, string className, ModuleDefinition module, TypeAttributes attributes = TypeAttributes.NestedPublic) + { + string name = ResourceParser.GetNestedTypeName (className); + string key = resourceDesigner.Name + name; + if (resourceClasses.ContainsKey (key)) { + return resourceClasses[key]; + } + var resourceClass = new TypeDefinition (string.Empty, name, attributes | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.Sealed, objectRef); + CreateCtor (resourceClass, module); + resourceDesigner.NestedTypes.Add (resourceClass); + resourceClasses[key] = resourceClass; + return resourceClass; + } + + PropertyDefinition CreateProperty (string propertyName, int value, ModuleDefinition module, MethodAttributes attributes = MethodAttributes.Public) + { + var p = new PropertyDefinition (propertyName, PropertyAttributes.None, intRef); + var getter = new MethodDefinition ($"get_{propertyName}", attributes | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Static, intRef); + p.GetMethod = getter; + p.SetMethod = null; + var il = p.GetMethod.Body.GetILProcessor (); + il.Emit (OpCodes.Ldc_I4, value); + il.Emit (OpCodes.Ret); + return p; + } + + FieldDefinition CreateField (string fieldName, int value, ModuleDefinition module, FieldAttributes attributes = FieldAttributes.Public) + { + var f = new FieldDefinition (fieldName, attributes | FieldAttributes.Literal | FieldAttributes.Static | FieldAttributes.HasDefault, intRef); + f.Constant = value; + return f; + } + + FieldDefinition CreateArrayField (string fieldName, int[] values, ModuleDefinition module, FieldAttributes attributes = FieldAttributes.Public) + { + var f = new FieldDefinition (fieldName, attributes | FieldAttributes.Static | FieldAttributes.HasDefault, intArray); + f.Constant = values; + return f; + } + + PropertyDefinition CreateArrayProperty (string propertyName, int[] values, ModuleDefinition module, MethodAttributes attributes = MethodAttributes.Public) + { + var p = new PropertyDefinition (propertyName, PropertyAttributes.None, intArray); + var getter = new MethodDefinition ($"get_{propertyName}", attributes | MethodAttributes.Static, intArray); + p.GetMethod = getter; + p.SetMethod = null; + var il = p.GetMethod.Body.GetILProcessor (); + il.Emit (OpCodes.Ldc_I4, values.Length); + il.Emit (OpCodes.Newarr, intRef); + int index = 0; + foreach (int value in values) { + il.Emit (OpCodes.Dup); + il.Emit (OpCodes.Ldc_I4, index++); + il.Emit (OpCodes.Ldc_I4, value); + il.Emit (OpCodes.Stelem_I4); + } + il.Emit (OpCodes.Ret); + return p; + } + + void StrongNameAssembly (AssemblyNameDefinition name) + { + using (Stream stream = typeof (GenerateResourceDesignerAssembly).Assembly.GetManifestResourceStream ("Resource.Designer.snk")) { + byte[] publicKey = new byte[stream.Length]; + stream.Read (publicKey, 0, publicKey.Length); + name.HashAlgorithm = AssemblyHashAlgorithm.SHA1; + name.PublicKey = SigningHelper.GetPublicKey (publicKey); + name.HasPublicKey = true; + name.Attributes |= AssemblyAttributes.PublicKey; + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerIntermediateClass.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerIntermediateClass.cs new file mode 100644 index 00000000000..9247993a554 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateResourceDesignerIntermediateClass.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.CodeDom.Compiler; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Android.Build.Tasks; +using MonoDroid.Tuner; + +namespace Xamarin.Android.Tasks +{ + public class GenerateResourceDesignerIntermediateClass : AndroidTask + { + public override string TaskPrefix => "GRDIC"; + + private const string ResourceDesigner = $"{FixLegacyResourceDesignerStep.DesignerAssemblyNamespace}.Resource"; + private const string ResourceDesignerConstants = $"{FixLegacyResourceDesignerStep.DesignerAssemblyNamespace}.ResourceConstant"; + + private const string CSharpTemplate = @"// This is an Auto Generated file DO NOT EDIT +using System; + +namespace %NAMESPACE% { + public partial class Resource : %BASECLASS% { + } +} +"; + private const string FSharpTemplate = @"// This is an Auto Generated file DO NOT EDIT +namespace %NAMESPACE% + +type Resource = %BASECLASS% +"; + + public string Namespace { get; set; } + public bool IsApplication { get; set; } = false; + public ITaskItem OutputFile { get; set; } + public override bool RunTask () + { + string ns = IsApplication ? ResourceDesignerConstants : ResourceDesigner; + var extension = Path.GetExtension (OutputFile.ItemSpec); + var language = string.Compare (extension, ".fs", StringComparison.OrdinalIgnoreCase) == 0 ? "F#" : CodeDomProvider.GetLanguageFromExtension (extension); + //bool isVB = string.Equals (extension, ".vb", StringComparison.OrdinalIgnoreCase); + bool isFSharp = string.Equals (language, "F#", StringComparison.OrdinalIgnoreCase); + bool isCSharp = string.Equals (language, "C#", StringComparison.OrdinalIgnoreCase); + string template = ""; + if (isCSharp) + template = CSharpTemplate.Replace ("%NAMESPACE%", Namespace).Replace ("%BASECLASS%", ns); + else if (isFSharp) + template = FSharpTemplate.Replace ("%NAMESPACE%", Namespace).Replace ("%BASECLASS%", ns); + + Files.CopyIfStringChanged (template, OutputFile.ItemSpec); + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateRtxt.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateRtxt.cs new file mode 100644 index 00000000000..a9927347626 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateRtxt.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2022 Microsoft Ltd, Inc. All rights reserved. +using System; +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tasks +{ + public class GenerateRtxt : AndroidTask + { + public override string TaskPrefix => "GR"; + + [Required] + public string RTxtFile { get; set; } + + [Required] + public string ResourceDirectory { get; set; } + public string[] AdditionalResourceDirectories { get; set; } + + [Required] + public string JavaPlatformJarPath { get; set; } + + public string ResourceFlagFile { get; set; } + public string CaseMapFile { get; set; } + + public override bool RunTask () + { + // Parse the Resource files and then generate an R.txt file + var writer = new RtxtWriter (); + + var resource_fixup = MonoAndroidHelper.LoadMapFile (BuildEngine4, CaseMapFile, StringComparer.OrdinalIgnoreCase); + + var javaPlatformDirectory = Path.GetDirectoryName (JavaPlatformJarPath); + var parser = new FileResourceParser () { Log = Log, JavaPlatformDirectory = javaPlatformDirectory, ResourceFlagFile = ResourceFlagFile}; + var resources = parser.Parse (ResourceDirectory, AdditionalResourceDirectories, resource_fixup); + + // only update if it changed. + writer.Write (RTxtFile, resources); + + return !Log.HasLoggedErrors; + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssemblies.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssemblies.cs index 55c7bf7d804..67c72c382da 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssemblies.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssemblies.cs @@ -58,6 +58,8 @@ public class LinkAssemblies : AndroidTask, ML.ILogger public bool LinkResources { get; set; } + public bool UseDesignerAssembly { get; set; } + IEnumerable GetRetainAssemblies (DirectoryAssemblyResolver res) { List retainList = null; @@ -109,6 +111,7 @@ bool Execute (DirectoryAssemblyResolver res) options.PreserveJniMarshalMethods = PreserveJniMarshalMethods; options.DeterministicOutput = Deterministic; options.LinkResources = LinkResources; + options.UseDesignerAssembly = UseDesignerAssembly; var skiplist = new List (); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs index 25da6cbde08..5d4c65c8509 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/LinkAssembliesNoShrink.cs @@ -38,6 +38,8 @@ public class LinkAssembliesNoShrink : AndroidTask public bool AddKeepAlives { get; set; } + public bool UseDesignerAssembly { get; set; } + public bool Deterministic { get; set; } public override bool RunTask () @@ -64,6 +66,9 @@ public override bool RunTask () var cache = new TypeDefinitionCache (); var fixAbstractMethodsStep = new FixAbstractMethodsStep (resolver, cache, Log); var addKeepAliveStep = new AddKeepAlivesStep (resolver, cache, Log, UsingAndroidNETSdk); + var fixLegacyResourceDesignerStep = new FixLegacyResourceDesignerStep (resolver, cache, Log); + if (UseDesignerAssembly) + fixLegacyResourceDesignerStep.Load (); for (int i = 0; i < SourceFiles.Length; i++) { var source = SourceFiles [i]; var destination = DestinationFiles [i]; @@ -91,8 +96,12 @@ public override bool RunTask () if (assemblyDefinition == null) assemblyDefinition = resolver.GetAssembly (source.ItemSpec); - if (fixAbstractMethodsStep.FixAbstractMethods (assemblyDefinition) || - (AddKeepAlives && addKeepAliveStep.AddKeepAlives (assemblyDefinition))) { + bool save = fixAbstractMethodsStep.FixAbstractMethods (assemblyDefinition); + if (UseDesignerAssembly) + save |= fixLegacyResourceDesignerStep.ProcessAssemblyDesigner (assemblyDefinition); + if (AddKeepAlives) + save |= addKeepAliveStep.AddKeepAlives (assemblyDefinition); + if (save) { Log.LogDebugMessage ($"Saving modified assembly: {destination.ItemSpec}"); writerParameters.WriteSymbols = assemblyDefinition.MainModule.HasSymbols; assemblyDefinition.Write (destination.ItemSpec, writerParameters); @@ -119,6 +128,33 @@ void CopyIfChanged (ITaskItem source, ITaskItem destination) } } + class FixLegacyResourceDesignerStep : MonoDroid.Tuner.FixLegacyResourceDesignerStep + { + readonly DirectoryAssemblyResolver resolver; + readonly TaskLoggingHelper logger; + + public FixLegacyResourceDesignerStep (DirectoryAssemblyResolver resolver, TypeDefinitionCache cache, TaskLoggingHelper logger) + : base (cache) + { + this.resolver = resolver; + this.logger = logger; + } + + public void Load () { + LoadDesigner (); + } + + public override void LogMessage (string message) + { + logger.LogDebugMessage ("{0}", message); + } + + public override AssemblyDefinition Resolve (AssemblyNameReference name) + { + return resolver.Resolve (name); + } + } + class FixAbstractMethodsStep : MonoDroid.Tuner.FixAbstractMethodsStep { readonly DirectoryAssemblyResolver resolver; diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs index f675f9f1bc9..f55838abac1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/ResolveLibraryProjectImports.cs @@ -352,6 +352,7 @@ void Extract ( string importsDir = Path.Combine (outDirForDll, ImportsDirectory); string resDir = Path.Combine (importsDir, "res"); string resDirArchive = Path.Combine (resDir, "..", "res.zip"); + string rTxt = Path.Combine (importsDir, "R.txt"); string assetsDir = Path.Combine (importsDir, "assets"); string proguardFile = Path.Combine (importsDir, "proguard.txt"); @@ -367,7 +368,7 @@ void Extract ( AddJar (jars, Path.GetFullPath (file)); } } - if (Directory.Exists (resDir)) { + if (Directory.Exists (resDir) || File.Exists (rTxt)) { var skipProcessing = aarFile.GetMetadata (AndroidSkipResourceProcessing); if (string.IsNullOrEmpty (skipProcessing)) { skipProcessing = "True"; @@ -424,8 +425,9 @@ void Extract ( Log.LogErrorFromException (new PathTooLongException ($"Error extracting resources from \"{aarFile.ItemSpec}\"", ex)); } } - if (Directory.Exists (resDir)) { - CreateResourceArchive (resDir, resDirArchive); + if (Directory.Exists (resDir) || File.Exists (rTxt)) { + if (Directory.Exists (resDir)) + CreateResourceArchive (resDir, resDirArchive); var skipProcessing = aarFile.GetMetadata (AndroidSkipResourceProcessing); if (string.IsNullOrEmpty (skipProcessing)) { skipProcessing = "True"; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs index 6858b962cae..35b5bf82aed 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AndroidUpdateResourcesTest.cs @@ -18,8 +18,11 @@ namespace Xamarin.Android.Build.Tests public class AndroidUpdateResourcesTest : BaseTest { [Test] - public void CheckMultipleLibraryProjectReferenceAlias ([Values (true, false)] bool withGlobal) + public void CheckMultipleLibraryProjectReferenceAlias ([Values (true, false)] bool withGlobal, [Values (true, false)] bool useDesignerAssembly) { + if (useDesignerAssembly && !Builder.UseDotNet) { + Assert.Ignore ($"Skipping, {useDesignerAssembly} not supported in Legacy."); + } var path = Path.Combine (Root, "temp", TestName); var library1 = new XamarinAndroidLibraryProject () { ProjectName = "Library1", @@ -38,6 +41,9 @@ public void CheckMultipleLibraryProjectReferenceAlias ([Values (true, false)] bo }, }, }; + library1.SetProperty ("AndroidUseDesignerAssembly", "false"); + library2.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); + proj.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); using (var builder1 = CreateDllBuilder (Path.Combine (path, library1.ProjectName), cleanupAfterSuccessfulBuild: false, cleanupOnDispose: false)) { builder1.ThrowOnBuildFailure = false; Assert.IsTrue (builder1.Build (library1), "Library should have built."); @@ -47,10 +53,12 @@ public void CheckMultipleLibraryProjectReferenceAlias ([Values (true, false)] bo using (var b = CreateApkBuilder (Path.Combine (path, proj.ProjectName), cleanupAfterSuccessfulBuild: false, cleanupOnDispose: false)) { b.ThrowOnBuildFailure = false; Assert.IsTrue (b.Build (proj), "Project should have built."); - string resource_designer_cs = GetResourceDesignerPath (b, proj); - string [] text = File.ReadAllLines (resource_designer_cs); - Assert.IsTrue (text.Count (x => x.Contains ("Library1.Resource.String.library_name")) == 2, "library_name resource should be present exactly once for each library"); - Assert.IsTrue (text.Count (x => x == "extern alias Lib1A;" || x == "extern alias Lib1B;") <= 1, "No more than one extern alias should be present for each library."); + if (!useDesignerAssembly) { + string resource_designer_cs = GetResourceDesignerPath (b, proj); + string [] text = GetResourceDesignerLines (proj, resource_designer_cs); + Assert.IsTrue (text.Count (x => x.Contains ("Library1.Resource.String.library_name")) == 2, "library_name resource should be present exactly once for each library"); + Assert.IsTrue (text.Count (x => x == "extern alias Lib1A;" || x == "extern alias Lib1B;") <= 1, "No more than one extern alias should be present for each library."); + } } } } @@ -92,6 +100,7 @@ public void DesignTimeBuild ([Values(false, true)] bool isRelease, [Values (fals IsRelease = isRelease, }; lib.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", useManagedParser.ToString ()); + lib.SetProperty ("AndroidUseDesignerAssembly", "false"); lib.AndroidUseAapt2 = useAapt2; var proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, @@ -101,6 +110,7 @@ public void DesignTimeBuild ([Values(false, true)] bool isRelease, [Values (fals }; var intermediateOutputPath = Path.Combine (path, proj.ProjectName, proj.IntermediateOutputPath); proj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", useManagedParser.ToString ()); + proj.SetProperty ("AndroidUseDesignerAssembly", "false"); proj.AndroidUseAapt2 = useAapt2; using (var l = CreateDllBuilder (Path.Combine (path, lib.ProjectName), false, false)) { using (var b = CreateApkBuilder (Path.Combine (path, proj.ProjectName), false, false)) { @@ -528,13 +538,20 @@ public void CheckResourceDesignerIsCreated (bool isRelease, ProjectLanguage lang using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "Build should have succeeded."); // Intermediate designer file support is not compatible with F# projects using Xamarin.Android.FSharp.ResourceProvider. - var outputFile = isFSharp ? Path.Combine (Root, b.ProjectDirectory, "Resources", "Resource.designer" + proj.Language.DefaultDesignerExtension) - : Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "Resource.designer" + proj.Language.DefaultDesignerExtension); - Assert.IsTrue (File.Exists (outputFile), "Resource.designer{1} should have been created in {0}", - isFSharp ? Path.Combine (Root, b.ProjectDirectory, "Resources") : proj.IntermediateOutputPath, - proj.Language.DefaultDesignerExtension); + string outputFile; + if (Builder.UseDotNet) { + outputFile = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "__Microsoft.Android.Resource.Designer" + proj.Language.DefaultDesignerExtension); + Assert.IsTrue (File.Exists (outputFile), $"{outputFile} should have been created in {proj.IntermediateOutputPath}"); + } else { + outputFile = isFSharp ? Path.Combine (Root, b.ProjectDirectory, "Resources", "Resource.designer" + proj.Language.DefaultDesignerExtension) + : Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "Resource.designer" + proj.Language.DefaultDesignerExtension); + Assert.IsTrue (File.Exists (outputFile), "Resource.designer{1} should have been created in {0}", + isFSharp ? Path.Combine (Root, b.ProjectDirectory, "Resources") : proj.IntermediateOutputPath, + proj.Language.DefaultDesignerExtension); + } + Assert.IsTrue (b.Clean (proj), "Clean should have succeeded."); - if (!isFSharp) { + if (!isFSharp || Builder.UseDotNet) { Assert.IsFalse (File.Exists (outputFile), "Resource.designer{1} should have been cleaned in {0}", proj.IntermediateOutputPath, proj.Language.DefaultDesignerExtension); } @@ -597,13 +614,12 @@ public void CheckOldResourceDesignerIsNotUsed ([Values (true, false)] bool isRel var fi = new FileInfo (Path.Combine (Root, b.ProjectDirectory, designer)); Assert.IsFalse (fi.Length > new [] { 0xef, 0xbb, 0xbf, 0x0d, 0x0a }.Length, "{0} should not contain anything.", designer); + var designerFile = Builder.UseDotNet ? "__Microsoft.Android.Resource.Designer" : "Resource.designer" ; var outputFile = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, - "Resource.designer" + proj.Language.DefaultDesignerExtension); - Assert.IsTrue (File.Exists (outputFile), "Resource.designer{1} should have been created in {0}", - proj.IntermediateOutputPath, proj.Language.DefaultDesignerExtension); + designerFile + proj.Language.DefaultDesignerExtension); + Assert.IsTrue (File.Exists (outputFile), $"{designerFile}{proj.Language.DefaultDesignerExtension} should have been created in {proj.IntermediateOutputPath}"); Assert.IsTrue (b.Clean (proj), "Clean should have succeeded."); - Assert.IsFalse (File.Exists (outputFile), "Resource.designer{1} should have been cleaned in {0}", - proj.IntermediateOutputPath, proj.Language.DefaultDesignerExtension); + Assert.IsFalse (File.Exists (outputFile), $"{designerFile}{proj.Language.DefaultDesignerExtension} should have been cleaned in {proj.IntermediateOutputPath}"); } } @@ -625,13 +641,56 @@ public void CheckOldResourceDesignerWithWrongCasingIsRemoved ([Values (true, fal Assert.IsFalse (File.Exists (Path.Combine (Root, b.ProjectDirectory, "Resources", "Resource.designer" + proj.Language.DefaultDesignerExtension)), "{0} should not exists", designer.Include ()); + var designerFile = Builder.UseDotNet ? "__Microsoft.Android.Resource.Designer" : "Resource.designer" ; var outputFile = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, - "Resource.designer" + proj.Language.DefaultDesignerExtension); - Assert.IsTrue (File.Exists (outputFile), "Resource.designer{1} should have been created in {0}", - proj.IntermediateOutputPath, proj.Language.DefaultDesignerExtension); + designerFile + proj.Language.DefaultDesignerExtension); + Assert.IsTrue (File.Exists (outputFile), $"{designerFile}{proj.Language.DefaultDesignerExtension} should have been created in {proj.IntermediateOutputPath}"); Assert.IsTrue (b.Clean (proj), "Clean should have succeeded."); - Assert.IsFalse (File.Exists (outputFile), "Resource.designer{1} should have been cleaned in {0}", - proj.IntermediateOutputPath, proj.Language.DefaultDesignerExtension); + Assert.IsFalse (File.Exists (outputFile), $"{designerFile}{proj.Language.DefaultDesignerExtension} should have been cleaned in {proj.IntermediateOutputPath}"); + } + } + + [Test] + public void CheckThatXA1034IsRaisedForInvalidConfiguration ([Values (true, false)] bool isRelease) + { + if (!Builder.UseDotNet) + Assert.Ignore ("Test uses designer assembly which does not work on Legacy projects."); + string path = Path.Combine (Root, "temp", TestName); + var foo = new BuildItem.Source ("Foo.cs") { + TextContent = () => @"using System; +namespace Lib1 { + public class Foo { + public static string GetFoo () { + return ""Foo""; + } + } +}" + }; + var library = new XamarinAndroidLibraryProject () { + IsRelease = isRelease, + ProjectName = "Lib1", + Sources = { foo }, + }; + library.SetProperty ("AndroidUseDesignerAssembly", "True"); + + var proj = new XamarinAndroidApplicationProject () { + IsRelease = isRelease, + ProjectName = "App1", + References = { + new BuildItem.ProjectReference ($"..\\{library.ProjectName}\\{library.ProjectName}.csproj", library.ProjectName, library.ProjectGuid), + }, + }; + proj.SetProperty ("AndroidUseDesignerAssembly", "False"); + proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", "Console.WriteLine (Lib1.Foo.GetFoo ());"); + using (var lb = CreateDllBuilder (Path.Combine (path, library.ProjectName))) { + lb.ThrowOnBuildFailure = false; + Assert.IsTrue (lb.Build (library), "Library project should have built."); + using (var pb = CreateApkBuilder (Path.Combine (path, proj.ProjectName))) { + pb.ThrowOnBuildFailure = false; + Assert.IsFalse (pb.Build (proj), "Application project build should have failed."); + StringAssertEx.ContainsText (pb.LastBuildOutput, "XA1034: "); + StringAssertEx.ContainsText (pb.LastBuildOutput, "1 Error(s)"); + } } } @@ -785,8 +844,10 @@ public void CheckFilesAreRemoved () { } [Test] - public void CheckDontUpdateResourceIfNotNeeded () + public void CheckDontUpdateResourceIfNotNeeded ([Values (true, false)] bool useDesignerAssembly) { + if (!Builder.UseDotNet && useDesignerAssembly) + Assert.Ignore ("Test uses designer assembly which does not work on Legacy projects."); var path = Path.Combine ("temp", TestName); var target = Builder.UseDotNet ? "_CreateAar" : "_CreateManagedLibraryResourceArchive"; var foo = new BuildItem.Source ("Foo.cs") { @@ -824,6 +885,7 @@ public string GetFoo () { }, }; libProj.SetProperty ("Deterministic", "true"); + libProj.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); var appProj = new XamarinAndroidApplicationProject () { IsRelease = true, ProjectName = "App1", @@ -831,6 +893,7 @@ public string GetFoo () { new BuildItem.ProjectReference (@"..\Lib1\Lib1.csproj", libProj.ProjectName, libProj.ProjectGuid), }, }; + appProj.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); using (var libBuilder = CreateDllBuilder (Path.Combine (path, libProj.ProjectName), false, false)) { Assert.IsTrue (libBuilder.Build (libProj), "Library project should have built"); using (var appBuilder = CreateApkBuilder (Path.Combine (path, appProj.ProjectName), false, false)) { @@ -864,7 +927,7 @@ public string GetFoo () { appBuilder.BuildLogFile = "build2.log"; Assert.IsTrue (appBuilder.Build (appProj, doNotCleanupOnUpdate: true, saveProject: false), "Application Build should have succeeded."); string resource_designer_cs = GetResourceDesignerPath (appBuilder, appProj); - string text = File.ReadAllText (resource_designer_cs); + string text = GetResourceDesignerText (appProj, resource_designer_cs); StringAssert.Contains ("theme_devicedefault_background2", text, "Resource.designer.cs was not updated."); appBuilder.Output.AssertTargetIsNotSkipped ("_UpdateAndroidResgen"); appBuilder.Output.AssertTargetIsNotSkipped ("_CreateBaseApk"); @@ -904,6 +967,7 @@ public void BuildAppWithManagedResourceParser() ProjectName = "App1", }; appProj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", "True"); + appProj.SetProperty ("AndroidUseDesignerAssembly", "false"); using (var appBuilder = CreateApkBuilder (Path.Combine (path, appProj.ProjectName))) { Assert.IsTrue (appBuilder.DesignTimeBuild (appProj), "DesignTime Application Build should have succeeded."); Assert.IsFalse (appProj.CreateBuildOutput (appBuilder).IsTargetSkipped ("_ManagedUpdateAndroidResgen"), @@ -961,6 +1025,7 @@ public void BuildAppWithManagedResourceParserAndLibraries () }, }; libProj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", "True"); + libProj.SetProperty ("AndroidUseDesignerAssembly", "false"); var appProj = new XamarinAndroidApplicationProject () { IsRelease = true, ProjectName = "App1", @@ -978,6 +1043,7 @@ public void BuildAppWithManagedResourceParserAndLibraries () }, }; appProj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", "True"); + appProj.SetProperty ("AndroidUseDesignerAssembly", "false"); using (var libBuilder = CreateDllBuilder (Path.Combine (path, libProj.ProjectName), false, false)) { libBuilder.AutomaticNuGetRestore = false; Assert.IsTrue (libBuilder.RunTarget (libProj, "Restore"), "Library project should have restored."); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs index f9641bff999..3f2ee58ed3a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/AotTests.cs @@ -276,6 +276,14 @@ public void BuildAMassiveApp () AotAssemblies = true, IsRelease = true, }; + if (Builder.UseDotNet) { + //TODO Re-enable if this test fails. + // app1.PackageReferences.Clear (); + // app1.PackageReferences.Add (KnownPackages.XamarinForms_5_0_0_2515); + // app1.PackageReferences.Add (KnownPackages.XamarinFormsMaps_5_0_0_2515); + // app1.PackageReferences.Add (KnownPackages.Xamarin_Build_Download_0_11_3); + + } //NOTE: BuildingInsideVisualStudio prevents the projects from being built as dependencies sb.BuildingInsideVisualStudio = false; app1.Imports.Add (new Import ("foo.targets") { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index 3e777babfde..e5a7d05a09c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -477,8 +477,11 @@ public void CheckItemMetadata ([Values (true, false)] bool isRelease) // Context https://bugzilla.xamarin.com/show_bug.cgi?id=29706 [Test] - public void CheckLogicalNamePathSeperators ([Values (false, true)] bool isRelease) + public void CheckLogicalNamePathSeperators ([Values (false, true)] bool isRelease, [Values (false, true)] bool useDesignerAssembly) { + if (useDesignerAssembly && !Builder.UseDotNet) { + Assert.Ignore ($"Skipping, {useDesignerAssembly} not supported in Legacy."); + } var illegalSeperator = IsWindows ? "/" : @"\"; var dll = new XamarinAndroidLibraryProject () { ProjectName = "Library1", @@ -503,20 +506,18 @@ public void CheckLogicalNamePathSeperators ([Values (false, true)] bool isReleas new BuildItem ("ProjectReference","..\\Library1\\Library1.csproj"), }, }; + if (!useDesignerAssembly) + dll.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); + proj.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); var path = Path.Combine ("temp", TestName); using (var b = CreateDllBuilder (Path.Combine (path, dll.ProjectName))) { Assert.IsTrue (b.Build (dll), "Build should have succeeded."); using (var builder = CreateApkBuilder (Path.Combine (path, proj.ProjectName), isRelease)) { Assert.IsTrue (builder.Build (proj), "Build should have succeeded"); - string resource_designer_cs; - if (Builder.UseDotNet) { - resource_designer_cs = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "Resource.designer.cs"); - } else { - resource_designer_cs = Path.Combine (Root, builder.ProjectDirectory, "Resources", "Resource.designer.cs"); - } - var contents = File.ReadAllText (resource_designer_cs); - StringAssert.Contains ("public const int foo = ", contents); - StringAssert.Contains ("public const int foo2 = ", contents); + string resource_designer_cs = GetResourceDesignerPath (builder, proj); + var contents = GetResourceDesignerText (proj, resource_designer_cs); + StringAssert.Contains ("public const int foo =", contents); + StringAssert.Contains ("public const int foo2 =", contents); } } } @@ -2020,8 +2021,11 @@ public void LibraryReferenceWithHigherTFVShouldDisplayWarning ([Values (true, fa } [Test] - public void AllResourcesInClassLibrary ([Values (true, false)] bool useAapt2) + public void AllResourcesInClassLibrary ([Values (true, false)] bool useAapt2, [Values (false, true)] bool useDesignerAssembly) { + if (useDesignerAssembly && !Builder.UseDotNet) { + Assert.Ignore ($"Skipping, {useDesignerAssembly} not supported in Legacy."); + } AssertAaptSupported (useAapt2); var path = Path.Combine ("temp", TestName); @@ -2035,6 +2039,7 @@ public void AllResourcesInClassLibrary ([Values (true, false)] bool useAapt2) } }; lib.SetProperty ("AndroidApplication", "False"); + lib.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); lib.AndroidUseAapt2 = useAapt2; if (Builder.UseDotNet) { lib.RemoveProperty ("OutputType"); @@ -2054,6 +2059,7 @@ public void AllResourcesInClassLibrary ([Values (true, false)] bool useAapt2) }, } }; + app.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); app.AndroidResources.Clear (); // No Resources if (Builder.UseDotNet) { app.SetProperty (KnownProperties.OutputType, "Exe"); @@ -2075,7 +2081,7 @@ public void AllResourcesInClassLibrary ([Values (true, false)] bool useAapt2) var resource_designer_cs = GetResourceDesignerPath (appBuilder, app); FileAssert.Exists (resource_designer_cs); - var contents = File.ReadAllText (resource_designer_cs); + var contents = GetResourceDesignerText (app, resource_designer_cs); Assert.AreNotEqual ("", contents); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DesignerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DesignerTests.cs index 925e80472c4..63c6603fed7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DesignerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/DesignerTests.cs @@ -93,10 +93,14 @@ public CustomTextView(Context context, IAttributeSet attributes) : base(context, " }); + lib.SetProperty ("AndroidUseDesignerAssembly", "False"); + proj.SetProperty ("AndroidUseDesignerAssembly", "False"); + using (var libb = CreateDllBuilder (Path.Combine (path, lib.ProjectName), false, false)) using (var appb = CreateApkBuilder (Path.Combine (path, proj.ProjectName), false, false)) { // Save the library project, but don't build it yet libb.Save (lib); + appb.BuildLogFile = "build1.log"; appb.Target = target; Assert.IsTrue (appb.Build (proj, parameters: DesignerParameters), $"build should have succeeded for target `{target}`"); Assert.IsTrue (appb.Output.AreTargetsAllBuilt ("_UpdateAndroidResgen"), "_UpdateAndroidResgen should have run completely."); @@ -111,6 +115,7 @@ public CustomTextView(Context context, IAttributeSet attributes) : base(context, // Build the library project now Assert.IsTrue (libb.Build (lib, doNotCleanupOnUpdate: true), "library build should have succeeded."); appb.Target = "Build"; + appb.BuildLogFile = "build2.log"; Assert.IsTrue (appb.Build (proj, doNotCleanupOnUpdate: true), "app build should have succeeded."); Assert.IsTrue (appb.Output.AreTargetsAllBuilt ("_UpdateAndroidResgen"), "_UpdateAndroidResgen should have run completely."); Assert.IsTrue (appb.Output.AreTargetsAllBuilt ("_Foo"), "_Foo should have run completely"); @@ -120,6 +125,7 @@ public CustomTextView(Context context, IAttributeSet attributes) : base(context, Assert.IsNull (doc.Element ("LinearLayout").Element ("unnamedproject.CustomTextView"), "unnamedproject.CustomTextView should have been replaced with a $(Hash).CustomTextView"); appb.Target = target; + appb.BuildLogFile = "build3.log"; Assert.IsTrue (appb.Build (proj, parameters: DesignerParameters, doNotCleanupOnUpdate: true), $"build should have succeeded for target `{target}`"); Assert.IsTrue (appb.Output.AreTargetsAllSkipped ("_UpdateAndroidResgen"), "_UpdateAndroidResgen should have been skipped."); Assert.IsTrue (appb.Output.AreTargetsAllBuilt ("_Foo"), "_Foo should have run completely"); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index 3aa68042cdd..24fe06606b7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -156,6 +156,7 @@ public void IncrementalCleanDuringClean () IsRelease = true, }; proj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", "True"); + proj.SetProperty ("AndroidUseDesignerAssembly", "False"); using (var b = CreateApkBuilder (path)) { b.Target = "Compile"; Assert.IsTrue(b.Build (proj), "DesignTime Build should have succeeded"); @@ -830,6 +831,7 @@ public void ResolveLibraryProjectImports ([Values (true, false)] bool useAapt2) FileAssert.Exists (stamp); File.Delete (stamp); + b.BuildLogFile = "build2.log"; Assert.IsTrue (b.Build (proj), "second build should have succeeded."); var actual = ReadCache (cacheFile); CollectionAssert.AreEqual (actual.Jars.Select (j => j.ItemSpec), @@ -843,6 +845,7 @@ public void ResolveLibraryProjectImports ([Values (true, false)] bool useAapt2) }; proj.OtherBuildItems.Add (aar); + b.BuildLogFile = "build3.log"; Assert.IsTrue (b.Build (proj), "third build should have succeeded."); actual = ReadCache (cacheFile); Assert.AreEqual (expected.Jars.Length + 1, actual.Jars.Length, @@ -856,6 +859,7 @@ public void ResolveLibraryProjectImports ([Values (true, false)] bool useAapt2) } // Build with no changes, checking we are skipping targets appropriately + b.BuildLogFile = "build4.log"; Assert.IsTrue (b.Build (proj), "fourth build should have succeeded."); var targets = new List { "_UpdateAndroidResgen", @@ -1274,6 +1278,7 @@ public void AndroidResourceChange () // AndroidResource change proj.LayoutMain += $"{Environment.NewLine}"; proj.Touch ("Resources\\layout\\Main.axml"); + builder.BuildLogFile = "build2.log"; Assert.IsTrue (builder.Build (proj), "second build should succeed"); builder.Output.AssertTargetIsSkipped ("_ResolveLibraryProjectImports"); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs index 3601da8a073..f69eeb8b5c1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/PackagingTest.cs @@ -95,6 +95,7 @@ public void CheckIncludedAssemblies ([Values (false, true)] bool usesAssemblySto "System.Runtime.InteropServices.dll", "System.Linq.dll", "UnnamedProject.dll", + "_Microsoft.Android.Resource.Designer.dll", } : new [] { "Java.Interop.dll", diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateResourceCaseMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateResourceCaseMapTests.cs new file mode 100644 index 00000000000..8cfd159b009 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateResourceCaseMapTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.ProjectTools; + +namespace Xamarin.Android.Build.Tests { + [TestFixture] + [Category ("Node-5")] + [Parallelizable (ParallelScope.Children)] + public class GenerateResourceCaseMapTests : BaseTest { + + public void CreateResourceDirectory (string path) + { + Directory.CreateDirectory (Path.Combine (Root, path)); + Directory.CreateDirectory (Path.Combine (Root, path, "res", "drawable")); + Directory.CreateDirectory (Path.Combine (Root, path, "res", "values")); + using (var stream = typeof (XamarinAndroidCommonProject).Assembly.GetManifestResourceStream ("Xamarin.ProjectTools.Resources.Base.Icon.png")) { + var icon_binary_mdpi = new byte [stream.Length]; + stream.Read (icon_binary_mdpi, 0, (int)stream.Length); + File.WriteAllBytes (Path.Combine (Root, path, "res", "drawable", "IMALLCAPS.png"), icon_binary_mdpi); + } + } + + [Test] + public void CaseMapAllCapsWorks () + { + var path = Path.Combine ("temp", TestName + " Some Space"); + CreateResourceDirectory (path); + var task = new GenerateResourceCaseMap () { + BuildEngine = new MockBuildEngine (TestContext.Out) + }; + task.ProjectDir = Path.Combine (Root, path); + task.ResourceDirectory = Path.Combine (Root, path, "res") + Path.DirectorySeparatorChar; + task.Resources = new TaskItem [] { + new TaskItem (Path.Combine (Root, path, "res", "values", "strings.xml"), new Dictionary () { + { "LogicalName", "values\\strings.xml" }, + }), + new TaskItem (Path.Combine (Root, path, "res", "drawable", "IMALLCAPS.png")), + }; + task.OutputFile = new TaskItem (Path.Combine (Root, path, "case_map.txt")); + + Assert.IsTrue (task.Execute (), "Task should have run successfully."); + FileAssert.Exists (task.OutputFile.ItemSpec, $"'{task.OutputFile}' should have been created."); + var content1 = File.ReadAllText (task.OutputFile.ItemSpec); + StringAssert.Contains ($"drawable{Path.DirectorySeparatorChar}IMALLCAPS;IMALLCAPS", content1, "File should contain 'IMALLCAPS'"); + } + } +} 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 55737bd0610..7a6f89c90b2 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 @@ -230,7 +230,7 @@ int string app_name 0x7f110000 int string foo 0x7f110002 int string hello 0x7f110003 int string menu_settings 0x7f110004 -int[] styleable CustomFonts { 0x10100D2, 0x7F040000, 0x7F040000, 0x7F040001 } +int[] styleable CustomFonts { 0x010100d2, 0x7f040000, 0x7f040000, 0x7f040001 } int styleable CustomFonts_android_scrollX 0 int styleable CustomFonts_customFont 1 int styleable CustomFonts_customFont 2 @@ -309,6 +309,7 @@ void BuildLibraryWithResources (string path) var libraryStrings = library.AndroidResources.FirstOrDefault (r => r.Include () == @"Resources\values\Strings.xml"); + library.SetProperty ("AndroidUseDesignerAssembly", "false"); library.AndroidResources.Clear (); library.AndroidResources.Add (libraryStrings); library.AndroidResources.Add (new AndroidItem.AndroidResource (Path.Combine ("Resources", "animator", "slide_in_bottom.xml")) { TextContent = () => Animator }); @@ -355,6 +356,25 @@ void CompareFilesIgnoreRuntimeInfoString (string file1, string file2) } } + GenerateResourceCaseMap CreateCaseMapTask (string path) + { + var task = new GenerateResourceCaseMap () { + BuildEngine = new MockBuildEngine (TestContext.Out) + }; + task.ProjectDir = Path.Combine (Root, path); + task.ResourceDirectory = Path.Combine (Root, path, "res") + Path.DirectorySeparatorChar; + task.Resources = new TaskItem [] { + new TaskItem (Path.Combine (Root, path, "res", "values", "strings.xml"), new Dictionary () { + { "LogicalName", "values\\strings.xml" }, + }), + }; + task.AdditionalResourceDirectories = new TaskItem [] { + new TaskItem (Path.Combine (Root, path, "lp", "res")), + }; + task.OutputFile = new TaskItem (Path.Combine (Root, path, "case_map.txt")); + return task; + } + GenerateResourceDesigner CreateTask (string path) { var task = new GenerateResourceDesigner { @@ -375,6 +395,7 @@ GenerateResourceDesigner CreateTask (string path) task.AdditionalResourceDirectories = new TaskItem [] { new TaskItem (Path.Combine (Root, path, "lp", "res")), }; + task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); task.IsApplication = true; task.JavaPlatformJarPath = Path.Combine (AndroidSdkDirectory, "platforms", "android-27", "android.jar"); return task; @@ -400,6 +421,8 @@ public void GenerateDesignerFileWithÜmläüts () { 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 task = CreateTask (path); Assert.IsTrue (task.Execute (), "Task should have executed successfully."); AssertResourceDesigner (task, "GenerateDesignerFileExpected.cs"); @@ -411,6 +434,8 @@ public void GenerateDesignerFileFromRtxt ([Values (false, true)] bool withLibrar { 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 task = CreateTask (path); task.RTxtFile = Path.Combine (Root, path, "R.txt"); File.WriteAllText (task.RTxtFile, Rtxt); @@ -444,6 +469,8 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt { var path = Path.Combine ("temp", TestName + " Some Space"); CreateResourceDirectory (path); + var mapTask = CreateCaseMapTask (path); + Assert.IsTrue (mapTask.Execute (), "Map Task should have executed successfully."); if (useRtxt) File.WriteAllText (Path.Combine (Root, path, "R.txt"), Rtxt); IBuildEngine engine = new MockBuildEngine (TestContext.Out); @@ -465,6 +492,7 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt new TaskItem (Path.Combine (Root, path, "lp", "res")), }; task.ResourceFlagFile = Path.Combine (Root, path, "AndroidResgen.flag"); + task.CaseMapFile = Path.Combine (Root, path, "case_map.txt"); File.WriteAllText (task.ResourceFlagFile, string.Empty); task.IsApplication = true; task.JavaPlatformJarPath = Path.Combine (AndroidSdkDirectory, "platforms", "android-27", "android.jar"); @@ -484,6 +512,38 @@ public void UpdateLayoutIdIsIncludedInDesigner ([Values(true, false)] bool useRt Directory.Delete (Path.Combine (Root, path), recursive: true); } + [Test] + [Category ("SmokeTests")] + public void RtxtGeneratorOutput () + { + var path = Path.Combine ("temp", TestName); + int platform = AndroidSdkResolver.GetMaxInstalledPlatform (); + string resPath = Path.Combine (Root, path, "res"); + string rTxt = Path.Combine (Root, path, "R.txt"); + string expectedrTxt = Path.Combine (Root, path, "expectedR.txt"); + CreateResourceDirectory (path); + File.WriteAllText (expectedrTxt, Rtxt); + List errors = new List (); + List messages = new List (); + IBuildEngine engine = new MockBuildEngine (TestContext.Out, errors: errors, messages: messages); + var generateRtxt = new GenerateRtxt () { + BuildEngine = engine, + RTxtFile = rTxt, + ResourceDirectory = resPath, + JavaPlatformJarPath = Path.Combine (AndroidSdkDirectory, "platforms", $"android-{platform}", "android.jar"), + ResourceFlagFile = Path.Combine (Root, path, "res.flag"), + AdditionalResourceDirectories = new string[] { + Path.Combine (Root, path, "lp", "res"), + }, + }; + Assert.IsTrue (generateRtxt.Execute (), "Task should have succeeded."); + FileAssert.Exists (rTxt, $"{rTxt} should have been created."); + + CompareFilesIgnoreRuntimeInfoString (rTxt, expectedrTxt); + + Directory.Delete (Path.Combine (Root, path), recursive: true); + } + [Test] [Category ("SmokeTests")] public void CompareAapt2AndManagedParserOutput () diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs index d773e523ceb..a8f678a4d9a 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs @@ -12,6 +12,8 @@ using Xamarin.ProjectTools; using Microsoft.Android.Build.Tasks; using System.Runtime.CompilerServices; +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; namespace Xamarin.Android.Build.Tests { @@ -556,12 +558,37 @@ protected string GetResourceDesignerPath (ProjectBuilder builder, XamarinAndroid string path; if (Builder.UseDotNet) { path = Path.Combine (Root, builder.ProjectDirectory, project.IntermediateOutputPath); + if (string.Compare (project.GetProperty ("AndroidUseDesignerAssembly"), "True", ignoreCase: true) == 0) { + return Path.Combine (path, "_Microsoft.Android.Resource.Designer.dll"); + } } else { path = Path.Combine (Root, builder.ProjectDirectory, "Resources"); } return Path.Combine (path, "Resource.designer" + project.Language.DefaultDesignerExtension); } + protected string GetResourceDesignerText (XamarinAndroidProject project, string path) + { + if (Builder.UseDotNet) { + if (string.Compare (project.GetProperty ("AndroidUseDesignerAssembly"), "True", ignoreCase: true) == 0) { + var decompiler = new CSharpDecompiler (path, new DecompilerSettings () { }); + return decompiler.DecompileWholeModuleAsString (); + } + } + return File.ReadAllText (path); + } + + protected string[] GetResourceDesignerLines (XamarinAndroidProject project, string path) + { + if (Builder.UseDotNet) { + if (string.Compare (project.GetProperty ("AndroidUseDesignerAssembly"), "True", ignoreCase: true) == 0) { + var decompiler = new CSharpDecompiler (path, new DecompilerSettings () { }); + return decompiler.DecompileWholeModuleAsString ().Split (Environment.NewLine[0]); + } + } + return File.ReadAllLines (path); + } + /// /// Asserts that a AndroidManifest.xml file contains the expected //application/@android:extractNativeLibs value. /// diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 81f094ee126..462dcc7228b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -28,23 +28,41 @@ public class XASdkTests : BaseTest static readonly object [] DotNetBuildLibrarySource = new object [] { new object [] { - /* isRelease */ false, - /* duplicateAar */ false, + /* isRelease */ false, + /* duplicateAar */ false, + /* useDesignerAssembly */ false, }, new object [] { - /* isRelease */ false, - /* duplicateAar */ true, + /* isRelease */ false, + /* duplicateAar */ true, + /* useDesignerAssembly */ false, }, new object [] { - /* isRelease */ true, - /* duplicateAar */ false, + /* isRelease */ true, + /* duplicateAar */ false, + /* useDesignerAssembly */ false, + }, + new object [] { + /* isRelease */ false, + /* duplicateAar */ false, + /* useDesignerAssembly */ true, + }, + new object [] { + /* isRelease */ false, + /* duplicateAar */ true, + /* useDesignerAssembly */ true, + }, + new object [] { + /* isRelease */ true, + /* duplicateAar */ false, + /* useDesignerAssembly */ true, }, }; [Test] [Category ("SmokeTests")] [TestCaseSource (nameof (DotNetBuildLibrarySource))] - public void DotNetBuildLibrary (bool isRelease, bool duplicateAar) + public void DotNetBuildLibrary (bool isRelease, bool duplicateAar, bool useDesignerAssembly) { var path = Path.Combine ("temp", TestName); var env_var = "MY_ENVIRONMENT_VAR"; @@ -70,6 +88,7 @@ public void DotNetBuildLibrary (bool isRelease, bool duplicateAar) libC.OtherBuildItems.Add (new AndroidItem.AndroidAsset ("Assets\\bar\\bar.txt") { BinaryContent = () => Array.Empty (), }); + libC.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); var activity = libC.Sources.FirstOrDefault (s => s.Include () == "MainActivity.cs"); if (activity != null) libC.Sources.Remove (activity); @@ -141,6 +160,7 @@ public Foo () BinaryContent = () => Array.Empty (), }); libB.AddReference (libC); + libB.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); activity = libB.Sources.FirstOrDefault (s => s.Include () == "MainActivity.cs"); if (activity != null) @@ -190,6 +210,7 @@ public Foo () // Test a duplicate @(AndroidLibrary) item with the same path of LibraryB.aar appA.OtherBuildItems.Add (new AndroidItem.AndroidLibrary (aarPath)); } + appA.SetProperty ("AndroidUseDesignerAssembly", useDesignerAssembly.ToString ()); var appBuilder = CreateDotNetBuilder (appA, Path.Combine (path, appA.ProjectName)); Assert.IsTrue (appBuilder.Build (), $"{appA.ProjectName} should succeed"); @@ -223,11 +244,13 @@ public Foo () Assert.AreEqual (env_val, actual, $"{env_var} should be {env_val}"); // Check Resource.designer.cs - var resource_designer_cs = Path.Combine (intermediate, "Resource.designer.cs"); - FileAssert.Exists (resource_designer_cs); - var resource_designer_text = File.ReadAllText (resource_designer_cs); - StringAssert.Contains ("public const int MyLayout", resource_designer_text); - StringAssert.Contains ("global::LibraryB.Resource.Drawable.IMALLCAPS = global::AppA.Resource.Drawable.IMALLCAPS", resource_designer_text); + if (!useDesignerAssembly) { + var resource_designer_cs = Path.Combine (intermediate, "Resource.designer.cs"); + FileAssert.Exists (resource_designer_cs); + var resource_designer_text = File.ReadAllText (resource_designer_cs); + StringAssert.Contains ("public const int MyLayout", resource_designer_text); + StringAssert.Contains ("global::LibraryB.Resource.Drawable.IMALLCAPS = global::AppA.Resource.Drawable.IMALLCAPS", resource_designer_text); + } } [Test] @@ -443,6 +466,7 @@ public void GenerateResourceDesigner_false() }; // Turn off Resource.designer.cs and remove usage of it proj.SetProperty ("AndroidGenerateResourceDesigner", "false"); + proj.SetProperty ("AndroidUseDesignerAssembly", "false"); proj.MainActivity = proj.DefaultMainActivity .Replace ("Resource.Layout.Main", "0") .Replace ("Resource.Id.myButton", "0"); @@ -1037,16 +1061,18 @@ public void DotNetIncremental ([Values (true, false)] bool isRelease, [Values (" }; appA.AddReference (libB); var appBuilder = CreateDotNetBuilder (appA, Path.Combine (path, appA.ProjectName)); + appBuilder.BuildLogFile = Path.Combine (Root, path, appA.ProjectName, "build1.log"); Assert.IsTrue (appBuilder.Build (runtimeIdentifier: runtimeIdentifier), $"{appA.ProjectName} should succeed"); - appBuilder.AssertTargetIsNotSkipped ("CoreCompile"); + appBuilder.AssertTargetIsNotSkipped ("CoreCompile", occurrence: 1); if (isRelease) { appBuilder.AssertTargetIsNotSkipped ("_RemoveRegisterAttribute"); appBuilder.AssertTargetIsNotSkipped ("_AndroidAot"); } // Build again, no changes + appBuilder.BuildLogFile = Path.Combine (Root, path, appA.ProjectName, "build2.log"); Assert.IsTrue (appBuilder.Build (runtimeIdentifier: runtimeIdentifier), $"{appA.ProjectName} should succeed"); - appBuilder.AssertTargetIsSkipped ("CoreCompile"); + appBuilder.AssertTargetIsSkipped ("CoreCompile", occurrence: 2); if (isRelease) { appBuilder.AssertTargetIsSkipped ("_RemoveRegisterAttribute"); appBuilder.AssertTargetIsSkipped ("_AndroidAotCompilation"); @@ -1114,6 +1140,34 @@ public Foo () { helper.AssertContainsEntry ($"assemblies/{libC.ProjectName}.dll"); } + [Test] + public void DotNetDesignTimeBuild () + { + var proj = new XASdkProject (); + proj.SetProperty ("AndroidUseDesignerAssembly", "true"); + var builder = CreateDotNetBuilder (proj); + var parameters = new [] { "BuildingInsideVisualStudio=true"}; + builder.BuildLogFile = "update.log"; + Assert.IsTrue (builder.Build ("Compile", parameters: parameters), $"{proj.ProjectName} should succeed"); + builder.AssertTargetIsNotSkipped ("_GenerateResourceCaseMap", occurrence: 1); + builder.AssertTargetIsNotSkipped ("_GenerateRtxt"); + builder.AssertTargetIsNotSkipped ("_GenerateResourceDesignerIntermediateClass"); + builder.AssertTargetIsNotSkipped ("_GenerateResourceDesignerAssembly", occurrence: 1); + parameters = new [] { "BuildingInsideVisualStudio=true" }; + builder.BuildLogFile = "build1.log"; + Assert.IsTrue (builder.Build ("SignAndroidPackage", parameters: parameters), $"{proj.ProjectName} should succeed"); + builder.AssertTargetIsNotSkipped ("_GenerateResourceCaseMap", occurrence: 2); + builder.AssertTargetIsSkipped ("_GenerateRtxt", occurrence: 1); + builder.AssertTargetIsSkipped ("_GenerateResourceDesignerIntermediateClass", occurrence: 1); + builder.AssertTargetIsSkipped ("_GenerateResourceDesignerAssembly", occurrence: 2); + builder.BuildLogFile = "build2.log"; + Assert.IsTrue (builder.Build ("SignAndroidPackage", parameters: parameters), $"{proj.ProjectName} should succeed 2"); + builder.AssertTargetIsNotSkipped ("_GenerateResourceCaseMap", occurrence: 3); + builder.AssertTargetIsSkipped ("_GenerateRtxt", occurrence: 2); + builder.AssertTargetIsSkipped ("_GenerateResourceDesignerIntermediateClass", occurrence: 2); + builder.AssertTargetIsSkipped ("_GenerateResourceDesignerAssembly"); + } + [Test] public void SignAndroidPackage () { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj index be74e205c2d..8fab24d3966 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Xamarin.Android.Build.Tests.csproj @@ -59,6 +59,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownPackages.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownPackages.cs index 3cac7b400be..6fb9b090e2e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownPackages.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/KnownPackages.cs @@ -203,11 +203,21 @@ public static class KnownPackages Version = "4.7.0.1142", TargetFramework = "MonoAndroid10.0", }; + public static Package XamarinForms_5_0_0_2515 = new Package { + Id = "Xamarin.Forms", + Version = "5.0.0.2515", + TargetFramework = "MonoAndroid10.0", + }; public static Package XamarinFormsMaps_4_7_0_1142 = new Package { Id = "Xamarin.Forms.Maps", Version = "4.7.0.1142", TargetFramework = "MonoAndroid10.0", }; + public static Package XamarinFormsMaps_5_0_0_2515 = new Package { + Id = "Xamarin.Forms.Maps", + Version = "5.0.0.2515", + TargetFramework = "MonoAndroid10.0", + }; public static Package XamarinFormsMaps_4_0_0_425677 = new Package { Id = "Xamarin.Forms.Maps", Version = "4.0.0.425677", @@ -474,6 +484,11 @@ public static class KnownPackages Id = "Xamarin.Build.Download", Version = "0.11.2", }; + + public static Package Xamarin_Build_Download_0_11_3 = new Package { + Id = "Xamarin.Build.Download", + Version = "0.11.3", + }; // NOTE: old version required for some tests public static Package Xamarin_Build_Download_0_4_11 = new Package { Id = "Xamarin.Build.Download", diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidCommonProject.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidCommonProject.cs index cb964c0963b..51a4772a395 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidCommonProject.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidCommonProject.cs @@ -54,8 +54,12 @@ protected XamarinAndroidCommonProject (string debugConfigurationName = "Debug", AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\drawable-xxhdpi\\Icon.png") { BinaryContent = () => icon_binary_xxhdpi }); AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\drawable-xxxhdpi\\Icon.png") { BinaryContent = () => icon_binary_xxxhdpi }); //AndroidResources.Add (new AndroidItem.AndroidResource ("Resources\\drawable-nodpi\\Icon.png") { BinaryContent = () => icon_binary }); + if (Builder.UseDotNet) { + // set our default + SetProperty ("AndroidUseDesignerAssembly", "True"); + } } - + public override string ProjectTypeGuid { get { return "FAE04EC0-301F-11D3-BF4B-00C04F79EFBC"; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidProjectLanguage.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidProjectLanguage.cs index 7c77da35bbc..4365ce6bab1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidProjectLanguage.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/XamarinAndroidProjectLanguage.cs @@ -34,7 +34,7 @@ public override string DefaultExtension { get { return ".fs"; } } public override string DefaultDesignerExtension { - get { return ".cs"; } + get { return Builder.UseDotNet ? ".fs" : ".cs"; } } public override string DefaultProjectExtension { get { return ".fsproj"; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs index b2f383bcedd..9b64f3d7922 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Common/DotNetCLI.cs @@ -147,13 +147,15 @@ List GetDefaultCommandLineArgs (string verb, string target = null, strin if (string.IsNullOrEmpty (BuildLogFile)) BuildLogFile = Path.Combine (testDir, "build.log"); - var binlog = string.IsNullOrEmpty (target) ? "msbuild" : target; + var binlog = string.IsNullOrEmpty (target) ? Path.GetFileNameWithoutExtension (string.IsNullOrEmpty (BuildLogFile) ? "msbuild" : BuildLogFile) : target; var arguments = new List { verb, $"\"{projectOrSolution}\"", "/noconsolelogger", $"/flp1:LogFile=\"{BuildLogFile}\";Encoding=UTF-8;Verbosity={Verbosity}", $"/bl:\"{Path.Combine (testDir, $"{binlog}.binlog")}\"", + "-m:1", + "-nr:false", "/p:_DisableParallelAot=true", }; if (!string.IsNullOrEmpty (target)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc index 7ca259a29f0..f5a8ad55bfe 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.apkdesc @@ -4,14 +4,17 @@ "AndroidManifest.xml": { "Size": 3032 }, + "assemblies/_Microsoft.Android.Resource.Designer.dll": { + "Size": 1028 + }, "assemblies/Java.Interop.dll": { - "Size": 58924 + "Size": 58926 }, "assemblies/Mono.Android.dll": { - "Size": 87710 + "Size": 87074 }, "assemblies/Mono.Android.Runtime.dll": { - "Size": 5924 + "Size": 5833 }, "assemblies/rc.bin": { "Size": 1182 @@ -23,19 +26,19 @@ "Size": 9253 }, "assemblies/System.Private.CoreLib.dll": { - "Size": 484001 + "Size": 483854 }, "assemblies/System.Runtime.dll": { - "Size": 2629 + "Size": 2608 }, "assemblies/System.Runtime.InteropServices.dll": { "Size": 2269 }, "assemblies/UnnamedProject.dll": { - "Size": 3628 + "Size": 3276 }, "classes.dex": { - "Size": 18968 + "Size": 19020 }, "lib/arm64-v8a/libmono-component-marshal-ilgen.so": { "Size": 93552 @@ -56,16 +59,16 @@ "Size": 150584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 16632 + "Size": 16728 }, "META-INF/BNDLTOOL.RSA": { "Size": 1213 }, "META-INF/BNDLTOOL.SF": { - "Size": 2914 + "Size": 3037 }, "META-INF/MANIFEST.MF": { - "Size": 2787 + "Size": 2910 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -92,5 +95,5 @@ "Size": 1904 } }, - "PackageSize": 2603241 + "PackageSize": 2603338 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleLegacy.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleLegacy.apkdesc index cb7562922f1..adfe30f0e3b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleLegacy.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleLegacy.apkdesc @@ -5,22 +5,22 @@ "Size": 2604 }, "assemblies/Java.Interop.dll": { - "Size": 68913 + "Size": 68923 }, "assemblies/Mono.Android.dll": { - "Size": 265169 + "Size": 265140 }, "assemblies/mscorlib.dll": { - "Size": 769018 + "Size": 769036 }, "assemblies/System.Core.dll": { - "Size": 28199 + "Size": 28216 }, "assemblies/System.dll": { - "Size": 9180 + "Size": 9192 }, "assemblies/UnnamedProject.dll": { - "Size": 2882 + "Size": 2897 }, "classes.dex": { "Size": 370828 @@ -32,7 +32,7 @@ "Size": 750976 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 332936 + "Size": 333128 }, "lib/arm64-v8a/libmonosgen-2.0.so": { "Size": 4039176 @@ -41,7 +41,7 @@ "Size": 66184 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 21256 + "Size": 21264 }, "META-INF/ANDROIDD.RSA": { "Size": 1213 diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc index 87875ce958d..efc6347e949 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc @@ -4,17 +4,20 @@ "AndroidManifest.xml": { "Size": 3568 }, + "assemblies/_Microsoft.Android.Resource.Designer.dll": { + "Size": 1945 + }, "assemblies/FormsViewGroup.dll": { "Size": 7314 }, "assemblies/Java.Interop.dll": { - "Size": 66797 + "Size": 66801 }, "assemblies/Mono.Android.dll": { - "Size": 444767 + "Size": 444662 }, "assemblies/Mono.Android.Runtime.dll": { - "Size": 5924 + "Size": 5833 }, "assemblies/mscorlib.dll": { "Size": 3856 @@ -101,7 +104,7 @@ "Size": 16805 }, "assemblies/System.Runtime.dll": { - "Size": 2791 + "Size": 2756 }, "assemblies/System.Runtime.InteropServices.dll": { "Size": 2269 @@ -128,7 +131,7 @@ "Size": 1858 }, "assemblies/UnnamedProject.dll": { - "Size": 117399 + "Size": 5347 }, "assemblies/Xamarin.AndroidX.Activity.dll": { "Size": 5872 @@ -185,10 +188,10 @@ "Size": 528450 }, "assemblies/Xamarin.Forms.Platform.Android.dll": { - "Size": 384799 + "Size": 337827 }, "assemblies/Xamarin.Forms.Platform.dll": { - "Size": 56878 + "Size": 11087 }, "assemblies/Xamarin.Forms.Xaml.dll": { "Size": 60774 @@ -197,7 +200,7 @@ "Size": 40159 }, "classes.dex": { - "Size": 3090508 + "Size": 3141008 }, "lib/arm64-v8a/libmono-component-marshal-ilgen.so": { "Size": 93552 @@ -218,7 +221,7 @@ "Size": 150584 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 333632 + "Size": 333728 }, "META-INF/android.support.design_material.version": { "Size": 12 @@ -332,13 +335,13 @@ "Size": 1213 }, "META-INF/BNDLTOOL.SF": { - "Size": 79203 + "Size": 79326 }, "META-INF/com.google.android.material_material.version": { "Size": 10 }, "META-INF/MANIFEST.MF": { - "Size": 79076 + "Size": 79199 }, "META-INF/proguard/androidx-annotations.pro": { "Size": 339 @@ -1973,5 +1976,5 @@ "Size": 341228 } }, - "PackageSize": 7991971 + "PackageSize": 7803652 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsLegacy.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsLegacy.apkdesc index ac496de717d..00393868d67 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsLegacy.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsLegacy.apkdesc @@ -5,115 +5,115 @@ "Size": 3140 }, "assemblies/FormsViewGroup.dll": { - "Size": 7215 + "Size": 7230 }, "assemblies/Java.Interop.dll": { - "Size": 69956 + "Size": 69966 }, "assemblies/Mono.Android.dll": { - "Size": 572709 + "Size": 572670 }, "assemblies/Mono.Security.dll": { - "Size": 68432 + "Size": 68449 }, "assemblies/mscorlib.dll": { - "Size": 915408 + "Size": 915425 }, "assemblies/System.Core.dll": { - "Size": 164046 + "Size": 164059 }, "assemblies/System.dll": { - "Size": 388864 + "Size": 388883 }, "assemblies/System.Drawing.Common.dll": { - "Size": 12365 + "Size": 12370 }, "assemblies/System.Net.Http.dll": { - "Size": 110693 + "Size": 110718 }, "assemblies/System.Numerics.dll": { - "Size": 15683 + "Size": 15706 }, "assemblies/System.Runtime.Serialization.dll": { - "Size": 186660 + "Size": 186683 }, "assemblies/System.ServiceModel.Internals.dll": { - "Size": 26594 + "Size": 26604 }, "assemblies/System.Xml.dll": { - "Size": 395656 + "Size": 395668 }, "assemblies/UnnamedProject.dll": { - "Size": 116899 + "Size": 116997 }, "assemblies/Xamarin.AndroidX.Activity.dll": { - "Size": 7697 + "Size": 7711 }, "assemblies/Xamarin.AndroidX.AppCompat.AppCompatResources.dll": { - "Size": 6648 + "Size": 6664 }, "assemblies/Xamarin.AndroidX.AppCompat.dll": { - "Size": 125328 + "Size": 125346 }, "assemblies/Xamarin.AndroidX.CardView.dll": { - "Size": 7367 + "Size": 7380 }, "assemblies/Xamarin.AndroidX.CoordinatorLayout.dll": { - "Size": 18272 + "Size": 18289 }, "assemblies/Xamarin.AndroidX.Core.dll": { - "Size": 131930 + "Size": 131944 }, "assemblies/Xamarin.AndroidX.DrawerLayout.dll": { - "Size": 15426 + "Size": 15443 }, "assemblies/Xamarin.AndroidX.Fragment.dll": { - "Size": 43135 + "Size": 43150 }, "assemblies/Xamarin.AndroidX.Legacy.Support.Core.UI.dll": { - "Size": 6715 + "Size": 6727 }, "assemblies/Xamarin.AndroidX.Lifecycle.Common.dll": { - "Size": 7062 + "Size": 7078 }, "assemblies/Xamarin.AndroidX.Lifecycle.LiveData.Core.dll": { - "Size": 7193 + "Size": 7208 }, "assemblies/Xamarin.AndroidX.Lifecycle.ViewModel.dll": { - "Size": 4873 + "Size": 4886 }, "assemblies/Xamarin.AndroidX.Loader.dll": { - "Size": 13585 + "Size": 13596 }, "assemblies/Xamarin.AndroidX.RecyclerView.dll": { - "Size": 102327 + "Size": 102349 }, "assemblies/Xamarin.AndroidX.SavedState.dll": { - "Size": 6268 + "Size": 6294 }, "assemblies/Xamarin.AndroidX.SwipeRefreshLayout.dll": { - "Size": 11271 + "Size": 11284 }, "assemblies/Xamarin.AndroidX.ViewPager.dll": { - "Size": 19424 + "Size": 19438 }, "assemblies/Xamarin.Forms.Core.dll": { - "Size": 524736 + "Size": 524743 }, "assemblies/Xamarin.Forms.Platform.Android.dll": { - "Size": 384872 + "Size": 384885 }, "assemblies/Xamarin.Forms.Platform.dll": { "Size": 56878 }, "assemblies/Xamarin.Forms.Xaml.dll": { - "Size": 55801 + "Size": 55807 }, "assemblies/Xamarin.Google.Android.Material.dll": { - "Size": 43497 + "Size": 43514 }, "classes.dex": { - "Size": 3482812 + "Size": 3533252 }, "lib/arm64-v8a/libmono-btls-shared.so": { "Size": 1613872 @@ -122,7 +122,7 @@ "Size": 750976 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 332936 + "Size": 333128 }, "lib/arm64-v8a/libmonosgen-2.0.so": { "Size": 4039176 @@ -131,7 +131,7 @@ "Size": 66184 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 107024 + "Size": 107032 }, "META-INF/android.support.design_material.version": { "Size": 12 @@ -1883,5 +1883,5 @@ "Size": 341040 } }, - "PackageSize": 9521310 + "PackageSize": 9537694 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs new file mode 100644 index 00000000000..96f48253931 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/FileResourceParser.cs @@ -0,0 +1,389 @@ +using System; +using System.CodeDom; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Build.Utilities; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Tasks +{ + class FileResourceParser : ResourceParser + { + public string JavaPlatformDirectory { get; set; } + + public string ResourceFlagFile { get; set; } + + Dictionary arrayMapping = new Dictionary (); + Dictionary> foofoo = new Dictionary> (); + List custom_types = new List (); + XDocument publicXml; + + string[] publicXmlFiles = new string[] { + "public.xml", + "public-final.xml", + "public-staging.xml", + }; + + protected XDocument LoadPublicXml () { + string publicXmlPath = Path.Combine (JavaPlatformDirectory, "data", "res", "values"); + foreach (var file in publicXmlFiles) { + if (File.Exists (Path.Combine (publicXmlPath, file))) { + return XDocument.Load (Path.Combine (publicXmlPath, file)); + } + } + return null; + } + + public IList Parse (string resourceDirectory, IEnumerable additionalResourceDirectories, Dictionary resourceMap) + { + Log.LogDebugMessage ($"Parsing Directory {resourceDirectory}"); + string publicXmlPath = Path.Combine (JavaPlatformDirectory, "data", "res", "values"); + publicXml = LoadPublicXml (); + var result = new List (); + Dictionary> resources = new Dictionary> (); + foreach (var knownType in RtxtParser.knownTypes) { + if (knownType == "styleable") { + resources.Add (knownType, new List ()); + continue; + } + resources.Add (knownType, new SortedSet (new RComparer ())); + } + foreach (var dir in Directory.EnumerateDirectories (resourceDirectory, "*", SearchOption.TopDirectoryOnly)) { + foreach (var file in Directory.EnumerateFiles (dir, "*.*", SearchOption.AllDirectories)) { + ProcessResourceFile (file, resources); + } + } + foreach (var dir in additionalResourceDirectories ?? Array.Empty()) { + Log.LogDebugMessage ($"Processing Directory {dir}"); + if (Directory.Exists (dir)) { + foreach (var file in Directory.EnumerateFiles (dir, "*.*", SearchOption.AllDirectories)) { + ProcessResourceFile (file, resources); + } + } else { + Log.LogDebugMessage ($"Skipping non-existent directory: {dir}"); + } + } + + // now generate the Id's we need in a specific order + List declarationIds = new List (); + declarationIds.Add ("attr"); + declarationIds.Add ("drawable"); + declarationIds.Add ("mipmap"); + declarationIds.Add ("font"); + declarationIds.Add ("layout"); + declarationIds.Add ("anim"); + declarationIds.Add ("animator"); + declarationIds.Add ("transition"); + declarationIds.Add ("xml"); + declarationIds.Add ("raw"); + declarationIds.Add ("dimen"); + declarationIds.Add ("string"); + declarationIds.Add ("array"); + declarationIds.Add ("plurals"); + declarationIds.Add ("bool"); + declarationIds.Add ("color"); + declarationIds.Add ("integer"); + declarationIds.Add ("menu"); + declarationIds.Add ("id"); + // custom types + foreach (var customClass in custom_types) { + declarationIds.Add (customClass); + } + + declarationIds.Add ("interpolator"); + declarationIds.Add ("style"); + declarationIds.Add ("styleable"); + + declarationIds.Sort ((a, b) => { + return string.Compare (a, b, StringComparison.OrdinalIgnoreCase); + }); + + string itemPackageId = "0x7f"; + int typeid = 1; + + foreach (var t in declarationIds) { + int itemid = 0; + if (!resources.ContainsKey(t)) { + continue; + } + if (resources[t].Count == 0) { + continue; + } + foreach (R r in resources[t].OrderBy(x => x.ToSortedString(), StringComparer.Ordinal)) { + + int id = Convert.ToInt32 (itemPackageId + typeid.ToString ("X2") + itemid.ToString ("X4"), fromBase: 16); + if (r.Type == RType.Integer && r.Id == -1) { + itemid++; + r.UpdateId (id); + } else { + if (foofoo.ContainsKey (r.Identifier)) { + var items = foofoo[r.Identifier]; + if (r.Ids != null) { + // do something special cos its an array we need to replace *some* its. + int[] newIds = new int[r.Ids.Length]; + for (int i = 0; i < r.Ids.Length; i++) { + // we need to lookup the ID's for these from the ones generated. + newIds[i] = r.Ids[i]; + if (r.Ids[i] == -1) + newIds[i] = GetId (result, items[i]); + } + r.UpdateIds (newIds); + } + } + } + result.Add (r); + } + typeid++; + } + + result.Sort (new RComparer ()); + + return result; + } + + class RComparer : IComparer { + public int Compare(R a, R b) { + return string.Compare (a.ToSortedString (), b.ToSortedString (), StringComparison.Ordinal); + } + } + + HashSet resourceNamesToUseDirectly = new HashSet () { + "integer-array", + "string-array", + "declare-styleable", + "add-resource", + }; + + int GetId (ICollection resources, string identifier) + { + foreach (R r in resources) { + if (r.Identifier == identifier) { + return r.Id; + } + } + return -1; + } + + void ProcessResourceFile (string file, Dictionary> resources) + { + Log.LogDebugMessage ($"{nameof(ProcessResourceFile)} {file}"); + var fileName = Path.GetFileNameWithoutExtension (file); + if (string.IsNullOrEmpty (fileName)) + return; + if (fileName.EndsWith (".9", StringComparison.OrdinalIgnoreCase)) + fileName = Path.GetFileNameWithoutExtension (fileName); + var path = Directory.GetParent (file).Name; + var ext = Path.GetExtension (file); + switch (ext) { + case ".xml": + case ".axml": + if (string.Compare (path, "raw", StringComparison.OrdinalIgnoreCase) == 0) + goto default; + try { + ProcessXmlFile (file, resources); + } catch (XmlException ex) { + Log.LogCodedWarning ("XA1000", Properties.Resources.XA1000, file, ex); + } + break; + default: + break; + } + CreateResourceField (path, fileName, resources); + } + + void CreateResourceField (string root, string id, Dictionary> resources) { + var i = root.IndexOf ('-'); + var item = i < 0 ? root : root.Substring (0, i); + item = resourceNamesToUseDirectly.Contains (root) ? root : item; + switch (item.ToLowerInvariant ()) { + case "animation": + item = "anim"; + break; + case "array": + case "string-array": + case "integer-array": + item = "array"; + break; + case "enum": + case "flag": + item = "id"; + break; + } + var r = new R () { + ResourceTypeName = item, + Identifier = id, + Id = -1, + }; + if (!resources.ContainsKey (item)) { + Log.LogDebugMessage ($"Ignoring path:{item}"); + return; + } + resources[item].Add (r); + } + + void ProcessStyleable (XmlReader reader, Dictionary> resources) + { + Log.LogDebugMessage ($"{nameof(ProcessStyleable)}"); + string topName = null; + int fieldCount = 0; + List fields = new List (); + List attribs = new List (); + if (reader.HasAttributes) { + while (reader.MoveToNextAttribute ()) { + if (reader.Name.Replace ("android:", "") == "name") + topName = reader.Value; + } + } + while (reader.Read ()) { + if (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.Comment) + continue; + string name = null; + if (string.IsNullOrEmpty (topName)) { + if (reader.HasAttributes) { + while (reader.MoveToNextAttribute ()) { + if (reader.Name.Replace ("android:", "") == "name") + topName = reader.Value; + } + } + } + if (!reader.IsStartElement ()) + continue; + if (reader.HasAttributes) { + while (reader.MoveToNextAttribute ()) { + if (reader.Name.Replace ("android:", "") == "name") + name = reader.Value; + } + } + reader.MoveToElement (); + if (reader.LocalName == "attr") { + attribs.Add (name); + } + } + var field = new R () { + ResourceTypeName = "styleable", + Identifier = topName, + Type = RType.Array, + }; + if (!arrayMapping.ContainsKey (field)) { + foofoo.Add (field.Identifier, new List ()); + attribs.Sort (StringComparer.OrdinalIgnoreCase); + for (int i = 0; i < attribs.Count; i++) { + string name = attribs [i]; + if (!name.StartsWith ("android:", StringComparison.OrdinalIgnoreCase)) { + var r = new R () { + ResourceTypeName = "attr", + Identifier = $"{name}", + Id = -1, + }; + resources [r.ResourceTypeName].Add (r); + fields.Add (r); + } else { + // this is an android:xxx resource, we should not calculate the id + // we should get it from "somewhere" maybe the pubic.xml + name = name.Replace ("android:", string.Empty); + var element = publicXml?.XPathSelectElement ($"/resources/public[@name='{name}']") ?? null; + int value = Convert.ToInt32 (element?.Attribute ("id")?.Value ?? "0x0", fromBase: 16); + var r = new R () { + ResourceTypeName = "attr", + Identifier = $"{name}", + Id = value, + }; + fields.Add (r); + } + } + if (field.Type != RType.Array) + return; + arrayMapping.Add (field, fields.ToArray ()); + + field.Ids = new int [attribs.Count]; + for (int idx =0; idx < field.Ids.Length; idx++) + field.Ids[idx] = fields[idx].Id; + resources [field.ResourceTypeName].Add (field); + int id = 0; + foreach (string r in attribs) { + foofoo[field.Identifier].Add (r.Replace (":", "_")); + resources [field.ResourceTypeName].Add (new R () { + ResourceTypeName = field.ResourceTypeName, + Identifier = $"{field.Identifier}_{r.Replace (":", "_")}", + Id = id++, + }); + } + } + } + + void ProcessXmlFile (string file, Dictionary> resources) + { + Log.LogDebugMessage ($"{nameof(ProcessXmlFile)}"); + using (var reader = XmlReader.Create (file)) { + while (reader.Read ()) { + if (reader.NodeType == XmlNodeType.Whitespace || reader.NodeType == XmlNodeType.Comment) + continue; + if (reader.IsStartElement ()) { + var elementName = reader.Name; + if (elementName == "declare-styleable" || elementName == "configVarying" || elementName == "add-resource") { + ProcessStyleable (reader.ReadSubtree (), resources); + continue; + } + if (reader.HasAttributes) { + string name = null; + string type = null; + string id = null; + string custom_id = null; + while (reader.MoveToNextAttribute ()) { + if (reader.LocalName == "name") + name = reader.Value; + if (reader.LocalName == "type") + type = reader.Value; + if (reader.LocalName == "id") { + string[] values = reader.Value.Split ('/'); + if (values.Length != 2) { + id = reader.Value.Replace ("@+id/", "").Replace ("@id/", ""); + } else { + if (values [0] != "@+id" && values [0] != "@id" && !values [0].Contains ("android:")) { + custom_id = values [0].Replace ("@", "").Replace ("+", ""); + } + id = values [1]; + } + + } + if (reader.LocalName == "inflatedId") { + string inflateId = reader.Value.Replace ("@+id/", "").Replace ("@id/", ""); + var r = new R () { + ResourceTypeName = "id", + Identifier = inflateId, + Id = -1, + }; + Log.LogDebugMessage ($"Adding 1 {r}"); + resources[r.ResourceTypeName].Add (r); + } + } + if (name?.Contains ("android:") ?? false) + continue; + if (id?.Contains ("android:") ?? false) + continue; + // Move the reader back to the element node. + reader.MoveToElement (); + if (!string.IsNullOrEmpty (name)) { + CreateResourceField (type ?? elementName, name, resources); + } + if (!string.IsNullOrEmpty (custom_id) && !resources.ContainsKey (custom_id)) { + resources.Add (custom_id, new SortedSet (new RComparer ())); + custom_types.Add (custom_id); + } + if (!string.IsNullOrEmpty (id)) { + CreateResourceField (custom_id ?? "id", id.Replace ("-", "_").Replace (".", "_"), resources); + } + } + } + } + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/JavaResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/JavaResourceParser.cs index bb705dfbcdd..bf92442a733 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/JavaResourceParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/JavaResourceParser.cs @@ -23,7 +23,7 @@ public CodeTypeDeclaration Parse (string file, bool isApp, Dictionary new { Match = p.Key.Match (line), Handler = p.Value }).FirstOrDefault (x => x.Match.Success); - + if (info == null) continue; @@ -84,7 +84,7 @@ public JavaResourceParser () Parse (@"^ public static final int ([^ =]+)\s*=\s*([^;]+);$", (m, app, g, map) => { var name = ((CodeTypeDeclaration) g.Members [g.Members.Count-1]).Name; - var f = new CodeMemberField (typeof (int), GetResourceName (name, m.Groups[1].Value, map)) { + var f = new CodeMemberField (typeof (int), ResourceIdentifier.GetResourceName (name, m.Groups[1].Value, map, Log)) { Attributes = app ? MemberAttributes.Const | MemberAttributes.Public : MemberAttributes.Static | MemberAttributes.Public, InitExpression = new CodePrimitiveExpression (ToInt32 (m.Groups [2].Value, m.Groups [2].Value.IndexOf ("0x", StringComparison.Ordinal) == 0 ? 16 : 10)), Comments = { @@ -97,7 +97,7 @@ public JavaResourceParser () Parse (@"^ public static final int\[\] ([^ =]+) = {", (m, app, g, map) => { var name = ((CodeTypeDeclaration) g.Members [g.Members.Count-1]).Name; - var f = new CodeMemberField (typeof (int[]), GetResourceName (name, m.Groups[1].Value, map)) { + var f = new CodeMemberField (typeof (int[]), ResourceIdentifier.GetResourceName (name, m.Groups[1].Value, map, Log)) { // pity I can't make the member readonly... Attributes = MemberAttributes.Public | MemberAttributes.Static, }; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs index fa735737a36..07c4b1537c5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ManagedResourceParser.cs @@ -14,7 +14,7 @@ namespace Xamarin.Android.Tasks { - class ManagedResourceParser : ResourceParser + class ManagedResourceParser : FileResourceParser { class CompareTuple : IComparer<(int Key, CodeMemberField Value)> { @@ -37,10 +37,6 @@ public int Compare((int Key, CodeMemberField Value) x, (int Key, CodeMemberField XDocument publicXml; - public string JavaPlatformDirectory { get; set; } - - public string ResourceFlagFile { get; set; } - void SortMembers (CodeTypeDeclaration decl, StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) { CodeTypeMember [] members = new CodeTypeMember [decl.Members.Count]; @@ -87,10 +83,7 @@ public CodeTypeDeclaration Parse (string resourceDirectory, string rTxtFile, IEn transition = CreateClass ("Transition"); xml = CreateClass ("Xml"); - string publicXmlPath = Path.Combine (JavaPlatformDirectory, "data", "res", "values", "public.xml"); - if (File.Exists (publicXmlPath)) { - publicXml = XDocument.Load (publicXmlPath); - } + publicXml = LoadPublicXml (); var resModifiedDate = !string.IsNullOrEmpty (ResourceFlagFile) && File.Exists (ResourceFlagFile) ? File.GetLastWriteTimeUtc (ResourceFlagFile) @@ -296,104 +289,17 @@ public CodeTypeDeclaration Parse (string resourceDirectory, string rTxtFile, IEn void ProcessRtxtFile (string file) { - var lines = System.IO.File.ReadLines (file); - int lineNumber = 0; - foreach (var line in lines) { - lineNumber++; - var items = line.Split (new char [] { ' ' }, 4); - if (items.Length < 4) { - Log.LogDebugMessage ($"'{file}:{lineNumber}' ignoring contents '{line}', it does not have the correct number of elements."); - continue; - } - int value = items [1] != "styleable" ? Convert.ToInt32 (items [3], 16) : -1; - string itemName = items [2]; - switch (items [1]) { - case "anim": - CreateIntField (animation, itemName, value); - break; - case "animator": - CreateIntField (animator, itemName, value); - break; - case "attr": - CreateIntField (attrib, itemName, value); - break; - case "array": - CreateIntField (arrays, itemName, value); - break; - case "bool": - CreateIntField (boolean, itemName, value); - break; - case "color": - CreateIntField (colors, itemName, value); - break; - case "dimen": - CreateIntField (dimension, itemName, value); - break; - case "drawable": - CreateIntField (drawable, itemName, value); - break; - case "font": - CreateIntField (font, itemName, value); - break; - case "id": - CreateIntField (ids, itemName, value); - break; - case "integer": - CreateIntField (ints, itemName, value); - break; - case "interpolator": - CreateIntField (interpolators, itemName, value); - break; - case "layout": - CreateIntField (layout, itemName, value); - break; - case "menu": - CreateIntField (menu, itemName, value); - break; - case "mipmap": - CreateIntField (mipmaps, itemName, value); - break; - case "plurals": - CreateIntField (plurals, itemName, value); - break; - case "raw": - CreateIntField (raw, itemName, value); - break; - case "string": - CreateIntField (strings, itemName, value); - break; - case "style": - CreateIntField (style, itemName, value); - break; - case "styleable": - switch (items [0]) { - case "int": - CreateIntField (styleable, itemName, Convert.ToInt32 (items [3], 10)); + var parser = new RtxtParser (); + var resources = parser.Parse (file, Log, map); + foreach (var r in resources) { + var cl = CreateClass (r.ResourceTypeName); + switch (r.Type) { + case RType.Integer: + CreateIntField (cl, r.Identifier, r.Id); break; - case "int[]": - var arrayValues = items [3].Trim (new char [] { '{', '}' }) - .Replace (" ", "") - .Split (new char [] { ',' }); - CreateIntArrayField (styleable, itemName, arrayValues.Length, - arrayValues.Select (x => string.IsNullOrEmpty (x) ? -1 : Convert.ToInt32 (x, 16)).ToArray ()); + case RType.Array: + CreateIntArrayField (cl, r.Identifier, r.Ids.Length, r.Ids); break; - } - break; - case "transition": - CreateIntField (transition, itemName, value); - break; - case "xml": - CreateIntField (xml, itemName, value); - break; - // for custom views - default: - CodeTypeDeclaration customClass; - if (!custom_types.TryGetValue (items [1], out customClass)) { - customClass = CreateClass (items [1]); - custom_types.Add (items [1], customClass); - } - CreateIntField (customClass, itemName, value); - break; } } } @@ -441,15 +347,22 @@ CodeTypeDeclaration CreateResourceClass () return decl; } + Dictionary classMapping = new Dictionary (StringComparer.OrdinalIgnoreCase); + CodeTypeDeclaration CreateClass (string type) { - var t = new CodeTypeDeclaration (ResourceParser.GetNestedTypeName (type)) { + var typeName = ResourceParser.GetNestedTypeName (type); + if (classMapping.ContainsKey (typeName)) { + return classMapping [typeName]; + } + var t = new CodeTypeDeclaration (typeName) { IsPartial = true, TypeAttributes = TypeAttributes.Public, }; t.Members.Add (new CodeConstructor () { Attributes = MemberAttributes.Private, }); + classMapping.Add (typeName, t); return t; } @@ -464,7 +377,7 @@ void CreateField (CodeTypeDeclaration parentType, string name, Type type) CodeMemberField CreateIntField (CodeTypeDeclaration parentType, string name, int value = -1) { - string mappedName = GetResourceName (parentType.Name, name, map); + string mappedName = ResourceIdentifier.GetResourceName (parentType.Name, name, map, Log); CodeMemberField f = (CodeMemberField)parentType.Members.OfType ().FirstOrDefault (x => string.Compare (x.Name, mappedName, StringComparison.Ordinal) == 0); if (f != null) return f; @@ -484,7 +397,7 @@ CodeMemberField CreateIntField (CodeTypeDeclaration parentType, string name, int CodeMemberField CreateIntArrayField (CodeTypeDeclaration parentType, string name, int count, params int[] values) { - string mappedName = GetResourceName (parentType.Name, name, map); + string mappedName = ResourceIdentifier.GetResourceName (parentType.Name, name, map, Log); CodeMemberField f = (CodeMemberField)parentType.Members.OfType ().FirstOrDefault (x => string.Compare (x.Name, mappedName, StringComparison.Ordinal) == 0); if (f != null) return f; @@ -608,7 +521,6 @@ void CreateResourceField (string root, string fieldName, XmlReader element = nul void ProcessStyleable (XmlReader reader) { string topName = null; - int fieldCount = 0; List fields = new List (); List attribs = new List (); while (reader.Read ()) { diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index 5e41b62be37..19088a10f6b 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -325,6 +325,28 @@ public static bool HasMonoAndroidReference (MetadataReader reader) return false; } + public static bool HasResourceDesignerAssemblyReference (ITaskItem assembly) + { + if (!File.Exists (assembly.ItemSpec)) { + return false; + } + using var pe = new PEReader (File.OpenRead (assembly.ItemSpec)); + var reader = pe.GetMetadataReader (); + return HasResourceDesignerAssemblyReference (reader); + } + + public static bool HasResourceDesignerAssemblyReference (MetadataReader reader) + { + foreach (var handle in reader.AssemblyReferences) { + var reference = reader.GetAssemblyReference (handle); + var name = reader.GetString (reference.Name); + if (string.CompareOrdinal (name, "_Microsoft.Android.Resource.Designer") == 0) { + return true; + } + } + return false; + } + public static bool IsReferenceAssembly (string assembly) { using (var stream = File.OpenRead (assembly)) @@ -388,12 +410,26 @@ internal static IEnumerable GetFrameworkAssembliesToTreatAsUserAssemb } #endif - public static Dictionary LoadAcwMapFile (string acwPath) + public static bool SaveMapFile (IBuildEngine4 engine, string mapFile, Dictionary map) { - var acw_map = new Dictionary (); - if (!File.Exists (acwPath)) + engine?.RegisterTaskObjectAssemblyLocal (mapFile, map, RegisteredTaskObjectLifetime.Build); + using (var writer = MemoryStreamPool.Shared.CreateStreamWriter ()) { + foreach (var i in map.OrderBy (x => x.Key)) { + writer.WriteLine ($"{i.Key};{i.Value}"); + } + writer.Flush (); + return Files.CopyIfStreamChanged (writer.BaseStream, mapFile); + } + } + public static Dictionary LoadMapFile (IBuildEngine4 engine, string mapFile, StringComparer comparer) + { + var cachedMap = engine?.GetRegisteredTaskObjectAssemblyLocal> (mapFile, RegisteredTaskObjectLifetime.Build); + if (cachedMap != null) + return cachedMap; + var acw_map = new Dictionary (comparer); + if (!File.Exists (mapFile)) return acw_map; - foreach (var s in File.ReadLines (acwPath)) { + foreach (var s in File.ReadLines (mapFile)) { var items = s.Split (new char[] { ';' }, count: 2); if (!acw_map.ContainsKey (items [0])) acw_map.Add (items [0], items [1]); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs index 8e7ddacfd3e..d9bcf2ecf69 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceDesignerImportGenerator.cs @@ -47,6 +47,7 @@ public void CreateImportMethods (IEnumerable libraries) var reader = pe.GetMetadataReader (); var resourceDesignerName = GetResourceDesignerClass (reader); if (string.IsNullOrEmpty (resourceDesignerName)) { + Log.LogDebugMessage ($"Could not find 'ResourceDesignerAttribute' in {assemblyPath}"); continue; } string aliasMetaData = assemblyPath.GetMetadata ("Aliases"); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceIdentifier.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceIdentifier.cs index eb96d4f5aa0..7edfc38faf5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceIdentifier.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceIdentifier.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using Microsoft.Android.Build.Tasks; using Microsoft.Build.Utilities; namespace Xamarin.Android.Tasks @@ -42,5 +43,20 @@ public static string CreateValidIdentifier (string identifier) return result; } + + internal static string GetResourceName (string type, string name, Dictionary map, TaskLoggingHelper log) + { + string mappedValue; + string key = string.Format ("{0}{1}{2}", type, Path.DirectorySeparatorChar, name).ToLowerInvariant (); + + if (map.TryGetValue (key, out mappedValue)) { + log.LogDebugMessage (" - Remapping resource: {0}.{1} -> {2}", type, name, mappedValue); + return ResourceIdentifier.CreateValidIdentifier (mappedValue.Substring (mappedValue.LastIndexOf (Path.DirectorySeparatorChar) + 1)); + } + + log.LogDebugMessage (" - Not remapping resource: {0}.{1}", type, name); + + return ResourceIdentifier.CreateValidIdentifier (name); + } } -} \ No newline at end of file +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceParser.cs index d85b97299e9..3becd20d1ec 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ResourceParser.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ResourceParser.cs @@ -37,21 +37,5 @@ internal static string GetNestedTypeName (string name) default: return char.ToUpperInvariant (name[0]) + name.Substring (1); } } - - internal string GetResourceName (string type, string name, Dictionary map) - { - string mappedValue; - string key = string.Format ("{0}{1}{2}", type, Path.DirectorySeparatorChar, name).ToLowerInvariant (); - - if (map.TryGetValue (key, out mappedValue)) { - Log.LogDebugMessage (" - Remapping resource: {0}.{1} -> {2}", type, name, mappedValue); - return ResourceIdentifier.CreateValidIdentifier (mappedValue.Substring (mappedValue.LastIndexOf (Path.DirectorySeparatorChar) + 1)); - } - - Log.LogDebugMessage (" - Not remapping resource: {0}.{1}", type, name); - - return ResourceIdentifier.CreateValidIdentifier (name); - } - } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs new file mode 100644 index 00000000000..644df0e2e31 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtParser.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks +{ + public enum RType { + Integer, + Array, + } + + public enum ResourceType { + System, + Custom, + } + + public struct R : IComparable { + public RType Type; + public int Id; + public int [] Ids; + public string Identifier; + public string ResourceTypeName; + public ResourceType ResourceType; + + public string Key => $"{ResourceTypeName}:{Identifier}"; + + public override string ToString () + { + if (Type == RType.Integer) { + if (ResourceTypeName == "styleable") + return $"int {ResourceTypeName} {Identifier} {Id}"; + return $"int {ResourceTypeName} {Identifier} 0x{Id.ToString ("x8")}"; + } + return $"int[] {ResourceTypeName} {Identifier} {{ {String.Join (", ", Ids.Select (x => $"0x{x.ToString ("x8")}"))} }}"; + } + + public string ToSortedString () + { + return $"{ResourceTypeName}_{Identifier}"; + } + + public int CompareTo(R other) + { + return String.Compare (ToSortedString (), other.ToSortedString (), StringComparison.OrdinalIgnoreCase); + } + + public void UpdateId (int newId) + { + Id = newId; + } + + public void UpdateIds (int [] newIds) + { + Ids = newIds; + } + } + + public class RtxtParser { + + static readonly char[] EmptyChar = new char [] { ' ' }; + static readonly char[] CurlyBracketsChar = new char [] { '{', '}' }; + static readonly char[] CommaChar = new char [] { ',' }; + + TaskLoggingHelper log; + Dictionary map; + + public static HashSet knownTypes = new HashSet () { + "anim", + "animator", + "attr", + "array", + "bool", + "color", + "dimen", + "drawable", + "font", + "id", + "integer", + "interpolator", + "layout", + "menu", + "mipmap", + "plurals", + "raw", + "string", + "style", + "styleable", + "transition", + "xml", + }; + + public IEnumerable Parse (string file, TaskLoggingHelper logger, Dictionary mapping) + { + log = logger; + map = mapping; + var result = new List (); + if (File.Exists (file)) + ProcessRtxtFile (file, result); + return result; + } + + void ProcessRtxtFile (string file, IList result) + { + int lineNumber = 0; + foreach (var line in File.ReadLines (file)) { + lineNumber++; + var items = line.Split (EmptyChar, 4); + if (items.Length < 4) { + log.LogDebugMessage ($"'{file}:{lineNumber}' ignoring contents '{line}', it does not have the correct number of elements."); + continue; + } + int value = items [1] != "styleable" ? Convert.ToInt32 (items [3], 16) : -1; + string itemName = ResourceIdentifier.GetResourceName(items [1], items [2], map, log); + if (knownTypes.Contains (items [1])) { + if (items [1] != "styleable") { + result.Add (new R () { + ResourceTypeName = items [1], + Identifier = itemName, + Id = value, + }); + continue; + } + switch (items [0]) { + case "int": + result.Add (new R () { + ResourceTypeName = items [1], + Identifier = itemName, + Id = Convert.ToInt32 (items [3], 10), + }); + break; + case "int[]": + var arrayValues = items [3].Trim (CurlyBracketsChar) + .Replace (" ", "") + .Split (CommaChar); + + result.Add (new R () { + ResourceTypeName = items [1], + Type = RType.Array, + Identifier = itemName, + Ids = arrayValues.Select (x => string.IsNullOrEmpty (x) ? -1 : Convert.ToInt32 (x, 16)).ToArray (), + }); + break; + } + continue; + } + result.Add (new R () { + ResourceTypeName = items[1], + ResourceType = ResourceType.Custom, + Identifier = itemName, + Id = value, + }); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/RtxtWriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtWriter.cs new file mode 100644 index 00000000000..eb3a8abf038 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Utilities/RtxtWriter.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Linq; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks +{ + /// Write a list of Item to a file + /// + public class RtxtWriter { + public void Write (string file, IList items) + { + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { + foreach (var item in items) { + sw.WriteLine (item.ToString ()); + } + sw.Flush (); + Files.CopyIfStreamChanged (sw.BaseStream, file); + } + } + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj index ee9e29a5c39..dd902240461 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -348,6 +348,12 @@ Utilities\%(Filename)%(Extension) + + Utilities\%(Filename)%(Extension) + + + Utilities\%(Filename)%(Extension) + @@ -407,6 +413,9 @@ JavaInteropTypeManager.java + + Resource.Designer.snk + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets index 7cb21493b26..cd4782e7bba 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets @@ -88,6 +88,10 @@ PreserveNewest Xamarin.Android.Designer.targets + + PreserveNewest + Xamarin.Android.Resource.Designer.targets + PreserveNewest Xamarin.Android.Aapt.targets diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index af4d1456749..14418bb0e23 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -57,6 +57,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -375,6 +376,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -574,11 +576,13 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + Condition=" '$(Language)' == 'C#' and '$(ManagedDesignTimeBuild)' != 'true' "> - - - + + + + + @@ -998,6 +1002,7 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidSupportedAbis=$(AndroidSupportedAbis)" /> <_PropertyCacheItems Include="AndroidManifestPlaceholders=$(AndroidManifestPlaceholders)" /> <_PropertyCacheItems Include="ProjectFullPath=$(MSBuildProjectFullPath)" /> + <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> - + DependsOnTargets="_CreatePropertiesCache;_ExtractLibraryProjectImports;_ValidateAndroidPackageProperties;_GenerateResourceCaseMap;_BeforeManagedUpdateAndroidResgen"> @@ -1080,11 +1086,11 @@ because xbuild doesn't support framework reference assemblies. - + @@ -1244,6 +1250,7 @@ because xbuild doesn't support framework reference assemblies. _GenerateAndroidResourceDir; _IncludeLayoutBindingSources; _DefineBuildTargetAbis; + _GenerateResourceCaseMap; <_UpdateAndroidResgenInputs> @(_AndroidMSBuildAllProjects); @@ -1265,7 +1272,7 @@ because xbuild doesn't support framework reference assemblies. + + + + + + - - + + - - - - + + + + @@ -1460,6 +1491,7 @@ because xbuild doesn't support framework reference assemblies. DestinationFiles="@(ResolvedAssemblies->'$(MonoAndroidIntermediateAssemblyDir)%(DestinationSubPath)')" TargetName="$(TargetName)" AddKeepAlives="$(AndroidAddKeepAlives)" + UseDesignerAssembly="$(AndroidUseDesignerAssembly)" Deterministic="$(Deterministic)" UsingAndroidNETSdk="$(UsingAndroidNETSdk)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.DesignTime.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.DesignTime.targets index 2102d915d28..00f3f2cfdd5 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.DesignTime.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.DesignTime.targets @@ -42,6 +42,7 @@ This file is used by all project types, including binding projects. true true + True False <_AndroidLibraryImportsCache Condition=" '$(DesignTimeBuild)' == 'true' And !Exists ('$(_AndroidLibraryImportsCache)') ">$(_AndroidLibraryImportsDesignTimeCache) <_AndroidLibraryProjectImportsCache Condition=" '$(DesignTimeBuild)' == 'true' And !Exists ('$(_AndroidLibraryProjectImportsCache)') ">$(_AndroidLibraryProjectImportsDesignTimeCache) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Legacy.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Legacy.targets index b08a0d94736..800cabe865b 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Legacy.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Legacy.targets @@ -108,8 +108,8 @@ projects. .NET 5 projects will not import this file. $(CoreResolveReferencesDependsOn); - UpdateAndroidInterfaceProxies; UpdateAndroidResources; + UpdateAndroidInterfaceProxies; $(DeferredBuildDependsOn); @@ -300,6 +300,10 @@ projects. .NET 5 projects will not import this file. Include="$(OutDir)$(TargetFileName)" Condition="Exists ('$(OutDir)$(TargetFileName)')" /> + + diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index f79b85b3222..7f780147517 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -706,6 +706,63 @@ public void RunWithLLVMEnabled () Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log"))); } + [Test] + public void ResourceDesignerWithNuGetReference ([Values ("net8.0-android33.0")] string dotnetTargetFramework) + { + AssertHasDevices (); + + string path = Path.Combine (Root, "temp", TestName); + + if (!Builder.UseDotNet) { + Assert.Ignore ("Skipping. Test not relevant under Classic."); + } + // Build a NuGet Package + var nuget = new XASdkProject (outputType: "Library") { + Sdk = "Xamarin.Legacy.Sdk/0.2.0-alpha2", + ProjectName = "Test.Nuget.Package", + IsRelease = true, + }; + nuget.AddNuGetSourcesForOlderTargetFrameworks (); + nuget.Sources.Clear (); + nuget.Sources.Add (new AndroidItem.AndroidResource ("Resources/values/Strings.xml") { + TextContent = () => @" + Library Resource From Nuget +", + }); + nuget.SetProperty ("PackageName", "Test.Nuget.Package"); + var legacyTargetFrameworkVersion = "13.0"; + var legacyTargetFramework = $"monoandroid{legacyTargetFrameworkVersion}"; + nuget.SetProperty ("TargetFramework", value: ""); + nuget.SetProperty ("TargetFrameworks", value: $"{dotnetTargetFramework};{legacyTargetFramework}"); + + string directory = Path.Combine ("temp", TestName, "Test.Nuget.Package"); + var dotnet = CreateDotNetBuilder (nuget, directory); + Assert.IsTrue (dotnet.Pack (), "`dotnet pack` should succeed"); + + // Build an app which references it. + var proj = new XamarinAndroidApplicationProject () { + IsRelease = true, + }; + proj.SetAndroidSupportedAbis ("armeabi-v7a", "x86", "x86_64"); + proj.OtherBuildItems.Add (new BuildItem ("None", "NuGet.config") { + TextContent = () => @" + + + + +", + }); + proj.PackageReferences.Add (new Package { + Id = "Test.Nuget.Package", + Version = "1.0.0", + }); + builder = CreateApkBuilder (Path.Combine (path, proj.ProjectName)); + Assert.IsTrue (builder.Install (proj, doNotCleanupOnUpdate: true), "Install should have succeeded."); + string resource_designer = GetResourceDesignerPath (builder, proj); + var contents = GetResourceDesignerText (proj, resource_designer); + StringAssert.Contains ("public const int library_resouce_from_nuget =", contents); + } + [Test] public void SingleProject_ApplicationId () { @@ -841,5 +898,30 @@ public MyLayout (Android.Content.Context context, Android.Util.IAttributeSet att Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log")); Assert.IsTrue (didStart, "Activity should have started."); } + + DotNetCLI CreateDotNetBuilder (string relativeProjectDir = null) + { + if (string.IsNullOrEmpty (relativeProjectDir)) { + relativeProjectDir = Path.Combine ("temp", TestName); + } + string fullProjectDirectory = Path.Combine (Root, relativeProjectDir); + TestOutputDirectories [TestContext.CurrentContext.Test.ID] = fullProjectDirectory; + + new XASdkProject ().CopyNuGetConfig (relativeProjectDir); + return new DotNetCLI (Path.Combine (fullProjectDirectory, $"{TestName}.csproj")); + } + + DotNetCLI CreateDotNetBuilder (XASdkProject project, string relativeProjectDir = null) + { + if (string.IsNullOrEmpty (relativeProjectDir)) { + relativeProjectDir = Path.Combine ("temp", TestName); + } + string fullProjectDirectory = Path.Combine (Root, relativeProjectDir); + TestOutputDirectories [TestContext.CurrentContext.Test.ID] = fullProjectDirectory; + var files = project.Save (); + project.Populate (relativeProjectDir, files); + project.CopyNuGetConfig (relativeProjectDir); + return new DotNetCLI (project, Path.Combine (fullProjectDirectory, project.ProjectFilePath)); + } } } diff --git a/tests/MSBuildDeviceIntegration/Tests/InstantRunTest.cs b/tests/MSBuildDeviceIntegration/Tests/InstantRunTest.cs index d3ff4f02248..6605cba3a8d 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstantRunTest.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstantRunTest.cs @@ -56,6 +56,7 @@ public void TargetsSkipped ([Values(false, true)] bool useManagedResourceGenerat UseLatestPlatformSdk = true, }; proj.SetProperty ("AndroidUseManagedDesignTimeResourceGenerator", useManagedResourceGenerator.ToString ()); + proj.SetProperty ("AndroidUseDesignerAssembly", "False"); var b = CreateApkBuilder ($"temp/InstantRunTargetsSkipped_{useManagedResourceGenerator}", cleanupOnDispose: false); Assert.IsTrue (b.Build (proj), "1 build should have succeeded."); diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.csproj index 84fbd2ae182..80c5cb93874 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.csproj @@ -59,6 +59,7 @@ + diff --git a/tests/Mono.Android-Tests/Mono.Android-Test.Shared.projitems b/tests/Mono.Android-Tests/Mono.Android-Test.Shared.projitems index 16b42acd14b..052219da8fc 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Test.Shared.projitems +++ b/tests/Mono.Android-Tests/Mono.Android-Test.Shared.projitems @@ -13,8 +13,8 @@ - + diff --git a/tests/Mono.Android-Tests/Runtime-MultiDex/Mono.Android-TestsMultiDex.csproj b/tests/Mono.Android-Tests/Runtime-MultiDex/Mono.Android-TestsMultiDex.csproj index 848607e2899..b7146ae1ca7 100644 --- a/tests/Mono.Android-Tests/Runtime-MultiDex/Mono.Android-TestsMultiDex.csproj +++ b/tests/Mono.Android-Tests/Runtime-MultiDex/Mono.Android-TestsMultiDex.csproj @@ -24,6 +24,7 @@ <_SkipJniAddNativeMethodRegistrationAttributeScan>True <_MonoAndroidTestPackage>Mono.Android_TestsMultiDex -MultiDex + True @@ -67,7 +68,6 @@ -