Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Xamarin.Android.Build.Tasks] generate R.java like Android Studio #2896

Merged
merged 2 commits into from
Apr 1, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -138,32 +138,4 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved.
</ItemGroup>
</Target>

<Target Name="_GenerateJavaDesignerForComponentAapt2"
Condition=" '$(_AndroidUseAapt2)' == 'True' "
Inputs="@(_AdditonalAndroidResourceCacheFiles);@(_LibraryResourceDirectoryStamps);$(_AndroidResgenFlagFile)"
Outputs="$(_AndroidComponentResgenFlagFile)">
<!-- Run aapt to generate R.java for additional Android resources-->
<Aapt2Link
Condition=" '$(_AndroidUseAapt2)' == 'True' "
ContinueOnError="$(DesignTimeBuild)"
AdditionalAndroidResourcePaths="@(_LibraryResourceHashDirectories)"
AdditionalResourceArchives="@(_LibraryResourceHashDirectories->'$(_AndroidLibraryFlatArchivesDirectory)%(Hash).flata')"
ApplicationName="$(_AndroidPackage)"
AssemblyIdentityMapFile="$(_AndroidLibrayProjectAssemblyMapFile)"
CompiledResourceFlatArchive="$(_AndroidLibraryFlatArchivesDirectory)\compiled.flata"
ExtraArgs="$(AndroidAapt2LinkExtraArgs)"
ImportsDirectory="$(_LibraryProjectImportsDirectoryName)"
JavaDesignerOutputDirectory="$(IntermediateOutputPath)android\src"
JavaPlatformJarPath="$(JavaPlatformJarPath)"
ManifestFiles="@(_AdditonalAndroidResourceCachePaths->'%(Identity)\AndroidManifest.xml');@(LibraryResourceDirectories->'%(Identity)\..\AndroidManifest.xml')"
OutputImportDirectory="$(_AndroidLibrayProjectIntermediatePath)"
ResourceNameCaseMap="$(_AndroidResourceNameCaseMap)"
ResourceDirectories="$(MonoAndroidResDirIntermediate)"
ToolPath="$(Aapt2ToolPath)"
ToolExe="$(Aapt2ToolExe)"
UseShortFileNames="$(UseShortFileNames)"
YieldDuringToolExecution="$(YieldDuringToolExecution)"
/>
<Touch Files="$(_AndroidComponentResgenFlagFile)" AlwaysCreate="True" />
</Target>
</Project>
234 changes: 234 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/GenerateLibraryResources.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
public class GenerateLibraryResources : AsyncTask
{
/// <summary>
/// The main R.txt for the app
/// </summary>
[Required]
public string ResourceSymbolsTextFile { get; set; }

/// <summary>
/// The output directory for Java source code, such as: $(IntermediateOutputPath)android\src
/// </summary>
[Required]
public string OutputDirectory { get; set; }

/// <summary>
/// The list of R.txt files for each library
/// </summary>
public string [] LibraryTextFiles { get; set; }

/// <summary>
/// The accompanying manifest file for each library
/// </summary>
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<Tuple<string, string>, string> r_txt_mapping;
jonathanpeppers marked this conversation as resolved.
Show resolved Hide resolved

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<Tuple<string, string>, string> ();
using (var reader = File.OpenText (main_r_txt)) {
foreach (var line in ParseFile (reader)) {
var key = new Tuple<string, string> (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);
}

/// <summary>
/// A quick class to combine the paths to R.txt and Manifest
/// </summary>
class Library
{
public Library (string textFile, string manifestFile)
{
TextFile = Path.GetFullPath (textFile);
ManifestFile = Path.GetFullPath (manifestFile);
}

/// <summary>
/// A full path to the R.txt file
/// </summary>
public string TextFile { get; }

/// <summary>
/// A full path to the AndroidManifest.xml file
/// </summary>
public string ManifestFile { get; }
}

/// <summary>
/// NOTE: all file paths used in this method should be full paths. (Or use AsyncTask.WorkingDirectory)
/// </summary>
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 = new Tuple<string, string> (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;
}

/// <summary>
/// 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.
/// </summary>
IEnumerable<string []> ParseFile (StreamReader reader)
{
while (!reader.EndOfStream) {
var line = reader.ReadLine ();
var items = line.Split (Delimiter, 4);
yield return items;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand Down Expand Up @@ -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}`!");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};
Expand Down Expand Up @@ -455,7 +454,6 @@ public void CheckTimestamps ([Values (true, false)] bool isRelease)
var targetsToBeSkipped = new [] {
isRelease ? "_LinkAssembliesShrink" : "_LinkAssembliesNoShrink",
"_UpdateAndroidResgen",
"_GenerateJavaDesignerForComponentAapt2",
"_BuildLibraryImportsCache",
"_CompileJava",
};
Expand Down Expand Up @@ -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<string> 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}");
}
}
}

Expand Down
Loading