diff --git a/lib/common b/lib/common index f62630430e..b77ec28c3e 160000 --- a/lib/common +++ b/lib/common @@ -1 +1 @@ -Subproject commit f62630430e91e21fb86793aadf72cdd5c8a2682e +Subproject commit b77ec28c3efff729381c226d096be4e538742e5b diff --git a/lib/services/android-project-service.ts b/lib/services/android-project-service.ts index a248528a09..c67d58f056 100644 --- a/lib/services/android-project-service.ts +++ b/lib/services/android-project-service.ts @@ -5,21 +5,19 @@ import * as shell from "shelljs"; import Future = require("fibers/future"); import * as constants from "../constants"; import * as semver from "semver"; -import * as androidProjectPropertiesManagerLib from "./android-project-properties-manager"; 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 RES_DIRNAME = "res"; private static VALUES_DIRNAME = "values"; 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 targetApi: string; private _androidProjectPropertiesManagers: IDictionary; constructor(private $androidEmulatorServices: Mobile.IEmulatorPlatformServices, @@ -30,6 +28,7 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService private $logger: ILogger, private $options: IOptions, private $projectData: IProjectData, + private $projectDataService: IProjectDataService, private $propertiesParser: IPropertiesParser, private $sysInfo: ISysInfo, $fs: IFileSystem) { @@ -45,19 +44,21 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService this._platformData = { frameworkPackageName: "tns-android", normalizedPlatformName: "Android", - appDestinationDirectoryPath: path.join(projectRoot, "assets"), - appResourcesDestinationDirectoryPath: path.join(projectRoot, "res"), + appDestinationDirectoryPath: path.join(projectRoot, "src", "main", "assets"), + appResourcesDestinationDirectoryPath: path.join(projectRoot, "src", "main", "res"), platformProjectService: this, emulatorServices: this.$androidEmulatorServices, projectRoot: projectRoot, - deviceBuildOutputPath: path.join(this.$projectData.platformsDir, "android", "bin"), + deviceBuildOutputPath: path.join(projectRoot, "build", "outputs", "apk"), validPackageNamesForDevice: [ `${this.$projectData.projectName}-debug.apk`, - `${this.$projectData.projectName}-release.apk` + `${this.$projectData.projectName}-release.apk`, + "android-debug.apk", + "android-release.apk" ], frameworkFilesExtensions: [".jar", ".dat", ".so"], configurationFileName: "AndroidManifest.xml", - configurationFilePath: path.join(this.$projectData.platformsDir, "android", "AndroidManifest.xml"), + configurationFilePath: path.join(projectRoot, "src", "main", "AndroidManifest.xml"), mergeXmlConfig: [{ "nodename": "manifest", "attrname": "*" }, {"nodename": "application", "attrname": "*"}] }; } @@ -71,47 +72,44 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService this.validateProjectName(this.$projectData.projectName); this.checkJava().wait(); - this.checkAnt().wait(); this.checkAndroid().wait(); }).future()(); } public createProject(projectRoot: string, frameworkDir: string): IFuture { return (() => { + let frameworkVersion = this.$fs.readJson(path.join(frameworkDir, "../", "package.json")).wait().version; + if(semver.lt(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE)) { + this.$errors.fail(`The NativeScript CLI requires Android runtime ${AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE} or later to work properly.`); + } + + this.checkGradle().wait(); + this.$fs.ensureDirectoryExists(projectRoot).wait(); - let newTarget = this.getAndroidTarget(frameworkDir).wait(); + let newTarget = this.getAndroidTarget().wait(); this.$logger.trace(`Using Android SDK '${newTarget}'.`); let versionNumber = _.last(newTarget.split("-")); if(this.$options.symlink) { - this.copyResValues(projectRoot, frameworkDir, versionNumber).wait(); - this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties custom_rules.xml", "-f").wait(); - - this.symlinkDirectory("assets", projectRoot, frameworkDir).wait(); + this.symlinkDirectory("build-tools", projectRoot, frameworkDir).wait(); this.symlinkDirectory("libs", projectRoot, frameworkDir).wait(); - } else { - this.copyResValues(projectRoot, frameworkDir, versionNumber).wait(); - this.copy(projectRoot, frameworkDir, "assets libs", "-R").wait(); - this.copy(projectRoot, frameworkDir, ".project AndroidManifest.xml project.properties custom_rules.xml", "-f").wait(); - } + this.symlinkDirectory("src", projectRoot, frameworkDir).wait(); - if(newTarget) { - this.updateTarget(projectRoot, newTarget).wait(); + this.$fs.symlink(path.join(frameworkDir, "build.gradle"), path.join(projectRoot, "build.gradle")).wait(); + } else { + this.copy(projectRoot, frameworkDir, "build-tools libs src", "-R"); + this.copy(projectRoot, frameworkDir, "build.gradle", "-f"); } - // Create src folder - let packageName = this.$projectData.projectId; - let packageAsPath = packageName.replace(/\./g, path.sep); - let activityDir = path.join(projectRoot, 'src', packageAsPath); - this.$fs.createDirectory(activityDir).wait(); + this.copyResValues(projectRoot, frameworkDir, versionNumber).wait(); }).future()(); } private copyResValues(projectRoot: string, frameworkDir: string, versionNumber: string): IFuture { return (() => { - let resSourceDir = path.join(frameworkDir, AndroidProjectService.RES_DIRNAME); - let resDestinationDir = path.join(projectRoot, AndroidProjectService.RES_DIRNAME); + let resSourceDir = path.join(frameworkDir, "src", "main", "res"); + let resDestinationDir = this.platformData.appResourcesDestinationDirectoryPath; this.$fs.createDirectory(resDestinationDir).wait(); let versionDirName = AndroidProjectService.VALUES_VERSION_DIRNAME_PREFIX + versionNumber; let directoriesToCopy = [AndroidProjectService.VALUES_DIRNAME]; @@ -133,43 +131,38 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService directoriesToCopy.push(versionDir); } - this.copy(resDestinationDir, resSourceDir, directoriesToCopy.join(" "), "-R").wait(); + this.copy(resDestinationDir, resSourceDir, directoriesToCopy.join(" "), "-Rf"); }).future()(); } public interpolateData(projectRoot: string): IFuture { return (() => { // Interpolate the activity name and package - let manifestPath = path.join(projectRoot, "AndroidManifest.xml"); + let manifestPath = this.platformData.configurationFilePath; shell.sed('-i', /__PACKAGE__/, this.$projectData.projectId, manifestPath); - shell.sed('-i', /__APILEVEL__/, this.getTarget(projectRoot).wait().split('-')[1], manifestPath); + shell.sed('-i', /__APILEVEL__/, this.getApiLevel().wait(), manifestPath); - let stringsFilePath = path.join(projectRoot, 'res', 'values', 'strings.xml'); + let stringsFilePath = path.join(this.platformData.appResourcesDestinationDirectoryPath, 'values', 'strings.xml'); shell.sed('-i', /__NAME__/, this.$projectData.projectName, stringsFilePath); shell.sed('-i', /__TITLE_ACTIVITY__/, this.$projectData.projectName, stringsFilePath); - shell.sed('-i', /__NAME__/, this.$projectData.projectName, path.join(projectRoot, '.project')); }).future()(); } public afterCreateProject(projectRoot: string): IFuture { return (() => { - let targetApi = this.getTarget(projectRoot).wait(); - this.$logger.trace("Android target: %s", targetApi); - this.runAndroidUpdate(projectRoot, targetApi).wait(); - this.adjustMinSdk(projectRoot); + let targetApi = this.getAndroidTarget().wait(); + this.$logger.trace(`Adroid target: ${targetApi}`); + this.adjustMinSdk(projectRoot).wait(); }).future()(); } - private adjustMinSdk(projectRoot: string): void { - let manifestPath = path.join(projectRoot, "AndroidManifest.xml"); - let apiLevel = this.getTarget(projectRoot).wait().split('-')[1]; - if (apiLevel === "MNC") { // MNC SDK requires that minSdkVersion is set to "MNC" - shell.sed('-i', /android:minSdkVersion=".*?"/, `android:minSdkVersion="${apiLevel}"`, manifestPath); - } - } - - public getDebugOnDeviceSetup(): Mobile.IDebugOnDeviceSetup { - return { }; + 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()(); } public canUpdatePlatform(currentVersion: string, newVersion: string): IFuture { @@ -181,49 +174,31 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService } public buildProject(projectRoot: string): IFuture { - let buildConfiguration = this.$options.release ? "release" : "debug"; - let args = this.getAntArgs(buildConfiguration, projectRoot); - return this.spawn('ant', args); - } - - public isPlatformPrepared(projectRoot: string): IFuture { - return this.$fs.exists(path.join(projectRoot, "assets", constants.APP_FOLDER_NAME)); - } - - private getProjectPropertiesManager(filePath: string): IAndroidProjectPropertiesManager { - if(!this._androidProjectPropertiesManagers[filePath]) { - this._androidProjectPropertiesManagers[filePath] = this.$injector.resolve(androidProjectPropertiesManagerLib.AndroidProjectPropertiesManager, { directoryPath: filePath }); - } - - return this._androidProjectPropertiesManagers[filePath]; - } - - private parseProjectProperties(projDir: string, destDir: string): IFuture { // projDir is libraryPath, targetPath is the path to lib folder return (() => { - projDir = projDir.trim(); - let projProp = path.join(projDir, "project.properties"); - if (!this.$fs.exists(projProp).wait()) { - this.$logger.warn("Warning: File %s does not exist", projProp); - return; - } - - let projectPropertiesManager = this.getProjectPropertiesManager(projDir); - let references = projectPropertiesManager.getProjectReferences().wait(); - _.each(references, reference => { - let adjustedPath = this.$fs.isRelativePath(reference.path) ? path.join(projDir, reference.path) : reference.path; - this.parseProjectProperties(adjustedPath, destDir).wait(); - }); + if(this.canUseGradle().wait()) { + let buildOptions = ["buildapk"]; + if(this.$options.release) { + buildOptions.push("-Prelease"); + buildOptions.push(`-PksPath=${this.$options.keyStorePath}`); + buildOptions.push(`-Palias=${this.$options.keyStoreAlias}`); + buildOptions.push(`-Ppassword=${this.$options.keyStoreAliasPassword}`); + buildOptions.push(`-PksPassword=${this.$options.keyStorePassword}`); + } - this.$logger.info("Copying %s", projDir); - shell.cp("-Rf", projDir, destDir); + this.spawn("gradle", buildOptions, { stdio: "inherit", cwd: this.platformData.projectRoot }).wait(); + } else { + this.checkAnt().wait(); - let targetDir = path.join(destDir, path.basename(projDir)); - let targetSdk = `android-${this.$options.sdk || AndroidProjectService.MIN_SUPPORTED_VERSION}`; - this.$logger.info("Generate build.xml for %s", targetDir); - this.runAndroidUpdate(targetDir, targetSdk).wait(); + let args = this.getAntArgs(this.$options.release ? "release" : "debug", projectRoot); + this.spawn('ant', args).wait(); + } }).future()(); } + public isPlatformPrepared(projectRoot: string): IFuture { + return this.$fs.exists(path.join(this.platformData.appDestinationDirectoryPath, constants.APP_FOLDER_NAME)); + } + public addLibrary(libraryPath: string): IFuture { return (() => { let name = path.basename(libraryPath); @@ -232,20 +207,10 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService let targetPath = path.dirname(targetLibPath); this.$fs.ensureDirectoryExists(targetPath).wait(); - if(this.$fs.exists(path.join(libraryPath, "project.properties")).wait()) { - this.parseProjectProperties(libraryPath, targetPath).wait(); - } - shell.cp("-f", path.join(libraryPath, "*.jar"), targetPath); let projectLibsDir = path.join(this.platformData.projectRoot, "libs"); this.$fs.ensureDirectoryExists(projectLibsDir).wait(); shell.cp("-f", path.join(libraryPath, "*.jar"), projectLibsDir); - - let libProjProp = path.join(libraryPath, "project.properties"); - if (this.$fs.exists(libProjProp).wait()) { - this.updateProjectReferences(this.platformData.projectRoot, targetLibPath).wait(); - this.runAndroidUpdate(targetLibPath, this.getTarget(this.platformData.projectRoot).wait()).wait(); - } }).future()(); } @@ -277,25 +242,16 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService if(this.$fs.exists(libsFolderPath).wait()) { this.addLibrary(libsFolderPath).wait(); } - - // Handle android libraries - let librarries = this.getAllLibrariesForPlugin(pluginData).wait(); - _.each(librarries, libraryName => this.addLibrary(path.join(pluginPlatformsFolderPath, libraryName)).wait()); }).future()(); } public removePluginNativeCode(pluginData: IPluginData): IFuture { return (() => { - let projectPropertiesManager = this.getProjectPropertiesManager(this.platformData.projectRoot); - - let libraries = this.getAllLibrariesForPlugin(pluginData).wait(); - _.each(libraries, libraryName => { - let libraryPath = this.getLibraryPath(libraryName); - let libraryRelativePath = this.getLibraryRelativePath(this.platformData.projectRoot, libraryPath); - projectPropertiesManager.removeProjectReference(libraryRelativePath).wait(); - this.$fs.deleteDirectory(libraryPath).wait(); - }); + let pluginPlatformsFolderPath = this.getPluginPlatformsFolderPath(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME); + let pluginJars = this.$fs.enumerateFilesInDirectorySync(path.join(pluginPlatformsFolderPath, AndroidProjectService.LIBS_FOLDER_NAME)); + let libsFolderPath = path.join(pluginPlatformsFolderPath, AndroidProjectService.LIBS_FOLDER_NAME); + _.each(pluginJars, jarName => this.$fs.deleteFile(path.join(libsFolderPath, jarName)).wait()); }).future()(); } @@ -303,42 +259,30 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService return Future.fromResult(); } - private getLibraryRelativePath(basePath: string, libraryPath: string): string { - return path.relative(basePath, libraryPath).split("\\").join("/"); + private canUseGradle(): IFuture { + return (() => { + this.$projectDataService.initialize(this.$projectData.projectDir); + let frameworkVersion = this.$projectDataService.getValue(this.platformData.frameworkPackageName).wait().version; + return semver.gte(frameworkVersion, AndroidProjectService.MIN_RUNTIME_VERSION_WITH_GRADLE); + }).future()(); } private getLibraryPath(libraryName: string): string { return path.join(this.$projectData.projectDir, "lib", this.platformData.normalizedPlatformName, libraryName); } - private updateProjectReferences(projDir: string, libraryPath: string): IFuture { - let relLibDir = this.getLibraryRelativePath(projDir, libraryPath); - - let projectPropertiesManager = this.getProjectPropertiesManager(projDir); - return projectPropertiesManager.addProjectReference(relLibDir); - } - - private getAllLibrariesForPlugin(pluginData: IPluginData): IFuture { - return (() => { - let filterCallback = (fileName: string, pluginPlatformsFolderPath: string) => fileName !== AndroidProjectService.LIBS_FOLDER_NAME && this.$fs.exists(path.join(pluginPlatformsFolderPath, fileName, "project.properties")).wait(); - return this.getAllNativeLibrariesForPlugin(pluginData, AndroidProjectService.ANDROID_PLATFORM_NAME, filterCallback).wait(); - }).future()(); - } - - private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): IFuture { - return (() => { - let paths = files.split(' ').map(p => path.join(frameworkDir, p)); - shell.cp(cpArg, paths, projectRoot); - }).future()(); + private copy(projectRoot: string, frameworkDir: string, files: string, cpArg: string): void { + let paths = files.split(' ').map(p => path.join(frameworkDir, p)); + shell.cp(cpArg, paths, projectRoot); } - private spawn(command: string, args: string[]): IFuture { + private spawn(command: string, args: string[], opts?: any): IFuture { if (this.$hostInfo.isWindows) { args.unshift('/s', '/c', command); command = process.env.COMSPEC || 'cmd.exe'; } - return this.$childProcess.spawnFromEvent(command, args, "close", {stdio: "inherit"}); + return this.$childProcess.spawnFromEvent(command, args, "close", opts || { stdio: "inherit"}); } private getAntArgs(configuration: string, projectRoot: string): string[] { @@ -367,16 +311,6 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService return args; } - private runAndroidUpdate(projectPath: string, targetApi: string): IFuture { - let args = [ - "--path", projectPath, - "--target", targetApi, - "--name", this.$projectData.projectName - ]; - - return this.spawn("android", ['update', 'project'].concat(args)); - } - private validatePackageName(packageName: string): void { //Make the package conform to Java package types //Enforce underscore limitation @@ -401,9 +335,9 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService } } - private getAndroidTarget(frameworkDir: string): IFuture { + private getAndroidTarget(): IFuture { return ((): string => { - let newTarget = this.$options.sdk ? `${AndroidProjectService.ANDROID_TARGET_PREFIX}-${this.$options.sdk}` : this.getLatestValidAndroidTarget(frameworkDir).wait(); + 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)) { @@ -421,7 +355,7 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }).future()(); } - private getLatestValidAndroidTarget(frameworkDir: string): IFuture { + private getLatestValidAndroidTarget(): IFuture { return (() => { let installedTargets = this.getInstalledTargets().wait(); @@ -437,22 +371,10 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }).future()(); } - private updateTarget(projectRoot: string, newTarget: string): IFuture { + private getApiLevel(): IFuture { return (() => { - let file = path.join(projectRoot, "project.properties"); - let editor = this.$propertiesParser.createEditor(file).wait(); - editor.set("target", newTarget); - let future = new Future(); - editor.save((err:any) => { - if (err) { - future.throw(err); - } else { - this.targetApi = null; // so that later we can repopulate the cache - future.return(); - } - }); - future.wait(); - }).future()(); + return this.getAndroidTarget().wait().split('-')[1]; + }).future()(); } private installedTargetsCache: string[] = null; @@ -467,21 +389,6 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }).future()(); } - private getTarget(projectRoot: string): IFuture { - return (() => { - if(!this.targetApi) { - let projectPropertiesFilePath = path.join(projectRoot, "project.properties"); - - if (this.$fs.exists(projectPropertiesFilePath).wait()) { - let properties = this.$propertiesParser.createEditor(projectPropertiesFilePath).wait(); - this.targetApi = properties.get("target"); - } - } - - return this.targetApi; - }).future()(); - } - private checkJava(): IFuture { return (() => { try { @@ -505,6 +412,16 @@ class AndroidProjectService extends projectServiceBaseLib.PlatformProjectService }).future()(); } + private checkGradle(): IFuture { + return (() => { + try { + this.$childProcess.exec("gradle -v").wait(); + } catch(error) { + this.$errors.fail("Error executing commands 'gradle', make sure you have gradle installed and added to your PATH."); + } + }).future()(); + } + private checkAndroid(): IFuture { return (() => { try { diff --git a/lib/services/doctor-service.ts b/lib/services/doctor-service.ts index 87f70ce474..9983b862f9 100644 --- a/lib/services/doctor-service.ts +++ b/lib/services/doctor-service.ts @@ -1,8 +1,10 @@ /// "use strict"; import {EOL} from "os"; +import * as semver from "semver"; class DoctorService implements IDoctorService { + private static MIN_SUPPORTED_GRADLE_VERSION = "2.3"; constructor( private $hostInfo: IHostInfo, @@ -63,6 +65,24 @@ class DoctorService implements IDoctorService { result = true; } + if(!sysInfo.gradleVer) { + this.$logger.warn("WARNING: Gradle is not installed or is not configured properly."); + this.$logger.out("You will not be able to build your projects for Android or run them in the emulator or on a connected device." + EOL + + "To be able to build for Android and run apps in the emulator on on a connected device, verify that you have installed Gradle."); + } + if(sysInfo.gradleVer && semver.lt(sysInfo.gradleVer, DoctorService.MIN_SUPPORTED_GRADLE_VERSION)) { + this.$logger.warn(`WARNING: Gradle version is lower than ${DoctorService.MIN_SUPPORTED_GRADLE_VERSION}.`); + this.$logger.out("You will not be able to build your projects for Android or run them in the emulator or on a connected device." + EOL + + `To be able to build for Android and run apps in the emulator on on a connected device, verify thqt you have at least ${DoctorService.MIN_SUPPORTED_GRADLE_VERSION} version installed.`); + } + + 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."); + } + return result; } diff --git a/test/stubs.ts b/test/stubs.ts index bf2bfc9439..4a7a2be1f1 100644 --- a/test/stubs.ts +++ b/test/stubs.ts @@ -301,9 +301,6 @@ export class PlatformProjectServiceStub implements IPlatformProjectService { addLibrary(libraryPath: string): IFuture { return Future.fromResult(); } - getDebugOnDeviceSetup(): Mobile.IDebugOnDeviceSetup { - return null; - } canUpdatePlatform(currentVersion: string, newVersion: string): IFuture { return Future.fromResult(false); }