Skip to content

Commit 5983804

Browse files
Improve Getting-started for Android
* Remove requirement to have `<android-sdk>/tools` and `<android-sdk>/platform-tools` directories to your PATH - use them based on ANDROID_HOME variable. * Based on the above point - replace all calls to `android` directly with correct path to the `android` executable. * Remove requirement for JAVA added in your PATH - validate it by using JAVA_HOME. * Improve error messages when ANDROID_HOME is not set - for example warning about missing compile SDK will inform you in case ANDROID_HOME is not set. * Improve ANDROID_HOME checks - validate that directories like "extras", "tools", "platform-tools" exist (at least on of them). * Validate JAVA version in doctor command. * Sometimes `build-tools` directories are not called with version(`22.0.1`), but have the name: `build-tools-22.0.1` - make sure we will be able to use such dirs * Skip cocoapods warnings on Linux and Windows (for `tns doctor` command).
1 parent c66d037 commit 5983804

File tree

6 files changed

+199
-79
lines changed

6 files changed

+199
-79
lines changed

lib/android-tools-info.ts

+130-18
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,73 @@
33

44
import * as path from "path";
55
import * as semver from "semver";
6+
import {EOL} from "os";
67

78
export class AndroidToolsInfo implements IAndroidToolsInfo {
89
private static ANDROID_TARGET_PREFIX = "android";
910
private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23"];
1011
private static MIN_REQUIRED_COMPILE_TARGET = 22;
1112
private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=22";
12-
private static VERSION_REGEX = /^(\d+\.){2}\d+$/;
13+
private static VERSION_REGEX = /((\d+\.){2}\d+)/;
14+
private static MIN_JAVA_VERSION = "1.7.0";
15+
1316
private showWarningsAsErrors: boolean;
1417
private toolsInfo: IAndroidToolsInfoData;
1518
private selectedCompileSdk: number;
1619
private installedTargetsCache: string[] = null;
1720
private androidHome = process.env["ANDROID_HOME"];
21+
private pathToAndroidExecutable: string;
22+
private _androidExecutableName: string;
23+
private get androidExecutableName(): string {
24+
if(!this._androidExecutableName) {
25+
this._androidExecutableName = "android";
26+
if(this.$hostInfo.isWindows) {
27+
this._androidExecutableName += ".bat";
28+
}
29+
}
30+
31+
return this._androidExecutableName;
32+
}
1833

1934
constructor(private $childProcess: IChildProcess,
2035
private $errors: IErrors,
2136
private $fs: IFileSystem,
37+
private $hostInfo: IHostInfo,
2238
private $logger: ILogger,
2339
private $options: IOptions) {}
2440

41+
public getPathToAndroidExecutable(): IFuture<string> {
42+
return ((): string => {
43+
if (!this.pathToAndroidExecutable) {
44+
if(this.validateAndroidHomeEnvVariable(this.androidHome).wait()) {
45+
let androidPath = path.join(this.androidHome, "tools", this.androidExecutableName);
46+
if(!this.trySetAndroidPath(androidPath).wait() && !this.trySetAndroidPath(this.androidExecutableName).wait()) {
47+
this.$errors.failWithoutHelp(`Unable to find "${this.androidExecutableName}" executable file. Make sure you have set ANDROID_HOME environment variable correctly.`);
48+
}
49+
} else {
50+
this.$errors.failWithoutHelp("ANDROID_HOME environment variable is not set correctly.");
51+
}
52+
}
53+
54+
return this.pathToAndroidExecutable;
55+
}).future<string>()();
56+
}
57+
58+
private trySetAndroidPath(androidPath: string): IFuture<boolean> {
59+
return ((): boolean => {
60+
let isAndroidPathCorrect = true;
61+
try {
62+
this.$childProcess.spawnFromEvent(androidPath, ["--help"], "close").wait();
63+
this.pathToAndroidExecutable = androidPath;
64+
} catch(err) {
65+
this.$logger.trace("Error occurred while checking androidExecutable from ANDROID_HOME." + err.message);
66+
isAndroidPathCorrect = false;
67+
}
68+
69+
return isAndroidPathCorrect;
70+
}).future<boolean>()();
71+
}
72+
2573
public getToolsInfo(): IFuture<IAndroidToolsInfoData> {
2674
return ((): IAndroidToolsInfoData => {
2775
if(!this.toolsInfo) {
@@ -44,11 +92,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
4492
let detectedErrors = false;
4593
this.showWarningsAsErrors = options && options.showWarningsAsErrors;
4694
let toolsInfoData = this.getToolsInfo().wait();
47-
if(!toolsInfoData.androidHomeEnvVar || !this.$fs.exists(toolsInfoData.androidHomeEnvVar).wait()) {
48-
this.printMessage("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.",
49-
"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.");
50-
}
51-
95+
let isAndroidHomeValid = this.validateAndroidHomeEnvVariable(toolsInfoData.androidHomeEnvVar).wait();
5296
if(!toolsInfoData.compileSdkVersion) {
5397
this.printMessage(`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.`,
5498
"Run `$ android` to manage your Android SDK versions.");
@@ -65,14 +109,21 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
65109
message = `You have to install version ${versionRangeMatches[1]}.`;
66110
}
67111

68-
this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message,
69-
'Run "android" from your command-line to install required Android Build Tools.');
112+
let invalidBuildToolsAdditionalMsg = 'Run `android` from your command-line to install required `Android Build Tools`.';
113+
if(!isAndroidHomeValid) {
114+
invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.';
115+
}
116+
117+
this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
70118
detectedErrors = true;
71119
}
72120

73121
if(!toolsInfoData.supportRepositoryVersion) {
74-
this.printMessage(`You need to have Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later and the latest Android Support Repository installed on your system.`,
75-
"Run `$ android` to manage the Android Support Repository.");
122+
let invalidSupportLibAdditionalMsg = 'Run `$ android` to manage the Android Support Repository.';
123+
if(!isAndroidHomeValid) {
124+
invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.';
125+
}
126+
this.printMessage(`You need to have Android SDK ${AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET} or later and the latest Android Support Repository installed on your system.`, invalidSupportLibAdditionalMsg);
76127
detectedErrors = true;
77128
}
78129

@@ -92,7 +143,31 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
92143
}
93144
}
94145

95-
return detectedErrors;
146+
return detectedErrors || isAndroidHomeValid;
147+
}).future<boolean>()();
148+
}
149+
150+
public validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean> {
151+
return ((): boolean => {
152+
let hasProblemWithJavaVersion = false;
153+
if(options) {
154+
this.showWarningsAsErrors = options.showWarningsAsErrors;
155+
}
156+
let additionalMessage = "You will not be able to build your projects for Android." + EOL
157+
+ "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 +
158+
" described in https://github.com/NativeScript/nativescript-cli#system-requirements.";
159+
let matchingVersion = (installedJavaVersion || "").match(AndroidToolsInfo.VERSION_REGEX);
160+
if(matchingVersion && matchingVersion[1]) {
161+
if(semver.lt(matchingVersion[1], AndroidToolsInfo.MIN_JAVA_VERSION)) {
162+
hasProblemWithJavaVersion = true;
163+
this.printMessage(`Javac version ${installedJavaVersion} is not supported. You have to install at least ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage);
164+
}
165+
} else {
166+
hasProblemWithJavaVersion = true;
167+
this.printMessage("Error executing command 'javac'. Make sure you have java installed and set JAVA_HOME environment variable.", additionalMessage);
168+
}
169+
170+
return hasProblemWithJavaVersion;
96171
}).future<boolean>()();
97172
}
98173

@@ -113,7 +188,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
113188
}
114189

115190
if(additionalMsg) {
116-
this.$logger.info(additionalMsg);
191+
this.$logger.printMarkdown(additionalMsg);
117192
}
118193
}
119194

@@ -157,10 +232,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
157232
return ((): string => {
158233
let selectedVersion: string;
159234
if(this.$fs.exists(pathToDir).wait()) {
160-
let subDirs = this.$fs.readDirectory(pathToDir).wait()
161-
.filter(buildTools => !!buildTools.match(AndroidToolsInfo.VERSION_REGEX));
162-
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirs.join(", ")}`);
163-
selectedVersion = semver.maxSatisfying(subDirs, versionRange);
235+
let subDirs = this.$fs.readDirectory(pathToDir).wait();
236+
this.$logger.trace(`Directories found in ${pathToDir} are ${subDirs.join(", ")}`);
237+
238+
let subDirsVersions = subDirs
239+
.map(dirName => {
240+
let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX);
241+
if(dirNameGroups) {
242+
return dirNameGroups[1];
243+
}
244+
245+
return null;
246+
})
247+
.filter(dirName => !!dirName);
248+
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirsVersions.join(", ")}`);
249+
let version = semver.maxSatisfying(subDirsVersions, versionRange);
250+
if(version) {
251+
selectedVersion = _.find(subDirs, dir => dir.indexOf(version) !== -1);
252+
}
164253
}
165254

166255
return selectedVersion;
@@ -225,8 +314,12 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
225314
return (() => {
226315
if (!this.installedTargetsCache) {
227316
this.installedTargetsCache = [];
228-
let output = this.$childProcess.exec('android list targets').wait();
229-
output.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
317+
try {
318+
let result = this.$childProcess.spawnFromEvent(this.getPathToAndroidExecutable().wait(), ["list", "targets"], "close", {}, {throwError: false}).wait();
319+
result.stdout.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
320+
} catch(err) {
321+
this.$logger.trace("Unable to get Android targets. Error is: " + err);
322+
}
230323
}
231324
return this.installedTargetsCache;
232325
}).future<string[]>()();
@@ -235,5 +328,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
235328
private getMaxSupportedVersion(): number {
236329
return this.parseAndroidSdkString(_.last(AndroidToolsInfo.SUPPORTED_TARGETS.sort()));
237330
}
331+
332+
private validateAndroidHomeEnvVariable(androidHomeEnvVar: string): IFuture<boolean> {
333+
return ((): boolean => {
334+
let isAndroidHomeCorrect = true;
335+
let expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
336+
if(!androidHomeEnvVar || !this.$fs.exists(androidHomeEnvVar).wait()) {
337+
this.printMessage("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.",
338+
"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.");
339+
isAndroidHomeCorrect = false;
340+
} else if(!_.any(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(androidHomeEnvVar, dir)).wait()))) {
341+
this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
342+
"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, " +
343+
"where you will find `tools` and `platform-tools` directories.");
344+
isAndroidHomeCorrect = false;
345+
}
346+
347+
return isAndroidHomeCorrect;
348+
}).future<boolean>()();
349+
}
238350
}
239351
$injector.register("androidToolsInfo", AndroidToolsInfo);

lib/config.ts

+24
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,29 @@ export class StaticConfig extends staticConfigBaseLibPath.StaticConfigBase imple
7777
public get PATH_TO_BOOTSTRAP() : string {
7878
return path.join(__dirname, "bootstrap");
7979
}
80+
81+
public getAdbFilePath(): IFuture<string> {
82+
return (() => {
83+
if(!this._adbFilePath) {
84+
let androidHomeEnvVar = process.env.ANDROID_HOME;
85+
if(androidHomeEnvVar) {
86+
let pathToAdb = path.join(androidHomeEnvVar, "platform-tools", "adb");
87+
let childProcess: IChildProcess = this.$injector.resolve("$childProcess");
88+
try {
89+
childProcess.execFile(pathToAdb, ["help"]).wait();
90+
this._adbFilePath = pathToAdb;
91+
} catch (err) {
92+
// adb does not exist, so ANDROID_HOME is not set correctly
93+
// try getting default adb path (included in CLI package)
94+
super.getAdbFilePath().wait();
95+
}
96+
} else {
97+
super.getAdbFilePath().wait();
98+
}
99+
}
100+
101+
return this._adbFilePath;
102+
}).future<string>()();
103+
}
80104
}
81105
$injector.register("staticConfig", StaticConfig);

lib/declarations.ts

+15
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ interface IAndroidToolsInfo {
115115
* @return {boolean} True if there are detected issues, false otherwise.
116116
*/
117117
validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture<boolean>;
118+
119+
/**
120+
* Validates the information about required JAVA version.
121+
* @param {string} installedJavaVersion The JAVA version that will be checked.
122+
* @param {any} options Defines if the warning messages should treated as error.
123+
* @return {boolean} True if there are detected issues, false otherwise.
124+
*/
125+
validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean>;
126+
127+
/**
128+
* Returns the path to `android` executable. It should be `$ANDROID_HOME/tools/android`.
129+
* In case ANDROID_HOME is not defined, check if `android` is part of $PATH.
130+
* @return {boolean} Path to the `android` executable.
131+
*/
132+
getPathToAndroidExecutable(): IFuture<string>;
118133
}
119134

120135
/**

lib/services/android-project-service.ts

+3-30
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
1313
private static VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v";
1414
private static ANDROID_PLATFORM_NAME = "android";
1515
private static LIBS_FOLDER_NAME = "libs";
16-
private static MIN_JAVA_VERSION = "1.7.0";
1716
private static MIN_RUNTIME_VERSION_WITH_GRADLE = "1.3.0";
1817

1918
private _androidProjectPropertiesManagers: IDictionary<IAndroidProjectPropertiesManager>;
@@ -80,8 +79,9 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
8079
this.validatePackageName(this.$projectData.projectId);
8180
this.validateProjectName(this.$projectData.projectName);
8281

83-
this.checkJava().wait();
84-
this.checkAndroid().wait();
82+
// this call will fail in case `android` is not set correctly.
83+
this.$androidToolsInfo.getPathToAndroidExecutable().wait();
84+
this.$androidToolsInfo.validateJavacVersion(this.$sysInfo.getSysInfo().javacVersion, {showWarningsAsErrors: true}).wait();
8585
}).future<void>()();
8686
}
8787

@@ -423,19 +423,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
423423
}).future<string>()();
424424
}
425425

426-
private checkJava(): IFuture<void> {
427-
return (() => {
428-
try {
429-
let javaVersion = this.$sysInfo.getSysInfo().javaVer;
430-
if(semver.lt(javaVersion, AndroidProjectService.MIN_JAVA_VERSION)) {
431-
this.$errors.failWithoutHelp(`Your current java version is ${javaVersion}. NativeScript CLI needs at least ${AndroidProjectService.MIN_JAVA_VERSION} version to work correctly. Ensure that you have at least ${AndroidProjectService.MIN_JAVA_VERSION} java version installed and try again.`);
432-
}
433-
} catch(error) {
434-
this.$errors.failWithoutHelp("Error executing command 'java'. Make sure you have java installed and added to your PATH.");
435-
}
436-
}).future<void>()();
437-
}
438-
439426
private checkAnt(): IFuture<void> {
440427
return (() => {
441428
try {
@@ -446,20 +433,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
446433
}).future<void>()();
447434
}
448435

449-
private checkAndroid(): IFuture<void> {
450-
return (() => {
451-
try {
452-
this.$childProcess.exec('android list targets').wait();
453-
} catch(error) {
454-
if (error.message.match(/command\snot\sfound/)) {
455-
this.$errors.fail("The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) is added to your path.");
456-
} else {
457-
this.$errors.fail("An error occurred while listing Android targets");
458-
}
459-
}
460-
}).future<void>()();
461-
}
462-
463436
private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture<void> {
464437
return (() => {
465438
this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait();

0 commit comments

Comments
 (0)