diff --git a/lib/android-tools-info.ts b/lib/android-tools-info.ts index 6728de2670..63dabba12b 100644 --- a/lib/android-tools-info.ts +++ b/lib/android-tools-info.ts @@ -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 { + 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()(); + } + + private trySetAndroidPath(androidPath: string): IFuture { + 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()(); + } + public getToolsInfo(): IFuture { return ((): IAndroidToolsInfoData => { if(!this.toolsInfo) { @@ -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."); @@ -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; } @@ -92,7 +149,31 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } } - return detectedErrors; + return detectedErrors || isAndroidHomeValid; + }).future()(); + } + + public validateJavacVersion(installedJavaVersion: string, options?: {showWarningsAsErrors: boolean}): IFuture { + 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()(); } @@ -113,7 +194,7 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { } if(additionalMsg) { - this.$logger.info(additionalMsg); + this.$logger.printMarkdown(additionalMsg); } } @@ -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; @@ -224,9 +319,16 @@ export class AndroidToolsInfo implements IAndroidToolsInfo { private getInstalledTargets(): IFuture { 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()(); @@ -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 { + 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()(); + } } $injector.register("androidToolsInfo", AndroidToolsInfo); diff --git a/lib/common b/lib/common index a4dd31f284..758abe4981 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit a4dd31f2842761b377c29ed4011e6fe033ced07e +Subproject commit 758abe4981541990a9e3c1b939643ecc5a1cd9f4 diff --git a/lib/config.ts b/lib/config.ts index 3bd4277f06..bddf220364 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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 { + 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()(); + } } $injector.register("staticConfig", StaticConfig); diff --git a/lib/declarations.ts b/lib/declarations.ts index 5122dbd02b..6b121b50ab 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -115,6 +115,21 @@ interface IAndroidToolsInfo { * @return {boolean} True if there are detected issues, false otherwise. */ validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture; + + /** + * 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; + + /** + * 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; } /** diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index 6ffaac0092..aa76271fab 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -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; @@ -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()(); } @@ -424,19 +424,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject }).future()(); } - private checkJava(): IFuture { - 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()(); - } - private checkAnt(): IFuture { return (() => { try { @@ -447,20 +434,6 @@ export class AndroidProjectService extends projectServiceBaseLib.PlatformProject }).future()(); } - private checkAndroid(): IFuture { - 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()(); - } - private symlinkDirectory(directoryName: string, projectRoot: string, frameworkDir: string): IFuture { return (() => { this.$fs.createDirectory(path.join(projectRoot, directoryName)).wait(); diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 698dfc908f..ebb4dac0f9 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -36,17 +36,34 @@ class DoctorService implements IDoctorService { this.printPackageManagerTip(); result = true; } - if (this.$hostInfo.isDarwin && !sysInfo.xcodeVer) { - this.$logger.warn("WARNING: Xcode is not installed or is not configured properly."); - this.$logger.out("You will not be able to build your projects for iOS or run them in the iOS Simulator." + EOL - + "To be able to build for iOS and run apps in the native emulator, verify that you have installed Xcode." + EOL); - result = true; - } - if (!this.$hostInfo.isDarwin) { + + if (this.$hostInfo.isDarwin) { + if(!sysInfo.xcodeVer) { + this.$logger.warn("WARNING: Xcode is not installed or is not configured properly."); + this.$logger.out("You will not be able to build your projects for iOS or run them in the iOS Simulator." + EOL + + "To be able to build for iOS and run apps in the native emulator, verify that you have installed Xcode." + EOL); + result = true; + } + + if(!sysInfo.cocoapodVer ) { + this.$logger.warn("WARNING: CocoaPod is not installed or is not configured properly."); + this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL + + "To be able to build such projects, verify that you have installed CocoaPod."); + result = true; + } + + if(sysInfo.cocoapodVer && semver.lt(sysInfo.cocoapodVer, DoctorService.MIN_SUPPORTED_POD_VERSION)) { + this.$logger.warn(`WARNING: CocoaPods version is lower than ${DoctorService.MIN_SUPPORTED_POD_VERSION}`); + this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL + + `To be able to build such projects, verify that you have at least ${DoctorService.MIN_SUPPORTED_POD_VERSION} version installed.`); + result = true; + } + } else { this.$logger.warn("WARNING: You can work with iOS only on Mac OS X systems."); this.$logger.out("To be able to work with iOS devices and projects, you need Mac OS X Mavericks or later." + EOL); result = true; } + if(!sysInfo.javaVer) { this.$logger.warn("WARNING: The Java Development Kit (JDK) is not installed or is not configured properly."); 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 { result = true; } - if(!sysInfo.javacVersion) { - this.$logger.warn("WARNING: Javac is not installed or is not configured properly."); - this.$logger.out("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."); - result = true; - } - - if(!sysInfo.cocoapodVer) { - this.$logger.warn("WARNING: CocoaPod is not installed or is not configured properly."); - this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL - + "To be able to build such projects, verify that you have installed CocoaPod."); - result = true; - } - - if(sysInfo.cocoapodVer && semver.lt(sysInfo.cocoapodVer, DoctorService.MIN_SUPPORTED_POD_VERSION)) { - this.$logger.warn(`WARNING: CocoaPod version is lower than ${DoctorService.MIN_SUPPORTED_POD_VERSION}`); - this.$logger.out("You will not be able to build your projects for iOS if they contain plugin with CocoaPod file." + EOL - + `To be able to build such projects, verify that you have at least ${DoctorService.MIN_SUPPORTED_POD_VERSION} version installed.`); - result = true; - } - let androidToolsIssues = this.$androidToolsInfo.validateInfo().wait(); - return result || androidToolsIssues; + let javaVersionIssue = this.$androidToolsInfo.validateJavacVersion(sysInfo.javacVersion).wait(); + return result || androidToolsIssues || javaVersionIssue; } private printPackageManagerTip() {