diff --git a/lib/android-tools-info.ts b/lib/android-tools-info.ts new file mode 100644 index 0000000000..4417591c45 --- /dev/null +++ b/lib/android-tools-info.ts @@ -0,0 +1,204 @@ +/// +"use strict"; + +import * as path from "path"; +import * as semver from "semver"; + +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"]; + private static MIN_REQUIRED_COMPILE_TARGET = 21; + private static REQUIRED_BUILD_TOOLS_RANGE_PREFIX = ">=22"; + private static VERSION_REGEX = /^(\d+\.){2}\d+$/; + private showWarningsAsErrors: boolean; + private toolsInfo: IAndroidToolsInfoData; + private selectedCompileSdk: number; + private installedTargetsCache: string[] = null; + private androidHome = process.env["ANDROID_HOME"]; + + constructor(private $childProcess: IChildProcess, + private $errors: IErrors, + private $fs: IFileSystem, + private $logger: ILogger, + private $options: IOptions) {} + + public getToolsInfo(): IFuture { + return ((): IAndroidToolsInfoData => { + if(!this.toolsInfo) { + let infoData: IAndroidToolsInfoData = Object.create(null); + infoData.androidHomeEnvVar = this.androidHome; + infoData.compileSdkVersion = this.getCompileSdk().wait(); + infoData.buildToolsVersion = this.getBuildToolsVersion().wait(); + infoData.targetSdkVersion = this.getTargetSdk().wait(); + infoData.supportLibraryVersion = this.getAndroidSupportLibVersion().wait(); + + this.toolsInfo = infoData; + } + + return this.toolsInfo; + }).future()(); + } + + public validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture { + return (() => { + 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."); + } + + 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."); + } + + if(!toolsInfoData.buildToolsVersion) { + this.printMessage(`You need to have the Android SDK Build-tools installed on your system. You can install any version in the following range: '${this.getBuildToolsRange()}'.`, + 'Run "android" from your command-line to install required Android Build Tools.'); + } + + if(!toolsInfoData.supportLibraryVersion) { + this.printMessage(`You need to have the Android Support Library installed on your system. You can install any version in the following range: ${this.getAppCompatRange().wait() || ">=" + AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET}}.`, + 'Run `$ android` to manage the Android Support Library.'); + } + + if(options && options.validateTargetSdk) { + let targetSdk = toolsInfoData.targetSdkVersion; + let newTarget = `${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-${targetSdk}`; + if(!_.contains(AndroidToolsInfo.SUPPORTED_TARGETS, newTarget)) { + let supportedVersions = AndroidToolsInfo.SUPPORTED_TARGETS.sort(); + let minSupportedVersion = this.parseAndroidSdkString(_.first(supportedVersions)); + + if(targetSdk && (targetSdk < minSupportedVersion)) { + this.printMessage(`The selected Android target SDK ${newTarget} is not supported. You пкяш target ${minSupportedVersion} or later.`); + } else if(!targetSdk || targetSdk > this.getMaxSupportedVersion()) { + this.$logger.warn(`Support for the selected Android target SDK ${newTarget} is not verified. Your Android app might not work as expected.`); + } + } + } + }).future()(); + } + + /** + * Prints messages on the screen. In case the showWarningsAsErrors flag is set to true, warnings are shown, else - errors. + * Uses logger.warn for warnings and errors.failWithoutHelp when erros must be shown. + * In case additional details must be shown as info message, use the second parameter. + * NOTE: The additional information will not be printed when showWarningsAsErrors flag is set. + * @param {string} msg The message that will be shown as warning or error. + * @param {string} additionalMsg The additional message that will be shown as info message. + * @return {void} + */ + private printMessage(msg: string, additionalMsg?: string): void { + if (this.showWarningsAsErrors) { + this.$errors.failWithoutHelp(msg); + } else { + this.$logger.warn(msg); + } + + if(additionalMsg) { + this.$logger.info(additionalMsg); + } + } + + private getCompileSdk(): IFuture { + return ((): number => { + if(!this.selectedCompileSdk) { + let latestValidAndroidTarget = this.getLatestValidAndroidTarget().wait(); + if(latestValidAndroidTarget) { + let integerVersion = this.parseAndroidSdkString(latestValidAndroidTarget); + + if(integerVersion && integerVersion >= AndroidToolsInfo.MIN_REQUIRED_COMPILE_TARGET) { + this.selectedCompileSdk = integerVersion; + } + } + } + + return this.selectedCompileSdk; + }).future()(); + } + + private getTargetSdk(): IFuture { + return ((): number => { + let targetSdk = this.$options.sdk ? parseInt(this.$options.sdk) : this.getCompileSdk().wait(); + this.$logger.trace(`Selected targetSdk is: ${targetSdk}`); + return targetSdk; + }).future()(); + } + + private getMatchingDir(pathToDir: string, versionRange: string): IFuture { + 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); + } + + return selectedVersion; + }).future()(); + } + + private getBuildToolsRange(): string { + return `${AndroidToolsInfo.REQUIRED_BUILD_TOOLS_RANGE_PREFIX} <=${this.getMaxSupportedVersion()}`; + } + + private getBuildToolsVersion(): IFuture { + return ((): string => { + let pathToBuildTools = path.join(this.androidHome, "build-tools"); + let buildToolsRange = this.getBuildToolsRange(); + + return this.getMatchingDir(pathToBuildTools, buildToolsRange).wait(); + }).future()(); + } + + private getAppCompatRange(): IFuture { + return ((): string => { + let compileSdkVersion = this.getCompileSdk().wait(); + let requiredAppCompatRange: string; + if(compileSdkVersion) { + requiredAppCompatRange = `>=${compileSdkVersion} <${compileSdkVersion + 1}`; + } + + return requiredAppCompatRange; + }).future()(); + } + + private getAndroidSupportLibVersion(): IFuture { + return ((): string => { + let pathToAppCompat = path.join(this.androidHome, "extras", "android", "m2repository", "com", "android", "support", "appcompat-v7"); + let requiredAppCompatRange = this.getAppCompatRange().wait(); + let selectedAppCompatVersion = requiredAppCompatRange ? this.getMatchingDir(pathToAppCompat, requiredAppCompatRange).wait() : undefined; + this.$logger.trace(`Selected AppCompat version is: ${selectedAppCompatVersion}`); + return selectedAppCompatVersion; + }).future()(); + } + + private getLatestValidAndroidTarget(): IFuture { + return (() => { + let installedTargets = this.getInstalledTargets().wait(); + return _.findLast(AndroidToolsInfo.SUPPORTED_TARGETS.sort(), supportedTarget => _.contains(installedTargets, supportedTarget)); + }).future()(); + } + + private parseAndroidSdkString(androidSdkString: string): number { + return parseInt(androidSdkString.replace(`${AndroidToolsInfo.ANDROID_TARGET_PREFIX}-`, "")); + } + + 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)); + } + return this.installedTargetsCache; + }).future()(); + } + + private getMaxSupportedVersion(): number { + return this.parseAndroidSdkString(_.last(AndroidToolsInfo.SUPPORTED_TARGETS.sort())); + } +} +$injector.register("androidToolsInfo", AndroidToolsInfo); diff --git a/lib/bootstrap.ts b/lib/bootstrap.ts index 57cbfd6d6d..5484694547 100644 --- a/lib/bootstrap.ts +++ b/lib/bootstrap.ts @@ -72,3 +72,4 @@ $injector.requireCommand("init", "./commands/init"); $injector.require("projectFilesManager", "./services/project-files-manager"); $injector.requireCommand("livesync", "./commands/livesync"); +$injector.require("androidToolsInfo", "./android-tools-info"); diff --git a/lib/declarations.ts b/lib/declarations.ts index 4bbe0e267f..24d80d3dba 100644 --- a/lib/declarations.ts +++ b/lib/declarations.ts @@ -90,3 +90,53 @@ interface IProjectFilesManager { interface IInitService { initialize(): IFuture; } + +/** + * Provides access to information about installed Android tools and SDKs versions. + */ +interface IAndroidToolsInfo { + /** + * Provides information about installed Android SDKs, Build Tools, Support Library + * and ANDROID_HOME environement variable. + * @return {IAndroidToolsInfoData} Information about installed Android Tools and SDKs. + */ + getToolsInfo(): IFuture; + + /** + * Validates the information about required Android tools and SDK versions. + * @param {any} options Defines if the warning messages should treated as error and if the targetSdk value should be validated as well. + * @return {void} + */ + validateInfo(options?: {showWarningsAsErrors: boolean, validateTargetSdk: boolean}): IFuture; +} + +/** + * Describes information about installed Android tools and SDKs. + */ +interface IAndroidToolsInfoData { + /** + * The value of ANDROID_HOME environment variable. + */ + androidHomeEnvVar: string; + + /** + * The latest installed version of Android Build Tools that satisfies CLI's requirements. + */ + buildToolsVersion: string; + + /** + * The latest installed version of Android SDK that satisfies CLI's requirements. + */ + compileSdkVersion: number; + + /** + * The latest installed version of Android Support Library that satisfies CLI's requirements. + */ + supportLibraryVersion: string; + + /** + * The Android targetSdkVersion specified by the user. + * In case it is not specified, compileSdkVersion will be used for targetSdkVersion. + */ + targetSdkVersion: number; +} diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index e2f8af0918..f414ff45f3 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -8,9 +8,6 @@ import * as semver from "semver"; import * as projectServiceBaseLib from "./platform-project-service-base"; class AndroidProjectService extends projectServiceBaseLib.PlatformProjectServiceBase implements IPlatformProjectService { - private static MIN_SUPPORTED_VERSION = 17; - private SUPPORTED_TARGETS = ["android-17", "android-18", "android-19", "android-21", "android-22"]; // forbidden for now: "android-MNC" - private static ANDROID_TARGET_PREFIX = "android"; private static VALUES_DIRNAME = "values"; private static VALUES_VERSION_DIRNAME_PREFIX = AndroidProjectService.VALUES_DIRNAME + "-v"; private static ANDROID_PLATFORM_NAME = "android"; @@ -21,6 +18,7 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService private _androidProjectPropertiesManagers: IDictionary; constructor(private $androidEmulatorServices: Mobile.IEmulatorPlatformServices, + private $androidToolsInfo: IAndroidToolsInfo, private $childProcess: IChildProcess, private $errors: IErrors, private $hostInfo: IHostInfo, @@ -86,13 +84,13 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService this.$errors.fail(`The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`); } + // TODO: Move these check to validate method once we do not support ant. this.checkGradle().wait(); this.$fs.ensureDirectoryExists(projectRoot).wait(); - - let newTarget = this.getAndroidTarget().wait(); + let androidToolsInfo = this.$androidToolsInfo.getToolsInfo().wait(); + let newTarget = androidToolsInfo.targetSdkVersion; this.$logger.trace(`Using Android SDK '${newTarget}'.`); - let versionNumber = _.last(newTarget.split("-")); if(this.$options.symlink) { this.symlinkDirectory("build-tools", projectRoot, frameworkDir).wait(); this.symlinkDirectory("libs", projectRoot, frameworkDir).wait(); @@ -105,16 +103,15 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService this.copy(projectRoot, frameworkDir, "build.gradle settings.gradle", "-f"); } - this.cleanResValues(versionNumber).wait(); + this.cleanResValues(newTarget).wait(); }).future()(); } - private cleanResValues(versionNumber: string): IFuture { + private cleanResValues(versionNumber: number): IFuture { return (() => { let resDestinationDir = this.getAppResourcesDestinationDirectoryPath().wait(); let directoriesInResFolder = this.$fs.readDirectory(resDestinationDir).wait(); - let integerFrameworkVersion = parseInt(versionNumber); let directoriesToClean = directoriesInResFolder .map(dir => { return { dirName: dir, @@ -123,7 +120,7 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }) .filter(dir => dir.dirName.match(AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX) && dir.sdkNum - && (!integerFrameworkVersion || (integerFrameworkVersion < dir.sdkNum))) + && (!versionNumber || (versionNumber < dir.sdkNum))) .map(dir => path.join(resDestinationDir, dir.dirName)); this.$logger.trace("Directories to clean:"); this.$logger.trace(directoriesToClean); @@ -143,24 +140,12 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService let gradleSettingsFilePath = path.join(this.platformData.projectRoot, "settings.gradle"); shell.sed('-i', /__PROJECT_NAME__/, this.$projectData.projectName, gradleSettingsFilePath); + shell.sed('-i', /__APILEVEL__/, this.$options.sdk || this.$androidToolsInfo.getToolsInfo().wait().compileSdkVersion.toString(), manifestPath); }).future()(); } public afterCreateProject(projectRoot: string): IFuture { - return (() => { - let targetApi = this.getAndroidTarget().wait(); - this.$logger.trace(`Adroid target: ${targetApi}`); - this.adjustMinSdk(projectRoot).wait(); - }).future()(); - } - - private adjustMinSdk(projectRoot: string): IFuture { - return (() => { - let apiLevel = this.getApiLevel().wait(); - if (apiLevel === "MNC") { // MNC SDK requires that minSdkVersion is set to "MNC" - shell.sed('-i', /android:minSdkVersion=".*?"/, `android:minSdkVersion="${apiLevel}"`, this.platformData.configurationFilePath); - } - }).future()(); + return Future.fromResult(); } public canUpdatePlatform(currentVersion: string, newVersion: string): IFuture { @@ -174,11 +159,17 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService public buildProject(projectRoot: string, buildConfig?: IBuildConfig): IFuture { return (() => { if(this.canUseGradle().wait()) { - // note, compileSdk and targetSdk should be the same - let targetSdk = this.getAndroidTarget().wait().replace("android-", ""); + this.checkGradle().wait(); + let androidToolsInfo = this.$androidToolsInfo.getToolsInfo().wait(); + let compileSdk = androidToolsInfo.compileSdkVersion; + let targetSdk = this.getTargetFromAndroidManifest().wait() || compileSdk; + let buildToolsVersion = androidToolsInfo.buildToolsVersion; + let appCompatVersion = androidToolsInfo.supportLibraryVersion; let buildOptions = ["buildapk", - `-PcompileSdk=${this.getAndroidTarget().wait()}`, - `-PtargetSdk=${targetSdk}` + `-PcompileSdk=android-${compileSdk}`, + `-PtargetSdk=${targetSdk}`, + `-PbuildToolsVersion=${buildToolsVersion}`, + `-PsupportVersion=${appCompatVersion}`, ]; if(this.$options.release) { @@ -349,60 +340,23 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService } } - private getAndroidTarget(): IFuture { + private getTargetFromAndroidManifest(): IFuture { return ((): string => { - let newTarget = this.$options.sdk ? `${AndroidProjectService.ANDROID_TARGET_PREFIX}-${this.$options.sdk}` : this.getLatestValidAndroidTarget().wait(); - if(!_.contains(this.SUPPORTED_TARGETS, newTarget)) { - let versionNumber = parseInt(_.last(newTarget.split("-"))); - if(versionNumber && (versionNumber < AndroidProjectService.MIN_SUPPORTED_VERSION)) { - this.$errors.failWithoutHelp(`The selected target SDK ${newTarget} is not supported. You should target at least ${AndroidProjectService.MIN_SUPPORTED_VERSION}.`); - } - - if(!_.contains(this.getInstalledTargets().wait(), newTarget)) { - this.$errors.failWithoutHelp(`You have selected to use ${newTarget}, but it is not currently installed.`+ - ' Run \"android\" from your command-line to install/update any missing SDKs or tools.'); + let versionInManifest: string; + if (this.$fs.exists(this.platformData.configurationFilePath).wait()) { + let targetFromAndroidManifest: string = this.$fs.readText(this.platformData.configurationFilePath).wait(); + if(targetFromAndroidManifest) { + let match = targetFromAndroidManifest.match(/.*?android:targetSdkVersion=\"(.*?)\"/); + if(match && match[1]) { + versionInManifest = match[1]; + } } - this.$logger.warn(`The selected Android target '${newTarget}' is not verified as supported. Some functionality may not work as expected.`); - } - - return newTarget; - }).future()(); - } - - private getLatestValidAndroidTarget(): IFuture { - return (() => { - let installedTargets = this.getInstalledTargets().wait(); - - // adjust to the latest available version - let newTarget = _(this.SUPPORTED_TARGETS).sort().findLast(supportedTarget => _.contains(installedTargets, supportedTarget)); - if (!newTarget) { - this.$errors.failWithoutHelp(`Could not find supported Android target. Please install one of the following: ${this.SUPPORTED_TARGETS.join(", ")}.` + - " Make sure you have the latest Android tools installed as well." + - ' Run "android" from your command-line to install/update any missing SDKs or tools.'); } - return newTarget; - }).future()(); - } - - private getApiLevel(): IFuture { - return (() => { - return this.getAndroidTarget().wait().split('-')[1]; + return versionInManifest; }).future()(); } - private installedTargetsCache: string[] = null; - 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)); - } - return this.installedTargetsCache; - }).future()(); - } - private checkJava(): IFuture { return (() => { try { @@ -428,11 +382,11 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService private checkGradle(): IFuture { return (() => { - try { - this.$childProcess.exec("gradle -v").wait(); - } catch(error) { + if(!this.$sysInfo.getSysInfo().gradleVer) { this.$errors.fail("Error executing commands 'gradle', make sure you have gradle installed and added to your PATH."); } + + this.$androidToolsInfo.validateInfo({showWarningsAsErrors: true, validateTargetSdk: true}).wait(); }).future()(); } diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 90002b34ec..dd12b92ca2 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -6,10 +6,10 @@ import * as helpers from "../common/helpers"; class DoctorService implements IDoctorService { private static MIN_SUPPORTED_GRADLE_VERSION = "2.3"; - constructor( + constructor(private $androidToolsInfo: IAndroidToolsInfo, private $hostInfo: IHostInfo, - private $sysInfo: ISysInfo, - private $logger: ILogger) { } + private $logger: ILogger, + private $sysInfo: ISysInfo) { } public printWarnings(): boolean { let result = false; @@ -84,6 +84,7 @@ class DoctorService implements IDoctorService { " described in https://github.com/NativeScript/nativescript-cli#system-requirements."); } + this.$androidToolsInfo.validateInfo().wait(); return result; }