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 00000000000..7f3d225c9d2 Binary files /dev/null and b/src/Xamarin.Android.Build.Tasks/Resources/Resource.Designer.snk differ diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs index c70cfb09fb7..4dd27c19321 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/Aapt2Link.cs @@ -82,6 +82,7 @@ public class Aapt2Link : Aapt2 { List 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 @@ -