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;
}