Skip to content

Commit b2d9fdf

Browse files
authored
[NDK] Locate and select only compatible NDK versions (#103)
Context: actions/runner-images#2420 Context: dotnet/android#5499 Context: dotnet/android#5526 Context: android/ndk#1427 Context: https://developer.android.com/studio/command-line/variables#envar Xamarin.Android is not (yet) compatible with the recently released Android NDK r22 version. Azure build images have recently rolled out an update which includes NDK r22 and, thus, it breaks builds for customers using any form of Xamarin.Android AOT build. In an attempt to detect broken/incompatible NDK versions as well as to select the "best one", this commit adds code to scan the known NDK locations in search of the preferred version. Given an `AndroidSdkInfo` creation of: var info = new AndroidSdkInfo (logger:logger, androidSdkPath: sdkPath, androidNdkPath: ndkPath, javaSdkPath: javaPath); var usedNdkPath = info.AndroidNdkPath; If `ndkPath` is not `null` and otherwise valid, then `usedNdkPath` is `ndkPath`. If `ndkPath` is `null` or is otherwise invalid (missing `ndk-stack`, etc.), then we search for an `info.AndroidNdkPath` value as follows: 1. If `androidSdkPath` is not `null` and valid, then we check for Android SDK-relative NDK locations, in: * `{androidSdkPath}/ndk/*` * `{androidSdkPath}/ndk-bundle` For each found SDK-relative NDK directory, we filter out NDKs for which we cannot determine the package version, as well as those which are "too old" (< `MinimumCompatibleNDKMajorVersion`) or "too new" (> `MaximumCompatibleNDKMajorVersion`), currently r22. We prefer the NDK location with the highest version number. 2. If `androidSdkPath` is not `null` and valid and if there are no Android SDK-relative NDK locations, then we use the user-selected "preferred NDK location". See also `AndroidSdkInfo.SetPreferredAndroidNdkPath()`. 3. If `androidSdkPath` is not `null` and valid and if the preferred NDK location isn't set or is invalid, then we check directories specified in `$PATH`, and use the directory which contains `ndk-stack`. 4. If `androidSdkPath` is not `null` and valid and `$PATH` didn't contain `ndk-stack`, then we continue looking for NDK locations within the Android SDK locations specified by the `$ANDROID_HOME` and `$ANDROID_SDK_ROOT` environment variables. As with (1), these likewise look for e.g. `${ANDROID_HOME}/ndk/*` or `${ANDROID_SDK_ROOT}/ndk-bundle` directories and select the NDK with the highest supported version. 5. If `androidSdkPath` is `null`, then *first* we try to find a valid Android SDK directory, using on Unix: a. The preferred Android SDK directory; see also `AndroidSdkInfo.SetPreferredAndroidSdkPath(). b. The `$ANDROID_HOME` and `ANDROID_SDK_ROOT` environment variables. c. Directories within `$PATH` that contain `adb`. Once an Android SDK is found, steps (1)…(4) are performed. In (1) and (4), we now look for the Android SDK packages containing the NDK. There are two kinds of such packages: * `ndk-bundle` is the older package which allows for installation of only one NDK inside the SDK directory * `ndk/*` is a newer package which allows for installation of several NDK versions in parallel. Each subdirectory of `ndk` is an `X.Y.Z` version number of the NDK. In each of these directories we look for the `source.properties` file from which we then extract the NDK version and then we sort thus discovered NDK instances using their version as the key, in the descending order. The latest compatible (currently: less than 22 and more than 15) version is selected and its path returned to the caller.
1 parent 5ff1702 commit b2d9fdf

File tree

4 files changed

+175
-7
lines changed

4 files changed

+175
-7
lines changed

src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkBase.cs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ namespace Xamarin.Android.Tools
99
{
1010
abstract class AndroidSdkBase
1111
{
12+
// When this changes, update the test: Xamarin.Android.Tools.Tests.AndroidSdkInfoTests.Ndk_MultipleNdkVersionsInSdk
13+
const int MinimumCompatibleNDKMajorVersion = 16;
14+
const int MaximumCompatibleNDKMajorVersion = 21;
15+
16+
static readonly char[] SourcePropertiesKeyValueSplit = new char[] { '=' };
17+
18+
// Per https://developer.android.com/studio/command-line/variables#envar
19+
protected static readonly string[] AndroidSdkEnvVars = {"ANDROID_HOME", "ANDROID_SDK_ROOT"};
20+
1221
string[]? allAndroidSdks;
1322

1423
public string[] AllAndroidSdks {
@@ -97,8 +106,9 @@ public virtual void Initialize (string? androidSdkPath = null, string? androidNd
97106
if (pathValidator (ctorParam))
98107
return ctorParam;
99108
foreach (var path in getAllPaths ()) {
100-
if (pathValidator (path))
109+
if (pathValidator (path)) {
101110
return path;
111+
}
102112
}
103113
return null;
104114
}
@@ -108,7 +118,7 @@ public virtual void Initialize (string? androidSdkPath = null, string? androidNd
108118
if (ValidateAndroidNdkLocation (ctorParam))
109119
return ctorParam;
110120
if (AndroidSdkPath != null) {
111-
string bundle = Path.Combine (AndroidSdkPath, "ndk-bundle");
121+
string bundle = FindBestNDK (AndroidSdkPath);
112122
if (Directory.Exists (bundle) && ValidateAndroidNdkLocation (bundle))
113123
return bundle;
114124
}
@@ -125,6 +135,18 @@ public virtual void Initialize (string? androidSdkPath = null, string? androidNd
125135
protected abstract IEnumerable<string> GetAllAvailableAndroidSdks ();
126136
protected abstract string GetShortFormPath (string path);
127137

138+
protected IEnumerable<string> GetSdkFromEnvironmentVariables ()
139+
{
140+
foreach (string envVar in AndroidSdkEnvVars) {
141+
string ev = Environment.GetEnvironmentVariable (envVar);
142+
if (String.IsNullOrEmpty (ev)) {
143+
continue;
144+
}
145+
146+
yield return ev;
147+
}
148+
}
149+
128150
protected virtual IEnumerable<string> GetAllAvailableAndroidNdks ()
129151
{
130152
// Look in PATH
@@ -139,7 +161,67 @@ protected virtual IEnumerable<string> GetAllAvailableAndroidNdks ()
139161
foreach (var sdk in GetAllAvailableAndroidSdks ()) {
140162
if (sdk == AndroidSdkPath)
141163
continue;
142-
yield return Path.Combine (sdk, "ndk-bundle");
164+
yield return FindBestNDK (sdk);
165+
}
166+
}
167+
168+
string FindBestNDK (string androidSdkPath)
169+
{
170+
var ndkInstances = new SortedDictionary<Version, string> (Comparer<Version>.Create ((Version l, Version r) => r.CompareTo (l)));
171+
172+
foreach (string ndkPath in Directory.EnumerateDirectories (androidSdkPath, "ndk*", SearchOption.TopDirectoryOnly)) {
173+
if (String.Compare ("ndk-bundle", Path.GetFileName (ndkPath), StringComparison.OrdinalIgnoreCase) == 0) {
174+
LoadNDKVersion (ndkPath);
175+
continue;
176+
}
177+
178+
if (String.Compare ("ndk", Path.GetFileName (ndkPath), StringComparison.OrdinalIgnoreCase) != 0) {
179+
continue;
180+
}
181+
182+
foreach (string versionedNdkPath in Directory.EnumerateDirectories (ndkPath, "*", SearchOption.TopDirectoryOnly)) {
183+
LoadNDKVersion (versionedNdkPath);
184+
}
185+
}
186+
187+
if (ndkInstances.Count == 0) {
188+
return String.Empty;
189+
}
190+
191+
var kvp = ndkInstances.First ();
192+
Logger (TraceLevel.Verbose, $"Best NDK selected: v{kvp.Key} in {kvp.Value}");
193+
return kvp.Value;
194+
195+
void LoadNDKVersion (string path)
196+
{
197+
string propsFilePath = Path.Combine (path, "source.properties");
198+
if (!File.Exists (propsFilePath)) {
199+
Logger (TraceLevel.Verbose, $"Skipping NDK in '{path}': no source.properties, cannot determine version");
200+
return;
201+
}
202+
203+
foreach (string line in File.ReadLines (propsFilePath)) {
204+
string[] parts = line.Split (SourcePropertiesKeyValueSplit, 2, StringSplitOptions.RemoveEmptyEntries);
205+
if (parts.Length != 2) {
206+
continue;
207+
}
208+
209+
if (String.Compare ("Pkg.Revision", parts[0].Trim (), StringComparison.Ordinal) != 0) {
210+
continue;
211+
}
212+
213+
if (!Version.TryParse (parts[1].Trim (), out Version? ndkVer) || ndkVer == null || ndkInstances.ContainsKey (ndkVer)) {
214+
continue;
215+
}
216+
217+
if (ndkVer.Major < MinimumCompatibleNDKMajorVersion || ndkVer.Major > MaximumCompatibleNDKMajorVersion) {
218+
Logger (TraceLevel.Verbose, $"Skipping NDK in '{path}': version {ndkVer} is out of the accepted range (major version must be between {MinimumCompatibleNDKMajorVersion} and {MaximumCompatibleNDKMajorVersion}");
219+
continue;
220+
}
221+
222+
ndkInstances.Add (ndkVer, path);
223+
return;
224+
}
143225
}
144226
}
145227

src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkUnix.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ protected override IEnumerable<string> GetAllAvailableAndroidSdks ()
9494
if (!string.IsNullOrEmpty (preferedSdkPath))
9595
yield return preferedSdkPath!;
9696

97+
foreach (string dir in GetSdkFromEnvironmentVariables ()) {
98+
yield return dir;
99+
}
100+
97101
// Look in PATH
98102
foreach (var adb in ProcessUtils.FindExecutablesInPath (Adb)) {
99103
var path = Path.GetDirectoryName (adb);

src/Xamarin.Android.Tools.AndroidSdk/Sdks/AndroidSdkWindows.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,10 @@ protected override IEnumerable<string> GetAllAvailableAndroidNdks ()
230230
if (CheckRegistryKeyForExecutable (root, regKey, MDREG_ANDROID_NDK, wow, ".", NdkStack))
231231
yield return RegistryEx.GetValueString (root, regKey, MDREG_ANDROID_NDK, wow) ?? "";
232232

233+
foreach (string dir in GetSdkFromEnvironmentVariables ()) {
234+
yield return dir;
235+
}
236+
233237
/*
234238
// Check for the key written by the Xamarin installer
235239
if (CheckRegistryKeyForExecutable (RegistryEx.CurrentUser, XAMARIN_ANDROID_INSTALLER_PATH, XAMARIN_ANDROID_INSTALLER_KEY, wow, "platform-tools", Adb))

tests/Xamarin.Android.Tools.AndroidSdk-Tests/AndroidSdkInfoTests.cs

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ namespace Xamarin.Android.Tools.Tests
1313
[TestFixture]
1414
public class AndroidSdkInfoTests
1515
{
16+
const string NdkVersion = "21.0.6113669";
17+
1618
string UnixConfigDirOverridePath;
1719
string PreferredJdksOverridePath;
1820

@@ -64,21 +66,73 @@ public void Constructor_Paths ()
6466
}
6567
}
6668

69+
[Test]
70+
public void Ndk_MultipleNdkVersionsInSdk ()
71+
{
72+
// Must match like-named constants in AndroidSdkBase
73+
const int MinimumCompatibleNDKMajorVersion = 16;
74+
const int MaximumCompatibleNDKMajorVersion = 21;
75+
76+
CreateSdks(out string root, out string jdk, out string ndk, out string sdk);
77+
78+
Action<TraceLevel, string> logger = (level, message) => {
79+
Console.WriteLine($"[{level}] {message}");
80+
};
81+
82+
var ndkVersions = new List<string> {
83+
"16.1.4479499",
84+
"17.2.4988734",
85+
"18.1.5063045",
86+
"19.2.5345600",
87+
"20.0.5594570",
88+
"20.1.5948944",
89+
"21.0.6113669",
90+
"21.1.6352462",
91+
"21.2.6472646",
92+
"21.3.6528147",
93+
"22.0.7026061",
94+
};
95+
string expectedVersion = "21.3.6528147";
96+
string expectedNdkPath = Path.Combine (sdk, "ndk", expectedVersion);
97+
98+
try {
99+
MakeNdkDir (Path.Combine (sdk, "ndk-bundle"), NdkVersion);
100+
101+
foreach (string ndkVer in ndkVersions) {
102+
MakeNdkDir (Path.Combine (sdk, "ndk", ndkVer), ndkVer);
103+
}
104+
105+
var info = new AndroidSdkInfo (logger, androidSdkPath: sdk, androidNdkPath: null, javaSdkPath: jdk);
106+
107+
Assert.AreEqual (expectedNdkPath, info.AndroidNdkPath, "AndroidNdkPath not found inside sdk!");
108+
109+
string ndkVersion = Path.GetFileName (info.AndroidNdkPath);
110+
if (!Version.TryParse (ndkVersion, out Version ver)) {
111+
Assert.Fail ($"Unable to parse '{ndkVersion}' as a valid version.");
112+
}
113+
114+
Assert.True (ver.Major >= MinimumCompatibleNDKMajorVersion, $"NDK version must be at least {MinimumCompatibleNDKMajorVersion}");
115+
Assert.True (ver.Major <= MaximumCompatibleNDKMajorVersion, $"NDK version must be at most {MinimumCompatibleNDKMajorVersion}");
116+
} finally {
117+
Directory.Delete (root, recursive: true);
118+
}
119+
}
120+
67121
[Test]
68122
public void Ndk_PathInSdk()
69123
{
70124
CreateSdks(out string root, out string jdk, out string ndk, out string sdk);
71125

72-
var logs = new StringWriter();
73126
Action<TraceLevel, string> logger = (level, message) => {
74-
logs.WriteLine($"[{level}] {message}");
127+
Console.WriteLine($"[{level}] {message}");
75128
};
76129

77130
try
78131
{
79132
var extension = OS.IsWindows ? ".cmd" : "";
80133
var ndkPath = Path.Combine(sdk, "ndk-bundle");
81134
Directory.CreateDirectory(ndkPath);
135+
File.WriteAllText(Path.Combine (ndkPath, "source.properties"), $"Pkg.Revision = {NdkVersion}");
82136
Directory.CreateDirectory(Path.Combine(ndkPath, "toolchains"));
83137
File.WriteAllText(Path.Combine(ndkPath, $"ndk-stack{extension}"), "");
84138

@@ -106,6 +160,8 @@ public void Constructor_SetValuesFromPath ()
106160
};
107161
var oldPath = Environment.GetEnvironmentVariable ("PATH");
108162
var oldJavaHome = Environment.GetEnvironmentVariable ("JAVA_HOME");
163+
var oldAndroidHome = Environment.GetEnvironmentVariable ("ANDROID_HOME");
164+
var oldAndroidSdkRoot = Environment.GetEnvironmentVariable ("ANDROID_SDK_ROOT");
109165
try {
110166
var paths = new List<string> () {
111167
Path.Combine (jdk, "bin"),
@@ -117,6 +173,12 @@ public void Constructor_SetValuesFromPath ()
117173
if (!string.IsNullOrEmpty (oldJavaHome)) {
118174
Environment.SetEnvironmentVariable ("JAVA_HOME", string.Empty);
119175
}
176+
if (!string.IsNullOrEmpty (oldAndroidHome)) {
177+
Environment.SetEnvironmentVariable ("ANDROID_HOME", string.Empty);
178+
}
179+
if (!string.IsNullOrEmpty (oldAndroidSdkRoot)) {
180+
Environment.SetEnvironmentVariable ("ANDROID_SDK_ROOT", string.Empty);
181+
}
120182

121183
var info = new AndroidSdkInfo (logger);
122184

@@ -129,6 +191,12 @@ public void Constructor_SetValuesFromPath ()
129191
if (!string.IsNullOrEmpty (oldJavaHome)) {
130192
Environment.SetEnvironmentVariable ("JAVA_HOME", oldJavaHome);
131193
}
194+
if (!string.IsNullOrEmpty (oldAndroidHome)) {
195+
Environment.SetEnvironmentVariable ("ANDROID_HOME", oldAndroidHome);
196+
}
197+
if (!string.IsNullOrEmpty (oldAndroidSdkRoot)) {
198+
Environment.SetEnvironmentVariable ("ANDROID_SDK_ROOT", oldAndroidSdkRoot);
199+
}
132200
Directory.Delete (root, recursive: true);
133201
}
134202
}
@@ -243,7 +311,7 @@ static void CreateSdks (out string root, out string jdk, out string ndk, out str
243311
Directory.CreateDirectory (jdk);
244312

245313
CreateFauxAndroidSdkDirectory (sdk, "26.0.0");
246-
CreateFauxAndroidNdkDirectory (ndk);
314+
CreateFauxAndroidNdkDirectory (ndk, NdkVersion);
247315
CreateFauxJavaSdkDirectory (jdk, "1.8.0", out var _, out var _);
248316
}
249317

@@ -311,8 +379,9 @@ struct ApiInfo {
311379
public string Id;
312380
}
313381

314-
static void CreateFauxAndroidNdkDirectory (string androidNdkDirectory)
382+
static void CreateFauxAndroidNdkDirectory (string androidNdkDirectory, string ndkVersion)
315383
{
384+
File.WriteAllText (Path.Combine (androidNdkDirectory, "source.properties"), $"Pkg.Revision = {ndkVersion}");
316385
File.WriteAllText (Path.Combine (androidNdkDirectory, "ndk-stack"), "");
317386
File.WriteAllText (Path.Combine (androidNdkDirectory, "ndk-stack.cmd"), "");
318387

@@ -474,5 +543,14 @@ public void GetBuildToolsPaths_StableVersionsFirst ()
474543
Directory.Delete (root, recursive: true);
475544
}
476545
}
546+
547+
void MakeNdkDir (string rootPath, string version)
548+
{
549+
var extension = OS.IsWindows ? ".cmd" : String.Empty;
550+
Directory.CreateDirectory(rootPath);
551+
File.WriteAllText(Path.Combine (rootPath, "source.properties"), $"Pkg.Revision = {version}");
552+
Directory.CreateDirectory(Path.Combine(rootPath, "toolchains"));
553+
File.WriteAllText(Path.Combine(rootPath, $"ndk-stack{extension}"), String.Empty);
554+
}
477555
}
478556
}

0 commit comments

Comments
 (0)