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 8f610a571db..fbfacb204a5 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 @@ -138,32 +138,4 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. - - - - - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLibraryResources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLibraryResources.cs new file mode 100644 index 00000000000..8b1ef47af77 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateLibraryResources.cs @@ -0,0 +1,234 @@ +using Microsoft.Build.Framework; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Xamarin.Android.Tools; +using Xamarin.Build; + +namespace Xamarin.Android.Tasks +{ + /// + /// We used to invoke aapt/aapt2 per library (many times!), this task does the work to generate R.java for libraries without calling aapt/aapt2. + /// + public class GenerateLibraryResources : AsyncTask + { + /// + /// The main R.txt for the app + /// + [Required] + public string ResourceSymbolsTextFile { get; set; } + + /// + /// The output directory for Java source code, such as: $(IntermediateOutputPath)android\src + /// + [Required] + public string OutputDirectory { get; set; } + + /// + /// The list of R.txt files for each library + /// + public string [] LibraryTextFiles { get; set; } + + /// + /// The accompanying manifest file for each library + /// + public string [] ManifestFiles { get; set; } + + public override bool Execute () + { + Yield (); + try { + this.RunTask (DoExecute).ContinueWith (Complete); + + base.Execute (); + } finally { + Reacquire (); + } + + return !Log.HasLoggedErrors; + } + + string main_r_txt; + string output_directory; + Dictionary r_txt_mapping; + + void DoExecute () + { + if (LibraryTextFiles == null || LibraryTextFiles.Length == 0) + return; + + // Load the "main" R.txt file into a dictionary + main_r_txt = Path.GetFullPath (ResourceSymbolsTextFile); + r_txt_mapping = new Dictionary (); + using (var reader = File.OpenText (main_r_txt)) { + foreach (var line in ParseFile (reader)) { + var key = line [Index.Class] + " " + line [Index.Name]; + r_txt_mapping [key] = line [Index.Value]; + } + } + + Directory.CreateDirectory (OutputDirectory); + output_directory = Path.GetFullPath (OutputDirectory); + + var libraries = LibraryTextFiles.Zip (ManifestFiles, (textFile, manifestFile) => new Library (textFile, manifestFile)); + this.ParallelForEach (libraries, GenerateJava); + } + + /// + /// A quick class to combine the paths to R.txt and Manifest + /// + class Library + { + public Library (string textFile, string manifestFile) + { + TextFile = Path.GetFullPath (textFile); + ManifestFile = Path.GetFullPath (manifestFile); + } + + /// + /// A full path to the R.txt file + /// + public string TextFile { get; } + + /// + /// A full path to the AndroidManifest.xml file + /// + public string ManifestFile { get; } + } + + /// + /// NOTE: all file paths used in this method should be full paths. (Or use AsyncTask.WorkingDirectory) + /// + void GenerateJava (Library library) + { + // In some cases (such as ancient support libraries), R.txt does not exist. + // We can just use the main app's R.txt file and write *all fields* in this case. + bool using_main_r_txt = false; + var r_txt = library.TextFile; + if (!File.Exists (r_txt)) { + LogDebugMessage ($"Using main R.txt, R.txt does not exist: {r_txt}"); + using_main_r_txt = true; + r_txt = main_r_txt; + } + + var manifestFile = library.ManifestFile; + if (!File.Exists (manifestFile)) { + LogDebugMessage ($"Skipping, AndroidManifest.xml does not exist: {manifestFile}"); + return; + } + + var manifest = AndroidAppManifest.Load (manifestFile, MonoAndroidHelper.SupportedVersions); + + using (var memory = new MemoryStream ()) + using (var writer = new StreamWriter (memory, Encoding)) { + // This code is based on the Android gradle plugin + // https://android.googlesource.com/platform/tools/base/+/908b391a9c006af569dfaff08b37f8fdd6c4da89/build-system/builder/src/main/java/com/android/builder/internal/SymbolWriter.java + + writer.WriteLine ("/* AUTO-GENERATED FILE. DO NOT MODIFY."); + writer.WriteLine (" *"); + writer.WriteLine (" * This class was automatically generated by"); + writer.WriteLine (" * Xamarin.Android from the resource data it found."); + writer.WriteLine (" * It should not be modified by hand."); + writer.WriteLine (" */"); + + writer.Write ("package "); + writer.Write (manifest.PackageName); + writer.WriteLine (';'); + writer.WriteLine (); + writer.WriteLine ("public final class R {"); + + using (var reader = File.OpenText (r_txt)) { + string currentClass = null; + foreach (var line in ParseFile (reader)) { + var type = line [Index.Type]; + var clazz = line [Index.Class]; + var name = line [Index.Name]; + if (GetValue (clazz, name, line, using_main_r_txt, out string value)) { + if (clazz != currentClass) { + // If not the first inner class + if (currentClass != null) { + writer.WriteLine ("\t}"); + } + + currentClass = clazz; + writer.Write ("\tpublic static final class "); + writer.Write (currentClass); + writer.WriteLine (" {"); + } + + writer.Write ("\t\tpublic static final "); + writer.Write (type); + writer.Write (' '); + writer.Write (name); + writer.Write (" = "); + // It may be an int[] + if (value.StartsWith ("{", StringComparison.Ordinal)) { + writer.Write ("new "); + writer.Write (type); + writer.Write (' '); + } + writer.Write (value); + writer.WriteLine (';'); + } else { + LogDebugMessage ($"{r_txt}: `{type} {clazz} {name}` value not found"); + } + } + + // If we wrote at least one inner class + if (currentClass != null) { + writer.WriteLine ("\t}"); + } + writer.WriteLine ('}'); + } + + writer.Flush (); + var r_java = Path.Combine (output_directory, manifest.PackageName.Replace ('.', Path.DirectorySeparatorChar), "R.java"); + if (MonoAndroidHelper.CopyIfStreamChanged (memory, r_java)) { + LogDebugMessage ($"Writing: {r_java}"); + } else { + LogDebugMessage ($"Up to date: {r_java}"); + } + } + } + + bool GetValue (string clazz, string name, string[] line, bool using_main_r_txt, out string value) + { + // If this is the main R.txt file, we don't need to do a lookup + if (using_main_r_txt) { + value = line [Index.Value]; + return true; + } + + var key = clazz + " " + name; + return r_txt_mapping.TryGetValue (key, out value); + } + + static readonly Encoding Encoding = new UTF8Encoding (encoderShouldEmitUTF8Identifier: false); + static readonly char [] Delimiter = new [] { ' ' }; + + class Index + { + public const int Type = 0; + public const int Class = 1; + public const int Name = 2; + public const int Value = 3; + } + + /// + /// R.txt is of the format: + /// int id icon 0x7f0c000a + /// int[] styleable ViewStubCompat { 0x010100d0, 0x010100f2, 0x010100f3 } + /// This returns a 4-length string[] of the parts. + /// + IEnumerable ParseFile (StreamReader reader) + { + while (!reader.EndOfStream) { + var line = reader.ReadLine (); + var items = line.Split (Delimiter, 4); + yield return items; + } + } + } +} 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 531eca325f4..573b873f803 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 @@ -638,36 +638,6 @@ public void CheckOldResourceDesignerWithWrongCasingIsRemoved (bool isRelease, Pr } } - [Test] - public void TargetGenerateJavaDesignerForComponentIsSkipped ([Values(false, true)] bool isRelease) - { - // build with packages... then tweak a package.. - var proj = new XamarinAndroidApplicationProject () { - IsRelease = isRelease, - }; - proj.PackageReferences.Add (KnownPackages.AndroidSupportV4_21_0_3_0); - proj.PackageReferences.Add (KnownPackages.SupportV7AppCompat_21_0_3_0); - proj.SetProperty ("TargetFrameworkVersion", "v5.0"); - using (var b = CreateApkBuilder (Path.Combine ("temp", TestContext.CurrentContext.Test.Name))) { - b.Verbosity = LoggerVerbosity.Diagnostic; - Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - StringAssertEx.DoesNotContain ("Skipping target \"_GenerateJavaDesignerForComponent\" because", - b.LastBuildOutput, "Target _GenerateJavaDesignerForComponent should not have been skipped"); - Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - StringAssertEx.Contains ("Skipping target \"_GenerateJavaDesignerForComponent\" because", - b.LastBuildOutput, "Target _GenerateJavaDesignerForComponent should have been skipped"); - var files = Directory.EnumerateFiles (Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "resourcecache") - , "abc_fade_in.xml", SearchOption.AllDirectories); - Assert.AreEqual (1, files.Count (), "There should only be one abc_fade_in.xml in the resourcecache"); - var resFile = files.First (); - Assert.IsTrue (File.Exists (resFile), "{0} should exist", resFile); - File.SetLastWriteTime (resFile, DateTime.UtcNow); - Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - StringAssertEx.DoesNotContain ("Skipping target \"_GenerateJavaDesignerForComponent\" because", - b.LastBuildOutput, "Target _GenerateJavaDesignerForComponent should not have been skipped"); - } - } - [Test] public void CheckAaptErrorRaisedForMissingResource () { @@ -1421,7 +1391,7 @@ public void CustomViewAddResourceId ([Values (false, true)] bool useAapt2) Assert.IsTrue (b.Build (proj, doNotCleanupOnUpdate: true), "second build should have succeeded"); - var r_java = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "src", "android", "support", "compat", "R.java"); + var r_java = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "src", "unnamedproject", "unnamedproject", "R.java"); FileAssert.Exists (r_java); var r_java_contents = File.ReadAllLines (r_java); Assert.IsTrue (StringAssertEx.ContainsText (r_java_contents, textView1), $"android/support/compat/R.java should contain `{textView1}`!"); 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 b8c8390825f..ebc37f7d914 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 @@ -350,7 +350,6 @@ public CustomTextView(Context context, IAttributeSet attributes) : base(context, // And so the built assembly changes between DTB and regular build, triggering `_LinkAssembliesNoShrink` //"_LinkAssembliesNoShrink", "_UpdateAndroidResgen", - "_GenerateJavaDesignerForComponentAapt2", "_BuildLibraryImportsCache", "_CompileJava", }; @@ -455,7 +454,6 @@ public void CheckTimestamps ([Values (true, false)] bool isRelease) var targetsToBeSkipped = new [] { isRelease ? "_LinkAssembliesShrink" : "_LinkAssembliesNoShrink", "_UpdateAndroidResgen", - "_GenerateJavaDesignerForComponentAapt2", "_BuildLibraryImportsCache", "_CompileJava", }; @@ -2443,6 +2441,37 @@ public void BuildReleaseApplicationWithNugetPackages () var assets = b.Output.GetIntermediaryAsText (Path.Combine ("..", "project.assets.json")); StringAssert.Contains ("Xamarin.Android.Support.v4", assets, "Nuget Package Xamarin.Android.Support.v4.21.0.3.0 should have been restored."); + + //Since this is using an old support library, its main R.java should "match" the library one + var src = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, "android", "src"); + var main_r_java = Path.Combine (src, "unnamedproject", "unnamedproject", "R.java"); + FileAssert.Exists (main_r_java); + var lib_r_java = Path.Combine (src, "android", "support", "v4", "R.java"); + FileAssert.Exists (lib_r_java); + + void TrimHeader (List lines) + { + for (int i = 0; i < lines.Count; i++) { + if (lines [i].StartsWith ("package ", StringComparison.Ordinal)) { + lines.RemoveRange (0, i + 1); + break; + } + } + } + + //Beyond the `package com.foo;` line, each line should match: ignoring whitespace + var main_r_contents = File.ReadAllLines (main_r_java).ToList (); + TrimHeader (main_r_contents); + var lib_r_contents = File.ReadAllLines (lib_r_java).ToList (); + TrimHeader (lib_r_contents); + var regex = new Regex (@"\s", RegexOptions.Compiled); + for (int i = 0; i < main_r_contents.Count && i < lib_r_contents.Count; i++) { + var main = main_r_contents [i]; + var lib = lib_r_contents [i]; + var expected = regex.Replace (main, ""); + var actual = regex.Replace (lib, ""); + Assert.AreEqual (expected, actual, $"Main R.java `{main}` does not match library R.java `{lib}"); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateLibraryResourcesTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateLibraryResourcesTests.cs new file mode 100644 index 00000000000..ac876bd52e6 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateLibraryResourcesTests.cs @@ -0,0 +1,219 @@ +using NUnit.Framework; +using System.IO; +using Xamarin.Android.Tasks; + +namespace Xamarin.Android.Build.Tests +{ + [TestFixture] + public class GenerateLibraryResourcesTests : BaseTest + { + string temp; + string main_r_txt; + string r_txt; + string manifest; + string output_dir; + + const string app_r_txt_contents = +@"int anim foo_anim 0x7f010000 +int attr foo_attr 0x7f030000 +int bool foo_bool 0x7f040000 +int color foo_color 0x7f050000 +int dimen foo_dimen 0x7f060000 +int drawable foo_drawable 0x7f070007 +int id foo_id 0x7f080000 +int integer foo_integer 0x7f090000 +int interpolator foo_interpolator 0x7f0a0000 +int layout foo_layout 0x7f0b0000 +int string foo_string 0x7f0c0000 +int style foo_style 0x7f0d0000 +int[] styleable Foo_styleable { 0x010100b3, 0x010100f4 } +int styleable Foo_styleable_bar 0 +int styleable Foo_styleable_baz 1"; + + const string AndroidManifest = + ""; + + const string Header = @"/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by + * Xamarin.Android from the resource data it found. + * It should not be modified by hand. + */ +"; + + [SetUp] + public void Setup () + { + string tempDirectoryName = Path.Combine ("temp", TestName); + temp = Path.Combine (Root, tempDirectoryName); + Directory.CreateDirectory (temp); + main_r_txt = Path.Combine (temp, "app-r.txt"); + manifest = Path.Combine (temp, "AndroidManifest.xml"); + r_txt = Path.Combine (temp, "R.txt"); + output_dir = Path.Combine (temp, "src"); + + File.WriteAllText (main_r_txt, app_r_txt_contents); + File.WriteAllText (manifest, AndroidManifest); + + var references = CreateFauxReferencesDirectory (Path.Combine (tempDirectoryName, "references"), new [] { + new ApiInfo { Id = "28", Level = 28, Name = "Pie", FrameworkVersion = "v9.0", Stable = true }, + }); + MonoAndroidHelper.RefreshSupportedVersions (new [] { + Path.Combine (references, "MonoAndroid"), + }); + } + + [TearDown] + public void TearDown () + { + Directory.Delete (temp, recursive: true); + } + + string TempDirectory () => Path.Combine (Path.GetTempPath (), Path.GetRandomFileName ()); + + string ReplaceLineEndings (string s) => s.Replace ("\r\n", "\n"); + + void RunTask (string expected = null, bool fileExists = true) + { + var task = new GenerateLibraryResources { + BuildEngine = new MockBuildEngine (TestContext.Out), + ResourceSymbolsTextFile = main_r_txt, + OutputDirectory = output_dir, + LibraryTextFiles = new [] { r_txt }, + ManifestFiles = new [] { manifest }, + }; + Assert.IsTrue (task.Execute (), "Execute() failed!"); + + var r_java = Path.Combine (output_dir, "com", "mycompanyname", "foo", "R.java"); + if (fileExists) { + FileAssert.Exists (r_java); + Assert.AreEqual (ReplaceLineEndings (Header + expected), ReplaceLineEndings (File.ReadAllText (r_java))); + } else { + FileAssert.DoesNotExist (r_java); + } + } + + [Test] + public void Anim () + { + File.WriteAllText (r_txt, "int anim foo_anim 0x00000000"); + RunTask (expected: +@"package com.mycompanyname.foo; + +public final class R { + public static final class anim { + public static final int foo_anim = 0x7f010000; + } +} +"); + } + + [Test] + public void Id () + { + File.WriteAllText (r_txt, "int id foo_id 0x00000000"); + RunTask (expected: +@"package com.mycompanyname.foo; + +public final class R { + public static final class id { + public static final int foo_id = 0x7f080000; + } +} +"); + } + + [Test] + public void Styleable () + { + File.WriteAllText (r_txt, +@"int[] styleable Foo_styleable { 0x00000000, 0x00000000 } +int styleable Foo_styleable_bar 0 +int styleable Foo_styleable_baz 1"); + RunTask (expected: +@"package com.mycompanyname.foo; + +public final class R { + public static final class styleable { + public static final int[] Foo_styleable = new int[] { 0x010100b3, 0x010100f4 }; + public static final int Foo_styleable_bar = 0; + public static final int Foo_styleable_baz = 1; + } +} +"); + } + + [Test] + public void NoMatches () + { + File.WriteAllText (r_txt, "int id asdf 0x00000000"); + RunTask (expected: +@"package com.mycompanyname.foo; + +public final class R { +} +"); + } + + //This one should just write the main R.txt file + [Test] + public void TextFileDoesNotExist () + { + RunTask (expected: +@"package com.mycompanyname.foo; + +public final class R { + public static final class anim { + public static final int foo_anim = 0x7f010000; + } + public static final class attr { + public static final int foo_attr = 0x7f030000; + } + public static final class bool { + public static final int foo_bool = 0x7f040000; + } + public static final class color { + public static final int foo_color = 0x7f050000; + } + public static final class dimen { + public static final int foo_dimen = 0x7f060000; + } + public static final class drawable { + public static final int foo_drawable = 0x7f070007; + } + public static final class id { + public static final int foo_id = 0x7f080000; + } + public static final class integer { + public static final int foo_integer = 0x7f090000; + } + public static final class interpolator { + public static final int foo_interpolator = 0x7f0a0000; + } + public static final class layout { + public static final int foo_layout = 0x7f0b0000; + } + public static final class string { + public static final int foo_string = 0x7f0c0000; + } + public static final class style { + public static final int foo_style = 0x7f0d0000; + } + public static final class styleable { + public static final int[] Foo_styleable = new int[] { 0x010100b3, 0x010100f4 }; + public static final int Foo_styleable_bar = 0; + public static final int Foo_styleable_baz = 1; + } +} +"); + } + + [Test] + public void ManifestDoesNotExist () + { + File.Delete (manifest); + File.WriteAllText (r_txt, "int id asdf 0x00000000"); + RunTask (fileExists: false); + } + } +} 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 60b12461050..efc79d80409 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 @@ -96,6 +96,7 @@ + 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 1a59b60e048..920a725d09c 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.csproj @@ -16,6 +16,7 @@ + portable True False $(XAInstallPrefix)xbuild\Xamarin\Android\ @@ -127,6 +128,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 4549747ff20..e29a0fb1c7d 100755 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -60,6 +60,7 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. + @@ -1216,7 +1217,6 @@ because xbuild doesn't support framework reference assemblies. <_AndroidAotBinDirectory>$(IntermediateOutputPath)aot <_AndroidResgenFlagFile>$(IntermediateOutputPath)R.cs.flag <_AndroidResFlagFile>$(IntermediateOutputPath)res.flag - <_AndroidComponentResgenFlagFile>$(IntermediateOutputPath)Component.R.cs.flag <_AndroidJniMarshalMethodsFlag>$(IntermediateOutputPath)jnimarshalmethods.flag <_AndroidLinkFlag>$(IntermediateOutputPath)link.flag <_AndroidApkPerAbiFlagFile>$(IntermediateOutputPath)android\bin\apk_per_abi.flag @@ -1332,7 +1332,7 @@ because xbuild doesn't support framework reference assemblies. + DependsOnTargets="$(CoreResolveReferencesDependsOn);_CreatePropertiesCache;_CheckForDeletedResourceFile;_ComputeAndroidResourcePaths;_UpdateAndroidResgen;_CreateManagedLibraryResourceArchive"> @@ -1518,53 +1518,6 @@ because xbuild doesn't support framework reference assemblies. - - - - - - - - - <_GenerateJavaDesignerForComponentDependsOnTargets> - _GetAdditionalResourcesFromAssemblies - ;_CreateAdditionalResourceCache - ;_CollectAdditionalResourceFiles - ;_CollectLibraryResourceDirectories - ;_CompileAndroidLibraryResources - ;_CompileResources - ;_GenerateJavaDesignerForComponentAapt - ;_GenerateJavaDesignerForComponentAapt2 - - - - - + + @@ -3252,7 +3213,6 @@ because xbuild doesn't support framework reference assemblies. -