Skip to content

Commit 63e5852

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 63e5852

File tree

6 files changed

+190
-65
lines changed

6 files changed

+190
-65
lines changed

lib/android-tools-info.ts

+120-18
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,63 @@
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+
private static ANDROID_EXECUTABLE_NAME = "android";
16+
1317
private showWarningsAsErrors: boolean;
1418
private toolsInfo: IAndroidToolsInfoData;
1519
private selectedCompileSdk: number;
1620
private installedTargetsCache: string[] = null;
1721
private androidHome = process.env["ANDROID_HOME"];
22+
private pathToAndroidExecutable: string;
1823

1924
constructor(private $childProcess: IChildProcess,
2025
private $errors: IErrors,
2126
private $fs: IFileSystem,
27+
private $hostInfo: IHostInfo,
2228
private $logger: ILogger,
2329
private $options: IOptions) {}
2430

31+
public getPathToAndroidExecutable(): IFuture<string> {
32+
return ((): string => {
33+
if (!this.pathToAndroidExecutable) {
34+
if(!this.validateAndroidHomeEnvVariable(this.androidHome).wait()) {
35+
let androidPath = path.join(this.androidHome, "tools", AndroidToolsInfo.ANDROID_EXECUTABLE_NAME) + (this.$hostInfo.isWindows ? ".bat" : "");
36+
if(!this.trySpawnAndroidHelp(androidPath).wait() && !this.trySpawnAndroidHelp(AndroidToolsInfo.ANDROID_EXECUTABLE_NAME).wait()) {
37+
this.$errors.failWithoutHelp(`Unable to find "${AndroidToolsInfo.ANDROID_EXECUTABLE_NAME}" executable file. Make sure you have set ANDROID_HOME environment variable correctly.`);
38+
}
39+
} else {
40+
this.$errors.failWithoutHelp("ANDROID_HOME environment variable is not set.");
41+
}
42+
}
43+
44+
return this.pathToAndroidExecutable;
45+
}).future<string>()();
46+
}
47+
48+
private trySpawnAndroidHelp(androidPath: string): IFuture<boolean>{
49+
return ((): boolean => {
50+
let isAndroidPathCorrect = true;
51+
try {
52+
this.$childProcess.spawnFromEvent(androidPath, ["--help"], "close").wait();
53+
this.pathToAndroidExecutable = androidPath;
54+
} catch(err) {
55+
this.$logger.trace("Error occurred while checking androidExecutable from ANDROID_HOME." + err.message);
56+
isAndroidPathCorrect = false;
57+
}
58+
59+
return isAndroidPathCorrect;
60+
}).future<boolean>()();
61+
}
62+
2563
public getToolsInfo(): IFuture<IAndroidToolsInfoData> {
2664
return ((): IAndroidToolsInfoData => {
2765
if(!this.toolsInfo) {
@@ -44,11 +82,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
4482
let detectedErrors = false;
4583
this.showWarningsAsErrors = options && options.showWarningsAsErrors;
4684
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-
85+
let isAndroidHomeInvalid = this.validateAndroidHomeEnvVariable(toolsInfoData.androidHomeEnvVar).wait();
5286
if(!toolsInfoData.compileSdkVersion) {
5387
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.`,
5488
"Run `$ android` to manage your Android SDK versions.");
@@ -65,14 +99,21 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
6599
message = `You have to install version ${versionRangeMatches[1]}.`;
66100
}
67101

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.');
102+
let invalidBuildToolsAdditionalMsg = 'Run `android` from your command-line to install required `Android Build Tools`.';
103+
if(isAndroidHomeInvalid) {
104+
invalidBuildToolsAdditionalMsg += ' In case you already have them installed, make sure `ANDROID_HOME` environment variable is set correctly.';
105+
}
106+
107+
this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
70108
detectedErrors = true;
71109
}
72110

73111
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.");
112+
let invalidSupportLibAdditionalMsg = 'Run `$ android` to manage the Android Support Repository.';
113+
if(isAndroidHomeInvalid) {
114+
invalidSupportLibAdditionalMsg += ' In case you already have it installed, make sure `ANDROID_HOME` environment variable is set correctly.';
115+
}
116+
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);
76117
detectedErrors = true;
77118
}
78119

@@ -92,7 +133,31 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
92133
}
93134
}
94135

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

@@ -113,7 +178,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
113178
}
114179

115180
if(additionalMsg) {
116-
this.$logger.info(additionalMsg);
181+
this.$logger.printMarkdown(additionalMsg);
117182
}
118183
}
119184

@@ -157,10 +222,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
157222
return ((): string => {
158223
let selectedVersion: string;
159224
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);
225+
let subDirs = this.$fs.readDirectory(pathToDir).wait();
226+
this.$logger.trace(`Directories found in ${pathToDir} are ${subDirs.join(", ")}`);
227+
228+
let subDirsVersions = subDirs
229+
.map(dirName => {
230+
let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX);
231+
if(dirNameGroups) {
232+
return dirNameGroups[1];
233+
}
234+
235+
return null;
236+
})
237+
.filter(dirName => !!dirName);
238+
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirsVersions.join(", ")}`);
239+
let version = semver.maxSatisfying(subDirsVersions, versionRange);
240+
if(version) {
241+
selectedVersion = _.find(subDirs, dir => dir.indexOf(version) !== -1);
242+
}
164243
}
165244

166245
return selectedVersion;
@@ -225,8 +304,12 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
225304
return (() => {
226305
if (!this.installedTargetsCache) {
227306
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));
307+
try {
308+
let output = this.$childProcess.execFile(this.getPathToAndroidExecutable().wait(), ["list", "targets"]).wait();
309+
output.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
310+
} catch(err) {
311+
this.$logger.trace("Unable to get Android targets. Error is: " + err);
312+
}
230313
}
231314
return this.installedTargetsCache;
232315
}).future<string[]>()();
@@ -235,5 +318,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
235318
private getMaxSupportedVersion(): number {
236319
return this.parseAndroidSdkString(_.last(AndroidToolsInfo.SUPPORTED_TARGETS.sort()));
237320
}
321+
322+
private validateAndroidHomeEnvVariable(androidHomeEnvVar: string): IFuture<boolean> {
323+
return ((): boolean => {
324+
let detectedErrors = false;
325+
let expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
326+
if(!androidHomeEnvVar || !this.$fs.exists(androidHomeEnvVar).wait()) {
327+
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.",
328+
"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.");
329+
detectedErrors = true;
330+
} else if(!_.any(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(androidHomeEnvVar, dir)).wait()))) {
331+
this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
332+
"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, " +
333+
"where you will find `tools` and `platform-tools` directories.");
334+
detectedErrors = true;
335+
}
336+
337+
return detectedErrors;
338+
}).future<boolean>()();
339+
}
238340
}
239341
$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

+4-16
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,8 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
8079
this.validatePackageName(this.$projectData.projectId);
8180
this.validateProjectName(this.$projectData.projectName);
8281

83-
this.checkJava().wait();
8482
this.checkAndroid().wait();
83+
this.$androidToolsInfo.validateJavacVersion(this.$sysInfo.getSysInfo().javacVersion, {showWarningsAsErrors: true}).wait();
8584
}).future<void>()();
8685
}
8786

@@ -423,19 +422,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
423422
}).future<string>()();
424423
}
425424

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-
439425
private checkAnt(): IFuture<void> {
440426
return (() => {
441427
try {
@@ -449,8 +435,10 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
449435
private checkAndroid(): IFuture<void> {
450436
return (() => {
451437
try {
452-
this.$childProcess.exec('android list targets').wait();
438+
let pathToAndroidExecutable = this.$androidToolsInfo.getPathToAndroidExecutable().wait();
439+
this.$childProcess.execFile(`${pathToAndroidExecutable}`, ["list", "targets"]).wait();
453440
} catch(error) {
441+
this.$logger.trace(error);
454442
if (error.message.match(/command\snot\sfound/)) {
455443
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.");
456444
} else {

lib/services/doctor-service.ts

+26-30
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,34 @@ class DoctorService implements IDoctorService {
3636
this.printPackageManagerTip();
3737
result = true;
3838
}
39-
if (this.$hostInfo.isDarwin && !sysInfo.xcodeVer) {
40-
this.$logger.warn("WARNING: Xcode is not installed or is not configured properly.");
41-
this.$logger.out("You will not be able to build your projects for iOS or run them in the iOS Simulator." + EOL
42-
+ "To be able to build for iOS and run apps in the native emulator, verify that you have installed Xcode." + EOL);
43-
result = true;
44-
}
45-
if (!this.$hostInfo.isDarwin) {
39+
40+
if (this.$hostInfo.isDarwin) {
41+
if(!sysInfo.xcodeVer) {
42+
this.$logger.warn("WARNING: Xcode is not installed or is not configured properly.");
43+
this.$logger.out("You will not be able to build your projects for iOS or run them in the iOS Simulator." + EOL
44+
+ "To be able to build for iOS and run apps in the native emulator, verify that you have installed Xcode." + EOL);
45+
result = true;
46+
}
47+
48+
if(!sysInfo.cocoapodVer ) {
49+
this.$logger.warn("WARNING: CocoaPod is not installed or is not configured properly.");
50+
this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL
51+
+ "To be able to build such projects, verify that you have installed CocoaPod.");
52+
result = true;
53+
}
54+
55+
if(sysInfo.cocoapodVer && semver.lt(sysInfo.cocoapodVer, DoctorService.MIN_SUPPORTED_POD_VERSION)) {
56+
this.$logger.warn(`WARNING: CocoaPod version is lower than ${DoctorService.MIN_SUPPORTED_POD_VERSION}`);
57+
this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL
58+
+ `To be able to build such projects, verify that you have at least ${DoctorService.MIN_SUPPORTED_POD_VERSION} version installed.`);
59+
result = true;
60+
}
61+
} else {
4662
this.$logger.warn("WARNING: You can work with iOS only on Mac OS X systems.");
4763
this.$logger.out("To be able to work with iOS devices and projects, you need Mac OS X Mavericks or later." + EOL);
4864
result = true;
4965
}
66+
5067
if(!sysInfo.javaVer) {
5168
this.$logger.warn("WARNING: The Java Development Kit (JDK) is not installed or is not configured properly.");
5269
this.$logger.out("You will not be able to work with the Android SDK and you might not be able" + EOL
@@ -57,30 +74,9 @@ class DoctorService implements IDoctorService {
5774
result = true;
5875
}
5976

60-
if(!sysInfo.javacVersion) {
61-
this.$logger.warn("WARNING: Javac is not installed or is not configured properly.");
62-
this.$logger.out("You will not be able to build your projects for Android." + EOL
63-
+ "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 +
64-
" described in https://github.com/NativeScript/nativescript-cli#system-requirements.");
65-
result = true;
66-
}
67-
68-
if(!sysInfo.cocoapodVer) {
69-
this.$logger.warn("WARNING: CocoaPod is not installed or is not configured properly.");
70-
this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL
71-
+ "To be able to build such projects, verify that you have installed CocoaPod.");
72-
result = true;
73-
}
74-
75-
if(sysInfo.cocoapodVer && semver.lt(sysInfo.cocoapodVer, DoctorService.MIN_SUPPORTED_POD_VERSION)) {
76-
this.$logger.warn(`WARNING: CocoaPod version is lower than ${DoctorService.MIN_SUPPORTED_POD_VERSION}`);
77-
this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL
78-
+ `To be able to build such projects, verify that you have at least ${DoctorService.MIN_SUPPORTED_POD_VERSION} version installed.`);
79-
result = true;
80-
}
81-
8277
let androidToolsIssues = this.$androidToolsInfo.validateInfo().wait();
83-
return result || androidToolsIssues;
78+
let javaVersionIssue = this.$androidToolsInfo.validateJavacVersion(sysInfo.javacVersion).wait();
79+
return result || androidToolsIssues || javaVersionIssue;
8480
}
8581

8682
private printPackageManagerTip() {

0 commit comments

Comments
 (0)