Skip to content

Commit 3395dbc

Browse files
dellis1972jonpryor
authored andcommitted
[Xamarin.Android.Build.Tasks] Add Support for aapt2. (#1573)
The Android SDK provides a new tool for packaging called `aapt2`. `aapt2` works slightly differently from the normal `aapt` process. When `aapt2` is used, the build process is split into two parts: an `aapt compile` step and an `aapt link` step. The `aapt2 compile` step produces a `.flata` zip file which contains the compiled "flat" resources. The format is internal to android tooling: aapt2 compile -o obj/Debug/res/compiled.flata -dir obj/Debug/res `aapt2 compile` is separately run for every `res` directory that the project contains, such as those from referenced assemblies or NuGet packages, and will store the `.flata` file into the various `$(IntermediateOutputPath)lp\*` directories. The intended benefit here is that when a resource is changed, only the `compiled.flata` for the project/assembly associated with that resource need be regenerated, not *everything*. The `aapt2 link` step consumes all the `.flata` archives and combines them into a single archive for use by the `.apk`: aapt2 link -o resources.apk.bk --manifest Foo.xml --java . --custom-package com.infinitespace_studios.blankforms -R foo2.flata -R foo.flata -v --auto-add-overlay --output-text-symbols obj/Debug/R.txt Note the `.flata` archives are passed using the `-R` option. The order is important just like it was for `aapt`. The last item will be the application resources. This allows developers to override resource values if needed. We also generate the `R.txt` file at this point, which is used to generate the design time `Resource.designer.cs` for IntelliSense. Use of the new `aapt2` model doesn't improve initial `.apk` build times, but it significantly helps with *rebuild* times: Tool | Clean Build | Build Touching only C# | Build Touching only Resource | ------|-------------|------------------------|------------------------------| Aapt | 00:00:51.74 | 00:00:08.91 | 00:00:25.26 | Aapt2 | 00:00:50.70 | 00:00:08.64 | 00:00:08.44 | The above are the build resources for a Blank Xamarin Forms App. Note that the initial build is roughly the same between both `aapt` and `aapt2`. `aapt2` really shines when a resource in a dependency is modified and the `.apk` is rebuilt, rebuilding the `.apk` in roughly a third of the time as `aapt` required. This happens because only the `.flata` file for that directory is re-compiled, then ALL the `.flata` archives were re-linked again to produce the `.apk`. Use of `aapt2` also disables using `R.java` files to generate the `Resource.designer.cs` file. Instead, the `R.txt` file which the `aapt2 link` command generates is instead parsed by the `ManagedResourceParse` to determine the Resource ID values. Using `R.txt` should be faster, as it's an easier format to process. `R.java` files are still *generated* as they are needed by `javac` and needed at runtime on-device. `aapt2` use is *disabled* by default, until more testing can be performed. To enable `aapt2` use, set the `$(AndroidUseAapt2)` MSBuild property to True by including the following XML fragment in your `.csproj`: <AndroidUseAapt2>True</AndroidUseAapt2> or by overriding on the `msbuild` command-line: msbuild /p:AndroidUseAapt2=True ... Note: if `$(AndroidUseAapt2)`=True but `aapt2` isn't present, the build will *fail* as we attempt to execute the (non-existent) `aapt2`. This will be improved in a future commit.
1 parent 90f9373 commit 3395dbc

27 files changed

+1187
-33
lines changed

Documentation/guides/BuildProcess.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -751,8 +751,27 @@ resources.
751751
including this property and setting it to `True`. When this
752752
property is set, the build process pre-crunches the .png files.
753753
754+
Note: This option is not compatible with the `$(AndroidUseAapt2)`
755+
option. If `$(AndroidUseAapt2)` is enabled, this functionality
756+
will be disabled. If you wish to continue to use this feature
757+
please set `$(AndroidUseAapt2)` to `False`.
758+
754759
**Experimental**. Added in Xamarin.Android 7.0.
755760
761+
- **AndroidUseAapt2** &ndash; A bool property which allows the developer to
762+
control the use of the `aapt2` tool for packaging.
763+
By default this will be set to false and we will use `aapt`.
764+
If the developer wishes too use the new `aapt2` functionality
765+
they can set
766+
767+
<AndroidUseAapt2>True</AndroidUseAapt2>
768+
769+
in their csproj. Alternatively provide the property on the command line
770+
via
771+
772+
/p:AndroidUseAapt2=True
773+
774+
Added in Xamarin.Android 8.3.
756775
757776
<a name="Signing_Properties" />
758777

Documentation/guides/messages/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
+ [XA0107](xa0107.md): `{Assmebly}` is a Reference Assembly.
3131
+ [XA0108](xa0108.md): Could not get version from `lint`.
3232
+ [XA0109](xa0109.md): Unsupported or invalid `$(TargetFrameworkVersion)` value of 'v4.5'.
33+
+ [XA0110](xa0110.md): Disabling $(AndroidExplicitCrunch) as it is not supported by `aapt2`. If you wish to use $(AndroidExplicitCrunch) please set $(AndroidUseAapt2) to false.
34+
+ [XA0111](xa0111.md): Could not get the `aapt2` version. Please check it is installed correctly.
35+
+ [XA0112](xa0112.md): `aapt2` is not installed. Disabling `aapt2` support. Please check it is installed correctly.
3336

3437
### XA1xxx Project Related
3538

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Compiler Warning XA0110
2+
3+
This warning is raised because your project is setup to use `$(AndroidExplicitCrunch)`.
4+
This setting in incompatible with the android `aapt2` resource processor.
5+
The `aapt2` processor is enabled/disabled via the `$(AndroidUseAapt2)` msbuild property.
6+
7+
If you wish to continue to use `$(AndroidExplicitCrunch)` you will need to disable
8+
`aapt2` processing by adding the following to your project file
9+
10+
<AndroidUseAapt2>False</AndroidUseAapt2>
11+
12+
Alternatively provide the property on the command line via
13+
14+
/p:AndroidUseAapt2=False
15+
16+
You can also disable `$(AndroidExplicitCrunch)` in a similar manner.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Compiler Warning XA0111
2+
3+
Could not get the `aapt2` version.
4+
5+
aapt2 -version
6+
7+
did not return an expected value. Try re-installing
8+
the android build-tools.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Compiler Warning XA0112
2+
3+
Your project has `$(AndroidUseAapt2)` set to `True`.
4+
However `aapt2` was not installed in your android-sdk
5+
build-tools.
6+
7+
If you want to use `aapt2` resource processing you need
8+
to install the appropriate build-tools.

src/Xamarin.Android.Build.Tasks/Tasks/Aapt.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,17 +96,6 @@ public class Aapt : AsyncTask
9696
AssemblyIdentityMap assemblyMap = new AssemblyIdentityMap ();
9797
string resourceDirectory;
9898

99-
struct OutputLine {
100-
public string Line;
101-
public bool StdError;
102-
103-
public OutputLine (string line, bool stdError)
104-
{
105-
Line = line;
106-
StdError = stdError;
107-
}
108-
}
109-
11099
bool ManifestIsUpToDate (string manifestFile)
111100
{
112101
return !String.IsNullOrEmpty (AndroidComponentResgenFlagFile) &&
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright (C) 2011 Xamarin, Inc. All rights reserved.
2+
3+
using System;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Xml;
9+
using System.Xml.Linq;
10+
using Microsoft.Build.Utilities;
11+
using Microsoft.Build.Framework;
12+
using System.Text.RegularExpressions;
13+
using System.Collections.Generic;
14+
using Xamarin.Android.Tools;
15+
using ThreadingTasks = System.Threading.Tasks;
16+
17+
namespace Xamarin.Android.Tasks {
18+
19+
public class Aapt2 : AsyncTask {
20+
21+
protected Dictionary<string, string> resource_name_case_map = new Dictionary<string, string> ();
22+
23+
public ITaskItem [] ResourceDirectories { get; set; }
24+
25+
public string ResourceNameCaseMap { get; set; }
26+
27+
public string ResourceSymbolsTextFile { get; set; }
28+
29+
protected string ToolName { get { return OS.IsWindows ? "aapt2.exe" : "aapt2"; } }
30+
31+
public string ToolPath { get; set; }
32+
33+
public string ToolExe { get; set; }
34+
35+
protected string ResourceDirectoryFullPath (string resourceDirectory)
36+
{
37+
return (Path.IsPathRooted (resourceDirectory) ? resourceDirectory : Path.Combine (WorkingDirectory, resourceDirectory)).TrimEnd ('\\');
38+
}
39+
40+
protected string GenerateFullPathToTool ()
41+
{
42+
return Path.Combine (ToolPath, string.IsNullOrEmpty (ToolExe) ? ToolName : ToolExe);
43+
}
44+
45+
protected bool RunAapt (string commandLine, IList<OutputLine> output)
46+
{
47+
var stdout_completed = new ManualResetEvent (false);
48+
var stderr_completed = new ManualResetEvent (false);
49+
var psi = new ProcessStartInfo () {
50+
FileName = GenerateFullPathToTool (),
51+
Arguments = commandLine,
52+
UseShellExecute = false,
53+
RedirectStandardOutput = true,
54+
RedirectStandardError = true,
55+
CreateNoWindow = true,
56+
WindowStyle = ProcessWindowStyle.Hidden,
57+
WorkingDirectory = WorkingDirectory,
58+
};
59+
object lockObject = new object ();
60+
using (var proc = new Process ()) {
61+
proc.OutputDataReceived += (sender, e) => {
62+
if (e.Data != null)
63+
lock (lockObject)
64+
output.Add (new OutputLine (e.Data, stdError: false));
65+
else
66+
stdout_completed.Set ();
67+
};
68+
proc.ErrorDataReceived += (sender, e) => {
69+
if (e.Data != null)
70+
lock (lockObject)
71+
output.Add (new OutputLine (e.Data, stdError: true));
72+
else
73+
stderr_completed.Set ();
74+
};
75+
LogDebugMessage ("Executing {0}", commandLine);
76+
proc.StartInfo = psi;
77+
proc.Start ();
78+
proc.BeginOutputReadLine ();
79+
proc.BeginErrorReadLine ();
80+
Token.Register (() => {
81+
try {
82+
proc.Kill ();
83+
} catch (Exception) {
84+
}
85+
});
86+
proc.WaitForExit ();
87+
if (psi.RedirectStandardError)
88+
stderr_completed.WaitOne (TimeSpan.FromSeconds (30));
89+
if (psi.RedirectStandardOutput)
90+
stdout_completed.WaitOne (TimeSpan.FromSeconds (30));
91+
return proc.ExitCode == 0 && !output.Any (x => x.StdError);
92+
}
93+
}
94+
95+
protected void LogEventsFromTextOutput (string singleLine, MessageImportance messageImportance, bool apptResult)
96+
{
97+
if (string.IsNullOrEmpty (singleLine))
98+
return;
99+
100+
var match = AndroidToolTask.AndroidErrorRegex.Match (singleLine.Trim ());
101+
102+
if (match.Success) {
103+
var file = match.Groups ["file"].Value;
104+
int line = 0;
105+
if (!string.IsNullOrEmpty (match.Groups ["line"]?.Value))
106+
line = int.Parse (match.Groups ["line"].Value) + 1;
107+
var level = match.Groups ["level"].Value.ToLowerInvariant ();
108+
var message = match.Groups ["message"].Value;
109+
if (message.Contains ("fakeLogOpen")) {
110+
LogMessage (singleLine, messageImportance);
111+
return;
112+
}
113+
if (message.Contains ("note:")) {
114+
LogMessage (singleLine, messageImportance);
115+
return;
116+
}
117+
if (message.Contains ("warn:")) {
118+
LogCodedWarning ("APT0000", singleLine);
119+
return;
120+
}
121+
if (level.Contains ("note")) {
122+
LogMessage (message, messageImportance);
123+
return;
124+
}
125+
if (level.Contains ("warning")) {
126+
LogCodedWarning ("APT0000", singleLine);
127+
return;
128+
}
129+
130+
// Try to map back to the original resource file, so when the user
131+
// double clicks the error, it won't take them to the obj/Debug copy
132+
if (ResourceDirectories != null) {
133+
foreach (var dir in ResourceDirectories) {
134+
var resourceDirectoryFullPath = ResourceDirectoryFullPath (dir.ItemSpec);
135+
if (file.StartsWith (resourceDirectoryFullPath, StringComparison.InvariantCultureIgnoreCase)) {
136+
var newfile = file.Substring (resourceDirectoryFullPath.Length).TrimStart (Path.DirectorySeparatorChar);
137+
newfile = resource_name_case_map.ContainsKey (newfile) ? resource_name_case_map [newfile] : newfile;
138+
newfile = Path.Combine ("Resources", newfile);
139+
file = newfile;
140+
break;
141+
}
142+
}
143+
}
144+
145+
// Strip any "Error:" text from aapt's output
146+
if (message.StartsWith ("error: ", StringComparison.InvariantCultureIgnoreCase))
147+
message = message.Substring ("error: ".Length);
148+
149+
if (level.Contains ("error") || (line != 0 && !string.IsNullOrEmpty (file))) {
150+
LogCodedError ("APT0000", message, file, line);
151+
return;
152+
}
153+
}
154+
155+
if (!apptResult) {
156+
LogCodedError ("APT0000", string.Format ("{0} \"{1}\".", singleLine.Trim (), singleLine.Substring (singleLine.LastIndexOfAny (new char [] { '\\', '/' }) + 1)), ToolName);
157+
} else {
158+
LogCodedWarning ("APT0000", singleLine);
159+
}
160+
}
161+
162+
protected void LoadResourceCaseMap ()
163+
{
164+
if (ResourceNameCaseMap != null)
165+
foreach (var arr in ResourceNameCaseMap.Split (';').Select (l => l.Split ('|')).Where (a => a.Length == 2))
166+
resource_name_case_map [arr [1]] = arr [0]; // lowercase -> original
167+
}
168+
}
169+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (C) 2011 Xamarin, Inc. All rights reserved.
2+
3+
using System;
4+
using System.Diagnostics;
5+
using System.IO;
6+
using System.Linq;
7+
using System.Threading;
8+
using System.Xml;
9+
using System.Xml.Linq;
10+
using Microsoft.Build.Utilities;
11+
using Microsoft.Build.Framework;
12+
using System.Text.RegularExpressions;
13+
using System.Collections.Generic;
14+
using Xamarin.Android.Tools;
15+
using ThreadingTasks = System.Threading.Tasks;
16+
17+
namespace Xamarin.Android.Tasks {
18+
19+
public class Aapt2Compile : Aapt2 {
20+
21+
List<ITaskItem> archives = new List<ITaskItem> ();
22+
23+
public bool ExplicitCrunch { get; set; }
24+
25+
[Output]
26+
public ITaskItem [] CompiledResourceFlatArchives => archives.ToArray ();
27+
28+
public override bool Execute ()
29+
{
30+
Log.LogDebugMessage ("Aapt2Compile Task");
31+
Log.LogDebugMessage (" ResourceNameCaseMap: {0}", ResourceNameCaseMap);
32+
Log.LogDebugMessage (" ResourceSymbolsTextFile: {0}", ResourceSymbolsTextFile);
33+
Log.LogDebugTaskItems (" ResourceDirectories: ", ResourceDirectories);
34+
35+
Yield ();
36+
try {
37+
var task = ThreadingTasks.Task.Run (() => {
38+
DoExecute ();
39+
}, Token);
40+
41+
task.ContinueWith (Complete);
42+
43+
base.Execute ();
44+
} finally {
45+
Reacquire ();
46+
}
47+
48+
return !Log.HasLoggedErrors;
49+
}
50+
51+
void DoExecute ()
52+
{
53+
LoadResourceCaseMap ();
54+
55+
ThreadingTasks.ParallelOptions options = new ThreadingTasks.ParallelOptions {
56+
CancellationToken = Token,
57+
TaskScheduler = ThreadingTasks.TaskScheduler.Default,
58+
};
59+
60+
ThreadingTasks.Parallel.ForEach (ResourceDirectories, options, ProcessDirectory);
61+
}
62+
63+
void ProcessDirectory (ITaskItem resourceDirectory)
64+
{
65+
if (!Directory.EnumerateDirectories (resourceDirectory.ItemSpec).Any ())
66+
return;
67+
68+
var output = new List<OutputLine> ();
69+
var outputArchive = Path.Combine (resourceDirectory.ItemSpec, "..", "compiled.flata");
70+
var success = RunAapt (GenerateCommandLineCommands (resourceDirectory, outputArchive), output);
71+
if (success && File.Exists (Path.Combine (WorkingDirectory, outputArchive))) {
72+
archives.Add (new TaskItem (outputArchive));
73+
}
74+
foreach (var line in output) {
75+
if (line.StdError) {
76+
LogEventsFromTextOutput (line.Line, MessageImportance.Normal, success);
77+
} else {
78+
LogMessage (line.Line, MessageImportance.Normal);
79+
}
80+
}
81+
}
82+
83+
protected string GenerateCommandLineCommands (ITaskItem dir, string outputArchive)
84+
{
85+
var cmd = new CommandLineBuilder ();
86+
cmd.AppendSwitch ("compile");
87+
cmd.AppendSwitchIfNotNull ("-o ", outputArchive);
88+
if (!string.IsNullOrEmpty (ResourceSymbolsTextFile))
89+
cmd.AppendSwitchIfNotNull ("--output-text-symbols ", ResourceSymbolsTextFile);
90+
cmd.AppendSwitchIfNotNull ("--dir ", ResourceDirectoryFullPath (dir.ItemSpec));
91+
if (ExplicitCrunch)
92+
cmd.AppendSwitch ("--no-crunch");
93+
if (MonoAndroidHelper.LogInternalExceptions)
94+
cmd.AppendSwitch ("-v");
95+
return cmd.ToString ();
96+
}
97+
98+
}
99+
}

0 commit comments

Comments
 (0)