Skip to content

Commit c7d05d1

Browse files
[Xamarin.Android.Build.Tasks] refactor/split up ResolveSdks
Context: http://work.devdiv.io/639234 The `ResolveSdks` MSBuild task was quite large, and had lots of inputs and outputs. Because of this, it is difficult to tell which parts of it are slow. As a first stab at refactoring, I split it into three tasks that are more focused in their concerns: - `ResolveSdks` - now focuses on resolving `AndroidSdkPath`, `AndroidNdkPath` and `JavaSdkPath`. It also sets up any static members of `MonoAndroidHelper`. - `ValidateJavaVersion` - shells out to `java` and `javac`, sets `MinimumRequiredJdkVersion` and `JdkVersion`. - `ResolveAndroidTooling` - does all the other work: calculates `TargetFrameworkVersion` and finds the paths of various Android command line tooling The goal here is to clean things up without really changing how anything works. And in `ResolveSdksTaskTests`, I kept the tests exactly the same but changed it to call all three tasks. After the refactoring, we have a much better picture of what we need to improve performance for. Here is the result of running the simplest test, `BuildBasicApplication`: ResolveSdks 21ms ValidateJavaVersion 298ms ResolveAndroidTooling 15ms So `ValidateJavaVersion` is where we should focus on performance improvements (caching). ~~ Improvements to logging ~~ In general we have been logging like this for every MSBuild task: public override bool Execute () { Log.LogDebugMessage ($"{nameof (MyMSBuildTask)}"); Log.LogDebugMessage ($" {nameof (InputValue1)}: {InputValue1}"); //Do stuff Log.LogDebugMessage ($"{nameof (MyMSBuildTask)} Outputs:"); Log.LogDebugMessage ($" {nameof (OutputValue1)}: {OutputValue1}"); return !Log.HasLoggedErrors; } The diagnostic log output would look something like: Task "MyMSBuildTask" (TaskId:8) Task Parameter:InputValue1=1234 (TaskId:8) MyMSBuildTask: (TaskId:8) InputValue1: 1234 (TaskId:8) MyMSBuildTask Outputs: (TaskId:8) OutputValue1: abcd (TaskId:8) Output Property: OutputValue1=abcd (TaskId:8) Done executing task "MyMSBuildTask". (TaskId:8) The log messages are mostly duplicate, and seem unneeded. However, the `Output Property` log only prints from MSBuild if this is specified in the target: <Output TaskParameter="OutputValue1" PropertyName="OutputValue1" /> Since you *could* omit this, or rename the property, we should continue logging all `[Output]` properties. However, I have removed input ones in the changed tasks since they seem completely duplicate. Other changes: - Removed `$(_TargetFrameworkVersion)` property from `Xamarin.Android.Common.targets` in favor of just using `$(TargetFrameworkVersion)`. This seemed to be leftover from the past and not used. - General refactoring/ordering of properties on MSBuild tasks - General refactoring/ordering of properties in `Xamarin.Android.Common.targets` / `Xamarin.Android.Bindings.targets` - Use `nameof` where appropriate in log messages - `ResolveAndroidTooling` should use `Path.PathSeparator` - Updated `ValidateUseLatestAndroid` test
1 parent 3558fd2 commit c7d05d1

File tree

8 files changed

+715
-666
lines changed

8 files changed

+715
-666
lines changed
Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
using Microsoft.Build.Framework;
2+
using Microsoft.Build.Utilities;
3+
using System;
4+
using System.Linq;
5+
using System.IO;
6+
using System.Text;
7+
using System.Text.RegularExpressions;
8+
using System.Globalization;
9+
10+
namespace Xamarin.Android.Tasks
11+
{
12+
/// <summary>
13+
/// ResolveAndroidTooling does lot of the grunt work ResolveSdks used to do:
14+
/// - Modify TargetFrameworkVersion
15+
/// - Calculate ApiLevel and ApiLevelName
16+
/// - Find the paths of various Android tooling that other tasks need to call
17+
/// </summary>
18+
public class ResolveAndroidTooling : Task
19+
{
20+
public string AndroidNdkPath { get; set; }
21+
22+
public string AndroidSdkPath { get; set; }
23+
24+
public string AndroidSdkBuildToolsVersion { get; set; }
25+
26+
public string ProjectFilePath { get; set; }
27+
28+
public string SequencePointsMode { get; set; }
29+
30+
public bool UseLatestAndroidPlatformSdk { get; set; }
31+
32+
public bool AotAssemblies { get; set; }
33+
34+
[Output]
35+
public string TargetFrameworkVersion { get; set; }
36+
37+
[Output]
38+
public string AndroidApiLevel { get; set; }
39+
40+
[Output]
41+
public string AndroidApiLevelName { get; set; }
42+
43+
[Output]
44+
public string SupportedApiLevel { get; set; }
45+
46+
[Output]
47+
public string AndroidSdkBuildToolsPath { get; set; }
48+
49+
[Output]
50+
public string AndroidSdkBuildToolsBinPath { get; set; }
51+
52+
[Output]
53+
public string ZipAlignPath { get; set; }
54+
55+
[Output]
56+
public string AndroidSequencePointsMode { get; set; }
57+
58+
[Output]
59+
public string LintToolPath { get; set; }
60+
61+
[Output]
62+
public string ApkSignerJar { get; set; }
63+
64+
[Output]
65+
public bool AndroidUseApkSigner { get; set; }
66+
67+
[Output]
68+
public bool AndroidUseAapt2 { get; set; }
69+
70+
[Output]
71+
public string Aapt2Version { get; set; }
72+
73+
static readonly bool IsWindows = Path.DirectorySeparatorChar == '\\';
74+
static readonly string ZipAlign = IsWindows ? "zipalign.exe" : "zipalign";
75+
static readonly string Aapt = IsWindows ? "aapt.exe" : "aapt";
76+
static readonly string Aapt2 = IsWindows ? "aapt2.exe" : "aapt2";
77+
static readonly string Android = IsWindows ? "android.bat" : "android";
78+
static readonly string Lint = IsWindows ? "lint.bat" : "lint";
79+
static readonly string ApkSigner = "apksigner.jar";
80+
81+
public override bool Execute ()
82+
{
83+
string toolsZipAlignPath = Path.Combine (AndroidSdkPath, "tools", ZipAlign);
84+
bool findZipAlign = (string.IsNullOrEmpty (ZipAlignPath) || !Directory.Exists (ZipAlignPath)) && !File.Exists (toolsZipAlignPath);
85+
86+
var lintPaths = new string [] {
87+
LintToolPath ?? string.Empty,
88+
Path.Combine (AndroidSdkPath, "tools"),
89+
Path.Combine (AndroidSdkPath, "tools", "bin"),
90+
};
91+
92+
LintToolPath = null;
93+
foreach (var path in lintPaths) {
94+
if (File.Exists (Path.Combine (path, Lint))) {
95+
LintToolPath = path;
96+
break;
97+
}
98+
}
99+
100+
foreach (var dir in MonoAndroidHelper.AndroidSdk.GetBuildToolsPaths (AndroidSdkBuildToolsVersion)) {
101+
Log.LogDebugMessage ("Trying build-tools path: {0}", dir);
102+
if (dir == null || !Directory.Exists (dir))
103+
continue;
104+
105+
var toolsPaths = new string [] {
106+
Path.Combine (dir),
107+
Path.Combine (dir, "bin"),
108+
};
109+
110+
string aapt = toolsPaths.FirstOrDefault (x => File.Exists (Path.Combine (x, Aapt)));
111+
if (string.IsNullOrEmpty (aapt)) {
112+
Log.LogDebugMessage ("Could not find `{0}`; tried: {1}", Aapt,
113+
string.Join (Path.PathSeparator.ToString (), toolsPaths.Select (x => Path.Combine (x, Aapt))));
114+
continue;
115+
}
116+
AndroidSdkBuildToolsPath = Path.GetFullPath (dir);
117+
AndroidSdkBuildToolsBinPath = Path.GetFullPath (aapt);
118+
119+
string zipalign = toolsPaths.FirstOrDefault (x => File.Exists (Path.Combine (x, ZipAlign)));
120+
if (findZipAlign && string.IsNullOrEmpty (zipalign)) {
121+
Log.LogDebugMessage ("Could not find `{0}`; tried: {1}", ZipAlign,
122+
string.Join (Path.PathSeparator.ToString (), toolsPaths.Select (x => Path.Combine (x, ZipAlign))));
123+
continue;
124+
} else
125+
break;
126+
}
127+
128+
if (string.IsNullOrEmpty (AndroidSdkBuildToolsPath)) {
129+
Log.LogCodedError ("XA5205",
130+
string.Format (
131+
"Cannot find `{0}`. Please install the Android SDK Build-tools package with the `{1}{2}tools{2}{3}` program.",
132+
Aapt, AndroidSdkPath, Path.DirectorySeparatorChar, Android));
133+
return false;
134+
}
135+
136+
ApkSignerJar = Path.Combine (AndroidSdkBuildToolsBinPath, "lib", ApkSigner);
137+
AndroidUseApkSigner = File.Exists (ApkSignerJar);
138+
139+
bool aapt2Installed = File.Exists (Path.Combine (AndroidSdkBuildToolsBinPath, Aapt2));
140+
if (aapt2Installed && AndroidUseAapt2) {
141+
if (!GetAapt2Version ()) {
142+
AndroidUseAapt2 = false;
143+
aapt2Installed = false;
144+
Log.LogCodedWarning ("XA0111", "Could not get the `aapt2` version. Disabling `aapt2` support. Please check it is installed correctly.");
145+
}
146+
}
147+
if (AndroidUseAapt2) {
148+
if (!aapt2Installed) {
149+
AndroidUseAapt2 = false;
150+
Log.LogCodedWarning ("XA0112", "`aapt2` is not installed. Disabling `aapt2` support. Please check it is installed correctly.");
151+
}
152+
}
153+
154+
if (string.IsNullOrEmpty (ZipAlignPath) || !Directory.Exists (ZipAlignPath)) {
155+
ZipAlignPath = new [] {
156+
Path.Combine (AndroidSdkBuildToolsPath),
157+
Path.Combine (AndroidSdkBuildToolsBinPath),
158+
Path.Combine (AndroidSdkPath, "tools"),
159+
}
160+
.Where (p => File.Exists (Path.Combine (p, ZipAlign)))
161+
.FirstOrDefault ();
162+
}
163+
if (string.IsNullOrEmpty (ZipAlignPath)) {
164+
Log.LogCodedError ("XA5205",
165+
string.Format (
166+
"Cannot find `{0}`. Please install the Android SDK Build-tools package with the `{1}{2}tools{2}{3}` program.",
167+
ZipAlign, AndroidSdkPath, Path.DirectorySeparatorChar, Android));
168+
return false;
169+
}
170+
171+
if (!ValidateApiLevels ())
172+
return false;
173+
174+
if (!MonoAndroidHelper.SupportedVersions.FrameworkDirectories.Any (p => Directory.Exists (Path.Combine (p, TargetFrameworkVersion)))) {
175+
Log.LogError (
176+
subcategory: string.Empty,
177+
errorCode: "XA0001",
178+
helpKeyword: string.Empty,
179+
file: ProjectFilePath,
180+
lineNumber: 0,
181+
columnNumber: 0,
182+
endLineNumber: 0,
183+
endColumnNumber: 0,
184+
message: "Unsupported or invalid $(TargetFrameworkVersion) value of '{0}'. Please update your Project Options.",
185+
messageArgs: new []{
186+
TargetFrameworkVersion,
187+
}
188+
);
189+
return false;
190+
}
191+
192+
int apiLevel;
193+
if (int.TryParse (AndroidApiLevel, out apiLevel)) {
194+
if (apiLevel < 26)
195+
Log.LogCodedWarning ("XA0113", $"Google Play requires that new applications must use a TargetFrameworkVersion of v8.0 (API level 26) or above. You are currently targeting {TargetFrameworkVersion} (API level {AndroidApiLevel}).");
196+
if (apiLevel < 26)
197+
Log.LogCodedWarning ("XA0114", $"Google Play requires that application updates must use a TargetFrameworkVersion of v8.0 (API level 26) or above. You are currently targeting {TargetFrameworkVersion} (API level {AndroidApiLevel}).");
198+
}
199+
200+
SequencePointsMode mode;
201+
if (!Aot.TryGetSequencePointsMode (SequencePointsMode ?? "None", out mode))
202+
Log.LogCodedError ("XA0104", "Invalid Sequence Point mode: {0}", SequencePointsMode);
203+
AndroidSequencePointsMode = mode.ToString ();
204+
205+
AndroidApiLevelName = MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (AndroidApiLevel);
206+
207+
Log.LogDebugMessage ($"{nameof (ResolveAndroidTooling)} Outputs:");
208+
Log.LogDebugMessage ($" {nameof (TargetFrameworkVersion)}: {TargetFrameworkVersion}");
209+
Log.LogDebugMessage ($" {nameof (AndroidApiLevel)}: {AndroidApiLevel}");
210+
Log.LogDebugMessage ($" {nameof (AndroidApiLevelName)}: {AndroidApiLevelName}");
211+
Log.LogDebugMessage ($" {nameof (SupportedApiLevel)}: {SupportedApiLevel}");
212+
Log.LogDebugMessage ($" {nameof (AndroidSdkBuildToolsPath)}: {AndroidSdkBuildToolsPath}");
213+
Log.LogDebugMessage ($" {nameof (AndroidSdkBuildToolsBinPath)}: {AndroidSdkBuildToolsBinPath}");
214+
Log.LogDebugMessage ($" {nameof (ZipAlignPath)}: {ZipAlignPath}");
215+
Log.LogDebugMessage ($" {nameof (AndroidSequencePointsMode)}: {AndroidSequencePointsMode}");
216+
Log.LogDebugMessage ($" {nameof (LintToolPath)}: {LintToolPath}");
217+
Log.LogDebugMessage ($" {nameof (ApkSignerJar)}: {ApkSignerJar}");
218+
Log.LogDebugMessage ($" {nameof (AndroidUseApkSigner)}: {AndroidUseApkSigner}");
219+
Log.LogDebugMessage ($" {nameof (AndroidUseAapt2)}: {AndroidUseAapt2}");
220+
Log.LogDebugMessage ($" {nameof (Aapt2Version)}: {Aapt2Version}");
221+
222+
return !Log.HasLoggedErrors;
223+
}
224+
225+
// Android Asset Packaging Tool (aapt) 2:19
226+
static readonly Regex Aapt2VersionRegex = new Regex (@"(?<version>[\d\:]+)(\d+)?");
227+
228+
bool GetAapt2Version ()
229+
{
230+
var sb = new StringBuilder ();
231+
var aapt2Tool = Path.Combine (AndroidSdkBuildToolsBinPath, Aapt2);
232+
try {
233+
MonoAndroidHelper.RunProcess (aapt2Tool, "version", (s, e) => {
234+
if (!string.IsNullOrEmpty (e.Data))
235+
sb.AppendLine (e.Data);
236+
}, (s, e) => {
237+
if (!string.IsNullOrEmpty (e.Data))
238+
sb.AppendLine (e.Data);
239+
}
240+
);
241+
} catch (Exception ex) {
242+
Log.LogWarningFromException (ex);
243+
return false;
244+
}
245+
var versionInfo = sb.ToString ();
246+
var versionNumberMatch = Aapt2VersionRegex.Match (versionInfo);
247+
Log.LogDebugMessage ($"`{aapt2Tool} version` returned: ```{versionInfo}```");
248+
if (versionNumberMatch.Success && Version.TryParse (versionNumberMatch.Groups ["version"]?.Value.Replace (":", "."), out Version versionNumber)) {
249+
Aapt2Version = versionNumber.ToString ();
250+
return true;
251+
}
252+
return false;
253+
}
254+
255+
bool ValidateApiLevels ()
256+
{
257+
// Priority:
258+
// $(UseLatestAndroidPlatformSdk) > $(AndroidApiLevel) > $(TargetFrameworkVersion)
259+
//
260+
// If $(TargetFrameworkVersion) isn't set, and $(AndroidApiLevel) isn't
261+
// set, act as if $(UseLatestAndroidPlatformSdk) is True
262+
//
263+
// If $(UseLatestAndroidPlatformSdk) is true, we do as it says: use the
264+
// latest installed version.
265+
//
266+
// Otherwise, if $(AndroidApiLevel) is set, use it and set $(TargetFrameworkVersion).
267+
// Rationale: monodroid/samples/xbuild.make uses $(AndroidApiLevel)
268+
// to build for a specific API level.
269+
// Otherwise, if $(TargetFrameworkVersion) is set, use it and set $(AndroidApiLevel).
270+
271+
UseLatestAndroidPlatformSdk = UseLatestAndroidPlatformSdk ||
272+
(string.IsNullOrWhiteSpace (AndroidApiLevel) && string.IsNullOrWhiteSpace (TargetFrameworkVersion));
273+
274+
if (UseLatestAndroidPlatformSdk) {
275+
AndroidApiLevel = GetMaxInstalledApiLevel ().ToString ();
276+
SupportedApiLevel = GetMaxStableApiLevel ().ToString ();
277+
int maxInstalled, maxSupported = 0;
278+
if (int.TryParse (AndroidApiLevel, out maxInstalled) && int.TryParse (SupportedApiLevel, out maxSupported) && maxInstalled > maxSupported) {
279+
Log.LogDebugMessage ($"API Level {AndroidApiLevel} is greater than the maximum supported API level of {SupportedApiLevel}. " +
280+
"Support for this API will be added in a future release.");
281+
AndroidApiLevel = SupportedApiLevel;
282+
}
283+
if (!string.IsNullOrWhiteSpace (TargetFrameworkVersion)) {
284+
var userSelected = MonoAndroidHelper.SupportedVersions.GetApiLevelFromFrameworkVersion (TargetFrameworkVersion);
285+
// overwrite using user version only if it is
286+
// above the maxStableApi and a valid apiLevel.
287+
if (userSelected != null && userSelected > maxSupported && userSelected <= maxInstalled) {
288+
AndroidApiLevel = userSelected.ToString ();
289+
SupportedApiLevel = userSelected.ToString ();
290+
}
291+
}
292+
TargetFrameworkVersion = GetTargetFrameworkVersionFromApiLevel ();
293+
return TargetFrameworkVersion != null;
294+
}
295+
296+
if (!string.IsNullOrWhiteSpace (TargetFrameworkVersion)) {
297+
TargetFrameworkVersion = TargetFrameworkVersion.Trim ();
298+
string id = MonoAndroidHelper.SupportedVersions.GetIdFromFrameworkVersion (TargetFrameworkVersion);
299+
if (id == null) {
300+
Log.LogCodedError ("XA0000",
301+
"Could not determine API level for $(TargetFrameworkVersion) of '{0}'.",
302+
TargetFrameworkVersion);
303+
return false;
304+
}
305+
AndroidApiLevel = MonoAndroidHelper.SupportedVersions.GetApiLevelFromId (id).ToString ();
306+
SupportedApiLevel = AndroidApiLevel;
307+
return true;
308+
}
309+
310+
if (!string.IsNullOrWhiteSpace (AndroidApiLevel)) {
311+
AndroidApiLevel = AndroidApiLevel.Trim ();
312+
SupportedApiLevel = GetMaxSupportedApiLevel (AndroidApiLevel);
313+
TargetFrameworkVersion = GetTargetFrameworkVersionFromApiLevel ();
314+
return TargetFrameworkVersion != null;
315+
}
316+
317+
Log.LogCodedError ("XA0000", "Could not determine $(AndroidApiLevel) or $(TargetFrameworkVersion); SHOULD NOT BE REACHED.");
318+
return false;
319+
}
320+
321+
int GetMaxInstalledApiLevel ()
322+
{
323+
string platformsDir = Path.Combine (AndroidSdkPath, "platforms");
324+
var apiIds = Directory.EnumerateDirectories (platformsDir)
325+
.Select (platformDir => Path.GetFileName (platformDir))
326+
.Where (dir => dir.StartsWith ("android-", StringComparison.OrdinalIgnoreCase))
327+
.Select (dir => dir.Substring ("android-".Length))
328+
.Select (apiName => MonoAndroidHelper.SupportedVersions.GetIdFromApiLevel (apiName));
329+
int maxApiLevel = int.MinValue;
330+
foreach (var id in apiIds) {
331+
int? v = MonoAndroidHelper.SupportedVersions.GetApiLevelFromId (id);
332+
if (!v.HasValue)
333+
continue;
334+
maxApiLevel = Math.Max (maxApiLevel, v.Value);
335+
}
336+
if (maxApiLevel < 0)
337+
Log.LogCodedError ("XA5300",
338+
"No Android platforms installed at '{0}'. Please install an SDK Platform with the `{1}{2}tools{2}{3}` program.",
339+
platformsDir, Path.DirectorySeparatorChar, Android);
340+
return maxApiLevel;
341+
}
342+
343+
int GetMaxStableApiLevel ()
344+
{
345+
return MonoAndroidHelper.SupportedVersions.MaxStableVersion.ApiLevel;
346+
}
347+
348+
string GetMaxSupportedApiLevel (string apiLevel)
349+
{
350+
int level = 0;
351+
if (!int.TryParse (apiLevel, NumberStyles.Integer, CultureInfo.InvariantCulture, out level))
352+
return apiLevel;
353+
var referenceAssemblyPaths = MonoAndroidHelper.TargetFrameworkDirectories;
354+
if (referenceAssemblyPaths == null)
355+
return apiLevel;
356+
foreach (string versionedDir in referenceAssemblyPaths) {
357+
string parent = Path.GetDirectoryName (versionedDir.TrimEnd (Path.DirectorySeparatorChar));
358+
for (int l = level; l > 0; l--) {
359+
string tfv = MonoAndroidHelper.SupportedVersions.GetFrameworkVersionFromApiLevel (l);
360+
if (tfv == null)
361+
continue;
362+
string dir = Path.Combine (parent, tfv);
363+
if (Directory.Exists (dir))
364+
return l.ToString ();
365+
}
366+
}
367+
return apiLevel;
368+
}
369+
370+
string GetTargetFrameworkVersionFromApiLevel ()
371+
{
372+
string targetFramework = MonoAndroidHelper.SupportedVersions.GetFrameworkVersionFromId (SupportedApiLevel) ??
373+
MonoAndroidHelper.SupportedVersions.GetFrameworkVersionFromId (AndroidApiLevel);
374+
if (targetFramework != null)
375+
return targetFramework;
376+
Log.LogCodedError ("XA0000",
377+
"Could not determine $(TargetFrameworkVersion) for API level '{0}.'",
378+
AndroidApiLevel);
379+
return null;
380+
}
381+
}
382+
}

0 commit comments

Comments
 (0)