Skip to content

Commit 5f3c4e7

Browse files
Add AndroidToolsInfo
1 parent 0d996a9 commit 5f3c4e7

File tree

5 files changed

+347
-6
lines changed

5 files changed

+347
-6
lines changed

lib/android-tools-info.ts

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import * as path from "path";
2+
import * as semver from "semver";
3+
import { EOL } from "os";
4+
import { ChildProcess } from "./wrappers/child-process";
5+
import { FileSystem } from "./wrappers/file-system";
6+
import { HostInfo } from "./host-info";
7+
8+
export class AndroidToolsInfo implements NativeScriptDoctor.IAndroidToolsInfo {
9+
private static ANDROID_TARGET_PREFIX = "android";
10+
private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23", "android-24", "android-25"];
11+
private static MIN_REQUIRED_COMPILE_TARGET = 22;
12+
private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=23";
13+
private static VERSION_REGEX = /((\d+\.){2}\d+)/;
14+
private static MIN_JAVA_VERSION = "1.8.0";
15+
16+
private toolsInfo: NativeScriptDoctor.IAndroidToolsInfoData;
17+
private androidHome = process.env["ANDROID_HOME"];
18+
19+
constructor(private childProcess: ChildProcess,
20+
private fs: FileSystem,
21+
private hostInfo: HostInfo) { }
22+
23+
public getToolsInfo(): NativeScriptDoctor.IAndroidToolsInfoData {
24+
if (!this.toolsInfo) {
25+
const infoData: NativeScriptDoctor.IAndroidToolsInfoData = Object.create(null);
26+
infoData.androidHomeEnvVar = this.androidHome;
27+
infoData.compileSdkVersion = this.getCompileSdk();
28+
infoData.buildToolsVersion = this.getBuildToolsVersion();
29+
infoData.supportRepositoryVersion = this.getAndroidSupportRepositoryVersion();
30+
31+
this.toolsInfo = infoData;
32+
}
33+
34+
return this.toolsInfo;
35+
}
36+
37+
public validateInfo(): string[] {
38+
const errors: string[] = [];
39+
const toolsInfoData = this.getToolsInfo();
40+
const isAndroidHomeValid = this.validateAndroidHomeEnvVariable();
41+
if (!toolsInfoData.compileSdkVersion) {
42+
errors.push(`Cannot find a compatible Android SDK for compilation. To be able to build for Android, install Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later.`,
43+
`Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage your Android SDK versions.`);
44+
}
45+
46+
if (!toolsInfoData.buildToolsVersion) {
47+
const buildToolsRange = this.getBuildToolsRange();
48+
const versionRangeMatches = buildToolsRange.match(/^.*?([\d\.]+)\s+.*?([\d\.]+)$/);
49+
let message = `You can install any version in the following range: '${buildToolsRange}'.`;
50+
51+
// Improve message in case buildToolsRange is something like: ">=22.0.0 <=22.0.0" - same numbers on both sides
52+
if (versionRangeMatches && versionRangeMatches[1] && versionRangeMatches[2] && versionRangeMatches[1] === versionRangeMatches[2]) {
53+
message = `You have to install version ${versionRangeMatches[1]}.`;
54+
}
55+
56+
let invalidBuildToolsAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` from your command-line to install required \`Android Build Tools\`.`;
57+
if (!isAndroidHomeValid) {
58+
invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.';
59+
}
60+
61+
errors.push("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
62+
}
63+
64+
if (!toolsInfoData.supportRepositoryVersion) {
65+
let invalidSupportLibAdditionalMsg = `Run \`\$ ${this.getPathToSdkManagementTool()}\` to manage the Android Support Repository.`;
66+
if (!isAndroidHomeValid) {
67+
invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.';
68+
}
69+
errors.push(`You need to have Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later and the latest Android Support Repository installed on your system.`, invalidSupportLibAdditionalMsg);
70+
}
71+
72+
return errors;
73+
}
74+
75+
public validateJavacVersion(installedJavaVersion: string): string[] {
76+
const errors: string[] = [];
77+
78+
let additionalMessage = "You will not be able to build your projects for Android." + EOL
79+
+ "To be able to build for Android, verify that you have installed The Java Development Kit (JDK) and configured it according to system requirements as" + EOL +
80+
" described in " + this.getSystemRequirementsLink();
81+
let matchingVersion = (installedJavaVersion || "").match(AndroidToolsInfo.VERSION_REGEX);
82+
if (matchingVersion && matchingVersion[1]) {
83+
if (semver.lt(matchingVersion[1], AndroidToolsInfo.MIN_JAVA_VERSION)) {
84+
errors.push(`Javac version ${installedJavaVersion} is not supported. You have to install at least ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage);
85+
}
86+
} else {
87+
errors.push("Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", additionalMessage);
88+
}
89+
90+
return errors;
91+
}
92+
93+
public async getPathToAdbFromAndroidHome(): Promise<string> {
94+
if (this.androidHome) {
95+
let pathToAdb = path.join(this.androidHome, "platform-tools", "adb");
96+
try {
97+
await this.childProcess.execFile(pathToAdb, ["help"]);
98+
return pathToAdb;
99+
} catch (err) {
100+
return null;
101+
}
102+
}
103+
104+
return null;
105+
}
106+
107+
public validateAndroidHomeEnvVariable(): string[] {
108+
const errors: string[] = [];
109+
const expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
110+
111+
if (!this.androidHome || !this.fs.exists(this.androidHome)) {
112+
errors.push("The ANDROID_HOME environment variable is not set or it points to a non-existent directory. You will not be able to perform any build-related operations for Android.",
113+
"To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory.");
114+
} else if (expectedDirectoriesInAndroidHome.map(dir => this.fs.exists(path.join(this.androidHome, dir))).length === 0) {
115+
errors.push("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
116+
"To be able to perform Android build-related operations, set the `ANDROID_HOME` variable to point to the root of your Android SDK installation directory, " +
117+
"where you will find `tools` and `platform-tools` directories.");
118+
}
119+
120+
return errors;
121+
}
122+
123+
private getPathToSdkManagementTool(): string {
124+
const sdkmanagerName = "sdkmanager";
125+
let sdkManagementToolPath = sdkmanagerName;
126+
127+
const isAndroidHomeValid = this.validateAndroidHomeEnvVariable();
128+
129+
if (isAndroidHomeValid) {
130+
// In case ANDROID_HOME is correct, check if sdkmanager exists and if not it means the SDK has not been updated.
131+
// In this case user shoud use `android` from the command-line instead of sdkmanager.
132+
const pathToSdkmanager = path.join(this.androidHome, "tools", "bin", sdkmanagerName);
133+
const pathToAndroidExecutable = path.join(this.androidHome, "tools", "android");
134+
const pathToExecutable = this.fs.exists(pathToSdkmanager) ? pathToSdkmanager : pathToAndroidExecutable;
135+
136+
sdkManagementToolPath = pathToExecutable.replace(this.androidHome, this.hostInfo.isWindows ? "%ANDROID_HOME%" : "$ANDROID_HOME");
137+
}
138+
139+
return sdkManagementToolPath;
140+
}
141+
142+
private getCompileSdk(): number {
143+
let latestValidAndroidTarget = this.getLatestValidAndroidTarget();
144+
if (latestValidAndroidTarget) {
145+
let integerVersion = this.parseAndroidSdkString(latestValidAndroidTarget);
146+
147+
if (integerVersion && integerVersion >= AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET) {
148+
return integerVersion;
149+
}
150+
}
151+
}
152+
153+
private getMatchingDir(pathToDir: string, versionRange: string): string {
154+
let selectedVersion: string;
155+
if (this.fs.exists(pathToDir)) {
156+
let subDirs = this.fs.readDirectory(pathToDir);
157+
158+
let subDirsVersions = subDirs
159+
.map(dirName => {
160+
let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX);
161+
if (dirNameGroups) {
162+
return dirNameGroups[1];
163+
}
164+
165+
return null;
166+
})
167+
.filter(dirName => !!dirName);
168+
169+
let version = semver.maxSatisfying(subDirsVersions, versionRange);
170+
if (version) {
171+
selectedVersion = subDirs.find(dir => dir.indexOf(version) !== -1);
172+
}
173+
}
174+
175+
return selectedVersion;
176+
}
177+
178+
private getBuildToolsRange(): string {
179+
return `${AndroidToolsInfo.REQUIRED_BUILD_TOOLS_RANGE_PREFIX} <=${this.getMaxSupportedVersion()}`;
180+
}
181+
182+
private getBuildToolsVersion(): string {
183+
let buildToolsVersion: string;
184+
if (this.androidHome) {
185+
let pathToBuildTools = path.join(this.androidHome, "build-tools");
186+
let buildToolsRange = this.getBuildToolsRange();
187+
buildToolsVersion = this.getMatchingDir(pathToBuildTools, buildToolsRange);
188+
}
189+
190+
return buildToolsVersion;
191+
}
192+
193+
private getAppCompatRange(): string {
194+
let compileSdkVersion = this.getCompileSdk();
195+
let requiredAppCompatRange: string;
196+
if (compileSdkVersion) {
197+
requiredAppCompatRange = `>=${compileSdkVersion} <${compileSdkVersion + 1}`;
198+
}
199+
200+
return requiredAppCompatRange;
201+
}
202+
203+
private getAndroidSupportRepositoryVersion(): string {
204+
let selectedAppCompatVersion: string;
205+
const requiredAppCompatRange = this.getAppCompatRange();
206+
if (this.androidHome && requiredAppCompatRange) {
207+
const pathToAppCompat = path.join(this.androidHome, "extras", "android", "m2repository", "com", "android", "support", "appcompat-v7");
208+
selectedAppCompatVersion = this.getMatchingDir(pathToAppCompat, requiredAppCompatRange);
209+
}
210+
211+
return selectedAppCompatVersion;
212+
}
213+
214+
private getLatestValidAndroidTarget(): string {
215+
const installedTargets = this.getInstalledTargets();
216+
let latestValidAndroidTarget: string;
217+
const sortedAndroidToolsInfo = AndroidToolsInfo.SUPPORTED_TARGETS.sort();
218+
219+
sortedAndroidToolsInfo.forEach(s => {
220+
if (installedTargets.includes(s)) {
221+
latestValidAndroidTarget = s;
222+
}
223+
});
224+
225+
return latestValidAndroidTarget;
226+
}
227+
228+
private parseAndroidSdkString(androidSdkString: string): number {
229+
return parseInt(androidSdkString.replace(`${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-`, ""));
230+
}
231+
232+
private getInstalledTargets(): string[] {
233+
try {
234+
const pathToInstalledTargets = path.join(this.androidHome, "platforms");
235+
if (!this.fs.exists(pathToInstalledTargets)) {
236+
throw new Error("No Android Targets installed.");
237+
}
238+
239+
return this.fs.readDirectory(pathToInstalledTargets);
240+
} catch (err) {
241+
return [];
242+
}
243+
}
244+
245+
private getMaxSupportedVersion(): number {
246+
return this.parseAndroidSdkString(AndroidToolsInfo.SUPPORTED_TARGETS.sort()[AndroidToolsInfo.SUPPORTED_TARGETS.length - 1]);
247+
}
248+
249+
private getSystemRequirementsLink(): string {
250+
let linkToSystemRequirements: string;
251+
switch (process.platform) {
252+
case "linux":
253+
linkToSystemRequirements = "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-linux.html#system-requirements";
254+
break;
255+
case "win32":
256+
linkToSystemRequirements = "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-win.html#system-requirements";
257+
break;
258+
case "darwin":
259+
linkToSystemRequirements = "http://docs.nativescript.org/setup/ns-cli-setup/ns-setup-os-x.html#system-requirements";
260+
break;
261+
default:
262+
linkToSystemRequirements = "";
263+
}
264+
265+
return linkToSystemRequirements;
266+
}
267+
}

lib/local-build-requirements/android-local-build-requirements.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ export class AndroidLocalBuildRequirements {
22
constructor(private sysInfo: NativeScriptDoctor.ISysInfo) { }
33

44
public async checkRequirements(): Promise<boolean> {
5-
if (!await this.sysInfo.isAndroidInstalled() ||
6-
!await this.sysInfo.getJavaCompilerVersion() ||
5+
if (!await this.sysInfo.getJavaCompilerVersion() ||
76
!await this.sysInfo.getJavaVersion() ||
87
!await this.sysInfo.getAdbVersion()) {
98
return false;

lib/wrappers/child-process.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,16 @@ export class ChildProcess {
7575
});
7676
});
7777
}
78+
79+
public execFile(command: string, args: string[]): Promise<any> {
80+
return new Promise<any>((resolve, reject) => {
81+
childProcess.execFile(command, args, (error: any, stdout: NodeBuffer) => {
82+
if (error) {
83+
reject(error);
84+
} else {
85+
resolve(stdout);
86+
}
87+
});
88+
});
89+
}
7890
}

lib/wrappers/file-system.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import * as fs from "fs";
22
import { Extract } from "unzip";
33

44
export class FileSystem {
5-
public exists(path: string): Promise<boolean> {
6-
return new Promise<boolean>((resolve) => {
7-
fs.exists(path, resolve);
8-
});
5+
public exists(path: string): boolean {
6+
return fs.existsSync(path);
97
}
108

119
public extractZip(pathToZip: string, outputDir: string): Promise<void> {
@@ -15,4 +13,8 @@ export class FileSystem {
1513
stream.on("error", reject);
1614
});
1715
}
16+
17+
public readDirectory(path: string): string[] {
18+
return fs.readdirSync(path);
19+
}
1820
}

typings/interfaces.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,65 @@ declare module NativeScriptDoctor {
331331
IOS_PLATFORM_NAME: string;
332332
SUPPORTED_PLATFORMS: string[];
333333
}
334+
335+
/**
336+
* Describes methods for getting and validating Android tools.
337+
*/
338+
interface IAndroidToolsInfo {
339+
/**
340+
* Returns the Android tools info.
341+
* @return {NativeScriptDoctor.IAndroidToolsInfoData} returns the Android tools info.
342+
*/
343+
getToolsInfo(): NativeScriptDoctor.IAndroidToolsInfoData;
344+
345+
/**
346+
* Checks if the Android tools are valid.
347+
* @return {string[]} An array of errors from the validation checks. If there are no errors will return [].
348+
*/
349+
validateInfo(): string[];
350+
351+
/**
352+
* Checks if the current javac version is valid.
353+
* @param {string} installedJavaVersion The version of javac to check.
354+
* @return {string[]} An array of errors from the validation checks. If there are no errors will return [].
355+
*/
356+
validateJavacVersion(installedJavaVersion: string): string[];
357+
358+
/**
359+
* Returns the path to the adb which is located in ANDROID_HOME.
360+
* @return {Promise<string>} Path to the adb which is located in ANDROID_HOME.
361+
*/
362+
getPathToAdbFromAndroidHome(): Promise<string>;
363+
364+
/**
365+
* Checks if the ANDROID_HOME variable is set to the correct folder.
366+
* @return {string[]} An array of errors from the validation checks. If there are no errors will return [].
367+
*/
368+
validateAndroidHomeEnvVariable(): string[];
369+
}
370+
371+
/**
372+
* Describes information about installed Android tools and SDKs.
373+
*/
374+
interface IAndroidToolsInfoData {
375+
/**
376+
* The value of ANDROID_HOME environment variable.
377+
*/
378+
androidHomeEnvVar: string;
379+
380+
/**
381+
* The latest installed version of Android Build Tools that satisfies CLI's requirements.
382+
*/
383+
buildToolsVersion: string;
384+
385+
/**
386+
* The latest installed version of Android SDK that satisfies CLI's requirements.
387+
*/
388+
compileSdkVersion: number;
389+
390+
/**
391+
* The latest installed version of Android Support Repository that satisfies CLI's requirements.
392+
*/
393+
supportRepositoryVersion: string;
394+
}
334395
}

0 commit comments

Comments
 (0)