Skip to content

Improve Getting-started for Android #1159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 9, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 143 additions & 19 deletions lib/android-tools-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,79 @@

import * as path from "path";
import * as semver from "semver";
import {EOL} from "os";

export class AndroidToolsInfo implements IAndroidToolsInfo {
private static ANDROID_TARGET_PREFIX = "android";
private static SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22", "android-23"];
private static MIN_REQUIRED_COMPILE_TARGET = 22;
private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=22";
private static VERSION_REGEX = /^(\d+\.){2}\d+$/;
private static VERSION_REGEX = /((\d+\.){2}\d+)/;
private static MIN_JAVA_VERSION = "1.7.0";

private showWarningsAsErrors: boolean;
private toolsInfo: IAndroidToolsInfoData;
private selectedCompileSdk: number;
private installedTargetsCache: string[] = null;
private androidHome = process.env["ANDROID_HOME"];
private pathToAndroidExecutable: string;
private _androidExecutableName: string;
private get androidExecutableName(): string {
if(!this._androidExecutableName) {
this._androidExecutableName = "android";
if(this.$hostInfo.isWindows) {
this._androidExecutableName += ".bat";
}
}

return this._androidExecutableName;
}

constructor(private $childProcess: IChildProcess,
private $errors: IErrors,
private $fs: IFileSystem,
private $hostInfo: IHostInfo,
private $logger: ILogger,
private $options: IOptions) {}

public getPathToAndroidExecutable(): IFuture<string> {
return ((): string => {
if (!this.pathToAndroidExecutable) {
if(this.validateAndroidHomeEnvVariable(this.androidHome).wait()) {
let androidPath = path.join(this.androidHome, "tools", this.androidExecutableName);
if(!this.trySetAndroidPath(androidPath).wait() && !this.trySetAndroidPath(this.androidExecutableName).wait()) {
this.$errors.failWithoutHelp(`Unable to find "${this.androidExecutableName}" executable file. Make sure you have set ANDROID_HOME environment variable correctly.`);
}
} else {
this.$errors.failWithoutHelp("ANDROID_HOME environment variable is not set correctly.");
}
}

return this.pathToAndroidExecutable;
}).future<string>()();
}

private trySetAndroidPath(androidPath: string): IFuture<boolean> {
return ((): boolean => {
let isAndroidPathCorrect = true;
try {
let result = this.$childProcess.spawnFromEvent(androidPath, ["--help"], "close", {}, {throwError: false}).wait();
if(result && result.stdout) {
this.$logger.trace(result.stdout);
this.pathToAndroidExecutable = androidPath;
} else {
this.$logger.trace(`Unable to find android executable from '${androidPath}'.`);
isAndroidPathCorrect = false;
}
} catch(err) {
this.$logger.trace(`Error occurred while checking androidExecutable from '${androidPath}'. ${err.message}`);
isAndroidPathCorrect = false;
}

return isAndroidPathCorrect;
}).future<boolean>()();
}

public getToolsInfo(): IFuture<IAndroidToolsInfoData> {
return ((): IAndroidToolsInfoData => {
if(!this.toolsInfo) {
Expand All @@ -44,11 +98,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
let detectedErrors = false;
this.showWarningsAsErrors = options && options.showWarningsAsErrors;
let toolsInfoData = this.getToolsInfo().wait();
if(!toolsInfoData.androidHomeEnvVar || !this.$fs.exists(toolsInfoData.androidHomeEnvVar).wait()) {
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.",
"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.");
}

let isAndroidHomeValid = this.validateAndroidHomeEnvVariable(toolsInfoData.androidHomeEnvVar).wait();
if(!toolsInfoData.compileSdkVersion) {
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.`,
"Run `$ android` to manage your Android SDK versions.");
Expand All @@ -65,14 +115,21 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
message = `You have to install version ${versionRangeMatches[1]}.`;
}

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

this.printMessage("You need to have the Android SDK Build-tools installed on your system. " + message, invalidBuildToolsAdditionalMsg);
detectedErrors = true;
}

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

Expand All @@ -92,7 +149,31 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
}
}

return detectedErrors;
return detectedErrors || isAndroidHomeValid;
}).future<boolean>()();
}

public validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean> {
return ((): boolean => {
let hasProblemWithJavaVersion = false;
if(options) {
this.showWarningsAsErrors = options.showWarningsAsErrors;
}
let additionalMessage = "You will not be able to build your projects for Android." + EOL
+ "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 +
" described in https://github.com/NativeScript/nativescript-cli#system-requirements.";
let matchingVersion = (installedJavaVersion || "").match(AndroidToolsInfo.VERSION_REGEX);
if(matchingVersion && matchingVersion[1]) {
if(semver.lt(matchingVersion[1], AndroidToolsInfo.MIN_JAVA_VERSION)) {
hasProblemWithJavaVersion = true;
this.printMessage(`Javac version ${installedJavaVersion} is not supported. You have to install at least ${AndroidToolsInfo.MIN_JAVA_VERSION}.`, additionalMessage);
}
} else {
hasProblemWithJavaVersion = true;
this.printMessage("Error executing command 'javac'. Make sure you have installed The Java Development Kit (JDK) and set JAVA_HOME environment variable.", additionalMessage);
}

return hasProblemWithJavaVersion;
}).future<boolean>()();
}

Expand All @@ -113,7 +194,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
}

if(additionalMsg) {
this.$logger.info(additionalMsg);
this.$logger.printMarkdown(additionalMsg);
}
}

Expand Down Expand Up @@ -157,10 +238,24 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
return ((): string => {
let selectedVersion: string;
if(this.$fs.exists(pathToDir).wait()) {
let subDirs = this.$fs.readDirectory(pathToDir).wait()
.filter(buildTools => !!buildTools.match(AndroidToolsInfo.VERSION_REGEX));
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirs.join(", ")}`);
selectedVersion = semver.maxSatisfying(subDirs, versionRange);
let subDirs = this.$fs.readDirectory(pathToDir).wait();
this.$logger.trace(`Directories found in ${pathToDir} are ${subDirs.join(", ")}`);

let subDirsVersions = subDirs
.map(dirName => {
let dirNameGroups = dirName.match(AndroidToolsInfo.VERSION_REGEX);
if(dirNameGroups) {
return dirNameGroups[1];
}

return null;
})
.filter(dirName => !!dirName);
this.$logger.trace(`Versions found in ${pathToDir} are ${subDirsVersions.join(", ")}`);
let version = semver.maxSatisfying(subDirsVersions, versionRange);
if(version) {
selectedVersion = _.find(subDirs, dir => dir.indexOf(version) !== -1);
}
}

return selectedVersion;
Expand Down Expand Up @@ -224,9 +319,16 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
private getInstalledTargets(): IFuture<string[]> {
return (() => {
if (!this.installedTargetsCache) {
this.installedTargetsCache = [];
let output = this.$childProcess.exec('android list targets').wait();
output.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
try {
let result = this.$childProcess.spawnFromEvent(this.getPathToAndroidExecutable().wait(), ["list", "targets"], "close", {}, {throwError: false}).wait();
if(result.stdout) {
this.$logger.trace(result.stdout);
this.installedTargetsCache = [];
result.stdout.replace(/id: \d+ or "(.+)"/g, (m:string, p1:string) => (this.installedTargetsCache.push(p1), m));
}
} catch(err) {
this.$logger.trace("Unable to get Android targets. Error is: " + err);
}
}
return this.installedTargetsCache;
}).future<string[]>()();
Expand All @@ -235,5 +337,27 @@ export class AndroidToolsInfo implements IAndroidToolsInfo {
private getMaxSupportedVersion(): number {
return this.parseAndroidSdkString(_.last(AndroidToolsInfo.SUPPORTED_TARGETS.sort()));
}

private _cachedAndroidHomeValidationResult: boolean = null;
private validateAndroidHomeEnvVariable(androidHomeEnvVar: string): IFuture<boolean> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually validate methods return true when validation is successful and false otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed :)

return ((): boolean => {
if(this._cachedAndroidHomeValidationResult === null) {
this._cachedAndroidHomeValidationResult = true;
let expectedDirectoriesInAndroidHome = ["build-tools", "tools", "platform-tools", "extras"];
if(!androidHomeEnvVar || !this.$fs.exists(androidHomeEnvVar).wait()) {
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.",
"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.");
this._cachedAndroidHomeValidationResult = false;
} else if(!_.any(expectedDirectoriesInAndroidHome.map(dir => this.$fs.exists(path.join(androidHomeEnvVar, dir)).wait()))) {
this.printMessage("The ANDROID_HOME environment variable points to incorrect directory. You will not be able to perform any build-related operations for Android.",
"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, " +
"where you will find `tools` and `platform-tools` directories.");
this._cachedAndroidHomeValidationResult = false;
}
}

return this._cachedAndroidHomeValidationResult;
}).future<boolean>()();
}
}
$injector.register("androidToolsInfo", AndroidToolsInfo);
2 changes: 1 addition & 1 deletion lib/common
24 changes: 24 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,29 @@ export class StaticConfig extends staticConfigBaseLibPath.StaticConfigBase imple
public get PATH_TO_BOOTSTRAP() : string {
return path.join(__dirname, "bootstrap");
}

public getAdbFilePath(): IFuture<string> {
return (() => {
if(!this._adbFilePath) {
let androidHomeEnvVar = process.env.ANDROID_HOME;
if(androidHomeEnvVar) {
let pathToAdb = path.join(androidHomeEnvVar, "platform-tools", "adb");
let childProcess: IChildProcess = this.$injector.resolve("$childProcess");
try {
childProcess.execFile(pathToAdb, ["help"]).wait();
this._adbFilePath = pathToAdb;
} catch (err) {
// adb does not exist, so ANDROID_HOME is not set correctly
// try getting default adb path (included in CLI package)
super.getAdbFilePath().wait();
}
} else {
super.getAdbFilePath().wait();
}
}

return this._adbFilePath;
}).future<string>()();
}
}
$injector.register("staticConfig", StaticConfig);
15 changes: 15 additions & 0 deletions lib/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ interface IAndroidToolsInfo {
* @return {boolean} True if there are detected issues, false otherwise.
*/
validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture<boolean>;

/**
* Validates the information about required JAVA version.
* @param {string} installedJavaVersion The JAVA version that will be checked.
* @param {any} options Defines if the warning messages should treated as error.
* @return {boolean} True if there are detected issues, false otherwise.
*/
validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture<boolean>;

/**
* Returns the path to `android` executable. It should be `$ANDROID_HOME/tools/android`.
* In case ANDROID_HOME is not defined, check if `android` is part of $PATH.
* @return {boolean} Path to the `android` executable.
*/
getPathToAndroidExecutable(): IFuture<string>;
}

/**
Expand Down
33 changes: 3 additions & 30 deletions lib/services/android-project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
private static VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v";
private static ANDROID_PLATFORM_NAME = "android";
private static LIBS_FOLDER_NAME = "libs";
private static MIN_JAVA_VERSION = "1.7.0";
private static MIN_RUNTIME_VERSION_WITH_GRADLE = "1.3.0";

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

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

Expand Down Expand Up @@ -424,19 +424,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
}).future<string>()();
}

private checkJava(): IFuture<void> {
return (() => {
try {
let javaVersion = this.$sysInfo.getSysInfo().javaVer;
if(semver.lt(javaVersion, AndroidProjectService.MIN_JAVA_VERSION)) {
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.`);
}
} catch(error) {
this.$errors.failWithoutHelp("Error executing command 'java'. Make sure you have java installed and added to your PATH.");
}
}).future<void>()();
}

private checkAnt(): IFuture<void> {
return (() => {
try {
Expand All @@ -447,20 +434,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject
}).future<void>()();
}

private checkAndroid(): IFuture<void> {
return (() => {
try {
this.$childProcess.exec('android list targets').wait();
} catch(error) {
if (error.message.match(/command\snot\sfound/)) {
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.");
} else {
this.$errors.fail("An error occurred while listing Android targets");
}
}
}).future<void>()();
}

private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture<void> {
return (() => {
this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait();
Expand Down
Loading