Skip to content

Commit d415621

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 2babf48 commit d415621

File tree

6 files changed

+212
-80
lines changed

6 files changed

+212
-80
lines changed

lib/android-tools-info.ts

+143-19
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,79 @@
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+
let result = this.$childProcess.spawnFromEvent(androidPath, ["--help"], "close", {}, {throwError: false}).wait();
63+
if(result && result.stdout) {
64+
this.$logger.trace(result.stdout);
65+
this.pathToAndroidExecutable = androidPath;
66+
} else {
67+
this.$logger.trace(`Unable to find android executable from '${androidPath}'.`);
68+
isAndroidPathCorrect = false;
69+
}
70+
} catch(err) {
71+
this.$logger.trace(`Error occurred while checking androidExecutable from '${androidPath}'. ${err.message}`);
72+
isAndroidPathCorrect = false;
73+
}
74+
75+
return isAndroidPathCorrect;
76+
}).future<boolean>()();
77+
}
78+
2579
public getToolsInfo(): IFuture<IAndroidToolsInfoData> {
2680
return ((): IAndroidToolsInfoData => {
2781
if(!this.toolsInfo) {
@@ -44,11 +98,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
4498
let detectedErrors = false;
4599
this.showWarningsAsErrors = options && options.showWarningsAsErrors;
46100
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-
101+
let isAndroidHomeValid = this.validateAndroidHomeEnvVariable(toolsInfoData.androidHomeEnvVar).wait();
52102
if(!toolsInfoData.compileSdkVersion) {
53103
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.`,
54104
"Run `$ android` to manage your Android SDK versions.");
@@ -65,14 +115,21 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
65115
message = `You have to install version ${versionRangeMatches[1]}.`;
66116
}
67117

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.');
118+
let invalidBuildToolsAdditionalMsg = 'Run `android` from your command-line to install required `Android Build Tools`.';
119+
if(!isAndroidHomeValid) {
120+
invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.';
121+
}
122+
123+
this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
70124
detectedErrors = true;
71125
}
72126

73127
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.");
128+
let invalidSupportLibAdditionalMsg = 'Run `$ android` to manage the Android Support Repository.';
129+
if(!isAndroidHomeValid) {
130+
invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.';
131+
}
132+
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);
76133
detectedErrors = true;
77134
}
78135

@@ -92,7 +149,31 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
92149
}
93150
}
94151

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

@@ -113,7 +194,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
113194
}
114195

115196
if(additionalMsg) {
116-
this.$logger.info(additionalMsg);
197+
this.$logger.printMarkdown(additionalMsg);
117198
}
118199
}
119200

@@ -157,10 +238,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
157238
return ((): string => {
158239
let selectedVersion: string;
159240
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);
241+
let subDirs = this.$fs.readDirectory(pathToDir).wait();
242+
this.$logger.trace(`Directories found in ${pathToDir} are ${subDirs.join(", ")}`);
243+
244+
let subDirsVersions = subDirs
245+
.map(dirName => {
246+
let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX);
247+
if(dirNameGroups) {
248+
return dirNameGroups[1];
249+
}
250+
251+
return null;
252+
})
253+
.filter(dirName => !!dirName);
254+
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirsVersions.join(", ")}`);
255+
let version = semver.maxSatisfying(subDirsVersions, versionRange);
256+
if(version) {
257+
selectedVersion = _.find(subDirs, dir => dir.indexOf(version) !== -1);
258+
}
164259
}
165260

166261
return selectedVersion;
@@ -224,9 +319,16 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
224319
private getInstalledTargets(): IFuture<string[]> {
225320
return (() => {
226321
if (!this.installedTargetsCache) {
227-
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));
322+
try {
323+
let result = this.$childProcess.spawnFromEvent(this.getPathToAndroidExecutable().wait(), ["list", "targets"], "close", {}, {throwError: false}).wait();
324+
if(result.stdout) {
325+
this.$logger.trace(result.stdout);
326+
this.installedTargetsCache = [];
327+
result.stdout.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
328+
}
329+
} catch(err) {
330+
this.$logger.trace("Unable to get Android targets. Error is: " + err);
331+
}
230332
}
231333
return this.installedTargetsCache;
232334
}).future<string[]>()();
@@ -235,5 +337,27 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
235337
private getMaxSupportedVersion(): number {
236338
return this.parseAndroidSdkString(_.last(AndroidToolsInfo.SUPPORTED_TARGETS.sort()));
237339
}
340+
341+
private _cachedAndroidHomeValidationResult: boolean = null;
342+
private validateAndroidHomeEnvVariable(androidHomeEnvVar: string): IFuture<boolean> {
343+
return ((): boolean => {
344+
if(this._cachedAndroidHomeValidationResult === null) {
345+
this._cachedAndroidHomeValidationResult = true;
346+
let expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
347+
if(!androidHomeEnvVar || !this.$fs.exists(androidHomeEnvVar).wait()) {
348+
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.",
349+
"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.");
350+
this._cachedAndroidHomeValidationResult = false;
351+
} else if(!_.any(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(androidHomeEnvVar, dir)).wait()))) {
352+
this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
353+
"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, " +
354+
"where you will find `tools` and `platform-tools` directories.");
355+
this._cachedAndroidHomeValidationResult = false;
356+
}
357+
}
358+
359+
return this._cachedAndroidHomeValidationResult;
360+
}).future<boolean>()();
361+
}
238362
}
239363
$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

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

427-
private checkJava(): IFuture<void> {
428-
return (() => {
429-
try {
430-
let javaVersion = this.$sysInfo.getSysInfo().javaVer;
431-
if(semver.lt(javaVersion, AndroidProjectService.MIN_JAVA_VERSION)) {
432-
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.`);
433-
}
434-
} catch(error) {
435-
this.$errors.failWithoutHelp("Error executing command 'java'. Make sure you have java installed and added to your PATH.");
436-
}
437-
}).future<void>()();
438-
}
439-
440427
private checkAnt(): IFuture<void> {
441428
return (() => {
442429
try {
@@ -447,20 +434,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
447434
}).future<void>()();
448435
}
449436

450-
private checkAndroid(): IFuture<void> {
451-
return (() => {
452-
try {
453-
this.$childProcess.exec('android list targets').wait();
454-
} catch(error) {
455-
if (error.message.match(/command\snot\sfound/)) {
456-
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.");
457-
} else {
458-
this.$errors.fail("An error occurred while listing Android targets");
459-
}
460-
}
461-
}).future<void>()();
462-
}
463-
464437
private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture<void> {
465438
return (() => {
466439
this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait();

0 commit comments

Comments
 (0)