diff --git a/LICENSE b/LICENSE index fd26dc71..c47288d4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ - + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/bin/check_reqs.bat b/bin/check_reqs.bat index cb2c6f54..fd7b7d17 100644 --- a/bin/check_reqs.bat +++ b/bin/check_reqs.bat @@ -5,9 +5,9 @@ :: to you under the Apache License, Version 2.0 (the :: "License"); you may not use this file except in compliance :: with the License. You may obtain a copy of the License at -:: +:: :: http://www.apache.org/licenses/LICENSE-2.0 -:: +:: :: Unless required by applicable law or agreed to in writing, :: software distributed under the License is distributed on an :: "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY diff --git a/bin/create b/bin/create index bdbbc80a..2052a27e 100755 --- a/bin/create +++ b/bin/create @@ -1,56 +1,56 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ -var path = require('path'); -var ConfigParser = require('cordova-common').ConfigParser; -var Api = require('./templates/cordova/Api'); - -var argv = require('nopt')({ - 'help' : Boolean, - 'cli' : Boolean, - 'shared' : Boolean, - 'link' : Boolean, - 'activity-name' : [String, undefined] -}); - -if (argv.help || argv.argv.remain.length === 0) { - console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'create')) + ' [] [--activity-name ] [--link]'); - console.log(' : Path to your new Cordova Android project'); - console.log(' : Package name, following reverse-domain style convention'); - console.log(' : Project name'); - console.log(' : Path to a custom application template to use'); - console.log(' --activity-name : Activity name'); - console.log(' --link will use the CordovaLib project directly instead of making a copy.'); - process.exit(1); -} - -var config = new ConfigParser(path.resolve(__dirname, 'templates/project/res/xml/config.xml')); - -if (argv.argv.remain[1]) config.setPackageName(argv.argv.remain[1]); -if (argv.argv.remain[2]) config.setName(argv.argv.remain[2]); -if (argv['activity-name']) config.setName(argv['activity-name']); - -var options = { - link: argv.link || argv.shared, - customTemplate: argv.argv.remain[3], - activityName: argv['activity-name'] -}; - -Api.createPlatform(argv.argv.remain[0], config, options).done(); +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +var path = require('path'); +var ConfigParser = require('cordova-common').ConfigParser; +var Api = require('./templates/cordova/Api'); + +var argv = require('nopt')({ + 'help' : Boolean, + 'cli' : Boolean, + 'shared' : Boolean, + 'link' : Boolean, + 'activity-name' : [String, undefined] +}); + +if (argv.help || argv.argv.remain.length === 0) { + console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'create')) + ' [] [--activity-name ] [--link]'); + console.log(' : Path to your new Cordova Android project'); + console.log(' : Package name, following reverse-domain style convention'); + console.log(' : Project name'); + console.log(' : Path to a custom application template to use'); + console.log(' --activity-name : Activity name'); + console.log(' --link will use the CordovaLib project directly instead of making a copy.'); + process.exit(1); +} + +var config = new ConfigParser(path.resolve(__dirname, 'templates/project/res/xml/config.xml')); + +if (argv.argv.remain[1]) config.setPackageName(argv.argv.remain[1]); +if (argv.argv.remain[2]) config.setName(argv.argv.remain[2]); +if (argv['activity-name']) config.setName(argv['activity-name']); + +var options = { + link: argv.link || argv.shared, + customTemplate: argv.argv.remain[3], + activityName: argv['activity-name'] +}; + +Api.createPlatform(argv.argv.remain[0], config, options).done(); diff --git a/bin/lib/check_reqs.js b/bin/lib/check_reqs.js index 57e1f47b..d3a93e1a 100644 --- a/bin/lib/check_reqs.js +++ b/bin/lib/check_reqs.js @@ -1,326 +1,326 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -/* jshint sub:true */ - -var shelljs = require('shelljs'), - child_process = require('child_process'), - Q = require('q'), - path = require('path'), - fs = require('fs'), - ROOT = path.join(__dirname, '..', '..'); -var CordovaError = require('cordova-common').CordovaError; - -var isWindows = process.platform == 'win32'; - -function forgivingWhichSync(cmd) { - try { - return fs.realpathSync(shelljs.which(cmd)); - } catch (e) { - return ''; - } -} - -function tryCommand(cmd, errMsg, catchStderr) { - var d = Q.defer(); - child_process.exec(cmd, function(err, stdout, stderr) { - if (err) d.reject(new CordovaError(errMsg)); - // Sometimes it is necessary to return an stderr instead of stdout in case of success, since - // some commands prints theirs output to stderr instead of stdout. 'javac' is the example - else d.resolve((catchStderr ? stderr : stdout).trim()); - }); - return d.promise; -} - -// Get valid target from framework/project.properties -module.exports.get_target = function() { - function extractFromFile(filePath) { - var target = shelljs.grep(/\btarget=/, filePath); - if (!target) { - throw new Error('Could not find android target within: ' + filePath); - } - return target.split('=')[1].trim(); - } - if (fs.existsSync(path.join(ROOT, 'framework', 'project.properties'))) { - return extractFromFile(path.join(ROOT, 'framework', 'project.properties')); - } - if (fs.existsSync(path.join(ROOT, 'project.properties'))) { - // if no target found, we're probably in a project and project.properties is in ROOT. - return extractFromFile(path.join(ROOT, 'project.properties')); - } - throw new Error('Could not find android target. File missing: ' + path.join(ROOT, 'project.properties')); -}; - -// Returns a promise. Called only by build and clean commands. -module.exports.check_ant = function() { - return tryCommand('ant -version', 'Failed to run "ant -version", make sure you have ant installed and added to your PATH.') - .then(function (output) { - // Parse Ant version from command output - return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; - }); -}; - -// Returns a promise. Called only by build and clean commands. -module.exports.check_gradle = function() { - var sdkDir = process.env['ANDROID_HOME']; - if (!sdkDir) - return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.\n' + - 'Might need to install Android SDK or set up \'ANDROID_HOME\' env variable.')); - - var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); - if (!fs.existsSync(wrapperDir)) { - return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + - 'Looked here: ' + wrapperDir)); - } - return Q.when(); -}; - -// Returns a promise. -module.exports.check_java = function() { - var javacPath = forgivingWhichSync('javac'); - var hasJavaHome = !!process.env['JAVA_HOME']; - return Q().then(function() { - if (hasJavaHome) { - // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh). - if (!javacPath) { - process.env['PATH'] += path.delimiter + path.join(process.env['JAVA_HOME'], 'bin'); - } - } else { - if (javacPath) { - var msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting setting it manually.'; - // OS X has a command for finding JAVA_HOME. - if (fs.existsSync('/usr/libexec/java_home')) { - return tryCommand('/usr/libexec/java_home', msg) - .then(function(stdout) { - process.env['JAVA_HOME'] = stdout.trim(); - }); - } else { - // See if we can derive it from javac's location. - // fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK - var maybeJavaHome = path.dirname(path.dirname(javacPath)); - if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { - process.env['JAVA_HOME'] = maybeJavaHome; - } else { - throw new CordovaError(msg); - } - } - } else if (isWindows) { - // Try to auto-detect java in the default install paths. - var oldSilent = shelljs.config.silent; - shelljs.config.silent = true; - var firstJdkDir = - shelljs.ls(process.env['ProgramFiles'] + '\\java\\jdk*')[0] || - shelljs.ls('C:\\Program Files\\java\\jdk*')[0] || - shelljs.ls('C:\\Program Files (x86)\\java\\jdk*')[0]; - shelljs.config.silent = oldSilent; - if (firstJdkDir) { - // shelljs always uses / in paths. - firstJdkDir = firstJdkDir.replace(/\//g, path.sep); - if (!javacPath) { - process.env['PATH'] += path.delimiter + path.join(firstJdkDir, 'bin'); - } - process.env['JAVA_HOME'] = firstJdkDir; - } - } - } - }).then(function() { - var msg = - 'Failed to run "java -version", make sure that you have a JDK installed.\n' + - 'You can get it from: http://www.oracle.com/technetwork/java/javase/downloads.\n'; - if (process.env['JAVA_HOME']) { - msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'] + '\n'; - } - return tryCommand('java -version', msg) - .then(function() { - // We use tryCommand with catchStderr = true, because - // javac writes version info to stderr instead of stdout - return tryCommand('javac -version', msg, true); - }).then(function (output) { - var match = /javac ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; - return match && match[1]; - }); - }); -}; - -// Returns a promise. -module.exports.check_android = function() { - return Q().then(function() { - var androidCmdPath = forgivingWhichSync('android'); - var adbInPath = !!forgivingWhichSync('adb'); - var hasAndroidHome = !!process.env['ANDROID_HOME'] && fs.existsSync(process.env['ANDROID_HOME']); - function maybeSetAndroidHome(value) { - if (!hasAndroidHome && fs.existsSync(value)) { - hasAndroidHome = true; - process.env['ANDROID_HOME'] = value; - } - } - if (!hasAndroidHome && !androidCmdPath) { - if (isWindows) { - // Android Studio 1.0 installer - maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk')); - maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk')); - // Android Studio pre-1.0 installer - maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-studio', 'sdk')); - maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-studio', 'sdk')); - // Stand-alone installer - maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk')); - maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk')); - } else if (process.platform == 'darwin') { - // Android Studio 1.0 installer - maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk')); - // Android Studio pre-1.0 installer - maybeSetAndroidHome('/Applications/Android Studio.app/sdk'); - // Stand-alone zip file that user might think to put under /Applications - maybeSetAndroidHome('/Applications/android-sdk-macosx'); - maybeSetAndroidHome('/Applications/android-sdk'); - } - if (process.env['HOME']) { - // Stand-alone zip file that user might think to put under their home directory - maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk-macosx')); - maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk')); - } - } - if (hasAndroidHome && !androidCmdPath) { - process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools'); - } - if (androidCmdPath && !hasAndroidHome) { - var parentDir = path.dirname(androidCmdPath); - var grandParentDir = path.dirname(parentDir); - if (path.basename(parentDir) == 'tools') { - process.env['ANDROID_HOME'] = path.dirname(parentDir); - hasAndroidHome = true; - } else if (fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) { - process.env['ANDROID_HOME'] = grandParentDir; - hasAndroidHome = true; - } else { - throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + - 'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' + - 'Try reinstall Android SDK or update your PATH to include path to valid SDK directory.'); - } - } - if (hasAndroidHome && !adbInPath) { - process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); - } - if (!process.env['ANDROID_HOME']) { - throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + - 'Failed to find \'android\' command in your \'PATH\'. Try update your \'PATH\' to include path to valid SDK directory.'); - } - if (!fs.existsSync(process.env['ANDROID_HOME'])) { - throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env['ANDROID_HOME'] + - '\nTry update it manually to point to valid SDK directory.'); - } - }); -}; - -module.exports.getAbsoluteAndroidCmd = function() { - return forgivingWhichSync('android').replace(/(\s)/g, '\\$1'); -}; - -module.exports.check_android_target = function(valid_target) { - // valid_target can look like: - // android-19 - // android-L - // Google Inc.:Google APIs:20 - // Google Inc.:Glass Development Kit Preview:20 - if (!valid_target) valid_target = module.exports.get_target(); - var msg = 'Android SDK not found. Make sure that it is installed. If it is not at the default location, set the ANDROID_HOME environment variable.'; - return tryCommand('android list targets --compact', msg) - .then(function(output) { - var targets = output.split('\n'); - if (targets.indexOf(valid_target) >= 0) { - return targets; - } - - var androidCmd = module.exports.getAbsoluteAndroidCmd(); - throw new CordovaError('Please install Android target: "' + valid_target + '".\n\n' + - 'Hint: Open the SDK manager by running: ' + androidCmd + '\n' + - 'You will require:\n' + - '1. "SDK Platform" for ' + valid_target + '\n' + - '2. "Android SDK Platform-tools (latest)\n' + - '3. "Android SDK Build-tools" (latest)'); - }); -}; - -// Returns a promise. -module.exports.run = function() { - return Q.all([this.check_java(), this.check_android().then(this.check_android_target)]) - .then(function() { - console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']); - console.log('JAVA_HOME=' + process.env['JAVA_HOME']); - }); -}; - -/** - * Object thar represents one of requirements for current platform. - * @param {String} id The unique identifier for this requirements. - * @param {String} name The name of requirements. Human-readable field. - * @param {String} version The version of requirement installed. In some cases could be an array of strings - * (for example, check_android_target returns an array of android targets installed) - * @param {Boolean} installed Indicates whether the requirement is installed or not - */ -var Requirement = function (id, name, version, installed) { - this.id = id; - this.name = name; - this.installed = installed || false; - this.metadata = { - version: version, - }; -}; - -/** - * Methods that runs all checks one by one and returns a result of checks - * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method - * - * @return Promise Array of requirements. Due to implementation, promise is always fulfilled. - */ -module.exports.check_all = function() { - - var requirements = [ - new Requirement('java', 'Java JDK'), - new Requirement('androidSdk', 'Android SDK'), - new Requirement('androidTarget', 'Android target'), - new Requirement('gradle', 'Gradle') - ]; - - var checkFns = [ - this.check_java, - this.check_android, - this.check_android_target, - this.check_gradle - ]; - - // Then execute requirement checks one-by-one - return checkFns.reduce(function (promise, checkFn, idx) { - // Update each requirement with results - var requirement = requirements[idx]; - return promise.then(checkFn) - .then(function (version) { - requirement.installed = true; - requirement.metadata.version = version; - }, function (err) { - requirement.metadata.reason = err instanceof Error ? err.message : err; - }); - }, Q()) - .then(function () { - // When chain is completed, return requirements array to upstream API - return requirements; - }); -}; +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +/* jshint sub:true */ + +var shelljs = require('shelljs'), + child_process = require('child_process'), + Q = require('q'), + path = require('path'), + fs = require('fs'), + ROOT = path.join(__dirname, '..', '..'); +var CordovaError = require('cordova-common').CordovaError; + +var isWindows = process.platform == 'win32'; + +function forgivingWhichSync(cmd) { + try { + return fs.realpathSync(shelljs.which(cmd)); + } catch (e) { + return ''; + } +} + +function tryCommand(cmd, errMsg, catchStderr) { + var d = Q.defer(); + child_process.exec(cmd, function(err, stdout, stderr) { + if (err) d.reject(new CordovaError(errMsg)); + // Sometimes it is necessary to return an stderr instead of stdout in case of success, since + // some commands prints theirs output to stderr instead of stdout. 'javac' is the example + else d.resolve((catchStderr ? stderr : stdout).trim()); + }); + return d.promise; +} + +// Get valid target from framework/project.properties +module.exports.get_target = function() { + function extractFromFile(filePath) { + var target = shelljs.grep(/\btarget=/, filePath); + if (!target) { + throw new Error('Could not find android target within: ' + filePath); + } + return target.split('=')[1].trim(); + } + if (fs.existsSync(path.join(ROOT, 'framework', 'project.properties'))) { + return extractFromFile(path.join(ROOT, 'framework', 'project.properties')); + } + if (fs.existsSync(path.join(ROOT, 'project.properties'))) { + // if no target found, we're probably in a project and project.properties is in ROOT. + return extractFromFile(path.join(ROOT, 'project.properties')); + } + throw new Error('Could not find android target. File missing: ' + path.join(ROOT, 'project.properties')); +}; + +// Returns a promise. Called only by build and clean commands. +module.exports.check_ant = function() { + return tryCommand('ant -version', 'Failed to run "ant -version", make sure you have ant installed and added to your PATH.') + .then(function (output) { + // Parse Ant version from command output + return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; + }); +}; + +// Returns a promise. Called only by build and clean commands. +module.exports.check_gradle = function() { + var sdkDir = process.env['ANDROID_HOME']; + if (!sdkDir) + return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Could not find Android SDK directory.\n' + + 'Might need to install Android SDK or set up \'ANDROID_HOME\' env variable.')); + + var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); + if (!fs.existsSync(wrapperDir)) { + return Q.reject(new CordovaError('Could not find gradle wrapper within Android SDK. Might need to update your Android SDK.\n' + + 'Looked here: ' + wrapperDir)); + } + return Q.when(); +}; + +// Returns a promise. +module.exports.check_java = function() { + var javacPath = forgivingWhichSync('javac'); + var hasJavaHome = !!process.env['JAVA_HOME']; + return Q().then(function() { + if (hasJavaHome) { + // Windows java installer doesn't add javac to PATH, nor set JAVA_HOME (ugh). + if (!javacPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['JAVA_HOME'], 'bin'); + } + } else { + if (javacPath) { + var msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting setting it manually.'; + // OS X has a command for finding JAVA_HOME. + if (fs.existsSync('/usr/libexec/java_home')) { + return tryCommand('/usr/libexec/java_home', msg) + .then(function(stdout) { + process.env['JAVA_HOME'] = stdout.trim(); + }); + } else { + // See if we can derive it from javac's location. + // fs.realpathSync is require on Ubuntu, which symplinks from /usr/bin -> JDK + var maybeJavaHome = path.dirname(path.dirname(javacPath)); + if (fs.existsSync(path.join(maybeJavaHome, 'lib', 'tools.jar'))) { + process.env['JAVA_HOME'] = maybeJavaHome; + } else { + throw new CordovaError(msg); + } + } + } else if (isWindows) { + // Try to auto-detect java in the default install paths. + var oldSilent = shelljs.config.silent; + shelljs.config.silent = true; + var firstJdkDir = + shelljs.ls(process.env['ProgramFiles'] + '\\java\\jdk*')[0] || + shelljs.ls('C:\\Program Files\\java\\jdk*')[0] || + shelljs.ls('C:\\Program Files (x86)\\java\\jdk*')[0]; + shelljs.config.silent = oldSilent; + if (firstJdkDir) { + // shelljs always uses / in paths. + firstJdkDir = firstJdkDir.replace(/\//g, path.sep); + if (!javacPath) { + process.env['PATH'] += path.delimiter + path.join(firstJdkDir, 'bin'); + } + process.env['JAVA_HOME'] = firstJdkDir; + } + } + } + }).then(function() { + var msg = + 'Failed to run "java -version", make sure that you have a JDK installed.\n' + + 'You can get it from: http://www.oracle.com/technetwork/java/javase/downloads.\n'; + if (process.env['JAVA_HOME']) { + msg += 'Your JAVA_HOME is invalid: ' + process.env['JAVA_HOME'] + '\n'; + } + return tryCommand('java -version', msg) + .then(function() { + // We use tryCommand with catchStderr = true, because + // javac writes version info to stderr instead of stdout + return tryCommand('javac -version', msg, true); + }).then(function (output) { + var match = /javac ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; + return match && match[1]; + }); + }); +}; + +// Returns a promise. +module.exports.check_android = function() { + return Q().then(function() { + var androidCmdPath = forgivingWhichSync('android'); + var adbInPath = !!forgivingWhichSync('adb'); + var hasAndroidHome = !!process.env['ANDROID_HOME'] && fs.existsSync(process.env['ANDROID_HOME']); + function maybeSetAndroidHome(value) { + if (!hasAndroidHome && fs.existsSync(value)) { + hasAndroidHome = true; + process.env['ANDROID_HOME'] = value; + } + } + if (!hasAndroidHome && !androidCmdPath) { + if (isWindows) { + // Android Studio 1.0 installer + maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'sdk')); + maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'sdk')); + // Android Studio pre-1.0 installer + maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-studio', 'sdk')); + maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-studio', 'sdk')); + // Stand-alone installer + maybeSetAndroidHome(path.join(process.env['LOCALAPPDATA'], 'Android', 'android-sdk')); + maybeSetAndroidHome(path.join(process.env['ProgramFiles'], 'Android', 'android-sdk')); + } else if (process.platform == 'darwin') { + // Android Studio 1.0 installer + maybeSetAndroidHome(path.join(process.env['HOME'], 'Library', 'Android', 'sdk')); + // Android Studio pre-1.0 installer + maybeSetAndroidHome('/Applications/Android Studio.app/sdk'); + // Stand-alone zip file that user might think to put under /Applications + maybeSetAndroidHome('/Applications/android-sdk-macosx'); + maybeSetAndroidHome('/Applications/android-sdk'); + } + if (process.env['HOME']) { + // Stand-alone zip file that user might think to put under their home directory + maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk-macosx')); + maybeSetAndroidHome(path.join(process.env['HOME'], 'android-sdk')); + } + } + if (hasAndroidHome && !androidCmdPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'tools'); + } + if (androidCmdPath && !hasAndroidHome) { + var parentDir = path.dirname(androidCmdPath); + var grandParentDir = path.dirname(parentDir); + if (path.basename(parentDir) == 'tools') { + process.env['ANDROID_HOME'] = path.dirname(parentDir); + hasAndroidHome = true; + } else if (fs.existsSync(path.join(grandParentDir, 'tools', 'android'))) { + process.env['ANDROID_HOME'] = grandParentDir; + hasAndroidHome = true; + } else { + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + 'Detected \'android\' command at ' + parentDir + ' but no \'tools\' directory found near.\n' + + 'Try reinstall Android SDK or update your PATH to include path to valid SDK directory.'); + } + } + if (hasAndroidHome && !adbInPath) { + process.env['PATH'] += path.delimiter + path.join(process.env['ANDROID_HOME'], 'platform-tools'); + } + if (!process.env['ANDROID_HOME']) { + throw new CordovaError('Failed to find \'ANDROID_HOME\' environment variable. Try setting setting it manually.\n' + + 'Failed to find \'android\' command in your \'PATH\'. Try update your \'PATH\' to include path to valid SDK directory.'); + } + if (!fs.existsSync(process.env['ANDROID_HOME'])) { + throw new CordovaError('\'ANDROID_HOME\' environment variable is set to non-existent path: ' + process.env['ANDROID_HOME'] + + '\nTry update it manually to point to valid SDK directory.'); + } + }); +}; + +module.exports.getAbsoluteAndroidCmd = function() { + return forgivingWhichSync('android').replace(/(\s)/g, '\\$1'); +}; + +module.exports.check_android_target = function(valid_target) { + // valid_target can look like: + // android-19 + // android-L + // Google Inc.:Google APIs:20 + // Google Inc.:Glass Development Kit Preview:20 + if (!valid_target) valid_target = module.exports.get_target(); + var msg = 'Android SDK not found. Make sure that it is installed. If it is not at the default location, set the ANDROID_HOME environment variable.'; + return tryCommand('android list targets --compact', msg) + .then(function(output) { + var targets = output.split('\n'); + if (targets.indexOf(valid_target) >= 0) { + return targets; + } + + var androidCmd = module.exports.getAbsoluteAndroidCmd(); + throw new CordovaError('Please install Android target: "' + valid_target + '".\n\n' + + 'Hint: Open the SDK manager by running: ' + androidCmd + '\n' + + 'You will require:\n' + + '1. "SDK Platform" for ' + valid_target + '\n' + + '2. "Android SDK Platform-tools (latest)\n' + + '3. "Android SDK Build-tools" (latest)'); + }); +}; + +// Returns a promise. +module.exports.run = function() { + return Q.all([this.check_java(), this.check_android().then(this.check_android_target)]) + .then(function() { + console.log('ANDROID_HOME=' + process.env['ANDROID_HOME']); + console.log('JAVA_HOME=' + process.env['JAVA_HOME']); + }); +}; + +/** + * Object thar represents one of requirements for current platform. + * @param {String} id The unique identifier for this requirements. + * @param {String} name The name of requirements. Human-readable field. + * @param {String} version The version of requirement installed. In some cases could be an array of strings + * (for example, check_android_target returns an array of android targets installed) + * @param {Boolean} installed Indicates whether the requirement is installed or not + */ +var Requirement = function (id, name, version, installed) { + this.id = id; + this.name = name; + this.installed = installed || false; + this.metadata = { + version: version, + }; +}; + +/** + * Methods that runs all checks one by one and returns a result of checks + * as an array of Requirement objects. This method intended to be used by cordova-lib check_reqs method + * + * @return Promise Array of requirements. Due to implementation, promise is always fulfilled. + */ +module.exports.check_all = function() { + + var requirements = [ + new Requirement('java', 'Java JDK'), + new Requirement('androidSdk', 'Android SDK'), + new Requirement('androidTarget', 'Android target'), + new Requirement('gradle', 'Gradle') + ]; + + var checkFns = [ + this.check_java, + this.check_android, + this.check_android_target, + this.check_gradle + ]; + + // Then execute requirement checks one-by-one + return checkFns.reduce(function (promise, checkFn, idx) { + // Update each requirement with results + var requirement = requirements[idx]; + return promise.then(checkFn) + .then(function (version) { + requirement.installed = true; + requirement.metadata.version = version; + }, function (err) { + requirement.metadata.reason = err instanceof Error ? err.message : err; + }); + }, Q()) + .then(function () { + // When chain is completed, return requirements array to upstream API + return requirements; + }); +}; diff --git a/bin/lib/create.js b/bin/lib/create.js index 799836f0..9d4b4ae3 100755 --- a/bin/lib/create.js +++ b/bin/lib/create.js @@ -1,332 +1,332 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var shell = require('shelljs'), - Q = require('q'), - path = require('path'), - fs = require('fs'), - check_reqs = require('./check_reqs'), - ROOT = path.join(__dirname, '..', '..'); - -var MIN_SDK_VERSION = 14; - -var CordovaError = require('cordova-common').CordovaError; -var AndroidManifest = require('../templates/cordova/lib/AndroidManifest'); - -function setShellFatal(value, func) { - var oldVal = shell.config.fatal; - shell.config.fatal = value; - func(); - shell.config.fatal = oldVal; -} - -function getFrameworkDir(projectPath, shared) { - return shared ? path.join(ROOT, 'framework') : path.join(projectPath, 'CordovaLib'); -} - -function copyJsAndLibrary(projectPath, shared, projectName) { - var nestedCordovaLibPath = getFrameworkDir(projectPath, false); - var srcCordovaJsPath = path.join(ROOT, 'bin', 'templates', 'project', 'assets', 'www', 'cordova.js'); - shell.cp('-f', srcCordovaJsPath, path.join(projectPath, 'assets', 'www', 'cordova.js')); - - // Copy the cordova.js file to platforms//platform_www/ - // The www dir is nuked on each prepare so we keep cordova.js in platform_www - shell.mkdir('-p', path.join(projectPath, 'platform_www')); - shell.cp('-f', srcCordovaJsPath, path.join(projectPath, 'platform_www')); - - // Copy cordova-js-src directory into platform_www directory. - // We need these files to build cordova.js if using browserify method. - shell.cp('-rf', path.join(ROOT, 'cordova-js-src'), path.join(projectPath, 'platform_www')); - - // Don't fail if there are no old jars. - setShellFatal(false, function() { - shell.ls(path.join(projectPath, 'libs', 'cordova-*.jar')).forEach(function(oldJar) { - console.log('Deleting ' + oldJar); - shell.rm('-f', oldJar); - }); - var wasSymlink = true; - try { - // Delete the symlink if it was one. - fs.unlinkSync(nestedCordovaLibPath); - } catch (e) { - wasSymlink = false; - } - // Delete old library project if it existed. - if (shared) { - shell.rm('-rf', nestedCordovaLibPath); - } else if (!wasSymlink) { - // Delete only the src, since Eclipse / Android Studio can't handle their project files being deleted. - shell.rm('-rf', path.join(nestedCordovaLibPath, 'src')); - } - }); - if (shared) { - var relativeFrameworkPath = path.relative(projectPath, getFrameworkDir(projectPath, true)); - fs.symlinkSync(relativeFrameworkPath, nestedCordovaLibPath, 'dir'); - } else { - shell.mkdir('-p', nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'build.gradle'), nestedCordovaLibPath); - shell.cp('-f', path.join(ROOT, 'framework', 'cordova.gradle'), nestedCordovaLibPath); - shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath); - } -} - -function extractSubProjectPaths(data) { - var ret = {}; - var r = /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg; - var m; - while ((m = r.exec(data))) { - ret[m[1]] = 1; - } - return Object.keys(ret); -} - -function writeProjectProperties(projectPath, target_api) { - var dstPath = path.join(projectPath, 'project.properties'); - var templatePath = path.join(ROOT, 'bin', 'templates', 'project', 'project.properties'); - var srcPath = fs.existsSync(dstPath) ? dstPath : templatePath; - - var data = fs.readFileSync(srcPath, 'utf8'); - data = data.replace(/^target=.*/m, 'target=' + target_api); - var subProjects = extractSubProjectPaths(data); - subProjects = subProjects.filter(function(p) { - return !(/^CordovaLib$/m.exec(p) || - /[\\\/]cordova-android[\\\/]framework$/m.exec(p) || - /^(\.\.[\\\/])+framework$/m.exec(p) - ); - }); - subProjects.unshift('CordovaLib'); - data = data.replace(/^\s*android\.library\.reference\.\d+=.*\n/mg, ''); - if (!/\n$/.exec(data)) { - data += '\n'; - } - for (var i = 0; i < subProjects.length; ++i) { - data += 'android.library.reference.' + (i+1) + '=' + subProjects[i] + '\n'; - } - fs.writeFileSync(dstPath, data); -} - -function prepBuildFiles(projectPath) { - var buildModule = require(path.join(path.resolve(projectPath), 'cordova', 'lib', 'build')); - buildModule.prepBuildFiles(); -} - -function copyBuildRules(projectPath) { - var srcDir = path.join(ROOT, 'bin', 'templates', 'project'); - - shell.cp('-f', path.join(srcDir, 'build.gradle'), projectPath); -} - -function copyScripts(projectPath) { - var srcScriptsDir = path.join(ROOT, 'bin', 'templates', 'cordova'); - var destScriptsDir = path.join(projectPath, 'cordova'); - // Delete old scripts directory if this is an update. - shell.rm('-rf', destScriptsDir); - // Copy in the new ones. - shell.cp('-r', srcScriptsDir, projectPath); - shell.cp('-r', path.join(ROOT, 'node_modules'), destScriptsDir); - shell.cp(path.join(ROOT, 'bin', 'check_reqs*'), destScriptsDir); - shell.cp(path.join(ROOT, 'bin', 'lib', 'check_reqs.js'), path.join(projectPath, 'cordova', 'lib', 'check_reqs.js')); - shell.cp(path.join(ROOT, 'bin', 'android_sdk_version'), path.join(destScriptsDir, 'android_sdk_version')); - shell.cp(path.join(ROOT, 'bin', 'lib', 'android_sdk_version.js'), path.join(projectPath, 'cordova', 'lib', 'android_sdk_version.js')); -} - -/** - * Test whether a package name is acceptable for use as an android project. - * Returns a promise, fulfilled if the package name is acceptable; rejected - * otherwise. - */ -function validatePackageName(package_name) { - //Make the package conform to Java package types - //http://developer.android.com/guide/topics/manifest/manifest-element.html#package - //Enforce underscore limitation - var msg = 'Error validating package name. '; - if (!/^[a-zA-Z][a-zA-Z0-9_]+(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(package_name)) { - return Q.reject(new CordovaError(msg + 'Package name must look like: com.company.Name')); - } - - //Class is a reserved word - if(/\b[Cc]lass\b/.test(package_name)) { - return Q.reject(new CordovaError(msg + '"class" is a reserved word')); - } - - return Q.resolve(); -} - -/** - * Test whether a project name is acceptable for use as an android class. - * Returns a promise, fulfilled if the project name is acceptable; rejected - * otherwise. - */ -function validateProjectName(project_name) { - var msg = 'Error validating project name. '; - //Make sure there's something there - if (project_name === '') { - return Q.reject(new CordovaError(msg + 'Project name cannot be empty')); - } - - //Enforce stupid name error - if (project_name === 'CordovaActivity') { - return Q.reject(new CordovaError(msg + 'Project name cannot be CordovaActivity')); - } - - //Classes in Java don't begin with numbers - if (/^[0-9]/.test(project_name)) { - return Q.reject(new CordovaError(msg + 'Project name must not begin with a number')); - } - - return Q.resolve(); -} - -/** - * Creates an android application with the given options. - * - * @param {String} project_path Path to the new Cordova android project. - * @param {ConfigParser} config Instance of ConfigParser to retrieve basic - * project properties. - * @param {Object} [options={}] Various options - * @param {String} [options.activityName='MainActivity'] Name for the - * activity - * @param {Boolean} [options.link=false] Specifies whether javascript files - * and CordovaLib framework will be symlinked to created application. - * @param {String} [options.customTemplate] Path to project template - * (override) - * @param {EventEmitter} [events] An EventEmitter instance for logging - * events - * - * @return {Promise} Directory where application has been created - */ -exports.create = function(project_path, config, options, events) { - - options = options || {}; - - // Set default values for path, package and name - project_path = path.relative(process.cwd(), (project_path || 'CordovaExample')); - // Check if project already exists - if(fs.existsSync(project_path)) { - return Q.reject(new CordovaError('Project already exists! Delete and recreate')); - } - - var package_name = config.packageName() || 'my.cordova.project'; - var project_name = config.name() ? - config.name().replace(/[^\w.]/g,'_') : 'CordovaExample'; - - var safe_activity_name = config.android_activityName() || options.activityName || 'MainActivity'; - var target_api = check_reqs.get_target(); - - //Make the package conform to Java package types - return validatePackageName(package_name) - .then(function() { - validateProjectName(project_name); - }).then(function() { - // Log the given values for the project - events.emit('log', 'Creating Cordova project for the Android platform:'); - events.emit('log', '\tPath: ' + project_path); - events.emit('log', '\tPackage: ' + package_name); - events.emit('log', '\tName: ' + project_name); - events.emit('log', '\tActivity: ' + safe_activity_name); - events.emit('log', '\tAndroid target: ' + target_api); - - events.emit('verbose', 'Copying template files...'); - - setShellFatal(true, function() { - var project_template_dir = options.customTemplate || path.join(ROOT, 'bin', 'templates', 'project'); - // copy project template - shell.cp('-r', path.join(project_template_dir, 'assets'), project_path); - shell.cp('-r', path.join(project_template_dir, 'res'), project_path); - shell.cp(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore')); - - // Manually create directories that would be empty within the template (since git doesn't track directories). - shell.mkdir(path.join(project_path, 'libs')); - - // copy cordova.js, cordova.jar - copyJsAndLibrary(project_path, options.link, safe_activity_name); - - // interpolate the activity name and package - var packagePath = package_name.replace(/\./g, path.sep); - var activity_dir = path.join(project_path, 'src', packagePath); - var activity_path = path.join(activity_dir, safe_activity_name + '.java'); - shell.mkdir('-p', activity_dir); - shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path); - shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path); - shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml')); - shell.sed('-i', /__ID__/, package_name, activity_path); - - var manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml')); - manifest.setPackageId(package_name) - .setTargetSdkVersion(target_api.split('-')[1]) - .getActivity().setName(safe_activity_name); - - var manifest_path = path.join(project_path, 'AndroidManifest.xml'); - manifest.write(manifest_path); - - copyScripts(project_path); - copyBuildRules(project_path); - }); - // Link it to local android install. - writeProjectProperties(project_path, target_api); - prepBuildFiles(project_path); - events.emit('log', generateDoneMessage('create', options.link)); - }).thenResolve(project_path); -}; - -function generateDoneMessage(type, link) { - var pkg = require('../../package'); - var msg = 'Android project ' + (type == 'update' ? 'updated ' : 'created ') + 'with ' + pkg.name + '@' + pkg.version; - if (link) { - msg += ' and has a linked CordovaLib'; - } - return msg; -} - -// Returns a promise. -exports.update = function(projectPath, options, events) { - options = options || {}; - - return Q() - .then(function() { - - var manifest = new AndroidManifest(path.join(projectPath, 'AndroidManifest.xml')); - - if (Number(manifest.getMinSdkVersion()) < MIN_SDK_VERSION) { - events.emit('verbose', 'Updating minSdkVersion to ' + MIN_SDK_VERSION + ' in AndroidManifest.xml'); - manifest.setMinSDKVersion(MIN_SDK_VERSION); - } - - manifest.setDebuggable(false).write(); - - var projectName = manifest.getActivity().getName(); - var target_api = check_reqs.get_target(); - - copyJsAndLibrary(projectPath, options.link, projectName); - copyScripts(projectPath); - copyBuildRules(projectPath); - writeProjectProperties(projectPath, target_api); - prepBuildFiles(projectPath); - events.emit('log', generateDoneMessage('update', options.link)); - }).thenResolve(projectPath); -}; - - -// For testing -exports.validatePackageName = validatePackageName; -exports.validateProjectName = validateProjectName; +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var shell = require('shelljs'), + Q = require('q'), + path = require('path'), + fs = require('fs'), + check_reqs = require('./check_reqs'), + ROOT = path.join(__dirname, '..', '..'); + +var MIN_SDK_VERSION = 14; + +var CordovaError = require('cordova-common').CordovaError; +var AndroidManifest = require('../templates/cordova/lib/AndroidManifest'); + +function setShellFatal(value, func) { + var oldVal = shell.config.fatal; + shell.config.fatal = value; + func(); + shell.config.fatal = oldVal; +} + +function getFrameworkDir(projectPath, shared) { + return shared ? path.join(ROOT, 'framework') : path.join(projectPath, 'CordovaLib'); +} + +function copyJsAndLibrary(projectPath, shared, projectName) { + var nestedCordovaLibPath = getFrameworkDir(projectPath, false); + var srcCordovaJsPath = path.join(ROOT, 'bin', 'templates', 'project', 'assets', 'www', 'cordova.js'); + shell.cp('-f', srcCordovaJsPath, path.join(projectPath, 'assets', 'www', 'cordova.js')); + + // Copy the cordova.js file to platforms//platform_www/ + // The www dir is nuked on each prepare so we keep cordova.js in platform_www + shell.mkdir('-p', path.join(projectPath, 'platform_www')); + shell.cp('-f', srcCordovaJsPath, path.join(projectPath, 'platform_www')); + + // Copy cordova-js-src directory into platform_www directory. + // We need these files to build cordova.js if using browserify method. + shell.cp('-rf', path.join(ROOT, 'cordova-js-src'), path.join(projectPath, 'platform_www')); + + // Don't fail if there are no old jars. + setShellFatal(false, function() { + shell.ls(path.join(projectPath, 'libs', 'cordova-*.jar')).forEach(function(oldJar) { + console.log('Deleting ' + oldJar); + shell.rm('-f', oldJar); + }); + var wasSymlink = true; + try { + // Delete the symlink if it was one. + fs.unlinkSync(nestedCordovaLibPath); + } catch (e) { + wasSymlink = false; + } + // Delete old library project if it existed. + if (shared) { + shell.rm('-rf', nestedCordovaLibPath); + } else if (!wasSymlink) { + // Delete only the src, since Eclipse / Android Studio can't handle their project files being deleted. + shell.rm('-rf', path.join(nestedCordovaLibPath, 'src')); + } + }); + if (shared) { + var relativeFrameworkPath = path.relative(projectPath, getFrameworkDir(projectPath, true)); + fs.symlinkSync(relativeFrameworkPath, nestedCordovaLibPath, 'dir'); + } else { + shell.mkdir('-p', nestedCordovaLibPath); + shell.cp('-f', path.join(ROOT, 'framework', 'AndroidManifest.xml'), nestedCordovaLibPath); + shell.cp('-f', path.join(ROOT, 'framework', 'project.properties'), nestedCordovaLibPath); + shell.cp('-f', path.join(ROOT, 'framework', 'build.gradle'), nestedCordovaLibPath); + shell.cp('-f', path.join(ROOT, 'framework', 'cordova.gradle'), nestedCordovaLibPath); + shell.cp('-r', path.join(ROOT, 'framework', 'src'), nestedCordovaLibPath); + } +} + +function extractSubProjectPaths(data) { + var ret = {}; + var r = /^\s*android\.library\.reference\.\d+=(.*)(?:\s|$)/mg; + var m; + while ((m = r.exec(data))) { + ret[m[1]] = 1; + } + return Object.keys(ret); +} + +function writeProjectProperties(projectPath, target_api) { + var dstPath = path.join(projectPath, 'project.properties'); + var templatePath = path.join(ROOT, 'bin', 'templates', 'project', 'project.properties'); + var srcPath = fs.existsSync(dstPath) ? dstPath : templatePath; + + var data = fs.readFileSync(srcPath, 'utf8'); + data = data.replace(/^target=.*/m, 'target=' + target_api); + var subProjects = extractSubProjectPaths(data); + subProjects = subProjects.filter(function(p) { + return !(/^CordovaLib$/m.exec(p) || + /[\\\/]cordova-android[\\\/]framework$/m.exec(p) || + /^(\.\.[\\\/])+framework$/m.exec(p) + ); + }); + subProjects.unshift('CordovaLib'); + data = data.replace(/^\s*android\.library\.reference\.\d+=.*\n/mg, ''); + if (!/\n$/.exec(data)) { + data += '\n'; + } + for (var i = 0; i < subProjects.length; ++i) { + data += 'android.library.reference.' + (i+1) + '=' + subProjects[i] + '\n'; + } + fs.writeFileSync(dstPath, data); +} + +function prepBuildFiles(projectPath) { + var buildModule = require(path.join(path.resolve(projectPath), 'cordova', 'lib', 'build')); + buildModule.prepBuildFiles(); +} + +function copyBuildRules(projectPath) { + var srcDir = path.join(ROOT, 'bin', 'templates', 'project'); + + shell.cp('-f', path.join(srcDir, 'build.gradle'), projectPath); +} + +function copyScripts(projectPath) { + var srcScriptsDir = path.join(ROOT, 'bin', 'templates', 'cordova'); + var destScriptsDir = path.join(projectPath, 'cordova'); + // Delete old scripts directory if this is an update. + shell.rm('-rf', destScriptsDir); + // Copy in the new ones. + shell.cp('-r', srcScriptsDir, projectPath); + shell.cp('-r', path.join(ROOT, 'node_modules'), destScriptsDir); + shell.cp(path.join(ROOT, 'bin', 'check_reqs*'), destScriptsDir); + shell.cp(path.join(ROOT, 'bin', 'lib', 'check_reqs.js'), path.join(projectPath, 'cordova', 'lib', 'check_reqs.js')); + shell.cp(path.join(ROOT, 'bin', 'android_sdk_version'), path.join(destScriptsDir, 'android_sdk_version')); + shell.cp(path.join(ROOT, 'bin', 'lib', 'android_sdk_version.js'), path.join(projectPath, 'cordova', 'lib', 'android_sdk_version.js')); +} + +/** + * Test whether a package name is acceptable for use as an android project. + * Returns a promise, fulfilled if the package name is acceptable; rejected + * otherwise. + */ +function validatePackageName(package_name) { + //Make the package conform to Java package types + //http://developer.android.com/guide/topics/manifest/manifest-element.html#package + //Enforce underscore limitation + var msg = 'Error validating package name. '; + if (!/^[a-zA-Z][a-zA-Z0-9_]+(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(package_name)) { + return Q.reject(new CordovaError(msg + 'Package name must look like: com.company.Name')); + } + + //Class is a reserved word + if(/\b[Cc]lass\b/.test(package_name)) { + return Q.reject(new CordovaError(msg + '"class" is a reserved word')); + } + + return Q.resolve(); +} + +/** + * Test whether a project name is acceptable for use as an android class. + * Returns a promise, fulfilled if the project name is acceptable; rejected + * otherwise. + */ +function validateProjectName(project_name) { + var msg = 'Error validating project name. '; + //Make sure there's something there + if (project_name === '') { + return Q.reject(new CordovaError(msg + 'Project name cannot be empty')); + } + + //Enforce stupid name error + if (project_name === 'CordovaActivity') { + return Q.reject(new CordovaError(msg + 'Project name cannot be CordovaActivity')); + } + + //Classes in Java don't begin with numbers + if (/^[0-9]/.test(project_name)) { + return Q.reject(new CordovaError(msg + 'Project name must not begin with a number')); + } + + return Q.resolve(); +} + +/** + * Creates an android application with the given options. + * + * @param {String} project_path Path to the new Cordova android project. + * @param {ConfigParser} config Instance of ConfigParser to retrieve basic + * project properties. + * @param {Object} [options={}] Various options + * @param {String} [options.activityName='MainActivity'] Name for the + * activity + * @param {Boolean} [options.link=false] Specifies whether javascript files + * and CordovaLib framework will be symlinked to created application. + * @param {String} [options.customTemplate] Path to project template + * (override) + * @param {EventEmitter} [events] An EventEmitter instance for logging + * events + * + * @return {Promise} Directory where application has been created + */ +exports.create = function(project_path, config, options, events) { + + options = options || {}; + + // Set default values for path, package and name + project_path = path.relative(process.cwd(), (project_path || 'CordovaExample')); + // Check if project already exists + if(fs.existsSync(project_path)) { + return Q.reject(new CordovaError('Project already exists! Delete and recreate')); + } + + var package_name = config.packageName() || 'my.cordova.project'; + var project_name = config.name() ? + config.name().replace(/[^\w.]/g,'_') : 'CordovaExample'; + + var safe_activity_name = config.android_activityName() || options.activityName || 'MainActivity'; + var target_api = check_reqs.get_target(); + + //Make the package conform to Java package types + return validatePackageName(package_name) + .then(function() { + validateProjectName(project_name); + }).then(function() { + // Log the given values for the project + events.emit('log', 'Creating Cordova project for the Android platform:'); + events.emit('log', '\tPath: ' + project_path); + events.emit('log', '\tPackage: ' + package_name); + events.emit('log', '\tName: ' + project_name); + events.emit('log', '\tActivity: ' + safe_activity_name); + events.emit('log', '\tAndroid target: ' + target_api); + + events.emit('verbose', 'Copying template files...'); + + setShellFatal(true, function() { + var project_template_dir = options.customTemplate || path.join(ROOT, 'bin', 'templates', 'project'); + // copy project template + shell.cp('-r', path.join(project_template_dir, 'assets'), project_path); + shell.cp('-r', path.join(project_template_dir, 'res'), project_path); + shell.cp(path.join(project_template_dir, 'gitignore'), path.join(project_path, '.gitignore')); + + // Manually create directories that would be empty within the template (since git doesn't track directories). + shell.mkdir(path.join(project_path, 'libs')); + + // copy cordova.js, cordova.jar + copyJsAndLibrary(project_path, options.link, safe_activity_name); + + // interpolate the activity name and package + var packagePath = package_name.replace(/\./g, path.sep); + var activity_dir = path.join(project_path, 'src', packagePath); + var activity_path = path.join(activity_dir, safe_activity_name + '.java'); + shell.mkdir('-p', activity_dir); + shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path); + shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path); + shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml')); + shell.sed('-i', /__ID__/, package_name, activity_path); + + var manifest = new AndroidManifest(path.join(project_template_dir, 'AndroidManifest.xml')); + manifest.setPackageId(package_name) + .setTargetSdkVersion(target_api.split('-')[1]) + .getActivity().setName(safe_activity_name); + + var manifest_path = path.join(project_path, 'AndroidManifest.xml'); + manifest.write(manifest_path); + + copyScripts(project_path); + copyBuildRules(project_path); + }); + // Link it to local android install. + writeProjectProperties(project_path, target_api); + prepBuildFiles(project_path); + events.emit('log', generateDoneMessage('create', options.link)); + }).thenResolve(project_path); +}; + +function generateDoneMessage(type, link) { + var pkg = require('../../package'); + var msg = 'Android project ' + (type == 'update' ? 'updated ' : 'created ') + 'with ' + pkg.name + '@' + pkg.version; + if (link) { + msg += ' and has a linked CordovaLib'; + } + return msg; +} + +// Returns a promise. +exports.update = function(projectPath, options, events) { + options = options || {}; + + return Q() + .then(function() { + + var manifest = new AndroidManifest(path.join(projectPath, 'AndroidManifest.xml')); + + if (Number(manifest.getMinSdkVersion()) < MIN_SDK_VERSION) { + events.emit('verbose', 'Updating minSdkVersion to ' + MIN_SDK_VERSION + ' in AndroidManifest.xml'); + manifest.setMinSDKVersion(MIN_SDK_VERSION); + } + + manifest.setDebuggable(false).write(); + + var projectName = manifest.getActivity().getName(); + var target_api = check_reqs.get_target(); + + copyJsAndLibrary(projectPath, options.link, projectName); + copyScripts(projectPath); + copyBuildRules(projectPath); + writeProjectProperties(projectPath, target_api); + prepBuildFiles(projectPath); + events.emit('log', generateDoneMessage('update', options.link)); + }).thenResolve(projectPath); +}; + + +// For testing +exports.validatePackageName = validatePackageName; +exports.validateProjectName = validateProjectName; diff --git a/bin/templates/cordova/Api.js b/bin/templates/cordova/Api.js index 62899ef5..7b7731f6 100644 --- a/bin/templates/cordova/Api.js +++ b/bin/templates/cordova/Api.js @@ -1,492 +1,492 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'); -var fs = require('fs'); -var path = require('path'); -var shell = require('shelljs'); - -var CordovaError = require('cordova-common').CordovaError; -var PlatformJson = require('cordova-common').PlatformJson; -var ActionStack = require('cordova-common').ActionStack; -var AndroidProject = require('./lib/AndroidProject'); -var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger; -var PluginInfoProvider = require('cordova-common').PluginInfoProvider; - -var ConsoleLogger = require('./lib/ConsoleLogger'); -var pluginHandlers = require('./lib/pluginHandlers'); - -var PLATFORM = 'android'; - -/** - * Class, that acts as abstraction over particular platform. Encapsulates the - * platform's properties and methods. - * - * Platform that implements own PlatformApi instance _should implement all - * prototype methods_ of this class to be fully compatible with cordova-lib. - * - * The PlatformApi instance also should define the following field: - * - * * platform: String that defines a platform name. - */ -function Api(platform, platformRootDir, events) { - this.platform = PLATFORM; - this.root = path.resolve(__dirname, '..'); - this.events = events || ConsoleLogger.get(); - // NOTE: trick to share one EventEmitter instance across all js code - require('cordova-common').events = this.events; - - this._platformJson = PlatformJson.load(this.root, platform); - this._pluginInfoProvider = new PluginInfoProvider(); - this._munger = new PlatformMunger(this.platform, this.root, this._platformJson, this._pluginInfoProvider); - - var self = this; - - this.locations = { - root: self.root, - www: path.join(self.root, 'assets/www'), - platformWww: path.join(self.root, 'platform_www'), - configXml: path.join(self.root, 'res/xml/config.xml'), - defaultConfigXml: path.join(self.root, 'cordova/defaults.xml'), - strings: path.join(self.root, 'res/values/strings.xml'), - manifest: path.join(self.root, 'AndroidManifest.xml'), - // NOTE: Due to platformApi spec we need to return relative paths here - cordovaJs: 'bin/templates/project/assets/www/cordova.js', - cordovaJsSrc: 'cordova-js-src' - }; -} - -/** - * Installs platform to specified directory and creates a platform project. - * - * @param {String} destination Destination directory, where insatll platform to - * @param {ConfigParser} [config] ConfgiParser instance, used to retrieve - * project creation options, such as package id and project name. - * @param {Object} [options] An options object. The most common options are: - * @param {String} [options.customTemplate] A path to custom template, that - * should override the default one from platform. - * @param {Boolean} [options.link] Flag that indicates that platform's - * sources will be linked to installed platform instead of copying. - * @param {EventEmitter} [events] An EventEmitter instance that will be used for - * logging purposes. If no EventEmitter provided, all events will be logged to - * console - * - * @return {Promise} Promise either fulfilled with PlatformApi - * instance or rejected with CordovaError. - */ -Api.createPlatform = function (destination, config, options, events) { - return require('../../lib/create') - .create(destination, config, options, events || ConsoleLogger.get()) - .then(function (destination) { - var PlatformApi = require(path.resolve(destination, 'cordova/Api')); - return new PlatformApi(PLATFORM, destination, events); - }); -}; - -/** - * Updates already installed platform. - * - * @param {String} destination Destination directory, where platform installed - * @param {Object} [options] An options object. The most common options are: - * @param {String} [options.customTemplate] A path to custom template, that - * should override the default one from platform. - * @param {Boolean} [options.link] Flag that indicates that platform's - * sources will be linked to installed platform instead of copying. - * @param {EventEmitter} [events] An EventEmitter instance that will be used for - * logging purposes. If no EventEmitter provided, all events will be logged to - * console - * - * @return {Promise} Promise either fulfilled with PlatformApi - * instance or rejected with CordovaError. - */ -Api.updatePlatform = function (destination, options, events) { - return require('../../lib/create') - .update(destination, options, events || ConsoleLogger.get()) - .then(function (destination) { - var PlatformApi = require(path.resolve(destination, 'cordova/Api')); - return new PlatformApi('android', destination, events); - }); -}; - -/** - * Gets a CordovaPlatform object, that represents the platform structure. - * - * @return {CordovaPlatform} A structure that contains the description of - * platform's file structure and other properties of platform. - */ -Api.prototype.getPlatformInfo = function () { - var result = {}; - result.locations = this.locations; - result.root = this.root; - result.name = this.platform; - result.version = require('./version'); - result.projectConfig = this._config; - - return result; -}; - -/** - * Updates installed platform with provided www assets and new app - * configuration. This method is required for CLI workflow and will be called - * each time before build, so the changes, made to app configuration and www - * code, will be applied to platform. - * - * @param {CordovaProject} cordovaProject A CordovaProject instance, that defines a - * project structure and configuration, that should be applied to platform - * (contains project's www location and ConfigParser instance for project's - * config). - * - * @return {Promise} Return a promise either fulfilled, or rejected with - * CordovaError instance. - */ -Api.prototype.prepare = function (cordovaProject) { - return require('./lib/prepare').prepare.call(this, cordovaProject); -}; - -/** - * Installs a new plugin into platform. This method only copies non-www files - * (sources, libs, etc.) to platform. It also doesn't resolves the - * dependencies of plugin. Both of handling of www files, such as assets and - * js-files and resolving dependencies are the responsibility of caller. - * - * @param {PluginInfo} plugin A PluginInfo instance that represents plugin - * that will be installed. - * @param {Object} installOptions An options object. Possible options below: - * @param {Boolean} installOptions.link: Flag that specifies that plugin - * sources will be symlinked to app's directory instead of copying (if - * possible). - * @param {Object} installOptions.variables An object that represents - * variables that will be used to install plugin. See more details on plugin - * variables in documentation: - * https://cordova.apache.org/docs/en/4.0.0/plugin_ref_spec.md.html - * - * @return {Promise} Return a promise either fulfilled, or rejected with - * CordovaError instance. - */ -Api.prototype.addPlugin = function (plugin, installOptions) { - - if (!plugin || plugin.constructor.name !== 'PluginInfo') - return Q.reject(new CordovaError('The parameter is incorrect. The first parameter to addPlugin should be a PluginInfo instance')); - - installOptions = installOptions || {}; - installOptions.variables = installOptions.variables || {}; - - var self = this; - var actions = new ActionStack(); - var project = AndroidProject.getProjectFile(this.root); - - // gather all files needs to be handled during install - plugin.getFilesAndFrameworks(this.platform) - .concat(plugin.getAssets(this.platform)) - .concat(plugin.getJsModules(this.platform)) - .forEach(function(item) { - actions.push(actions.createAction( - pluginHandlers.getInstaller(item.itemType), [item, plugin, project, installOptions], - pluginHandlers.getUninstaller(item.itemType), [item, plugin, project, installOptions])); - }); - - // run through the action stack - return actions.process(this.platform) - .then(function () { - if (project) { - project.write(); - } - - // Add PACKAGE_NAME variable into vars - if (!installOptions.variables.PACKAGE_NAME) { - installOptions.variables.PACKAGE_NAME = project.getPackageName(); - } - - self._munger - // Ignore passed `is_top_level` option since platform itself doesn't know - // anything about managing dependencies - it's responsibility of caller. - .add_plugin_changes(plugin, installOptions.variables, /*is_top_level=*/true, /*should_increment=*/true) - .save_all(); - - var targetDir = installOptions.usePlatformWww ? - self.locations.platformWww : - self.locations.www; - - self._addModulesInfo(plugin, targetDir); - }); -}; - -/** - * Removes an installed plugin from platform. - * - * Since method accepts PluginInfo instance as input parameter instead of plugin - * id, caller shoud take care of managing/storing PluginInfo instances for - * future uninstalls. - * - * @param {PluginInfo} plugin A PluginInfo instance that represents plugin - * that will be installed. - * - * @return {Promise} Return a promise either fulfilled, or rejected with - * CordovaError instance. - */ -Api.prototype.removePlugin = function (plugin, uninstallOptions) { - - if (!plugin || plugin.constructor.name !== 'PluginInfo') - return Q.reject(new CordovaError('The parameter is incorrect. The first parameter to addPlugin should be a PluginInfo instance')); - - var self = this; - var actions = new ActionStack(); - var project = AndroidProject.getProjectFile(this.root); - - // queue up plugin files - plugin.getFilesAndFrameworks(this.platform) - .concat(plugin.getAssets(this.platform)) - .concat(plugin.getJsModules(this.platform)) - .forEach(function(item) { - actions.push(actions.createAction( - pluginHandlers.getUninstaller(item.itemType), [item, plugin, project, uninstallOptions], - pluginHandlers.getInstaller(item.itemType), [item, plugin, project, uninstallOptions])); - }); - - // run through the action stack - return actions.process(this.platform) - .then(function() { - if (project) { - project.write(); - } - - self._munger - // Ignore passed `is_top_level` option since platform itself doesn't know - // anything about managing dependencies - it's responsibility of caller. - .remove_plugin_changes(plugin, /*is_top_level=*/true) - .save_all(); - - var targetDir = uninstallOptions.usePlatformWww ? - self.locations.platformWww : - self.locations.www; - - self._removeModulesInfo(plugin, targetDir); - }); -}; - -/** - * Builds an application package for current platform. - * - * @param {Object} buildOptions A build options. This object's structure is - * highly depends on platform's specific. The most common options are: - * @param {Boolean} buildOptions.debug Indicates that packages should be - * built with debug configuration. This is set to true by default unless the - * 'release' option is not specified. - * @param {Boolean} buildOptions.release Indicates that packages should be - * built with release configuration. If not set to true, debug configuration - * will be used. - * @param {Boolean} buildOptions.device Specifies that built app is intended - * to run on device - * @param {Boolean} buildOptions.emulator: Specifies that built app is - * intended to run on emulator - * @param {String} buildOptions.target Specifies the device id that will be - * used to run built application. - * @param {Boolean} buildOptions.nobuild Indicates that this should be a - * dry-run call, so no build artifacts will be produced. - * @param {String[]} buildOptions.archs Specifies chip architectures which - * app packages should be built for. List of valid architectures is depends on - * platform. - * @param {String} buildOptions.buildConfig The path to build configuration - * file. The format of this file is depends on platform. - * @param {String[]} buildOptions.argv Raw array of command-line arguments, - * passed to `build` command. The purpose of this property is to pass a - * platform-specific arguments, and eventually let platform define own - * arguments processing logic. - * - * @return {Promise} A promise either fulfilled with an array of build - * artifacts (application packages) if package was built successfully, - * or rejected with CordovaError. The resultant build artifact objects is not - * strictly typed and may conatin arbitrary set of fields as in sample below. - * - * { - * architecture: 'x86', - * buildType: 'debug', - * path: '/path/to/build', - * type: 'app' - * } - * - * The return value in most cases will contain only one item but in some cases - * there could be multiple items in output array, e.g. when multiple - * arhcitectures is specified. - */ -Api.prototype.build = function (buildOptions) { - var self = this; - return require('./lib/check_reqs').run() - .then(function () { - return require('./lib/build').run.call(self, buildOptions); - }) - .then(function (buildResults) { - // Cast build result to array of build artifacts - return buildResults.apkPaths.map(function (apkPath) { - return { - buildType: buildResults.buildType, - buildMethod: buildResults.buildMethod, - path: apkPath, - type: 'apk' - }; - }); - }); -}; - -/** - * Builds an application package for current platform and runs it on - * specified/default device. If no 'device'/'emulator'/'target' options are - * specified, then tries to run app on default device if connected, otherwise - * runs the app on emulator. - * - * @param {Object} runOptions An options object. The structure is the same - * as for build options. - * - * @return {Promise} A promise either fulfilled if package was built and ran - * successfully, or rejected with CordovaError. - */ -Api.prototype.run = function(runOptions) { - var self = this; - return require('./lib/check_reqs').run() - .then(function () { - return require('./lib/run').run.call(self, runOptions); - }); -}; - -/** - * Cleans out the build artifacts from platform's directory. - * - * @return {Promise} Return a promise either fulfilled, or rejected with - * CordovaError. - */ -Api.prototype.clean = function(cleanOptions) { - var self = this; - return require('./lib/check_reqs').run() - .then(function () { - return require('./lib/build').runClean.call(self, cleanOptions); - }); -}; - -/** - * Performs a requirements check for current platform. Each platform defines its - * own set of requirements, which should be resolved before platform can be - * built successfully. - * - * @return {Promise} Promise, resolved with set of Requirement - * objects for current platform. - */ -Api.prototype.requirements = function() { - return require('./lib/check_reqs').check_all(); -}; - -module.exports = Api; - -/** - * Removes the specified modules from list of installed modules and updates - * platform_json and cordova_plugins.js on disk. - * - * @param {PluginInfo} plugin PluginInfo instance for plugin, which modules - * needs to be added. - * @param {String} targetDir The directory, where updated cordova_plugins.js - * should be written to. - */ -Api.prototype._addModulesInfo = function(plugin, targetDir) { - var installedModules = this._platformJson.root.modules || []; - - var installedPaths = installedModules.map(function (installedModule) { - return installedModule.file; - }); - - var modulesToInstall = plugin.getJsModules(this.platform) - .filter(function (moduleToInstall) { - return installedPaths.indexOf(moduleToInstall.file) === -1; - }).map(function (moduleToInstall) { - var moduleName = plugin.id + '.' + ( moduleToInstall.name || moduleToInstall.src.match(/([^\/]+)\.js/)[1] ); - var obj = { - file: ['plugins', plugin.id, moduleToInstall.src].join('/'), - id: moduleName - }; - if (moduleToInstall.clobbers.length > 0) { - obj.clobbers = moduleToInstall.clobbers.map(function(o) { return o.target; }); - } - if (moduleToInstall.merges.length > 0) { - obj.merges = moduleToInstall.merges.map(function(o) { return o.target; }); - } - if (moduleToInstall.runs) { - obj.runs = true; - } - - return obj; - }); - - this._platformJson.root.modules = installedModules.concat(modulesToInstall); - this._writePluginModules(targetDir); - this._platformJson.save(); -}; - -/** - * Removes the specified modules from list of installed modules and updates - * platform_json and cordova_plugins.js on disk. - * - * @param {PluginInfo} plugin PluginInfo instance for plugin, which modules - * needs to be removed. - * @param {String} targetDir The directory, where updated cordova_plugins.js - * should be written to. - */ -Api.prototype._removeModulesInfo = function(plugin, targetDir) { - var installedModules = this._platformJson.root.modules || []; - var modulesToRemove = plugin.getJsModules(this.platform) - .map(function (jsModule) { - return ['plugins', plugin.id, jsModule.src].join('/'); - }); - - var updatedModules = installedModules - .filter(function (installedModule) { - return (modulesToRemove.indexOf(installedModule.file) === -1); - }); - - this._platformJson.root.modules = updatedModules; - this._writePluginModules(targetDir); - this._platformJson.save(); -}; - -/** - * Fetches all installed modules, generates cordova_plugins contents and writes - * it to file. - * - * @param {String} targetDir Directory, where write cordova_plugins.js to. - * Ususally it is either /www or /platform_www - * directories. - */ -Api.prototype._writePluginModules = function (targetDir) { - var self = this; - // Write out moduleObjects as JSON wrapped in a cordova module to cordova_plugins.js - var final_contents = 'cordova.define(\'cordova/plugin_list\', function(require, exports, module) {\n'; - final_contents += 'module.exports = ' + JSON.stringify(this._platformJson.root.modules, null, ' ') + ';\n'; - final_contents += 'module.exports.metadata = \n'; - final_contents += '// TOP OF METADATA\n'; - - var pluginMetadata = Object.keys(this._platformJson.root.installed_plugins) - .reduce(function (metadata, plugin) { - metadata[plugin] = self._platformJson.root.installed_plugins[plugin].version; - return metadata; - }, {}); - - final_contents += JSON.stringify(pluginMetadata, null, 4) + '\n'; - final_contents += '// BOTTOM OF METADATA\n'; - final_contents += '});'; // Close cordova.define. - - shell.mkdir('-p', targetDir); - fs.writeFileSync(path.join(targetDir, 'cordova_plugins.js'), final_contents, 'utf-8'); -}; +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); + +var CordovaError = require('cordova-common').CordovaError; +var PlatformJson = require('cordova-common').PlatformJson; +var ActionStack = require('cordova-common').ActionStack; +var AndroidProject = require('./lib/AndroidProject'); +var PlatformMunger = require('cordova-common').ConfigChanges.PlatformMunger; +var PluginInfoProvider = require('cordova-common').PluginInfoProvider; + +var ConsoleLogger = require('./lib/ConsoleLogger'); +var pluginHandlers = require('./lib/pluginHandlers'); + +var PLATFORM = 'android'; + +/** + * Class, that acts as abstraction over particular platform. Encapsulates the + * platform's properties and methods. + * + * Platform that implements own PlatformApi instance _should implement all + * prototype methods_ of this class to be fully compatible with cordova-lib. + * + * The PlatformApi instance also should define the following field: + * + * * platform: String that defines a platform name. + */ +function Api(platform, platformRootDir, events) { + this.platform = PLATFORM; + this.root = path.resolve(__dirname, '..'); + this.events = events || ConsoleLogger.get(); + // NOTE: trick to share one EventEmitter instance across all js code + require('cordova-common').events = this.events; + + this._platformJson = PlatformJson.load(this.root, platform); + this._pluginInfoProvider = new PluginInfoProvider(); + this._munger = new PlatformMunger(this.platform, this.root, this._platformJson, this._pluginInfoProvider); + + var self = this; + + this.locations = { + root: self.root, + www: path.join(self.root, 'assets/www'), + platformWww: path.join(self.root, 'platform_www'), + configXml: path.join(self.root, 'res/xml/config.xml'), + defaultConfigXml: path.join(self.root, 'cordova/defaults.xml'), + strings: path.join(self.root, 'res/values/strings.xml'), + manifest: path.join(self.root, 'AndroidManifest.xml'), + // NOTE: Due to platformApi spec we need to return relative paths here + cordovaJs: 'bin/templates/project/assets/www/cordova.js', + cordovaJsSrc: 'cordova-js-src' + }; +} + +/** + * Installs platform to specified directory and creates a platform project. + * + * @param {String} destination Destination directory, where insatll platform to + * @param {ConfigParser} [config] ConfgiParser instance, used to retrieve + * project creation options, such as package id and project name. + * @param {Object} [options] An options object. The most common options are: + * @param {String} [options.customTemplate] A path to custom template, that + * should override the default one from platform. + * @param {Boolean} [options.link] Flag that indicates that platform's + * sources will be linked to installed platform instead of copying. + * @param {EventEmitter} [events] An EventEmitter instance that will be used for + * logging purposes. If no EventEmitter provided, all events will be logged to + * console + * + * @return {Promise} Promise either fulfilled with PlatformApi + * instance or rejected with CordovaError. + */ +Api.createPlatform = function (destination, config, options, events) { + return require('../../lib/create') + .create(destination, config, options, events || ConsoleLogger.get()) + .then(function (destination) { + var PlatformApi = require(path.resolve(destination, 'cordova/Api')); + return new PlatformApi(PLATFORM, destination, events); + }); +}; + +/** + * Updates already installed platform. + * + * @param {String} destination Destination directory, where platform installed + * @param {Object} [options] An options object. The most common options are: + * @param {String} [options.customTemplate] A path to custom template, that + * should override the default one from platform. + * @param {Boolean} [options.link] Flag that indicates that platform's + * sources will be linked to installed platform instead of copying. + * @param {EventEmitter} [events] An EventEmitter instance that will be used for + * logging purposes. If no EventEmitter provided, all events will be logged to + * console + * + * @return {Promise} Promise either fulfilled with PlatformApi + * instance or rejected with CordovaError. + */ +Api.updatePlatform = function (destination, options, events) { + return require('../../lib/create') + .update(destination, options, events || ConsoleLogger.get()) + .then(function (destination) { + var PlatformApi = require(path.resolve(destination, 'cordova/Api')); + return new PlatformApi('android', destination, events); + }); +}; + +/** + * Gets a CordovaPlatform object, that represents the platform structure. + * + * @return {CordovaPlatform} A structure that contains the description of + * platform's file structure and other properties of platform. + */ +Api.prototype.getPlatformInfo = function () { + var result = {}; + result.locations = this.locations; + result.root = this.root; + result.name = this.platform; + result.version = require('./version'); + result.projectConfig = this._config; + + return result; +}; + +/** + * Updates installed platform with provided www assets and new app + * configuration. This method is required for CLI workflow and will be called + * each time before build, so the changes, made to app configuration and www + * code, will be applied to platform. + * + * @param {CordovaProject} cordovaProject A CordovaProject instance, that defines a + * project structure and configuration, that should be applied to platform + * (contains project's www location and ConfigParser instance for project's + * config). + * + * @return {Promise} Return a promise either fulfilled, or rejected with + * CordovaError instance. + */ +Api.prototype.prepare = function (cordovaProject) { + return require('./lib/prepare').prepare.call(this, cordovaProject); +}; + +/** + * Installs a new plugin into platform. This method only copies non-www files + * (sources, libs, etc.) to platform. It also doesn't resolves the + * dependencies of plugin. Both of handling of www files, such as assets and + * js-files and resolving dependencies are the responsibility of caller. + * + * @param {PluginInfo} plugin A PluginInfo instance that represents plugin + * that will be installed. + * @param {Object} installOptions An options object. Possible options below: + * @param {Boolean} installOptions.link: Flag that specifies that plugin + * sources will be symlinked to app's directory instead of copying (if + * possible). + * @param {Object} installOptions.variables An object that represents + * variables that will be used to install plugin. See more details on plugin + * variables in documentation: + * https://cordova.apache.org/docs/en/4.0.0/plugin_ref_spec.md.html + * + * @return {Promise} Return a promise either fulfilled, or rejected with + * CordovaError instance. + */ +Api.prototype.addPlugin = function (plugin, installOptions) { + + if (!plugin || plugin.constructor.name !== 'PluginInfo') + return Q.reject(new CordovaError('The parameter is incorrect. The first parameter to addPlugin should be a PluginInfo instance')); + + installOptions = installOptions || {}; + installOptions.variables = installOptions.variables || {}; + + var self = this; + var actions = new ActionStack(); + var project = AndroidProject.getProjectFile(this.root); + + // gather all files needs to be handled during install + plugin.getFilesAndFrameworks(this.platform) + .concat(plugin.getAssets(this.platform)) + .concat(plugin.getJsModules(this.platform)) + .forEach(function(item) { + actions.push(actions.createAction( + pluginHandlers.getInstaller(item.itemType), [item, plugin, project, installOptions], + pluginHandlers.getUninstaller(item.itemType), [item, plugin, project, installOptions])); + }); + + // run through the action stack + return actions.process(this.platform) + .then(function () { + if (project) { + project.write(); + } + + // Add PACKAGE_NAME variable into vars + if (!installOptions.variables.PACKAGE_NAME) { + installOptions.variables.PACKAGE_NAME = project.getPackageName(); + } + + self._munger + // Ignore passed `is_top_level` option since platform itself doesn't know + // anything about managing dependencies - it's responsibility of caller. + .add_plugin_changes(plugin, installOptions.variables, /*is_top_level=*/true, /*should_increment=*/true) + .save_all(); + + var targetDir = installOptions.usePlatformWww ? + self.locations.platformWww : + self.locations.www; + + self._addModulesInfo(plugin, targetDir); + }); +}; + +/** + * Removes an installed plugin from platform. + * + * Since method accepts PluginInfo instance as input parameter instead of plugin + * id, caller shoud take care of managing/storing PluginInfo instances for + * future uninstalls. + * + * @param {PluginInfo} plugin A PluginInfo instance that represents plugin + * that will be installed. + * + * @return {Promise} Return a promise either fulfilled, or rejected with + * CordovaError instance. + */ +Api.prototype.removePlugin = function (plugin, uninstallOptions) { + + if (!plugin || plugin.constructor.name !== 'PluginInfo') + return Q.reject(new CordovaError('The parameter is incorrect. The first parameter to addPlugin should be a PluginInfo instance')); + + var self = this; + var actions = new ActionStack(); + var project = AndroidProject.getProjectFile(this.root); + + // queue up plugin files + plugin.getFilesAndFrameworks(this.platform) + .concat(plugin.getAssets(this.platform)) + .concat(plugin.getJsModules(this.platform)) + .forEach(function(item) { + actions.push(actions.createAction( + pluginHandlers.getUninstaller(item.itemType), [item, plugin, project, uninstallOptions], + pluginHandlers.getInstaller(item.itemType), [item, plugin, project, uninstallOptions])); + }); + + // run through the action stack + return actions.process(this.platform) + .then(function() { + if (project) { + project.write(); + } + + self._munger + // Ignore passed `is_top_level` option since platform itself doesn't know + // anything about managing dependencies - it's responsibility of caller. + .remove_plugin_changes(plugin, /*is_top_level=*/true) + .save_all(); + + var targetDir = uninstallOptions.usePlatformWww ? + self.locations.platformWww : + self.locations.www; + + self._removeModulesInfo(plugin, targetDir); + }); +}; + +/** + * Builds an application package for current platform. + * + * @param {Object} buildOptions A build options. This object's structure is + * highly depends on platform's specific. The most common options are: + * @param {Boolean} buildOptions.debug Indicates that packages should be + * built with debug configuration. This is set to true by default unless the + * 'release' option is not specified. + * @param {Boolean} buildOptions.release Indicates that packages should be + * built with release configuration. If not set to true, debug configuration + * will be used. + * @param {Boolean} buildOptions.device Specifies that built app is intended + * to run on device + * @param {Boolean} buildOptions.emulator: Specifies that built app is + * intended to run on emulator + * @param {String} buildOptions.target Specifies the device id that will be + * used to run built application. + * @param {Boolean} buildOptions.nobuild Indicates that this should be a + * dry-run call, so no build artifacts will be produced. + * @param {String[]} buildOptions.archs Specifies chip architectures which + * app packages should be built for. List of valid architectures is depends on + * platform. + * @param {String} buildOptions.buildConfig The path to build configuration + * file. The format of this file is depends on platform. + * @param {String[]} buildOptions.argv Raw array of command-line arguments, + * passed to `build` command. The purpose of this property is to pass a + * platform-specific arguments, and eventually let platform define own + * arguments processing logic. + * + * @return {Promise} A promise either fulfilled with an array of build + * artifacts (application packages) if package was built successfully, + * or rejected with CordovaError. The resultant build artifact objects is not + * strictly typed and may conatin arbitrary set of fields as in sample below. + * + * { + * architecture: 'x86', + * buildType: 'debug', + * path: '/path/to/build', + * type: 'app' + * } + * + * The return value in most cases will contain only one item but in some cases + * there could be multiple items in output array, e.g. when multiple + * arhcitectures is specified. + */ +Api.prototype.build = function (buildOptions) { + var self = this; + return require('./lib/check_reqs').run() + .then(function () { + return require('./lib/build').run.call(self, buildOptions); + }) + .then(function (buildResults) { + // Cast build result to array of build artifacts + return buildResults.apkPaths.map(function (apkPath) { + return { + buildType: buildResults.buildType, + buildMethod: buildResults.buildMethod, + path: apkPath, + type: 'apk' + }; + }); + }); +}; + +/** + * Builds an application package for current platform and runs it on + * specified/default device. If no 'device'/'emulator'/'target' options are + * specified, then tries to run app on default device if connected, otherwise + * runs the app on emulator. + * + * @param {Object} runOptions An options object. The structure is the same + * as for build options. + * + * @return {Promise} A promise either fulfilled if package was built and ran + * successfully, or rejected with CordovaError. + */ +Api.prototype.run = function(runOptions) { + var self = this; + return require('./lib/check_reqs').run() + .then(function () { + return require('./lib/run').run.call(self, runOptions); + }); +}; + +/** + * Cleans out the build artifacts from platform's directory. + * + * @return {Promise} Return a promise either fulfilled, or rejected with + * CordovaError. + */ +Api.prototype.clean = function(cleanOptions) { + var self = this; + return require('./lib/check_reqs').run() + .then(function () { + return require('./lib/build').runClean.call(self, cleanOptions); + }); +}; + +/** + * Performs a requirements check for current platform. Each platform defines its + * own set of requirements, which should be resolved before platform can be + * built successfully. + * + * @return {Promise} Promise, resolved with set of Requirement + * objects for current platform. + */ +Api.prototype.requirements = function() { + return require('./lib/check_reqs').check_all(); +}; + +module.exports = Api; + +/** + * Removes the specified modules from list of installed modules and updates + * platform_json and cordova_plugins.js on disk. + * + * @param {PluginInfo} plugin PluginInfo instance for plugin, which modules + * needs to be added. + * @param {String} targetDir The directory, where updated cordova_plugins.js + * should be written to. + */ +Api.prototype._addModulesInfo = function(plugin, targetDir) { + var installedModules = this._platformJson.root.modules || []; + + var installedPaths = installedModules.map(function (installedModule) { + return installedModule.file; + }); + + var modulesToInstall = plugin.getJsModules(this.platform) + .filter(function (moduleToInstall) { + return installedPaths.indexOf(moduleToInstall.file) === -1; + }).map(function (moduleToInstall) { + var moduleName = plugin.id + '.' + ( moduleToInstall.name || moduleToInstall.src.match(/([^\/]+)\.js/)[1] ); + var obj = { + file: ['plugins', plugin.id, moduleToInstall.src].join('/'), + id: moduleName + }; + if (moduleToInstall.clobbers.length > 0) { + obj.clobbers = moduleToInstall.clobbers.map(function(o) { return o.target; }); + } + if (moduleToInstall.merges.length > 0) { + obj.merges = moduleToInstall.merges.map(function(o) { return o.target; }); + } + if (moduleToInstall.runs) { + obj.runs = true; + } + + return obj; + }); + + this._platformJson.root.modules = installedModules.concat(modulesToInstall); + this._writePluginModules(targetDir); + this._platformJson.save(); +}; + +/** + * Removes the specified modules from list of installed modules and updates + * platform_json and cordova_plugins.js on disk. + * + * @param {PluginInfo} plugin PluginInfo instance for plugin, which modules + * needs to be removed. + * @param {String} targetDir The directory, where updated cordova_plugins.js + * should be written to. + */ +Api.prototype._removeModulesInfo = function(plugin, targetDir) { + var installedModules = this._platformJson.root.modules || []; + var modulesToRemove = plugin.getJsModules(this.platform) + .map(function (jsModule) { + return ['plugins', plugin.id, jsModule.src].join('/'); + }); + + var updatedModules = installedModules + .filter(function (installedModule) { + return (modulesToRemove.indexOf(installedModule.file) === -1); + }); + + this._platformJson.root.modules = updatedModules; + this._writePluginModules(targetDir); + this._platformJson.save(); +}; + +/** + * Fetches all installed modules, generates cordova_plugins contents and writes + * it to file. + * + * @param {String} targetDir Directory, where write cordova_plugins.js to. + * Ususally it is either /www or /platform_www + * directories. + */ +Api.prototype._writePluginModules = function (targetDir) { + var self = this; + // Write out moduleObjects as JSON wrapped in a cordova module to cordova_plugins.js + var final_contents = 'cordova.define(\'cordova/plugin_list\', function(require, exports, module) {\n'; + final_contents += 'module.exports = ' + JSON.stringify(this._platformJson.root.modules, null, ' ') + ';\n'; + final_contents += 'module.exports.metadata = \n'; + final_contents += '// TOP OF METADATA\n'; + + var pluginMetadata = Object.keys(this._platformJson.root.installed_plugins) + .reduce(function (metadata, plugin) { + metadata[plugin] = self._platformJson.root.installed_plugins[plugin].version; + return metadata; + }, {}); + + final_contents += JSON.stringify(pluginMetadata, null, 4) + '\n'; + final_contents += '// BOTTOM OF METADATA\n'; + final_contents += '});'; // Close cordova.define. + + shell.mkdir('-p', targetDir); + fs.writeFileSync(path.join(targetDir, 'cordova_plugins.js'), final_contents, 'utf-8'); +}; diff --git a/bin/templates/cordova/lib/Adb.js b/bin/templates/cordova/lib/Adb.js index e4fc3637..fc97f666 100644 --- a/bin/templates/cordova/lib/Adb.js +++ b/bin/templates/cordova/lib/Adb.js @@ -1,78 +1,78 @@ - -var Q = require('q'); -var os = require('os'); -var events = require('cordova-common').events; -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; - -var Adb = {}; - -function isDevice(line) { - return line.match(/\w+\tdevice/) && !line.match(/emulator/); -} - -function isEmulator(line) { - return line.match(/device/) && line.match(/emulator/); -} - -/** - * Lists available/connected devices and emulators - * - * @param {Object} opts Various options - * @param {Boolean} opts.emulators Specifies whether this method returns - * emulators only - * - * @return {Promise} list of available/connected - * devices/emulators - */ -Adb.devices = function (opts) { - return spawn('adb', ['devices'], {cwd: os.tmpdir()}) - .then(function(output) { - return output.split('\n').filter(function (line) { - // Filter out either real devices or emulators, depending on options - return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line); - }).map(function (line) { - return line.replace(/\tdevice/, '').replace('\r', ''); - }); - }); -}; - -Adb.install = function (target, packagePath, opts) { - events.emit('verbose', 'Installing apk ' + packagePath + ' on ' + target + '...'); - var args = ['-s', target, 'install']; - if (opts && opts.replace) args.push('-r'); - return spawn('adb', args.concat(packagePath), {cwd: os.tmpdir()}) - .then(function(output) { - // 'adb install' seems to always returns no error, even if installation fails - // so we catching output to detect installation failure - if (output.match(/Failure/)) - return Q.reject(new CordovaError('Failed to install apk to device: ' + output)); - }); -}; - -Adb.uninstall = function (target, packageId) { - events.emit('verbose', 'Uninstalling ' + packageId + ' from ' + target + '...'); - return spawn('adb', ['-s', target, 'uninstall', packageId], {cwd: os.tmpdir()}); -}; - -Adb.shell = function (target, shellCommand) { - events.emit('verbose', 'Running command "' + shellCommand + '" on ' + target + '...'); - var args = ['-s', target, 'shell']; - shellCommand = shellCommand.split(/\s+/); - return spawn('adb', args.concat(shellCommand), {cwd: os.tmpdir()}) - .catch(function (output) { - return Q.reject(new CordovaError('Failed to execute shell command "' + - shellCommand + '"" on device: ' + output)); - }); -}; - -Adb.start = function (target, activityName) { - events.emit('verbose', 'Starting application "' + activityName + '" on ' + target + '...'); - return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName) - .catch(function (output) { - return Q.reject(new CordovaError('Failed to start application "' + - activityName + '"" on device: ' + output)); - }); -}; - -module.exports = Adb; + +var Q = require('q'); +var os = require('os'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; + +var Adb = {}; + +function isDevice(line) { + return line.match(/\w+\tdevice/) && !line.match(/emulator/); +} + +function isEmulator(line) { + return line.match(/device/) && line.match(/emulator/); +} + +/** + * Lists available/connected devices and emulators + * + * @param {Object} opts Various options + * @param {Boolean} opts.emulators Specifies whether this method returns + * emulators only + * + * @return {Promise} list of available/connected + * devices/emulators + */ +Adb.devices = function (opts) { + return spawn('adb', ['devices'], {cwd: os.tmpdir()}) + .then(function(output) { + return output.split('\n').filter(function (line) { + // Filter out either real devices or emulators, depending on options + return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line); + }).map(function (line) { + return line.replace(/\tdevice/, '').replace('\r', ''); + }); + }); +}; + +Adb.install = function (target, packagePath, opts) { + events.emit('verbose', 'Installing apk ' + packagePath + ' on ' + target + '...'); + var args = ['-s', target, 'install']; + if (opts && opts.replace) args.push('-r'); + return spawn('adb', args.concat(packagePath), {cwd: os.tmpdir()}) + .then(function(output) { + // 'adb install' seems to always returns no error, even if installation fails + // so we catching output to detect installation failure + if (output.match(/Failure/)) + return Q.reject(new CordovaError('Failed to install apk to device: ' + output)); + }); +}; + +Adb.uninstall = function (target, packageId) { + events.emit('verbose', 'Uninstalling ' + packageId + ' from ' + target + '...'); + return spawn('adb', ['-s', target, 'uninstall', packageId], {cwd: os.tmpdir()}); +}; + +Adb.shell = function (target, shellCommand) { + events.emit('verbose', 'Running command "' + shellCommand + '" on ' + target + '...'); + var args = ['-s', target, 'shell']; + shellCommand = shellCommand.split(/\s+/); + return spawn('adb', args.concat(shellCommand), {cwd: os.tmpdir()}) + .catch(function (output) { + return Q.reject(new CordovaError('Failed to execute shell command "' + + shellCommand + '"" on device: ' + output)); + }); +}; + +Adb.start = function (target, activityName) { + events.emit('verbose', 'Starting application "' + activityName + '" on ' + target + '...'); + return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName) + .catch(function (output) { + return Q.reject(new CordovaError('Failed to start application "' + + activityName + '"" on device: ' + output)); + }); +}; + +module.exports = Adb; diff --git a/bin/templates/cordova/lib/AndroidManifest.js b/bin/templates/cordova/lib/AndroidManifest.js index 770d5274..3654ada1 100644 --- a/bin/templates/cordova/lib/AndroidManifest.js +++ b/bin/templates/cordova/lib/AndroidManifest.js @@ -1,161 +1,161 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var fs = require('fs'); -var et = require('elementtree'); -var xml= require('cordova-common').xmlHelpers; - -var DEFAULT_ORIENTATION = 'default'; - -/** Wraps an AndroidManifest file */ -function AndroidManifest(path) { - this.path = path; - this.doc = xml.parseElementtreeSync(path); - if (this.doc.getroot().tag !== 'manifest') { - throw new Error(path + ' has incorrect root node name (expected "manifest")'); - } -} - -AndroidManifest.prototype.getVersionName = function() { - return this.doc.getroot().attrib['android:versionName']; -}; - -AndroidManifest.prototype.setVersionName = function(versionName) { - this.doc.getroot().attrib['android:versionName'] = versionName; - return this; -}; - -AndroidManifest.prototype.getVersionCode = function() { - return this.doc.getroot().attrib['android:versionCode']; -}; - -AndroidManifest.prototype.setVersionCode = function(versionCode) { - this.doc.getroot().attrib['android:versionCode'] = versionCode; - return this; -}; - -AndroidManifest.prototype.getPackageId = function() { - /*jshint -W069 */ - return this.doc.getroot().attrib['package']; - /*jshint +W069 */ -}; - -AndroidManifest.prototype.setPackageId = function(pkgId) { - /*jshint -W069 */ - this.doc.getroot().attrib['package'] = pkgId; - /*jshint +W069 */ - return this; -}; - -AndroidManifest.prototype.getActivity = function() { - var activity = this.doc.getroot().find('./application/activity'); - return { - getName: function () { - return activity.attrib['android:name']; - }, - setName: function (name) { - if (!name) { - delete activity.attrib['android:name']; - } else { - activity.attrib['android:name'] = name; - } - return this; - }, - getOrientation: function () { - return activity.attrib['android:screenOrientation']; - }, - setOrientation: function (orientation) { - if (!orientation || orientation.toLowerCase() === DEFAULT_ORIENTATION) { - delete activity.attrib['android:screenOrientation']; - } else { - activity.attrib['android:screenOrientation'] = orientation; - } - return this; - }, - getLaunchMode: function () { - return activity.attrib['android:launchMode']; - }, - setLaunchMode: function (launchMode) { - if (!launchMode) { - delete activity.attrib['android:launchMode']; - } else { - activity.attrib['android:launchMode'] = launchMode; - } - return this; - } - }; -}; - -['minSdkVersion', 'maxSdkVersion', 'targetSdkVersion'] -.forEach(function(sdkPrefName) { - // Copy variable reference to avoid closure issues - var prefName = sdkPrefName; - - AndroidManifest.prototype['get' + capitalize(prefName)] = function() { - var usesSdk = this.doc.getroot().find('./uses-sdk'); - return usesSdk && usesSdk.attrib['android:' + prefName]; - }; - - AndroidManifest.prototype['set' + capitalize(prefName)] = function(prefValue) { - var usesSdk = this.doc.getroot().find('./uses-sdk'); - - if (!usesSdk && prefValue) { // if there is no required uses-sdk element, we should create it first - usesSdk = new et.Element('uses-sdk'); - this.doc.getroot().append(usesSdk); - } - - if (prefValue) { - usesSdk.attrib['android:' + prefName] = prefValue; - } - - return this; - }; -}); - -AndroidManifest.prototype.getDebuggable = function() { - return this.doc.getroot().find('./application').attrib['android:debuggable'] === 'true'; -}; - -AndroidManifest.prototype.setDebuggable = function(value) { - var application = this.doc.getroot().find('./application'); - if (value) { - application.attrib['android:debuggable'] = 'true'; - } else { - // The default value is "false", so we can remove attribute at all. - delete application.attrib['android:debuggable']; - } - return this; -}; - -/** - * Writes manifest to disk syncronously. If filename is specified, then manifest - * will be written to that file - * - * @param {String} [destPath] File to write manifest to. If omitted, - * manifest will be written to file it has been read from. - */ -AndroidManifest.prototype.write = function(destPath) { - fs.writeFileSync(destPath || this.path, this.doc.write({indent: 4}), 'utf-8'); -}; - -module.exports = AndroidManifest; - -function capitalize (str) { - return str.charAt(0).toUpperCase() + str.slice(1); -} +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var fs = require('fs'); +var et = require('elementtree'); +var xml= require('cordova-common').xmlHelpers; + +var DEFAULT_ORIENTATION = 'default'; + +/** Wraps an AndroidManifest file */ +function AndroidManifest(path) { + this.path = path; + this.doc = xml.parseElementtreeSync(path); + if (this.doc.getroot().tag !== 'manifest') { + throw new Error(path + ' has incorrect root node name (expected "manifest")'); + } +} + +AndroidManifest.prototype.getVersionName = function() { + return this.doc.getroot().attrib['android:versionName']; +}; + +AndroidManifest.prototype.setVersionName = function(versionName) { + this.doc.getroot().attrib['android:versionName'] = versionName; + return this; +}; + +AndroidManifest.prototype.getVersionCode = function() { + return this.doc.getroot().attrib['android:versionCode']; +}; + +AndroidManifest.prototype.setVersionCode = function(versionCode) { + this.doc.getroot().attrib['android:versionCode'] = versionCode; + return this; +}; + +AndroidManifest.prototype.getPackageId = function() { + /*jshint -W069 */ + return this.doc.getroot().attrib['package']; + /*jshint +W069 */ +}; + +AndroidManifest.prototype.setPackageId = function(pkgId) { + /*jshint -W069 */ + this.doc.getroot().attrib['package'] = pkgId; + /*jshint +W069 */ + return this; +}; + +AndroidManifest.prototype.getActivity = function() { + var activity = this.doc.getroot().find('./application/activity'); + return { + getName: function () { + return activity.attrib['android:name']; + }, + setName: function (name) { + if (!name) { + delete activity.attrib['android:name']; + } else { + activity.attrib['android:name'] = name; + } + return this; + }, + getOrientation: function () { + return activity.attrib['android:screenOrientation']; + }, + setOrientation: function (orientation) { + if (!orientation || orientation.toLowerCase() === DEFAULT_ORIENTATION) { + delete activity.attrib['android:screenOrientation']; + } else { + activity.attrib['android:screenOrientation'] = orientation; + } + return this; + }, + getLaunchMode: function () { + return activity.attrib['android:launchMode']; + }, + setLaunchMode: function (launchMode) { + if (!launchMode) { + delete activity.attrib['android:launchMode']; + } else { + activity.attrib['android:launchMode'] = launchMode; + } + return this; + } + }; +}; + +['minSdkVersion', 'maxSdkVersion', 'targetSdkVersion'] +.forEach(function(sdkPrefName) { + // Copy variable reference to avoid closure issues + var prefName = sdkPrefName; + + AndroidManifest.prototype['get' + capitalize(prefName)] = function() { + var usesSdk = this.doc.getroot().find('./uses-sdk'); + return usesSdk && usesSdk.attrib['android:' + prefName]; + }; + + AndroidManifest.prototype['set' + capitalize(prefName)] = function(prefValue) { + var usesSdk = this.doc.getroot().find('./uses-sdk'); + + if (!usesSdk && prefValue) { // if there is no required uses-sdk element, we should create it first + usesSdk = new et.Element('uses-sdk'); + this.doc.getroot().append(usesSdk); + } + + if (prefValue) { + usesSdk.attrib['android:' + prefName] = prefValue; + } + + return this; + }; +}); + +AndroidManifest.prototype.getDebuggable = function() { + return this.doc.getroot().find('./application').attrib['android:debuggable'] === 'true'; +}; + +AndroidManifest.prototype.setDebuggable = function(value) { + var application = this.doc.getroot().find('./application'); + if (value) { + application.attrib['android:debuggable'] = 'true'; + } else { + // The default value is "false", so we can remove attribute at all. + delete application.attrib['android:debuggable']; + } + return this; +}; + +/** + * Writes manifest to disk syncronously. If filename is specified, then manifest + * will be written to that file + * + * @param {String} [destPath] File to write manifest to. If omitted, + * manifest will be written to file it has been read from. + */ +AndroidManifest.prototype.write = function(destPath) { + fs.writeFileSync(destPath || this.path, this.doc.write({indent: 4}), 'utf-8'); +}; + +module.exports = AndroidManifest; + +function capitalize (str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/bin/templates/cordova/lib/AndroidProject.js b/bin/templates/cordova/lib/AndroidProject.js index 918a39bd..458b84e8 100644 --- a/bin/templates/cordova/lib/AndroidProject.js +++ b/bin/templates/cordova/lib/AndroidProject.js @@ -1,184 +1,184 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var fs = require('fs'); -var path = require('path'); -var properties_parser = require('properties-parser'); -var AndroidManifest = require('./AndroidManifest'); - -var projectFileCache = {}; - -function addToPropertyList(projectProperties, key, value) { - var i = 1; - while (projectProperties.get(key + '.' + i)) - i++; - - projectProperties.set(key + '.' + i, value); - projectProperties.dirty = true; -} - -function removeFromPropertyList(projectProperties, key, value) { - var i = 1; - var currentValue; - while ((currentValue = projectProperties.get(key + '.' + i))) { - if (currentValue === value) { - while ((currentValue = projectProperties.get(key + '.' + (i + 1)))) { - projectProperties.set(key + '.' + i, currentValue); - i++; - } - projectProperties.set(key + '.' + i); - break; - } - i++; - } - projectProperties.dirty = true; -} - -function getRelativeLibraryPath (parentDir, subDir) { - var libraryPath = path.relative(parentDir, subDir); - return (path.sep == '\\') ? libraryPath.replace(/\\/g, '/') : libraryPath; -} - -function AndroidProject(projectDir) { - this._propertiesEditors = {}; - this._subProjectDirs = {}; - this._dirty = false; - this.projectDir = projectDir; - this.platformWww = path.join(this.projectDir, 'platform_www'); - this.www = path.join(this.projectDir, 'assets/www'); -} - -AndroidProject.getProjectFile = function (projectDir) { - if (!projectFileCache[projectDir]) { - projectFileCache[projectDir] = new AndroidProject(projectDir); - } - - return projectFileCache[projectDir]; -}; - -AndroidProject.purgeCache = function (projectDir) { - if (projectDir) { - delete projectFileCache[projectDir]; - } else { - projectFileCache = {}; - } -}; - -/** - * Reads the package name out of the Android Manifest file - * - * @param {String} projectDir The absolute path to the directory containing the project - * - * @return {String} The name of the package - */ -AndroidProject.prototype.getPackageName = function() { - return new AndroidManifest(path.join(this.projectDir, 'AndroidManifest.xml')).getPackageId(); -}; - -AndroidProject.prototype.getCustomSubprojectRelativeDir = function(plugin_id, src) { - // All custom subprojects are prefixed with the last portion of the package id. - // This is to avoid collisions when opening multiple projects in Eclipse that have subprojects with the same name. - var packageName = this.getPackageName(); - var lastDotIndex = packageName.lastIndexOf('.'); - var prefix = packageName.substring(lastDotIndex + 1); - var subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src)); - return subRelativeDir; -}; - -AndroidProject.prototype.addSubProject = function(parentDir, subDir) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var subProjectFile = path.resolve(subDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - // TODO: Setting the target needs to happen only for pre-3.7.0 projects - if (fs.existsSync(subProjectFile)) { - var subProperties = this._getPropertiesFile(subProjectFile); - subProperties.set('target', parentProperties.get('target')); - subProperties.dirty = true; - this._subProjectDirs[subDir] = true; - } - addToPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir)); - - this._dirty = true; -}; - -AndroidProject.prototype.removeSubProject = function(parentDir, subDir) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - removeFromPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir)); - delete this._subProjectDirs[subDir]; - this._dirty = true; -}; - -AndroidProject.prototype.addGradleReference = function(parentDir, subDir) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - addToPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir)); - this._dirty = true; -}; - -AndroidProject.prototype.removeGradleReference = function(parentDir, subDir) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - removeFromPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir)); - this._dirty = true; -}; - -AndroidProject.prototype.addSystemLibrary = function(parentDir, value) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - addToPropertyList(parentProperties, 'cordova.system.library', value); - this._dirty = true; -}; - -AndroidProject.prototype.removeSystemLibrary = function(parentDir, value) { - var parentProjectFile = path.resolve(parentDir, 'project.properties'); - var parentProperties = this._getPropertiesFile(parentProjectFile); - removeFromPropertyList(parentProperties, 'cordova.system.library', value); - this._dirty = true; -}; - -AndroidProject.prototype.write = function() { - if (!this._dirty) { - return; - } - this._dirty = false; - - for (var filename in this._propertiesEditors) { - var editor = this._propertiesEditors[filename]; - if (editor.dirty) { - fs.writeFileSync(filename, editor.toString()); - editor.dirty = false; - } - } -}; - -AndroidProject.prototype._getPropertiesFile = function (filename) { - if (!this._propertiesEditors[filename]) { - if (fs.existsSync(filename)) { - this._propertiesEditors[filename] = properties_parser.createEditor(filename); - } else { - this._propertiesEditors[filename] = properties_parser.createEditor(); - } - } - - return this._propertiesEditors[filename]; -}; - - -module.exports = AndroidProject; +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var fs = require('fs'); +var path = require('path'); +var properties_parser = require('properties-parser'); +var AndroidManifest = require('./AndroidManifest'); + +var projectFileCache = {}; + +function addToPropertyList(projectProperties, key, value) { + var i = 1; + while (projectProperties.get(key + '.' + i)) + i++; + + projectProperties.set(key + '.' + i, value); + projectProperties.dirty = true; +} + +function removeFromPropertyList(projectProperties, key, value) { + var i = 1; + var currentValue; + while ((currentValue = projectProperties.get(key + '.' + i))) { + if (currentValue === value) { + while ((currentValue = projectProperties.get(key + '.' + (i + 1)))) { + projectProperties.set(key + '.' + i, currentValue); + i++; + } + projectProperties.set(key + '.' + i); + break; + } + i++; + } + projectProperties.dirty = true; +} + +function getRelativeLibraryPath (parentDir, subDir) { + var libraryPath = path.relative(parentDir, subDir); + return (path.sep == '\\') ? libraryPath.replace(/\\/g, '/') : libraryPath; +} + +function AndroidProject(projectDir) { + this._propertiesEditors = {}; + this._subProjectDirs = {}; + this._dirty = false; + this.projectDir = projectDir; + this.platformWww = path.join(this.projectDir, 'platform_www'); + this.www = path.join(this.projectDir, 'assets/www'); +} + +AndroidProject.getProjectFile = function (projectDir) { + if (!projectFileCache[projectDir]) { + projectFileCache[projectDir] = new AndroidProject(projectDir); + } + + return projectFileCache[projectDir]; +}; + +AndroidProject.purgeCache = function (projectDir) { + if (projectDir) { + delete projectFileCache[projectDir]; + } else { + projectFileCache = {}; + } +}; + +/** + * Reads the package name out of the Android Manifest file + * + * @param {String} projectDir The absolute path to the directory containing the project + * + * @return {String} The name of the package + */ +AndroidProject.prototype.getPackageName = function() { + return new AndroidManifest(path.join(this.projectDir, 'AndroidManifest.xml')).getPackageId(); +}; + +AndroidProject.prototype.getCustomSubprojectRelativeDir = function(plugin_id, src) { + // All custom subprojects are prefixed with the last portion of the package id. + // This is to avoid collisions when opening multiple projects in Eclipse that have subprojects with the same name. + var packageName = this.getPackageName(); + var lastDotIndex = packageName.lastIndexOf('.'); + var prefix = packageName.substring(lastDotIndex + 1); + var subRelativeDir = path.join(plugin_id, prefix + '-' + path.basename(src)); + return subRelativeDir; +}; + +AndroidProject.prototype.addSubProject = function(parentDir, subDir) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var subProjectFile = path.resolve(subDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + // TODO: Setting the target needs to happen only for pre-3.7.0 projects + if (fs.existsSync(subProjectFile)) { + var subProperties = this._getPropertiesFile(subProjectFile); + subProperties.set('target', parentProperties.get('target')); + subProperties.dirty = true; + this._subProjectDirs[subDir] = true; + } + addToPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir)); + + this._dirty = true; +}; + +AndroidProject.prototype.removeSubProject = function(parentDir, subDir) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + removeFromPropertyList(parentProperties, 'android.library.reference', getRelativeLibraryPath(parentDir, subDir)); + delete this._subProjectDirs[subDir]; + this._dirty = true; +}; + +AndroidProject.prototype.addGradleReference = function(parentDir, subDir) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + addToPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir)); + this._dirty = true; +}; + +AndroidProject.prototype.removeGradleReference = function(parentDir, subDir) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + removeFromPropertyList(parentProperties, 'cordova.gradle.include', getRelativeLibraryPath(parentDir, subDir)); + this._dirty = true; +}; + +AndroidProject.prototype.addSystemLibrary = function(parentDir, value) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + addToPropertyList(parentProperties, 'cordova.system.library', value); + this._dirty = true; +}; + +AndroidProject.prototype.removeSystemLibrary = function(parentDir, value) { + var parentProjectFile = path.resolve(parentDir, 'project.properties'); + var parentProperties = this._getPropertiesFile(parentProjectFile); + removeFromPropertyList(parentProperties, 'cordova.system.library', value); + this._dirty = true; +}; + +AndroidProject.prototype.write = function() { + if (!this._dirty) { + return; + } + this._dirty = false; + + for (var filename in this._propertiesEditors) { + var editor = this._propertiesEditors[filename]; + if (editor.dirty) { + fs.writeFileSync(filename, editor.toString()); + editor.dirty = false; + } + } +}; + +AndroidProject.prototype._getPropertiesFile = function (filename) { + if (!this._propertiesEditors[filename]) { + if (fs.existsSync(filename)) { + this._propertiesEditors[filename] = properties_parser.createEditor(filename); + } else { + this._propertiesEditors[filename] = properties_parser.createEditor(); + } + } + + return this._propertiesEditors[filename]; +}; + + +module.exports = AndroidProject; diff --git a/bin/templates/cordova/lib/ConsoleLogger.js b/bin/templates/cordova/lib/ConsoleLogger.js index 19b03ec7..cee2dc10 100644 --- a/bin/templates/cordova/lib/ConsoleLogger.js +++ b/bin/templates/cordova/lib/ConsoleLogger.js @@ -1,75 +1,75 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var loggerInstance; -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var CordovaError = require('cordova-common').CordovaError; - -/** - * @class ConsoleLogger - * @extends EventEmitter - * - * Implementing basic logging for platform. Inherits regular NodeJS - * EventEmitter. All events, emitted on this class instance are immediately - * logged to console. - * - * Also attaches handler to process' uncaught exceptions, so these exceptions - * logged to console similar to regular error events. - */ -function ConsoleLogger() { - EventEmitter.call(this); - - var isVerbose = process.argv.indexOf('-d') >= 0 || process.argv.indexOf('--verbose') >= 0; - // For CordovaError print only the message without stack trace unless we - // are in a verbose mode. - process.on('uncaughtException', function(err){ - if ((err instanceof CordovaError) && isVerbose) { - console.error(err.stack); - } else { - console.error(err.message); - } - process.exit(1); - }); - - this.on('results', console.log); - this.on('verbose', function () { - if (isVerbose) - console.log.apply(console, arguments); - }); - this.on('info', console.log); - this.on('log', console.log); - this.on('warn', console.warn); -} -util.inherits(ConsoleLogger, EventEmitter); - -/** - * Returns already instantiated/newly created instance of ConsoleLogger class. - * This method should be used instead of creating ConsoleLogger directly, - * otherwise we'll get multiple handlers attached to process' - * uncaughtException - * - * @return {ConsoleLogger} New or already created instance of ConsoleLogger - */ -ConsoleLogger.get = function () { - loggerInstance = loggerInstance || new ConsoleLogger(); - return loggerInstance; -}; - -module.exports = ConsoleLogger; +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var loggerInstance; +var util = require('util'); +var EventEmitter = require('events').EventEmitter; +var CordovaError = require('cordova-common').CordovaError; + +/** + * @class ConsoleLogger + * @extends EventEmitter + * + * Implementing basic logging for platform. Inherits regular NodeJS + * EventEmitter. All events, emitted on this class instance are immediately + * logged to console. + * + * Also attaches handler to process' uncaught exceptions, so these exceptions + * logged to console similar to regular error events. + */ +function ConsoleLogger() { + EventEmitter.call(this); + + var isVerbose = process.argv.indexOf('-d') >= 0 || process.argv.indexOf('--verbose') >= 0; + // For CordovaError print only the message without stack trace unless we + // are in a verbose mode. + process.on('uncaughtException', function(err){ + if ((err instanceof CordovaError) && isVerbose) { + console.error(err.stack); + } else { + console.error(err.message); + } + process.exit(1); + }); + + this.on('results', console.log); + this.on('verbose', function () { + if (isVerbose) + console.log.apply(console, arguments); + }); + this.on('info', console.log); + this.on('log', console.log); + this.on('warn', console.warn); +} +util.inherits(ConsoleLogger, EventEmitter); + +/** + * Returns already instantiated/newly created instance of ConsoleLogger class. + * This method should be used instead of creating ConsoleLogger directly, + * otherwise we'll get multiple handlers attached to process' + * uncaughtException + * + * @return {ConsoleLogger} New or already created instance of ConsoleLogger + */ +ConsoleLogger.get = function () { + loggerInstance = loggerInstance || new ConsoleLogger(); + return loggerInstance; +}; + +module.exports = ConsoleLogger; diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js index db692e90..9e57cb87 100644 --- a/bin/templates/cordova/lib/build.js +++ b/bin/templates/cordova/lib/build.js @@ -1,301 +1,301 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'), - path = require('path'), - fs = require('fs'), - nopt = require('nopt'); - -var Adb = require('./Adb'); - -var builders = require('./builders/builders'); -var events = require('cordova-common').events; -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; - -function parseOpts(options, resolvedTarget) { - options = options || {}; - options.argv = nopt({ - gradle: Boolean, - ant: Boolean, - prepenv: Boolean, - versionCode: String, - minSdkVersion: String, - gradleArg: String, - keystore: path, - alias: String, - storePassword: String, - password: String, - keystoreType: String - }, {}, options.argv, 0); - - var ret = { - buildType: options.release ? 'release' : 'debug', - buildMethod: process.env.ANDROID_BUILD || 'gradle', - prepEnv: options.argv.prepenv, - arch: resolvedTarget && resolvedTarget.arch, - extraArgs: [] - }; - - if (options.argv.ant || options.argv.gradle) - ret.buildMethod = options.argv.ant ? 'ant' : 'gradle'; - - if (options.nobuild) ret.buildMethod = 'none'; - - if (options.argv.versionCode) - ret.extraArgs.push('-PcdvVersionCode=' + options.versionCode); - - if (options.argv.minSdkVersion) - ret.extraArgs.push('-PcdvMinSdkVersion=' + options.minSdkVersion); - - if (options.argv.gradleArg) - ret.extraArgs.push(options.gradleArg); - - var packageArgs = {}; - - if (options.argv.keystore) - packageArgs.keystore = path.relative(this.root, path.resolve(options.argv.keystore)); - - ['alias','storePassword','password','keystoreType'].forEach(function (flagName) { - if (options.argv[flagName]) - packageArgs[flagName] = options.argv[flagName]; - }); - - var buildConfig = options.buildConfig; - - // If some values are not specified as command line arguments - use build config to supplement them. - // Command line arguemnts have precedence over build config. - if (buildConfig) { - if (!fs.existsSync(buildConfig)) { - throw new Error('Specified build config file does not exist: ' + buildConfig); - } - events.emit('log', 'Reading build config file: '+ path.resolve(buildConfig)); - var config = JSON.parse(fs.readFileSync(buildConfig, 'utf8')); - if (config.android && config.android[ret.buildType]) { - var androidInfo = config.android[ret.buildType]; - if(androidInfo.keystore && !packageArgs.keystore) { - packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore); - } - - ['alias', 'storePassword', 'password','keystoreType'].forEach(function (key){ - packageArgs[key] = packageArgs[key] || androidInfo[key]; - }); - } - } - - if (packageArgs.keystore && packageArgs.alias) { - ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword, - packageArgs.password, packageArgs.keystoreType); - } - - if(!ret.packageInfo) { - if(Object.keys(packageArgs).length > 0) { - events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); - } - } - - return ret; -} - -/* - * Builds the project with the specifed options - * Returns a promise. - */ -module.exports.runClean = function(options) { - var opts = parseOpts(options); - var builder = builders.getBuilder(opts.buildMethod); - return builder.prepEnv(opts) - .then(function() { - return builder.clean(opts); - }); -}; - -/** - * Builds the project with the specifed options. - * - * @param {BuildOptions} options A set of options. See PlatformApi.build - * method documentation for reference. - * @param {Object} optResolvedTarget A deployment target. Used to pass - * target architecture from upstream 'run' call. TODO: remove this option in - * favor of setting buildOptions.archs field. - * - * @return {Promise} Promise, resolved with built packages - * information. - */ -module.exports.run = function(options, optResolvedTarget) { - var opts = parseOpts(options, optResolvedTarget); - var builder = builders.getBuilder(opts.buildMethod); - var self = this; - return builder.prepEnv(opts) - .then(function() { - if (opts.prepEnv) { - self.events.emit('verbose', 'Build file successfully prepared.'); - return; - } - return builder.build(opts) - .then(function() { - var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); - self.events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t')); - return { - apkPaths: apkPaths, - buildType: opts.buildType, - buildMethod: opts.buildMethod - }; - }); - }); -}; - -// Called by plugman after installing plugins, and by create script after creating project. -module.exports.prepBuildFiles = function() { - return builders.getBuilder('gradle').prepBuildFiles(); -}; - -/* - * Detects the architecture of a device/emulator - * Returns "arm" or "x86". - */ -module.exports.detectArchitecture = function(target) { - function helper() { - return Adb.shell(target, 'cat /proc/cpuinfo') - .then(function(output) { - return /intel/i.exec(output) ? 'x86' : 'arm'; - }); - } - // It sometimes happens (at least on OS X), that this command will hang forever. - // To fix it, either unplug & replug device, or restart adb server. - return helper() - .timeout(1000, new CordovaError('Device communication timed out. Try unplugging & replugging the device.')) - .then(null, function(err) { - if (/timed out/.exec('' + err)) { - // adb kill-server doesn't seem to do the trick. - // Could probably find a x-platform version of killall, but I'm not actually - // sure that this scenario even happens on non-OSX machines. - return spawn('killall', ['adb']) - .then(function() { - events.emit('verbose', 'adb seems hung. retrying.'); - return helper() - .then(null, function() { - // The double kill is sadly often necessary, at least on mac. - events.emit('warn', 'Now device not found... restarting adb again.'); - return spawn('killall', ['adb']) - .then(function() { - return helper() - .then(null, function() { - return Q.reject(new CordovaError('USB is flakey. Try unplugging & replugging the device.')); - }); - }); - }); - }, function() { - // For non-killall OS's. - return Q.reject(err); - }); - } - throw err; - }); -}; - -module.exports.findBestApkForArchitecture = function(buildResults, arch) { - var paths = buildResults.apkPaths.filter(function(p) { - var apkName = path.basename(p); - if (buildResults.buildType == 'debug') { - return /-debug/.exec(apkName); - } - return !/-debug/.exec(apkName); - }); - var archPattern = new RegExp('-' + arch); - var hasArchPattern = /-x86|-arm/; - for (var i = 0; i < paths.length; ++i) { - var apkName = path.basename(paths[i]); - if (hasArchPattern.exec(apkName)) { - if (archPattern.exec(apkName)) { - return paths[i]; - } - } else { - return paths[i]; - } - } - throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType); -}; - -function PackageInfo(keystore, alias, storePassword, password, keystoreType) { - this.keystore = { - 'name': 'key.store', - 'value': keystore - }; - this.alias = { - 'name': 'key.alias', - 'value': alias - }; - if (storePassword) { - this.storePassword = { - 'name': 'key.store.password', - 'value': storePassword - }; - } - if (password) { - this.password = { - 'name': 'key.alias.password', - 'value': password - }; - } - if (keystoreType) { - this.keystoreType = { - 'name': 'key.store.type', - 'value': keystoreType - }; - } -} - -PackageInfo.prototype = { - toProperties: function() { - var self = this; - var result = ''; - Object.keys(self).forEach(function(key) { - result += self[key].name; - result += '='; - result += self[key].value.replace(/\\/g, '\\\\'); - result += '\n'; - }); - return result; - } -}; - -module.exports.help = function() { - console.log('Usage: ' + path.relative(process.cwd(), path.join('../build')) + ' [flags] [Signed APK flags]'); - console.log('Flags:'); - console.log(' \'--debug\': will build project in debug mode (default)'); - console.log(' \'--release\': will build project for release'); - console.log(' \'--ant\': will build project with ant'); - console.log(' \'--gradle\': will build project with gradle (default)'); - console.log(' \'--nobuild\': will skip build process (useful when using run command)'); - console.log(' \'--prepenv\': don\'t build, but copy in build scripts where necessary'); - console.log(' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs. Requires --gradle.'); - console.log(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.'); - console.log(' \'--gradleArg=\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true'); - console.log(''); - console.log('Signed APK flags (overwrites debug/release-signing.proprties) :'); - console.log(' \'--keystore=\': Key store used to build a signed archive. (Required)'); - console.log(' \'--alias=\': Alias for the key store. (Required)'); - console.log(' \'--storePassword=\': Password for the key store. (Optional - prompted)'); - console.log(' \'--password=\': Password for the key. (Optional - prompted)'); - console.log(' \'--keystoreType\': Type of the keystore. (Optional)'); - process.exit(0); -}; +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'), + path = require('path'), + fs = require('fs'), + nopt = require('nopt'); + +var Adb = require('./Adb'); + +var builders = require('./builders/builders'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; + +function parseOpts(options, resolvedTarget) { + options = options || {}; + options.argv = nopt({ + gradle: Boolean, + ant: Boolean, + prepenv: Boolean, + versionCode: String, + minSdkVersion: String, + gradleArg: String, + keystore: path, + alias: String, + storePassword: String, + password: String, + keystoreType: String + }, {}, options.argv, 0); + + var ret = { + buildType: options.release ? 'release' : 'debug', + buildMethod: process.env.ANDROID_BUILD || 'gradle', + prepEnv: options.argv.prepenv, + arch: resolvedTarget && resolvedTarget.arch, + extraArgs: [] + }; + + if (options.argv.ant || options.argv.gradle) + ret.buildMethod = options.argv.ant ? 'ant' : 'gradle'; + + if (options.nobuild) ret.buildMethod = 'none'; + + if (options.argv.versionCode) + ret.extraArgs.push('-PcdvVersionCode=' + options.versionCode); + + if (options.argv.minSdkVersion) + ret.extraArgs.push('-PcdvMinSdkVersion=' + options.minSdkVersion); + + if (options.argv.gradleArg) + ret.extraArgs.push(options.gradleArg); + + var packageArgs = {}; + + if (options.argv.keystore) + packageArgs.keystore = path.relative(this.root, path.resolve(options.argv.keystore)); + + ['alias','storePassword','password','keystoreType'].forEach(function (flagName) { + if (options.argv[flagName]) + packageArgs[flagName] = options.argv[flagName]; + }); + + var buildConfig = options.buildConfig; + + // If some values are not specified as command line arguments - use build config to supplement them. + // Command line arguemnts have precedence over build config. + if (buildConfig) { + if (!fs.existsSync(buildConfig)) { + throw new Error('Specified build config file does not exist: ' + buildConfig); + } + events.emit('log', 'Reading build config file: '+ path.resolve(buildConfig)); + var config = JSON.parse(fs.readFileSync(buildConfig, 'utf8')); + if (config.android && config.android[ret.buildType]) { + var androidInfo = config.android[ret.buildType]; + if(androidInfo.keystore && !packageArgs.keystore) { + packageArgs.keystore = path.resolve(path.dirname(buildConfig), androidInfo.keystore); + } + + ['alias', 'storePassword', 'password','keystoreType'].forEach(function (key){ + packageArgs[key] = packageArgs[key] || androidInfo[key]; + }); + } + } + + if (packageArgs.keystore && packageArgs.alias) { + ret.packageInfo = new PackageInfo(packageArgs.keystore, packageArgs.alias, packageArgs.storePassword, + packageArgs.password, packageArgs.keystoreType); + } + + if(!ret.packageInfo) { + if(Object.keys(packageArgs).length > 0) { + events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); + } + } + + return ret; +} + +/* + * Builds the project with the specifed options + * Returns a promise. + */ +module.exports.runClean = function(options) { + var opts = parseOpts(options); + var builder = builders.getBuilder(opts.buildMethod); + return builder.prepEnv(opts) + .then(function() { + return builder.clean(opts); + }); +}; + +/** + * Builds the project with the specifed options. + * + * @param {BuildOptions} options A set of options. See PlatformApi.build + * method documentation for reference. + * @param {Object} optResolvedTarget A deployment target. Used to pass + * target architecture from upstream 'run' call. TODO: remove this option in + * favor of setting buildOptions.archs field. + * + * @return {Promise} Promise, resolved with built packages + * information. + */ +module.exports.run = function(options, optResolvedTarget) { + var opts = parseOpts(options, optResolvedTarget); + var builder = builders.getBuilder(opts.buildMethod); + var self = this; + return builder.prepEnv(opts) + .then(function() { + if (opts.prepEnv) { + self.events.emit('verbose', 'Build file successfully prepared.'); + return; + } + return builder.build(opts) + .then(function() { + var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); + self.events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t')); + return { + apkPaths: apkPaths, + buildType: opts.buildType, + buildMethod: opts.buildMethod + }; + }); + }); +}; + +// Called by plugman after installing plugins, and by create script after creating project. +module.exports.prepBuildFiles = function() { + return builders.getBuilder('gradle').prepBuildFiles(); +}; + +/* + * Detects the architecture of a device/emulator + * Returns "arm" or "x86". + */ +module.exports.detectArchitecture = function(target) { + function helper() { + return Adb.shell(target, 'cat /proc/cpuinfo') + .then(function(output) { + return /intel/i.exec(output) ? 'x86' : 'arm'; + }); + } + // It sometimes happens (at least on OS X), that this command will hang forever. + // To fix it, either unplug & replug device, or restart adb server. + return helper() + .timeout(1000, new CordovaError('Device communication timed out. Try unplugging & replugging the device.')) + .then(null, function(err) { + if (/timed out/.exec('' + err)) { + // adb kill-server doesn't seem to do the trick. + // Could probably find a x-platform version of killall, but I'm not actually + // sure that this scenario even happens on non-OSX machines. + return spawn('killall', ['adb']) + .then(function() { + events.emit('verbose', 'adb seems hung. retrying.'); + return helper() + .then(null, function() { + // The double kill is sadly often necessary, at least on mac. + events.emit('warn', 'Now device not found... restarting adb again.'); + return spawn('killall', ['adb']) + .then(function() { + return helper() + .then(null, function() { + return Q.reject(new CordovaError('USB is flakey. Try unplugging & replugging the device.')); + }); + }); + }); + }, function() { + // For non-killall OS's. + return Q.reject(err); + }); + } + throw err; + }); +}; + +module.exports.findBestApkForArchitecture = function(buildResults, arch) { + var paths = buildResults.apkPaths.filter(function(p) { + var apkName = path.basename(p); + if (buildResults.buildType == 'debug') { + return /-debug/.exec(apkName); + } + return !/-debug/.exec(apkName); + }); + var archPattern = new RegExp('-' + arch); + var hasArchPattern = /-x86|-arm/; + for (var i = 0; i < paths.length; ++i) { + var apkName = path.basename(paths[i]); + if (hasArchPattern.exec(apkName)) { + if (archPattern.exec(apkName)) { + return paths[i]; + } + } else { + return paths[i]; + } + } + throw new Error('Could not find apk architecture: ' + arch + ' build-type: ' + buildResults.buildType); +}; + +function PackageInfo(keystore, alias, storePassword, password, keystoreType) { + this.keystore = { + 'name': 'key.store', + 'value': keystore + }; + this.alias = { + 'name': 'key.alias', + 'value': alias + }; + if (storePassword) { + this.storePassword = { + 'name': 'key.store.password', + 'value': storePassword + }; + } + if (password) { + this.password = { + 'name': 'key.alias.password', + 'value': password + }; + } + if (keystoreType) { + this.keystoreType = { + 'name': 'key.store.type', + 'value': keystoreType + }; + } +} + +PackageInfo.prototype = { + toProperties: function() { + var self = this; + var result = ''; + Object.keys(self).forEach(function(key) { + result += self[key].name; + result += '='; + result += self[key].value.replace(/\\/g, '\\\\'); + result += '\n'; + }); + return result; + } +}; + +module.exports.help = function() { + console.log('Usage: ' + path.relative(process.cwd(), path.join('../build')) + ' [flags] [Signed APK flags]'); + console.log('Flags:'); + console.log(' \'--debug\': will build project in debug mode (default)'); + console.log(' \'--release\': will build project for release'); + console.log(' \'--ant\': will build project with ant'); + console.log(' \'--gradle\': will build project with gradle (default)'); + console.log(' \'--nobuild\': will skip build process (useful when using run command)'); + console.log(' \'--prepenv\': don\'t build, but copy in build scripts where necessary'); + console.log(' \'--versionCode=#\': Override versionCode for this build. Useful for uploading multiple APKs. Requires --gradle.'); + console.log(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.'); + console.log(' \'--gradleArg=\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true'); + console.log(''); + console.log('Signed APK flags (overwrites debug/release-signing.proprties) :'); + console.log(' \'--keystore=\': Key store used to build a signed archive. (Required)'); + console.log(' \'--alias=\': Alias for the key store. (Required)'); + console.log(' \'--storePassword=\': Password for the key store. (Optional - prompted)'); + console.log(' \'--password=\': Password for the key. (Optional - prompted)'); + console.log(' \'--keystoreType\': Type of the keystore. (Optional)'); + process.exit(0); +}; diff --git a/bin/templates/cordova/lib/builders/AntBuilder.js b/bin/templates/cordova/lib/builders/AntBuilder.js index dd472278..d214f48e 100644 --- a/bin/templates/cordova/lib/builders/AntBuilder.js +++ b/bin/templates/cordova/lib/builders/AntBuilder.js @@ -1,141 +1,141 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'); -var fs = require('fs'); -var path = require('path'); -var util = require('util'); -var shell = require('shelljs'); -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; -var check_reqs = require('../check_reqs'); - -var SIGNING_PROPERTIES = '-signing.properties'; -var MARKER = 'YOUR CHANGES WILL BE ERASED!'; -var TEMPLATE = - '# This file is automatically generated.\n' + - '# Do not modify this file -- ' + MARKER + '\n'; - -var GenericBuilder = require('./GenericBuilder'); - -function AntBuilder (projectRoot) { - GenericBuilder.call(this, projectRoot); - - this.binDirs = {ant: this.binDirs.ant}; -} - -util.inherits(AntBuilder, GenericBuilder); - -AntBuilder.prototype.getArgs = function(cmd, opts) { - var args = [cmd, '-f', path.join(this.root, 'build.xml')]; - // custom_rules.xml is required for incremental builds. - if (hasCustomRules()) { - args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); - } - if(opts.packageInfo) { - args.push('-propertyfile=' + path.join(this.root, opts.buildType + SIGNING_PROPERTIES)); - } - return args; -}; - -AntBuilder.prototype.prepEnv = function(opts) { - var self = this; - return check_reqs.check_ant() - .then(function() { - // Copy in build.xml on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - /*jshint -W069 */ - var sdkDir = process.env['ANDROID_HOME']; - /*jshint +W069 */ - var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); - function writeBuildXml(projectPath) { - var newData = buildTemplate.replace('PROJECT_NAME', self.extractRealProjectNameFromManifest()); - fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); - if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { - fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); - } - } - writeBuildXml(self.root); - var propertiesObj = self.readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - writeBuildXml(path.join(self.root, subProjects[i])); - } - if (propertiesObj.systemLibs.length > 0) { - throw new CordovaError('Project contains at least one plugin that requires a system library. This is not supported with ANT. Please build using gradle.'); - } - - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(self.root, propertiesFile); - if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if(isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); -}; - -/* - * Builds the project with ant. - * Returns a promise. - */ -AntBuilder.prototype.build = function(opts) { - // Without our custom_rules.xml, we need to clean before building. - var ret = Q(); - if (!hasCustomRules()) { - // clean will call check_ant() for us. - ret = this.clean(opts); - } - - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return check_reqs.check_ant() - .then(function() { - return spawn('ant', args, {stdio: 'inherit'}); - }); -}; - -AntBuilder.prototype.clean = function(opts) { - var args = this.getArgs('clean', opts); - var self = this; - return check_reqs.check_ant() - .then(function() { - return spawn('ant', args, {stdio: 'inherit'}); - }) - .then(function () { - shell.rm('-rf', path.join(self.root, 'out')); - - ['debug', 'release'].forEach(function(config) { - var propertiesFilePath = path.join(self.root, config + SIGNING_PROPERTIES); - if(isAutoGenerated(propertiesFilePath)){ - shell.rm('-f', propertiesFilePath); - } - }); - }); -}; - -module.exports = AntBuilder; - -function hasCustomRules(projectRoot) { - return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); -} - -function isAutoGenerated(file) { - return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; -} +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var shell = require('shelljs'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var check_reqs = require('../check_reqs'); + +var SIGNING_PROPERTIES = '-signing.properties'; +var MARKER = 'YOUR CHANGES WILL BE ERASED!'; +var TEMPLATE = + '# This file is automatically generated.\n' + + '# Do not modify this file -- ' + MARKER + '\n'; + +var GenericBuilder = require('./GenericBuilder'); + +function AntBuilder (projectRoot) { + GenericBuilder.call(this, projectRoot); + + this.binDirs = {ant: this.binDirs.ant}; +} + +util.inherits(AntBuilder, GenericBuilder); + +AntBuilder.prototype.getArgs = function(cmd, opts) { + var args = [cmd, '-f', path.join(this.root, 'build.xml')]; + // custom_rules.xml is required for incremental builds. + if (hasCustomRules()) { + args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); + } + if(opts.packageInfo) { + args.push('-propertyfile=' + path.join(this.root, opts.buildType + SIGNING_PROPERTIES)); + } + return args; +}; + +AntBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_ant() + .then(function() { + // Copy in build.xml on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var buildTemplate = fs.readFileSync(path.join(sdkDir, 'tools', 'lib', 'build.template'), 'utf8'); + function writeBuildXml(projectPath) { + var newData = buildTemplate.replace('PROJECT_NAME', self.extractRealProjectNameFromManifest()); + fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); + if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { + fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); + } + } + writeBuildXml(self.root); + var propertiesObj = self.readProjectProperties(); + var subProjects = propertiesObj.libs; + for (var i = 0; i < subProjects.length; ++i) { + writeBuildXml(path.join(self.root, subProjects[i])); + } + if (propertiesObj.systemLibs.length > 0) { + throw new CordovaError('Project contains at least one plugin that requires a system library. This is not supported with ANT. Please build using gradle.'); + } + + var propertiesFile = opts.buildType + SIGNING_PROPERTIES; + var propertiesFilePath = path.join(self.root, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if(isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } + }); +}; + +/* + * Builds the project with ant. + * Returns a promise. + */ +AntBuilder.prototype.build = function(opts) { + // Without our custom_rules.xml, we need to clean before building. + var ret = Q(); + if (!hasCustomRules()) { + // clean will call check_ant() for us. + ret = this.clean(opts); + } + + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'inherit'}); + }); +}; + +AntBuilder.prototype.clean = function(opts) { + var args = this.getArgs('clean', opts); + var self = this; + return check_reqs.check_ant() + .then(function() { + return spawn('ant', args, {stdio: 'inherit'}); + }) + .then(function () { + shell.rm('-rf', path.join(self.root, 'out')); + + ['debug', 'release'].forEach(function(config) { + var propertiesFilePath = path.join(self.root, config + SIGNING_PROPERTIES); + if(isAutoGenerated(propertiesFilePath)){ + shell.rm('-f', propertiesFilePath); + } + }); + }); +}; + +module.exports = AntBuilder; + +function hasCustomRules(projectRoot) { + return fs.existsSync(path.join(projectRoot, 'custom_rules.xml')); +} + +function isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/bin/templates/cordova/lib/builders/GradleBuilder.js b/bin/templates/cordova/lib/builders/GradleBuilder.js index 7611423e..0f613cc5 100644 --- a/bin/templates/cordova/lib/builders/GradleBuilder.js +++ b/bin/templates/cordova/lib/builders/GradleBuilder.js @@ -1,213 +1,213 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'); -var fs = require('fs'); -var util = require('util'); -var path = require('path'); -var shell = require('shelljs'); -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; -var check_reqs = require('../check_reqs'); - -var GenericBuilder = require('./GenericBuilder'); - -var MARKER = 'YOUR CHANGES WILL BE ERASED!'; -var SIGNING_PROPERTIES = '-signing.properties'; -var TEMPLATE = - '# This file is automatically generated.\n' + - '# Do not modify this file -- ' + MARKER + '\n'; - -function GradleBuilder (projectRoot) { - GenericBuilder.call(this, projectRoot); - - this.binDirs = {gradle: this.binDirs.gradle}; -} - -util.inherits(GradleBuilder, GenericBuilder); - -GradleBuilder.prototype.getArgs = function(cmd, opts) { - if (cmd == 'release') { - cmd = 'cdvBuildRelease'; - } else if (cmd == 'debug') { - cmd = 'cdvBuildDebug'; - } - var args = [cmd, '-b', path.join(this.root, 'build.gradle')]; - if (opts.arch) { - args.push('-PcdvBuildArch=' + opts.arch); - } - - // 10 seconds -> 6 seconds - args.push('-Dorg.gradle.daemon=true'); - args.push.apply(args, opts.extraArgs); - // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet): - // args.push('-Dorg.gradle.parallel=true'); - return args; -}; - -// Makes the project buildable, minus the gradle wrapper. -GradleBuilder.prototype.prepBuildFiles = function() { - // Update the version of build.gradle in each dependent library. - var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle'); - var propertiesObj = this.readProjectProperties(); - var subProjects = propertiesObj.libs; - for (var i = 0; i < subProjects.length; ++i) { - if (subProjects[i] !== 'CordovaLib') { - shell.cp('-f', pluginBuildGradle, path.join(this.root, subProjects[i], 'build.gradle')); - } - } - - var name = this.extractRealProjectNameFromManifest(); - //Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149 - var settingsGradlePaths = subProjects.map(function(p){ - var realDir=p.replace(/[/\\]/g, ':'); - var libName=realDir.replace(name+'-',''); - var str='include ":'+libName+'"\n'; - if(realDir.indexOf(name+'-')!==-1) - str+='project(":'+libName+'").projectDir = new File("'+p+'")\n'; - return str; - }); - - // Write the settings.gradle file. - fs.writeFileSync(path.join(this.root, 'settings.gradle'), - '// GENERATED FILE - DO NOT EDIT\n' + - 'include ":"\n' + settingsGradlePaths.join('')); - // Update dependencies within build.gradle. - var buildGradle = fs.readFileSync(path.join(this.root, 'build.gradle'), 'utf8'); - var depsList = ''; - subProjects.forEach(function(p) { - var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); - depsList += ' debugCompile project(path: "' + libName + '", configuration: "debug")\n'; - depsList += ' releaseCompile project(path: "' + libName + '", configuration: "release")\n'; - }); - // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390 - var SYSTEM_LIBRARY_MAPPINGS = [ - [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'], - [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+'] - ]; - propertiesObj.systemLibs.forEach(function(p) { - var mavenRef; - // It's already in gradle form if it has two ':'s - if (/:.*:/.exec(p)) { - mavenRef = p; - } else { - for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) { - var pair = SYSTEM_LIBRARY_MAPPINGS[i]; - if (pair[0].exec(p)) { - mavenRef = p.replace(pair[0], pair[1]); - break; - } - } - if (!mavenRef) { - throw new CordovaError('Unsupported system library (does not work with gradle): ' + p); - } - } - depsList += ' compile "' + mavenRef + '"\n'; - }); - buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2'); - var includeList = ''; - propertiesObj.gradleIncludes.forEach(function(includePath) { - includeList += 'apply from: "' + includePath + '"\n'; - }); - buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2'); - fs.writeFileSync(path.join(this.root, 'build.gradle'), buildGradle); -}; - -GradleBuilder.prototype.prepEnv = function(opts) { - var self = this; - return check_reqs.check_gradle() - .then(function() { - return self.prepBuildFiles(); - }).then(function() { - // Copy the gradle wrapper on each build so that: - // A) we don't require the Android SDK at project creation time, and - // B) we always use the SDK's latest version of it. - // check_reqs ensures that this is set. - /*jshint -W069 */ - var sdkDir = process.env['ANDROID_HOME']; - /*jshint +W069 */ - var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); - if (process.platform == 'win32') { - shell.rm('-f', path.join(self.root, 'gradlew.bat')); - shell.cp(path.join(wrapperDir, 'gradlew.bat'), self.root); - } else { - shell.rm('-f', path.join(self.root, 'gradlew')); - shell.cp(path.join(wrapperDir, 'gradlew'), self.root); - } - shell.rm('-rf', path.join(self.root, 'gradle', 'wrapper')); - shell.mkdir('-p', path.join(self.root, 'gradle')); - shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(self.root, 'gradle')); - - // If the gradle distribution URL is set, make sure it points to version we want. - // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with. - // For some reason, using ^ and $ don't work. This does the job, though. - var distributionUrlRegex = /distributionUrl.*zip/; - /*jshint -W069 */ - var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; - /*jshint +W069 */ - var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties'); - shell.chmod('u+w', gradleWrapperPropertiesPath); - shell.sed('-i', distributionUrlRegex, 'distributionUrl='+distributionUrl, gradleWrapperPropertiesPath); - - var propertiesFile = opts.buildType + SIGNING_PROPERTIES; - var propertiesFilePath = path.join(self.root, propertiesFile); - if (opts.packageInfo) { - fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); - } else if (isAutoGenerated(propertiesFilePath)) { - shell.rm('-f', propertiesFilePath); - } - }); -}; - -/* - * Builds the project with gradle. - * Returns a promise. - */ -GradleBuilder.prototype.build = function(opts) { - var wrapper = path.join(this.root, 'gradlew'); - var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); - return Q().then(function() { - return spawn(wrapper, args, {stdio: 'inherit'}); - }); -}; - -GradleBuilder.prototype.clean = function(opts) { - var builder = this; - var wrapper = path.join(this.root, 'gradlew'); - var args = builder.getArgs('clean', opts); - return Q().then(function() { - return spawn(wrapper, args, {stdio: 'inherit'}); - }) - .then(function () { - shell.rm('-rf', path.join(builder.root, 'out')); - - ['debug', 'release'].forEach(function(config) { - var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES); - if(isAutoGenerated(propertiesFilePath)){ - shell.rm('-f', propertiesFilePath); - } - }); - }); -}; - -module.exports = GradleBuilder; - -function isAutoGenerated(file) { - return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; -} +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'); +var fs = require('fs'); +var util = require('util'); +var path = require('path'); +var shell = require('shelljs'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var check_reqs = require('../check_reqs'); + +var GenericBuilder = require('./GenericBuilder'); + +var MARKER = 'YOUR CHANGES WILL BE ERASED!'; +var SIGNING_PROPERTIES = '-signing.properties'; +var TEMPLATE = + '# This file is automatically generated.\n' + + '# Do not modify this file -- ' + MARKER + '\n'; + +function GradleBuilder (projectRoot) { + GenericBuilder.call(this, projectRoot); + + this.binDirs = {gradle: this.binDirs.gradle}; +} + +util.inherits(GradleBuilder, GenericBuilder); + +GradleBuilder.prototype.getArgs = function(cmd, opts) { + if (cmd == 'release') { + cmd = 'cdvBuildRelease'; + } else if (cmd == 'debug') { + cmd = 'cdvBuildDebug'; + } + var args = [cmd, '-b', path.join(this.root, 'build.gradle')]; + if (opts.arch) { + args.push('-PcdvBuildArch=' + opts.arch); + } + + // 10 seconds -> 6 seconds + args.push('-Dorg.gradle.daemon=true'); + args.push.apply(args, opts.extraArgs); + // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet): + // args.push('-Dorg.gradle.parallel=true'); + return args; +}; + +// Makes the project buildable, minus the gradle wrapper. +GradleBuilder.prototype.prepBuildFiles = function() { + // Update the version of build.gradle in each dependent library. + var pluginBuildGradle = path.join(this.root, 'cordova', 'lib', 'plugin-build.gradle'); + var propertiesObj = this.readProjectProperties(); + var subProjects = propertiesObj.libs; + for (var i = 0; i < subProjects.length; ++i) { + if (subProjects[i] !== 'CordovaLib') { + shell.cp('-f', pluginBuildGradle, path.join(this.root, subProjects[i], 'build.gradle')); + } + } + + var name = this.extractRealProjectNameFromManifest(); + //Remove the proj.id/name- prefix from projects: https://issues.apache.org/jira/browse/CB-9149 + var settingsGradlePaths = subProjects.map(function(p){ + var realDir=p.replace(/[/\\]/g, ':'); + var libName=realDir.replace(name+'-',''); + var str='include ":'+libName+'"\n'; + if(realDir.indexOf(name+'-')!==-1) + str+='project(":'+libName+'").projectDir = new File("'+p+'")\n'; + return str; + }); + + // Write the settings.gradle file. + fs.writeFileSync(path.join(this.root, 'settings.gradle'), + '// GENERATED FILE - DO NOT EDIT\n' + + 'include ":"\n' + settingsGradlePaths.join('')); + // Update dependencies within build.gradle. + var buildGradle = fs.readFileSync(path.join(this.root, 'build.gradle'), 'utf8'); + var depsList = ''; + subProjects.forEach(function(p) { + var libName=p.replace(/[/\\]/g, ':').replace(name+'-',''); + depsList += ' debugCompile project(path: "' + libName + '", configuration: "debug")\n'; + depsList += ' releaseCompile project(path: "' + libName + '", configuration: "release")\n'; + }); + // For why we do this mapping: https://issues.apache.org/jira/browse/CB-8390 + var SYSTEM_LIBRARY_MAPPINGS = [ + [/^\/?extras\/android\/support\/(.*)$/, 'com.android.support:support-$1:+'], + [/^\/?google\/google_play_services\/libproject\/google-play-services_lib\/?$/, 'com.google.android.gms:play-services:+'] + ]; + propertiesObj.systemLibs.forEach(function(p) { + var mavenRef; + // It's already in gradle form if it has two ':'s + if (/:.*:/.exec(p)) { + mavenRef = p; + } else { + for (var i = 0; i < SYSTEM_LIBRARY_MAPPINGS.length; ++i) { + var pair = SYSTEM_LIBRARY_MAPPINGS[i]; + if (pair[0].exec(p)) { + mavenRef = p.replace(pair[0], pair[1]); + break; + } + } + if (!mavenRef) { + throw new CordovaError('Unsupported system library (does not work with gradle): ' + p); + } + } + depsList += ' compile "' + mavenRef + '"\n'; + }); + buildGradle = buildGradle.replace(/(SUB-PROJECT DEPENDENCIES START)[\s\S]*(\/\/ SUB-PROJECT DEPENDENCIES END)/, '$1\n' + depsList + ' $2'); + var includeList = ''; + propertiesObj.gradleIncludes.forEach(function(includePath) { + includeList += 'apply from: "' + includePath + '"\n'; + }); + buildGradle = buildGradle.replace(/(PLUGIN GRADLE EXTENSIONS START)[\s\S]*(\/\/ PLUGIN GRADLE EXTENSIONS END)/, '$1\n' + includeList + '$2'); + fs.writeFileSync(path.join(this.root, 'build.gradle'), buildGradle); +}; + +GradleBuilder.prototype.prepEnv = function(opts) { + var self = this; + return check_reqs.check_gradle() + .then(function() { + return self.prepBuildFiles(); + }).then(function() { + // Copy the gradle wrapper on each build so that: + // A) we don't require the Android SDK at project creation time, and + // B) we always use the SDK's latest version of it. + // check_reqs ensures that this is set. + /*jshint -W069 */ + var sdkDir = process.env['ANDROID_HOME']; + /*jshint +W069 */ + var wrapperDir = path.join(sdkDir, 'tools', 'templates', 'gradle', 'wrapper'); + if (process.platform == 'win32') { + shell.rm('-f', path.join(self.root, 'gradlew.bat')); + shell.cp(path.join(wrapperDir, 'gradlew.bat'), self.root); + } else { + shell.rm('-f', path.join(self.root, 'gradlew')); + shell.cp(path.join(wrapperDir, 'gradlew'), self.root); + } + shell.rm('-rf', path.join(self.root, 'gradle', 'wrapper')); + shell.mkdir('-p', path.join(self.root, 'gradle')); + shell.cp('-r', path.join(wrapperDir, 'gradle', 'wrapper'), path.join(self.root, 'gradle')); + + // If the gradle distribution URL is set, make sure it points to version we want. + // If it's not set, do nothing, assuming that we're using a future version of gradle that we don't want to mess with. + // For some reason, using ^ and $ don't work. This does the job, though. + var distributionUrlRegex = /distributionUrl.*zip/; + /*jshint -W069 */ + var distributionUrl = process.env['CORDOVA_ANDROID_GRADLE_DISTRIBUTION_URL'] || 'http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; + /*jshint +W069 */ + var gradleWrapperPropertiesPath = path.join(self.root, 'gradle', 'wrapper', 'gradle-wrapper.properties'); + shell.chmod('u+w', gradleWrapperPropertiesPath); + shell.sed('-i', distributionUrlRegex, 'distributionUrl='+distributionUrl, gradleWrapperPropertiesPath); + + var propertiesFile = opts.buildType + SIGNING_PROPERTIES; + var propertiesFilePath = path.join(self.root, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if (isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } + }); +}; + +/* + * Builds the project with gradle. + * Returns a promise. + */ +GradleBuilder.prototype.build = function(opts) { + var wrapper = path.join(this.root, 'gradlew'); + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); + return Q().then(function() { + return spawn(wrapper, args, {stdio: 'inherit'}); + }); +}; + +GradleBuilder.prototype.clean = function(opts) { + var builder = this; + var wrapper = path.join(this.root, 'gradlew'); + var args = builder.getArgs('clean', opts); + return Q().then(function() { + return spawn(wrapper, args, {stdio: 'inherit'}); + }) + .then(function () { + shell.rm('-rf', path.join(builder.root, 'out')); + + ['debug', 'release'].forEach(function(config) { + var propertiesFilePath = path.join(builder.root, config + SIGNING_PROPERTIES); + if(isAutoGenerated(propertiesFilePath)){ + shell.rm('-f', propertiesFilePath); + } + }); + }); +}; + +module.exports = GradleBuilder; + +function isAutoGenerated(file) { + return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0; +} diff --git a/bin/templates/cordova/lib/device.js b/bin/templates/cordova/lib/device.js index 5dd79b52..e62e3db8 100644 --- a/bin/templates/cordova/lib/device.js +++ b/bin/templates/cordova/lib/device.js @@ -1,106 +1,106 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'), - build = require('./build'); -var path = require('path'); -var Adb = require('./Adb'); -var AndroidManifest = require('./AndroidManifest'); -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; -var events = require('cordova-common').events; - -/** - * Returns a promise for the list of the device ID's found - * @param lookHarder When true, try restarting adb if no devices are found. - */ -module.exports.list = function(lookHarder) { - return Adb.devices() - .then(function(list) { - if (list.length === 0 && lookHarder) { - // adb kill-server doesn't seem to do the trick. - // Could probably find a x-platform version of killall, but I'm not actually - // sure that this scenario even happens on non-OSX machines. - return spawn('killall', ['adb']) - .then(function() { - events.emit('verbose', 'Restarting adb to see if more devices are detected.'); - return Adb.devices(); - }, function() { - // For non-killall OS's. - return list; - }); - } - return list; - }); -}; - -module.exports.resolveTarget = function(target) { - return this.list(true) - .then(function(device_list) { - if (!device_list || !device_list.length) { - return Q.reject(new CordovaError('Failed to deploy to device, no devices found.')); - } - // default device - target = target || device_list[0]; - - if (device_list.indexOf(target) < 0) { - return Q.reject('ERROR: Unable to find target \'' + target + '\'.'); - } - - return build.detectArchitecture(target) - .then(function(arch) { - return { target: target, arch: arch, isEmulator: false }; - }); - }); -}; - -/* - * Installs a previously built application on the device - * and launches it. - * Returns a promise. - */ -module.exports.install = function(target, buildResults) { - return Q().then(function() { - if (target && typeof target == 'object') { - return target; - } - return module.exports.resolveTarget(target); - }).then(function(resolvedTarget) { - var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch); - var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); - var pkgName = manifest.getPackageId(); - var launchName = pkgName + '/.' + manifest.getActivity().getName(); - events.emit('log', 'Using apk: ' + apk_path); - // This promise is always resolved, even if 'adb uninstall' fails to uninstall app - // or the app doesn't installed at all, so no error catching needed. - return Adb.uninstall(resolvedTarget.target, pkgName) - .then(function() { - return Adb.install(resolvedTarget.target, apk_path, {replace: true}); - }).then(function() { - //unlock screen - return Adb.shell(resolvedTarget.target, 'input keyevent 82'); - }).then(function() { - return Adb.start(resolvedTarget.target, launchName); - }).then(function() { - events.emit('log', 'LAUNCH SUCCESS'); - }); - }); -}; +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'), + build = require('./build'); +var path = require('path'); +var Adb = require('./Adb'); +var AndroidManifest = require('./AndroidManifest'); +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; +var events = require('cordova-common').events; + +/** + * Returns a promise for the list of the device ID's found + * @param lookHarder When true, try restarting adb if no devices are found. + */ +module.exports.list = function(lookHarder) { + return Adb.devices() + .then(function(list) { + if (list.length === 0 && lookHarder) { + // adb kill-server doesn't seem to do the trick. + // Could probably find a x-platform version of killall, but I'm not actually + // sure that this scenario even happens on non-OSX machines. + return spawn('killall', ['adb']) + .then(function() { + events.emit('verbose', 'Restarting adb to see if more devices are detected.'); + return Adb.devices(); + }, function() { + // For non-killall OS's. + return list; + }); + } + return list; + }); +}; + +module.exports.resolveTarget = function(target) { + return this.list(true) + .then(function(device_list) { + if (!device_list || !device_list.length) { + return Q.reject(new CordovaError('Failed to deploy to device, no devices found.')); + } + // default device + target = target || device_list[0]; + + if (device_list.indexOf(target) < 0) { + return Q.reject('ERROR: Unable to find target \'' + target + '\'.'); + } + + return build.detectArchitecture(target) + .then(function(arch) { + return { target: target, arch: arch, isEmulator: false }; + }); + }); +}; + +/* + * Installs a previously built application on the device + * and launches it. + * Returns a promise. + */ +module.exports.install = function(target, buildResults) { + return Q().then(function() { + if (target && typeof target == 'object') { + return target; + } + return module.exports.resolveTarget(target); + }).then(function(resolvedTarget) { + var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch); + var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); + var pkgName = manifest.getPackageId(); + var launchName = pkgName + '/.' + manifest.getActivity().getName(); + events.emit('log', 'Using apk: ' + apk_path); + // This promise is always resolved, even if 'adb uninstall' fails to uninstall app + // or the app doesn't installed at all, so no error catching needed. + return Adb.uninstall(resolvedTarget.target, pkgName) + .then(function() { + return Adb.install(resolvedTarget.target, apk_path, {replace: true}); + }).then(function() { + //unlock screen + return Adb.shell(resolvedTarget.target, 'input keyevent 82'); + }).then(function() { + return Adb.start(resolvedTarget.target, launchName); + }).then(function() { + events.emit('log', 'LAUNCH SUCCESS'); + }); + }); +}; diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index 32335027..9e214b19 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -1,372 +1,372 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -/* jshint sub:true */ - -var retry = require('./retry'); -var build = require('./build'); -var check_reqs = require('./check_reqs'); -var path = require('path'); -var Adb = require('./Adb'); -var AndroidManifest = require('./AndroidManifest'); -var events = require('cordova-common').events; -var spawn = require('cordova-common').superspawn.spawn; -var CordovaError = require('cordova-common').CordovaError; - -var Q = require('q'); -var os = require('os'); -var child_process = require('child_process'); - -// constants -var ONE_SECOND = 1000; // in milliseconds -var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds -var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds -var NUM_INSTALL_RETRIES = 3; -var EXEC_KILL_SIGNAL = 'SIGKILL'; - -/** - * Returns a Promise for a list of emulator images in the form of objects - * { - name : , - path : , - target : , - abi : , - skin : - } - */ -module.exports.list_images = function() { - return spawn('android', ['list', 'avds']) - .then(function(output) { - var response = output.split('\n'); - var emulator_list = []; - for (var i = 1; i < response.length; i++) { - // To return more detailed information use img_obj - var img_obj = {}; - if (response[i].match(/Name:\s/)) { - img_obj['name'] = response[i].split('Name: ')[1].replace('\r', ''); - if (response[i + 1].match(/Path:\s/)) { - i++; - img_obj['path'] = response[i].split('Path: ')[1].replace('\r', ''); - } - if (response[i + 1].match(/\(API\slevel\s/)) { - i++; - img_obj['target'] = response[i].replace('\r', ''); - } - if (response[i + 1].match(/ABI:\s/)) { - i++; - img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', ''); - } - if (response[i + 1].match(/Skin:\s/)) { - i++; - img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', ''); - } - - emulator_list.push(img_obj); - } - /* To just return a list of names use this - if (response[i].match(/Name:\s/)) { - emulator_list.push(response[i].split('Name: ')[1].replace('\r', ''); - }*/ - - } - return emulator_list; - }); -}; - -/** - * Will return the closest avd to the projects target - * or undefined if no avds exist. - * Returns a promise. - */ -module.exports.best_image = function() { - return this.list_images() - .then(function(images) { - // Just return undefined if there is no images - if (images.length === 0) return; - - var closest = 9999; - var best = images[0]; - var project_target = check_reqs.get_target().replace('android-', ''); - for (var i in images) { - var target = images[i].target; - if(target) { - var num = target.split('(API level ')[1].replace(')', ''); - if (num == project_target) { - return images[i]; - } else if (project_target - num < closest && project_target > num) { - closest = project_target - num; - best = images[i]; - } - } - } - return best; - }); -}; - -// Returns a promise. -module.exports.list_started = function() { - return Adb.devices({emulators: true}); -}; - -// Returns a promise. -module.exports.list_targets = function() { - return spawn('android', ['list', 'targets'], {cwd: os.tmpdir()}) - .then(function(output) { - var target_out = output.split('\n'); - var targets = []; - for (var i = target_out.length; i >= 0; i--) { - if(target_out[i].match(/id:/)) { - targets.push(targets[i].split(' ')[1]); - } - } - return targets; - }); -}; - -/* - * Starts an emulator with the given ID, - * and returns the started ID of that emulator. - * If no ID is given it will use the first image available, - * if no image is available it will error out (maybe create one?). - * - * Returns a promise. - */ -module.exports.start = function(emulator_ID) { - var self = this; - - return Q().then(function() { - if (emulator_ID) return Q(emulator_ID); - - return self.best_image() - .then(function(best) { - if (best && best.name) { - events.emit('warn', 'No emulator specified, defaulting to ' + best.name); - return best.name; - } - - var androidCmd = check_reqs.getAbsoluteAndroidCmd(); - return Q.reject(new CordovaError('No emulator images (avds) found.\n' + - '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' + - '2. Create an AVD by running: ' + androidCmd + ' avd\n' + - 'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n')); - }); - }).then(function(emulatorId) { - var uuid = 'cordova_emulator_' + new Date().getTime(); - var uuidProp = 'emu.uuid=' + uuid; - var args = ['-avd', emulatorId, '-prop', uuidProp]; - // Don't wait for it to finish, since the emulator will probably keep running for a long time. - child_process - .spawn('emulator', args, { stdio: 'inherit', detached: true }) - .unref(); - - // wait for emulator to start - events.emit('log', 'Waiting for emulator...'); - return self.wait_for_emulator(uuid); - }).then(function(emulatorId) { - if (!emulatorId) - return Q.reject(new CordovaError('Failed to start emulator')); - - //wait for emulator to boot up - process.stdout.write('Booting up emulator (this may take a while)...'); - return self.wait_for_boot(emulatorId) - .then(function() { - events.emit('log','BOOT COMPLETE'); - //unlock screen - return Adb.shell(emulatorId, 'input keyevent 82'); - }).then(function() { - //return the new emulator id for the started emulators - return emulatorId; - }); - }); -}; - -/* - * Waits for an emulator with given uuid to apear on the started-emulator list. - * Returns a promise with this emulator's ID. - */ -module.exports.wait_for_emulator = function(uuid) { - var self = this; - return self.list_started() - .then(function(new_started) { - var emulator_id = null; - var promises = []; - - new_started.forEach(function (emulator) { - promises.push( - Adb.shell(emulator, 'getprop emu.uuid') - .then(function (output) { - if (output.indexOf(uuid) >= 0) { - emulator_id = emulator; - } - }) - ); - }); - - return Q.all(promises).then(function () { - return emulator_id || self.wait_for_emulator(uuid); - }); - }); -}; - -/* - * Waits for the core android process of the emulator to start - */ -module.exports.wait_for_boot = function(emulator_id) { - var self = this; - return Adb.shell(emulator_id, 'ps') - .then(function(output) { - if (output.match(/android\.process\.acore/)) { - return; - } else { - process.stdout.write('.'); - return Q.delay(3000).then(function() { - return self.wait_for_boot(emulator_id); - }); - } - }); -}; - -/* - * Create avd - * TODO : Enter the stdin input required to complete the creation of an avd. - * Returns a promise. - */ -module.exports.create_image = function(name, target) { - console.log('Creating avd named ' + name); - if (target) { - return spawn('android', ['create', 'avd', '--name', name, '--target', target]) - .then(null, function(error) { - console.error('ERROR : Failed to create emulator image : '); - console.error(' Do you have the latest android targets including ' + target + '?'); - console.error(error); - }); - } else { - console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.'); - return spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]) - .then(function() { - // TODO: This seems like another error case, even though it always happens. - console.error('ERROR : Unable to create an avd emulator, no targets found.'); - console.error('Please insure you have targets available by running the "android" command'); - return Q.reject(); - }, function(error) { - console.error('ERROR : Failed to create emulator image : '); - console.error(error); - }); - } -}; - -module.exports.resolveTarget = function(target) { - return this.list_started() - .then(function(emulator_list) { - if (emulator_list.length < 1) { - return Q.reject('No started emulators found, please start an emultor before deploying your project.'); - } - - // default emulator - target = target || emulator_list[0]; - if (emulator_list.indexOf(target) < 0) { - return Q.reject('Unable to find target \'' + target + '\'. Failed to deploy to emulator.'); - } - - return build.detectArchitecture(target) - .then(function(arch) { - return {target:target, arch:arch, isEmulator:true}; - }); - }); -}; - -/* - * Installs a previously built application on the emulator and launches it. - * If no target is specified, then it picks one. - * If no started emulators are found, error out. - * Returns a promise. - */ -module.exports.install = function(givenTarget, buildResults) { - - var target; - var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); - var pkgName = manifest.getPackageId(); - - // resolve the target emulator - return Q().then(function () { - if (givenTarget && typeof givenTarget == 'object') { - return givenTarget; - } else { - return module.exports.resolveTarget(givenTarget); - } - - // set the resolved target - }).then(function (resolvedTarget) { - target = resolvedTarget; - - // install the app - }).then(function () { - // This promise is always resolved, even if 'adb uninstall' fails to uninstall app - // or the app doesn't installed at all, so no error catching needed. - return Adb.uninstall(target.target, pkgName) - .then(function() { - - var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); - var execOptions = { - cwd: os.tmpdir(), - timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds - killSignal: EXEC_KILL_SIGNAL - }; - - events.emit('log', 'Using apk: ' + apk_path); - events.emit('verbose', 'Installing app on emulator...'); - - function exec(command, opts) { - return Q.promise(function (resolve, reject) { - child_process.exec(command, opts, function(err, stdout, stderr) { - if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr)); - else resolve(stdout); - }); - }); - } - - var retriedInstall = retry.retryPromise( - NUM_INSTALL_RETRIES, - exec, 'adb -s ' + target.target + ' install -r "' + apk_path + '"', execOptions - ); - - return retriedInstall.then(function (output) { - if (output.match(/Failure/)) { - return Q.reject(new CordovaError('Failed to install apk to emulator: ' + output)); - } else { - events.emit('log', 'INSTALL SUCCESS'); - } - }, function (err) { - return Q.reject(new CordovaError('Failed to install apk to emulator: ' + err)); - }); - }); - // unlock screen - }).then(function () { - - events.emit('verbose', 'Unlocking screen...'); - return Adb.shell(target.target, 'input keyevent 82'); - }).then(function () { - Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName()); - // report success or failure - }).then(function (output) { - events.emit('log', 'LAUNCH SUCCESS'); - }); -}; +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +/* jshint sub:true */ + +var retry = require('./retry'); +var build = require('./build'); +var check_reqs = require('./check_reqs'); +var path = require('path'); +var Adb = require('./Adb'); +var AndroidManifest = require('./AndroidManifest'); +var events = require('cordova-common').events; +var spawn = require('cordova-common').superspawn.spawn; +var CordovaError = require('cordova-common').CordovaError; + +var Q = require('q'); +var os = require('os'); +var child_process = require('child_process'); + +// constants +var ONE_SECOND = 1000; // in milliseconds +var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds +var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds +var NUM_INSTALL_RETRIES = 3; +var EXEC_KILL_SIGNAL = 'SIGKILL'; + +/** + * Returns a Promise for a list of emulator images in the form of objects + * { + name : , + path : , + target : , + abi : , + skin : + } + */ +module.exports.list_images = function() { + return spawn('android', ['list', 'avds']) + .then(function(output) { + var response = output.split('\n'); + var emulator_list = []; + for (var i = 1; i < response.length; i++) { + // To return more detailed information use img_obj + var img_obj = {}; + if (response[i].match(/Name:\s/)) { + img_obj['name'] = response[i].split('Name: ')[1].replace('\r', ''); + if (response[i + 1].match(/Path:\s/)) { + i++; + img_obj['path'] = response[i].split('Path: ')[1].replace('\r', ''); + } + if (response[i + 1].match(/\(API\slevel\s/)) { + i++; + img_obj['target'] = response[i].replace('\r', ''); + } + if (response[i + 1].match(/ABI:\s/)) { + i++; + img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', ''); + } + if (response[i + 1].match(/Skin:\s/)) { + i++; + img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', ''); + } + + emulator_list.push(img_obj); + } + /* To just return a list of names use this + if (response[i].match(/Name:\s/)) { + emulator_list.push(response[i].split('Name: ')[1].replace('\r', ''); + }*/ + + } + return emulator_list; + }); +}; + +/** + * Will return the closest avd to the projects target + * or undefined if no avds exist. + * Returns a promise. + */ +module.exports.best_image = function() { + return this.list_images() + .then(function(images) { + // Just return undefined if there is no images + if (images.length === 0) return; + + var closest = 9999; + var best = images[0]; + var project_target = check_reqs.get_target().replace('android-', ''); + for (var i in images) { + var target = images[i].target; + if(target) { + var num = target.split('(API level ')[1].replace(')', ''); + if (num == project_target) { + return images[i]; + } else if (project_target - num < closest && project_target > num) { + closest = project_target - num; + best = images[i]; + } + } + } + return best; + }); +}; + +// Returns a promise. +module.exports.list_started = function() { + return Adb.devices({emulators: true}); +}; + +// Returns a promise. +module.exports.list_targets = function() { + return spawn('android', ['list', 'targets'], {cwd: os.tmpdir()}) + .then(function(output) { + var target_out = output.split('\n'); + var targets = []; + for (var i = target_out.length; i >= 0; i--) { + if(target_out[i].match(/id:/)) { + targets.push(targets[i].split(' ')[1]); + } + } + return targets; + }); +}; + +/* + * Starts an emulator with the given ID, + * and returns the started ID of that emulator. + * If no ID is given it will use the first image available, + * if no image is available it will error out (maybe create one?). + * + * Returns a promise. + */ +module.exports.start = function(emulator_ID) { + var self = this; + + return Q().then(function() { + if (emulator_ID) return Q(emulator_ID); + + return self.best_image() + .then(function(best) { + if (best && best.name) { + events.emit('warn', 'No emulator specified, defaulting to ' + best.name); + return best.name; + } + + var androidCmd = check_reqs.getAbsoluteAndroidCmd(); + return Q.reject(new CordovaError('No emulator images (avds) found.\n' + + '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' + + '2. Create an AVD by running: ' + androidCmd + ' avd\n' + + 'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n')); + }); + }).then(function(emulatorId) { + var uuid = 'cordova_emulator_' + new Date().getTime(); + var uuidProp = 'emu.uuid=' + uuid; + var args = ['-avd', emulatorId, '-prop', uuidProp]; + // Don't wait for it to finish, since the emulator will probably keep running for a long time. + child_process + .spawn('emulator', args, { stdio: 'inherit', detached: true }) + .unref(); + + // wait for emulator to start + events.emit('log', 'Waiting for emulator...'); + return self.wait_for_emulator(uuid); + }).then(function(emulatorId) { + if (!emulatorId) + return Q.reject(new CordovaError('Failed to start emulator')); + + //wait for emulator to boot up + process.stdout.write('Booting up emulator (this may take a while)...'); + return self.wait_for_boot(emulatorId) + .then(function() { + events.emit('log','BOOT COMPLETE'); + //unlock screen + return Adb.shell(emulatorId, 'input keyevent 82'); + }).then(function() { + //return the new emulator id for the started emulators + return emulatorId; + }); + }); +}; + +/* + * Waits for an emulator with given uuid to apear on the started-emulator list. + * Returns a promise with this emulator's ID. + */ +module.exports.wait_for_emulator = function(uuid) { + var self = this; + return self.list_started() + .then(function(new_started) { + var emulator_id = null; + var promises = []; + + new_started.forEach(function (emulator) { + promises.push( + Adb.shell(emulator, 'getprop emu.uuid') + .then(function (output) { + if (output.indexOf(uuid) >= 0) { + emulator_id = emulator; + } + }) + ); + }); + + return Q.all(promises).then(function () { + return emulator_id || self.wait_for_emulator(uuid); + }); + }); +}; + +/* + * Waits for the core android process of the emulator to start + */ +module.exports.wait_for_boot = function(emulator_id) { + var self = this; + return Adb.shell(emulator_id, 'ps') + .then(function(output) { + if (output.match(/android\.process\.acore/)) { + return; + } else { + process.stdout.write('.'); + return Q.delay(3000).then(function() { + return self.wait_for_boot(emulator_id); + }); + } + }); +}; + +/* + * Create avd + * TODO : Enter the stdin input required to complete the creation of an avd. + * Returns a promise. + */ +module.exports.create_image = function(name, target) { + console.log('Creating avd named ' + name); + if (target) { + return spawn('android', ['create', 'avd', '--name', name, '--target', target]) + .then(null, function(error) { + console.error('ERROR : Failed to create emulator image : '); + console.error(' Do you have the latest android targets including ' + target + '?'); + console.error(error); + }); + } else { + console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.'); + return spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]) + .then(function() { + // TODO: This seems like another error case, even though it always happens. + console.error('ERROR : Unable to create an avd emulator, no targets found.'); + console.error('Please insure you have targets available by running the "android" command'); + return Q.reject(); + }, function(error) { + console.error('ERROR : Failed to create emulator image : '); + console.error(error); + }); + } +}; + +module.exports.resolveTarget = function(target) { + return this.list_started() + .then(function(emulator_list) { + if (emulator_list.length < 1) { + return Q.reject('No started emulators found, please start an emultor before deploying your project.'); + } + + // default emulator + target = target || emulator_list[0]; + if (emulator_list.indexOf(target) < 0) { + return Q.reject('Unable to find target \'' + target + '\'. Failed to deploy to emulator.'); + } + + return build.detectArchitecture(target) + .then(function(arch) { + return {target:target, arch:arch, isEmulator:true}; + }); + }); +}; + +/* + * Installs a previously built application on the emulator and launches it. + * If no target is specified, then it picks one. + * If no started emulators are found, error out. + * Returns a promise. + */ +module.exports.install = function(givenTarget, buildResults) { + + var target; + var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml')); + var pkgName = manifest.getPackageId(); + + // resolve the target emulator + return Q().then(function () { + if (givenTarget && typeof givenTarget == 'object') { + return givenTarget; + } else { + return module.exports.resolveTarget(givenTarget); + } + + // set the resolved target + }).then(function (resolvedTarget) { + target = resolvedTarget; + + // install the app + }).then(function () { + // This promise is always resolved, even if 'adb uninstall' fails to uninstall app + // or the app doesn't installed at all, so no error catching needed. + return Adb.uninstall(target.target, pkgName) + .then(function() { + + var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); + var execOptions = { + cwd: os.tmpdir(), + timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds + killSignal: EXEC_KILL_SIGNAL + }; + + events.emit('log', 'Using apk: ' + apk_path); + events.emit('verbose', 'Installing app on emulator...'); + + function exec(command, opts) { + return Q.promise(function (resolve, reject) { + child_process.exec(command, opts, function(err, stdout, stderr) { + if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr)); + else resolve(stdout); + }); + }); + } + + var retriedInstall = retry.retryPromise( + NUM_INSTALL_RETRIES, + exec, 'adb -s ' + target.target + ' install -r "' + apk_path + '"', execOptions + ); + + return retriedInstall.then(function (output) { + if (output.match(/Failure/)) { + return Q.reject(new CordovaError('Failed to install apk to emulator: ' + output)); + } else { + events.emit('log', 'INSTALL SUCCESS'); + } + }, function (err) { + return Q.reject(new CordovaError('Failed to install apk to emulator: ' + err)); + }); + }); + // unlock screen + }).then(function () { + + events.emit('verbose', 'Unlocking screen...'); + return Adb.shell(target.target, 'input keyevent 82'); + }).then(function () { + Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName()); + // report success or failure + }).then(function (output) { + events.emit('log', 'LAUNCH SUCCESS'); + }); +}; diff --git a/bin/templates/cordova/lib/pluginHandlers.js b/bin/templates/cordova/lib/pluginHandlers.js index cad95c25..381734d7 100644 --- a/bin/templates/cordova/lib/pluginHandlers.js +++ b/bin/templates/cordova/lib/pluginHandlers.js @@ -1,252 +1,252 @@ -/* - * - * Copyright 2013 Anis Kadri - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - * -*/ - -/* jshint unused: vars */ - -var fs = require('fs'); -var path = require('path'); -var shell = require('shelljs'); -var events = require('cordova-common').events; -var CordovaError = require('cordova-common').CordovaError; - -var handlers = { - 'source-file':{ - install:function(obj, plugin, project, options) { - if (!obj.src) throw new CordovaError(' element is missing "src" attribute for plugin: ' + plugin.id); - if (!obj.targetDir) throw new CordovaError(' element is missing "target-dir" attribute for plugin: ' + plugin.id); - var dest = path.join(obj.targetDir, path.basename(obj.src)); - copyNewFile(plugin.dir, obj.src, project.projectDir, dest, options && options.link); - }, - uninstall:function(obj, plugin, project, options) { - var dest = path.join(obj.targetDir, path.basename(obj.src)); - deleteJava(project.projectDir, dest); - } - }, - 'lib-file':{ - install:function(obj, plugin, project, options) { - var dest = path.join('libs', path.basename(obj.src)); - copyFile(plugin.dir, obj.src, project.projectDir, dest, options && options.link); - }, - uninstall:function(obj, plugin, project, options) { - var dest = path.join('libs', path.basename(obj.src)); - removeFile(project.projectDir, dest); - } - }, - 'resource-file':{ - install:function(obj, plugin, project, options) { - copyFile(plugin.dir, obj.src, project.projectDir, path.normalize(obj.target), options && options.link); - }, - uninstall:function(obj, plugin, project, options) { - removeFile(project.projectDir, path.normalize(obj.target)); - } - }, - 'framework': { - install:function(obj, plugin, project, options) { - var src = obj.src; - if (!src) throw new CordovaError('src not specified in for plugin: ' + plugin.id); - - events.emit('verbose', 'Installing Android library: ' + src); - var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir; - var subDir; - - if (obj.custom) { - var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src); - copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, options && options.link); - subDir = path.resolve(project.projectDir, subRelativeDir); - } else { - obj.type = 'sys'; - subDir = src; - } - - if (obj.type == 'gradleReference') { - project.addGradleReference(parentDir, subDir); - } else if (obj.type == 'sys') { - project.addSystemLibrary(parentDir, subDir); - } else { - project.addSubProject(parentDir, subDir); - } - }, - uninstall:function(obj, plugin, project, options) { - var src = obj.src; - if (!src) throw new CordovaError('src not specified in for plugin: ' + plugin.id); - - events.emit('verbose', 'Uninstalling Android library: ' + src); - var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir; - var subDir; - - if (obj.custom) { - var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src); - removeFile(project.projectDir, subRelativeDir); - subDir = path.resolve(project.projectDir, subRelativeDir); - // If it's the last framework in the plugin, remove the parent directory. - var parDir = path.dirname(subDir); - if (fs.readdirSync(parDir).length === 0) { - fs.rmdirSync(parDir); - } - } else { - obj.type = 'sys'; - subDir = src; - } - - if (obj.type == 'gradleReference') { - project.removeGradleReference(parentDir, subDir); - } else if (obj.type == 'sys') { - project.removeSystemLibrary(parentDir, subDir); - } else { - project.removeSubProject(parentDir, subDir); - } - } - }, - asset:{ - install:function(obj, plugin, project, options) { - if (!obj.src) { - throw new CordovaError(' tag without required "src" attribute. plugin=' + plugin.dir); - } - if (!obj.target) { - throw new CordovaError(' tag without required "target" attribute'); - } - - var www = options.usePlatformWww ? project.platformWww : project.www; - copyFile(plugin.dir, obj.src, www, obj.target); - }, - uninstall:function(obj, plugin, project, options) { - var target = obj.target || obj.src; - - if (!target) throw new CordovaError(' tag without required "target" attribute'); - - var www = options.usePlatformWww ? project.platformWww : project.www; - removeFile(www, target); - removeFileF(path.resolve(www, 'plugins', plugin.id)); - } - }, - 'js-module': { - install: function (obj, plugin, project, options) { - // Copy the plugin's files into the www directory. - var moduleSource = path.resolve(plugin.dir, obj.src); - var moduleName = plugin.id + '.' + (obj.name || path.parse(obj.src).name); - - // Read in the file, prepend the cordova.define, and write it back out. - var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM - if (moduleSource.match(/.*\.json$/)) { - scriptContent = 'module.exports = ' + scriptContent; - } - scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n'; - - var www = options.usePlatformWww ? project.platformWww : project.www; - var moduleDestination = path.resolve(www, 'plugins', plugin.id, obj.src); - shell.mkdir('-p', path.dirname(moduleDestination)); - fs.writeFileSync(moduleDestination, scriptContent, 'utf-8'); - }, - uninstall: function (obj, plugin, project, options) { - var pluginRelativePath = path.join('plugins', plugin.id, obj.src); - var www = options.usePlatformWww ? project.platformWww : project.www; - removeFileAndParents(www, pluginRelativePath); - } - } -}; - -module.exports.getInstaller = function (type) { - if (handlers[type] && handlers[type].install) { - return handlers[type].install; - } - - events.emit('verbose', '<' + type + '> is not supported for android plugins'); -}; - -module.exports.getUninstaller = function(type) { - if (handlers[type] && handlers[type].uninstall) { - return handlers[type].uninstall; - } - - events.emit('verbose', '<' + type + '> is not supported for android plugins'); -}; - -function copyFile (plugin_dir, src, project_dir, dest, link) { - src = path.resolve(plugin_dir, src); - if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!'); - - // check that src path is inside plugin directory - var real_path = fs.realpathSync(src); - var real_plugin_path = fs.realpathSync(plugin_dir); - if (real_path.indexOf(real_plugin_path) !== 0) - throw new CordovaError('"' + src + '" not located within plugin!'); - - dest = path.resolve(project_dir, dest); - - // check that dest path is located in project directory - if (dest.indexOf(project_dir) !== 0) - throw new CordovaError('"' + dest + '" not located within project!'); - - shell.mkdir('-p', path.dirname(dest)); - - if (link) { - fs.symlinkSync(path.relative(path.dirname(dest), src), dest); - } else if (fs.statSync(src).isDirectory()) { - // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq - shell.cp('-Rf', src+'/*', dest); - } else { - shell.cp('-f', src, dest); - } -} - -// Same as copy file but throws error if target exists -function copyNewFile (plugin_dir, src, project_dir, dest, link) { - var target_path = path.resolve(project_dir, dest); - if (fs.existsSync(target_path)) - throw new CordovaError('"' + target_path + '" already exists!'); - - copyFile(plugin_dir, src, project_dir, dest, !!link); -} - -// checks if file exists and then deletes. Error if doesn't exist -function removeFile (project_dir, src) { - var file = path.resolve(project_dir, src); - shell.rm('-Rf', file); -} - -// deletes file/directory without checking -function removeFileF (file) { - shell.rm('-Rf', file); -} - -// Sometimes we want to remove some java, and prune any unnecessary empty directories -function deleteJava (project_dir, destFile) { - removeFileAndParents(project_dir, destFile, 'src'); -} - -function removeFileAndParents (baseDir, destFile, stopper) { - stopper = stopper || '.'; - var file = path.resolve(baseDir, destFile); - if (!fs.existsSync(file)) return; - - removeFileF(file); - - // check if directory is empty - var curDir = path.dirname(file); - - while(curDir !== path.resolve(baseDir, stopper)) { - if(fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) { - fs.rmdirSync(curDir); - curDir = path.resolve(curDir, '..'); - } else { - // directory not empty...do nothing - break; - } - } -} +/* + * + * Copyright 2013 Anis Kadri + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * +*/ + +/* jshint unused: vars */ + +var fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var CordovaError = require('cordova-common').CordovaError; + +var handlers = { + 'source-file':{ + install:function(obj, plugin, project, options) { + if (!obj.src) throw new CordovaError(' element is missing "src" attribute for plugin: ' + plugin.id); + if (!obj.targetDir) throw new CordovaError(' element is missing "target-dir" attribute for plugin: ' + plugin.id); + var dest = path.join(obj.targetDir, path.basename(obj.src)); + copyNewFile(plugin.dir, obj.src, project.projectDir, dest, options && options.link); + }, + uninstall:function(obj, plugin, project, options) { + var dest = path.join(obj.targetDir, path.basename(obj.src)); + deleteJava(project.projectDir, dest); + } + }, + 'lib-file':{ + install:function(obj, plugin, project, options) { + var dest = path.join('libs', path.basename(obj.src)); + copyFile(plugin.dir, obj.src, project.projectDir, dest, options && options.link); + }, + uninstall:function(obj, plugin, project, options) { + var dest = path.join('libs', path.basename(obj.src)); + removeFile(project.projectDir, dest); + } + }, + 'resource-file':{ + install:function(obj, plugin, project, options) { + copyFile(plugin.dir, obj.src, project.projectDir, path.normalize(obj.target), options && options.link); + }, + uninstall:function(obj, plugin, project, options) { + removeFile(project.projectDir, path.normalize(obj.target)); + } + }, + 'framework': { + install:function(obj, plugin, project, options) { + var src = obj.src; + if (!src) throw new CordovaError('src not specified in for plugin: ' + plugin.id); + + events.emit('verbose', 'Installing Android library: ' + src); + var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir; + var subDir; + + if (obj.custom) { + var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src); + copyNewFile(plugin.dir, src, project.projectDir, subRelativeDir, options && options.link); + subDir = path.resolve(project.projectDir, subRelativeDir); + } else { + obj.type = 'sys'; + subDir = src; + } + + if (obj.type == 'gradleReference') { + project.addGradleReference(parentDir, subDir); + } else if (obj.type == 'sys') { + project.addSystemLibrary(parentDir, subDir); + } else { + project.addSubProject(parentDir, subDir); + } + }, + uninstall:function(obj, plugin, project, options) { + var src = obj.src; + if (!src) throw new CordovaError('src not specified in for plugin: ' + plugin.id); + + events.emit('verbose', 'Uninstalling Android library: ' + src); + var parentDir = obj.parent ? path.resolve(project.projectDir, obj.parent) : project.projectDir; + var subDir; + + if (obj.custom) { + var subRelativeDir = project.getCustomSubprojectRelativeDir(plugin.id, src); + removeFile(project.projectDir, subRelativeDir); + subDir = path.resolve(project.projectDir, subRelativeDir); + // If it's the last framework in the plugin, remove the parent directory. + var parDir = path.dirname(subDir); + if (fs.readdirSync(parDir).length === 0) { + fs.rmdirSync(parDir); + } + } else { + obj.type = 'sys'; + subDir = src; + } + + if (obj.type == 'gradleReference') { + project.removeGradleReference(parentDir, subDir); + } else if (obj.type == 'sys') { + project.removeSystemLibrary(parentDir, subDir); + } else { + project.removeSubProject(parentDir, subDir); + } + } + }, + asset:{ + install:function(obj, plugin, project, options) { + if (!obj.src) { + throw new CordovaError(' tag without required "src" attribute. plugin=' + plugin.dir); + } + if (!obj.target) { + throw new CordovaError(' tag without required "target" attribute'); + } + + var www = options.usePlatformWww ? project.platformWww : project.www; + copyFile(plugin.dir, obj.src, www, obj.target); + }, + uninstall:function(obj, plugin, project, options) { + var target = obj.target || obj.src; + + if (!target) throw new CordovaError(' tag without required "target" attribute'); + + var www = options.usePlatformWww ? project.platformWww : project.www; + removeFile(www, target); + removeFileF(path.resolve(www, 'plugins', plugin.id)); + } + }, + 'js-module': { + install: function (obj, plugin, project, options) { + // Copy the plugin's files into the www directory. + var moduleSource = path.resolve(plugin.dir, obj.src); + var moduleName = plugin.id + '.' + (obj.name || path.parse(obj.src).name); + + // Read in the file, prepend the cordova.define, and write it back out. + var scriptContent = fs.readFileSync(moduleSource, 'utf-8').replace(/^\ufeff/, ''); // Window BOM + if (moduleSource.match(/.*\.json$/)) { + scriptContent = 'module.exports = ' + scriptContent; + } + scriptContent = 'cordova.define("' + moduleName + '", function(require, exports, module) {\n' + scriptContent + '\n});\n'; + + var www = options.usePlatformWww ? project.platformWww : project.www; + var moduleDestination = path.resolve(www, 'plugins', plugin.id, obj.src); + shell.mkdir('-p', path.dirname(moduleDestination)); + fs.writeFileSync(moduleDestination, scriptContent, 'utf-8'); + }, + uninstall: function (obj, plugin, project, options) { + var pluginRelativePath = path.join('plugins', plugin.id, obj.src); + var www = options.usePlatformWww ? project.platformWww : project.www; + removeFileAndParents(www, pluginRelativePath); + } + } +}; + +module.exports.getInstaller = function (type) { + if (handlers[type] && handlers[type].install) { + return handlers[type].install; + } + + events.emit('verbose', '<' + type + '> is not supported for android plugins'); +}; + +module.exports.getUninstaller = function(type) { + if (handlers[type] && handlers[type].uninstall) { + return handlers[type].uninstall; + } + + events.emit('verbose', '<' + type + '> is not supported for android plugins'); +}; + +function copyFile (plugin_dir, src, project_dir, dest, link) { + src = path.resolve(plugin_dir, src); + if (!fs.existsSync(src)) throw new CordovaError('"' + src + '" not found!'); + + // check that src path is inside plugin directory + var real_path = fs.realpathSync(src); + var real_plugin_path = fs.realpathSync(plugin_dir); + if (real_path.indexOf(real_plugin_path) !== 0) + throw new CordovaError('"' + src + '" not located within plugin!'); + + dest = path.resolve(project_dir, dest); + + // check that dest path is located in project directory + if (dest.indexOf(project_dir) !== 0) + throw new CordovaError('"' + dest + '" not located within project!'); + + shell.mkdir('-p', path.dirname(dest)); + + if (link) { + fs.symlinkSync(path.relative(path.dirname(dest), src), dest); + } else if (fs.statSync(src).isDirectory()) { + // XXX shelljs decides to create a directory when -R|-r is used which sucks. http://goo.gl/nbsjq + shell.cp('-Rf', src+'/*', dest); + } else { + shell.cp('-f', src, dest); + } +} + +// Same as copy file but throws error if target exists +function copyNewFile (plugin_dir, src, project_dir, dest, link) { + var target_path = path.resolve(project_dir, dest); + if (fs.existsSync(target_path)) + throw new CordovaError('"' + target_path + '" already exists!'); + + copyFile(plugin_dir, src, project_dir, dest, !!link); +} + +// checks if file exists and then deletes. Error if doesn't exist +function removeFile (project_dir, src) { + var file = path.resolve(project_dir, src); + shell.rm('-Rf', file); +} + +// deletes file/directory without checking +function removeFileF (file) { + shell.rm('-Rf', file); +} + +// Sometimes we want to remove some java, and prune any unnecessary empty directories +function deleteJava (project_dir, destFile) { + removeFileAndParents(project_dir, destFile, 'src'); +} + +function removeFileAndParents (baseDir, destFile, stopper) { + stopper = stopper || '.'; + var file = path.resolve(baseDir, destFile); + if (!fs.existsSync(file)) return; + + removeFileF(file); + + // check if directory is empty + var curDir = path.dirname(file); + + while(curDir !== path.resolve(baseDir, stopper)) { + if(fs.existsSync(curDir) && fs.readdirSync(curDir).length === 0) { + fs.rmdirSync(curDir); + curDir = path.resolve(curDir, '..'); + } else { + // directory not empty...do nothing + break; + } + } +} diff --git a/bin/templates/cordova/lib/prepare.js b/bin/templates/cordova/lib/prepare.js index 67ac1749..a514c8cd 100644 --- a/bin/templates/cordova/lib/prepare.js +++ b/bin/templates/cordova/lib/prepare.js @@ -1,364 +1,364 @@ -/** - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -var Q = require('q'); -var fs = require('fs'); -var path = require('path'); -var shell = require('shelljs'); -var events = require('cordova-common').events; -var AndroidManifest = require('./AndroidManifest'); -var xmlHelpers = require('cordova-common').xmlHelpers; -var CordovaError = require('cordova-common').CordovaError; -var ConfigParser = require('cordova-common').ConfigParser; - -module.exports.prepare = function (cordovaProject) { - - var self = this; - - this._config = updateConfigFilesFrom(cordovaProject.projectConfig, - this._munger, this.locations); - - // Update own www dir with project's www assets and plugins' assets and js-files - return Q.when(updateWwwFrom(cordovaProject, this.locations)) - .then(function () { - // update project according to config.xml changes. - return updateProjectAccordingTo(self._config, self.locations); - }) - .then(function () { - handleIcons(cordovaProject.projectConfig, self.root); - handleSplashes(cordovaProject.projectConfig, self.root); - }) - .then(function () { - self.events.emit('verbose', 'updated project successfully'); - }); -}; - -/** - * Updates config files in project based on app's config.xml and config munge, - * generated by plugins. - * - * @param {ConfigParser} sourceConfig A project's configuration that will - * be merged into platform's config.xml - * @param {ConfigChanges} configMunger An initialized ConfigChanges instance - * for this platform. - * @param {Object} locations A map of locations for this platform - * - * @return {ConfigParser} An instance of ConfigParser, that - * represents current project's configuration. When returned, the - * configuration is already dumped to appropriate config.xml file. - */ -function updateConfigFilesFrom(sourceConfig, configMunger, locations) { - events.emit('verbose', 'Generating config.xml from defaults for platform "android"'); - - // First cleanup current config and merge project's one into own - // Overwrite platform config.xml with defaults.xml. - shell.cp('-f', locations.defaultConfigXml, locations.configXml); - - // Then apply config changes from global munge to all config files - // in project (including project's config) - configMunger.reapply_global_munge().save_all(); - - // Merge changes from app's config.xml into platform's one - var config = new ConfigParser(locations.configXml); - xmlHelpers.mergeXml(sourceConfig.doc.getroot(), - config.doc.getroot(), 'android', /*clobber=*/true); - - config.write(); - return config; -} - -/** - * Updates platform 'www' directory by replacing it with contents of - * 'platform_www' and app www. Also copies project's overrides' folder into - * the platform 'www' folder - * - * @param {Object} cordovaProject An object which describes cordova project. - * @param {Object} destinations An object that contains destination - * paths for www files. - */ -function updateWwwFrom(cordovaProject, destinations) { - shell.rm('-rf', destinations.www); - shell.mkdir('-p', destinations.www); - // Copy source files from project's www directory - shell.cp('-rf', path.join(cordovaProject.locations.www, '*'), destinations.www); - // Override www sources by files in 'platform_www' directory - shell.cp('-rf', path.join(destinations.platformWww, '*'), destinations.www); - - // If project contains 'merges' for our platform, use them as another overrides - var merges_path = path.join(cordovaProject.root, 'merges', 'android'); - if (fs.existsSync(merges_path)) { - events.emit('verbose', 'Found "merges" for android platform. Copying over existing "www" files.'); - var overrides = path.join(merges_path, '*'); - shell.cp('-rf', overrides, destinations.www); - } -} - -/** - * Updates project structure and AndroidManifest according to project's configuration. - * - * @param {ConfigParser} platformConfig A project's configuration that will - * be used to update project - * @param {Object} locations A map of locations for this platform - */ -function updateProjectAccordingTo(platformConfig, locations) { - // Update app name by editing res/values/strings.xml - var name = platformConfig.name(); - var strings = xmlHelpers.parseElementtreeSync(locations.strings); - strings.find('string[@name="app_name"]').text = name; - fs.writeFileSync(locations.strings, strings.write({indent: 4}), 'utf-8'); - events.emit('verbose', 'Wrote out Android application name to "' + name + '"'); - - // Java packages cannot support dashes - var pkg = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_'); - - var manifest = new AndroidManifest(locations.manifest); - var orig_pkg = manifest.getPackageId(); - - manifest.getActivity() - .setOrientation(findOrientationValue(platformConfig)) - .setLaunchMode(findAndroidLaunchModePreference(platformConfig)); - - manifest.setVersionName(platformConfig.version()) - .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version())) - .setPackageId(pkg) - .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android')) - .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android')) - .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android')) - .write(); - - var javaPattern = path.join(locations.root, 'src', orig_pkg.replace(/\./g, '/'), '*.java'); - var java_files = shell.ls(javaPattern).filter(function(f) { - return shell.grep(/extends\s+CordovaActivity/g, f); - }); - - if (java_files.length === 0) { - throw new CordovaError('No Java files found which extend CordovaActivity.'); - } else if(java_files.length > 1) { - events.emit('log', 'Multiple candidate Java files (.java files which extend CordovaActivity) found. Guessing at the first one, ' + java_files[0]); - } - - var destFile = path.join(locations.root, 'src', pkg.replace(/\./g, '/'), path.basename(java_files[0])); - shell.mkdir('-p', path.dirname(destFile)); - shell.sed(/package [\w\.]*;/, 'package ' + pkg + ';', java_files[0]).to(destFile); - events.emit('verbose', 'Wrote out Android package name to "' + pkg + '"'); -} - - -// Consturct the default value for versionCode as -// PATCH + MINOR * 100 + MAJOR * 10000 -// see http://developer.android.com/tools/publishing/versioning.html -function default_versionCode(version) { - var nums = version.split('-')[0].split('.'); - var versionCode = 0; - if (+nums[0]) { - versionCode += +nums[0] * 10000; - } - if (+nums[1]) { - versionCode += +nums[1] * 100; - } - if (+nums[2]) { - versionCode += +nums[2]; - } - return versionCode; -} - -function copyImage(src, resourcesDir, density, name) { - var destFolder = path.join(resourcesDir, (density ? 'drawable-': 'drawable') + density); - var isNinePatch = !!/\.9\.png$/.exec(src); - var ninePatchName = name.replace(/\.png$/, '.9.png'); - - // default template does not have default asset for this density - if (!fs.existsSync(destFolder)) { - fs.mkdirSync(destFolder); - } - - var destFilePath = path.join(destFolder, isNinePatch ? ninePatchName : name); - events.emit('verbose', 'copying image from ' + src + ' to ' + destFilePath); - shell.cp('-f', src, destFilePath); -} - -function handleSplashes(projectConfig, platformRoot) { - var resources = projectConfig.getSplashScreens('android'); - // if there are "splash" elements in config.xml - if (resources.length > 0) { - deleteDefaultResourceAt(platformRoot, 'screen.png'); - events.emit('verbose', 'splash screens: ' + JSON.stringify(resources)); - - // The source paths for icons and splashes are relative to - // project's config.xml location, so we use it as base path. - var projectRoot = path.dirname(projectConfig.path); - var destination = path.join(platformRoot, 'res'); - - var hadMdpi = false; - resources.forEach(function (resource) { - if (!resource.density) { - return; - } - if (resource.density == 'mdpi') { - hadMdpi = true; - } - copyImage(path.join(projectRoot, resource.src), destination, resource.density, 'screen.png'); - }); - // There's no "default" drawable, so assume default == mdpi. - if (!hadMdpi && resources.defaultResource) { - copyImage(path.join(projectRoot, resources.defaultResource.src), destination, 'mdpi', 'screen.png'); - } - } -} - -function handleIcons(projectConfig, platformRoot) { - var icons = projectConfig.getIcons('android'); - - // if there are icon elements in config.xml - if (icons.length === 0) { - events.emit('verbose', 'This app does not have launcher icons defined'); - return; - } - - deleteDefaultResourceAt(platformRoot, 'icon.png'); - - var android_icons = {}; - var default_icon; - // http://developer.android.com/design/style/iconography.html - var sizeToDensityMap = { - 36: 'ldpi', - 48: 'mdpi', - 72: 'hdpi', - 96: 'xhdpi', - 144: 'xxhdpi', - 192: 'xxxhdpi' - }; - // find the best matching icon for a given density or size - // @output android_icons - var parseIcon = function(icon, icon_size) { - // do I have a platform icon for that density already - var density = icon.density || sizeToDensityMap[icon_size]; - if (!density) { - // invalid icon defition ( or unsupported size) - return; - } - var previous = android_icons[density]; - if (previous && previous.platform) { - return; - } - android_icons[density] = icon; - }; - - // iterate over all icon elements to find the default icon and call parseIcon - for (var i=0; i= 0; - if (!valid) { - // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future - events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' + - launchMode + '. Expected values are: ' + expectedValues.join(', ')); - } - - return launchMode; -} - -/** - * Queries ConfigParser object for the orientation value. Warns if - * global preference value is not supported by platform. - * - * @param {Object} platformConfig ConfigParser object - * - * @return {String} Global/platform-specific orientation in lower-case - * (or empty string if both are undefined). - */ -function findOrientationValue(platformConfig) { - - var ORIENTATION_DEFAULT = 'default'; - - var orientation = platformConfig.getPreference('orientation'); - if (!orientation) { - return ORIENTATION_DEFAULT; - } - - var GLOBAL_ORIENTATIONS = ['default', 'portrait','landscape']; - function isSupported(orientation) { - return GLOBAL_ORIENTATIONS.indexOf(orientation.toLowerCase()) >= 0; - } - - // Check if the given global orientation is supported - if (orientation && isSupported(orientation)) { - return orientation; - } - - events.emit('warn', 'Unsupported global orientation: ' + orientation + - '. Defaulting to value: ' + ORIENTATION_DEFAULT); - return ORIENTATION_DEFAULT; -} +/** + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ + +var Q = require('q'); +var fs = require('fs'); +var path = require('path'); +var shell = require('shelljs'); +var events = require('cordova-common').events; +var AndroidManifest = require('./AndroidManifest'); +var xmlHelpers = require('cordova-common').xmlHelpers; +var CordovaError = require('cordova-common').CordovaError; +var ConfigParser = require('cordova-common').ConfigParser; + +module.exports.prepare = function (cordovaProject) { + + var self = this; + + this._config = updateConfigFilesFrom(cordovaProject.projectConfig, + this._munger, this.locations); + + // Update own www dir with project's www assets and plugins' assets and js-files + return Q.when(updateWwwFrom(cordovaProject, this.locations)) + .then(function () { + // update project according to config.xml changes. + return updateProjectAccordingTo(self._config, self.locations); + }) + .then(function () { + handleIcons(cordovaProject.projectConfig, self.root); + handleSplashes(cordovaProject.projectConfig, self.root); + }) + .then(function () { + self.events.emit('verbose', 'updated project successfully'); + }); +}; + +/** + * Updates config files in project based on app's config.xml and config munge, + * generated by plugins. + * + * @param {ConfigParser} sourceConfig A project's configuration that will + * be merged into platform's config.xml + * @param {ConfigChanges} configMunger An initialized ConfigChanges instance + * for this platform. + * @param {Object} locations A map of locations for this platform + * + * @return {ConfigParser} An instance of ConfigParser, that + * represents current project's configuration. When returned, the + * configuration is already dumped to appropriate config.xml file. + */ +function updateConfigFilesFrom(sourceConfig, configMunger, locations) { + events.emit('verbose', 'Generating config.xml from defaults for platform "android"'); + + // First cleanup current config and merge project's one into own + // Overwrite platform config.xml with defaults.xml. + shell.cp('-f', locations.defaultConfigXml, locations.configXml); + + // Then apply config changes from global munge to all config files + // in project (including project's config) + configMunger.reapply_global_munge().save_all(); + + // Merge changes from app's config.xml into platform's one + var config = new ConfigParser(locations.configXml); + xmlHelpers.mergeXml(sourceConfig.doc.getroot(), + config.doc.getroot(), 'android', /*clobber=*/true); + + config.write(); + return config; +} + +/** + * Updates platform 'www' directory by replacing it with contents of + * 'platform_www' and app www. Also copies project's overrides' folder into + * the platform 'www' folder + * + * @param {Object} cordovaProject An object which describes cordova project. + * @param {Object} destinations An object that contains destination + * paths for www files. + */ +function updateWwwFrom(cordovaProject, destinations) { + shell.rm('-rf', destinations.www); + shell.mkdir('-p', destinations.www); + // Copy source files from project's www directory + shell.cp('-rf', path.join(cordovaProject.locations.www, '*'), destinations.www); + // Override www sources by files in 'platform_www' directory + shell.cp('-rf', path.join(destinations.platformWww, '*'), destinations.www); + + // If project contains 'merges' for our platform, use them as another overrides + var merges_path = path.join(cordovaProject.root, 'merges', 'android'); + if (fs.existsSync(merges_path)) { + events.emit('verbose', 'Found "merges" for android platform. Copying over existing "www" files.'); + var overrides = path.join(merges_path, '*'); + shell.cp('-rf', overrides, destinations.www); + } +} + +/** + * Updates project structure and AndroidManifest according to project's configuration. + * + * @param {ConfigParser} platformConfig A project's configuration that will + * be used to update project + * @param {Object} locations A map of locations for this platform + */ +function updateProjectAccordingTo(platformConfig, locations) { + // Update app name by editing res/values/strings.xml + var name = platformConfig.name(); + var strings = xmlHelpers.parseElementtreeSync(locations.strings); + strings.find('string[@name="app_name"]').text = name; + fs.writeFileSync(locations.strings, strings.write({indent: 4}), 'utf-8'); + events.emit('verbose', 'Wrote out Android application name to "' + name + '"'); + + // Java packages cannot support dashes + var pkg = (platformConfig.android_packageName() || platformConfig.packageName()).replace(/-/g, '_'); + + var manifest = new AndroidManifest(locations.manifest); + var orig_pkg = manifest.getPackageId(); + + manifest.getActivity() + .setOrientation(findOrientationValue(platformConfig)) + .setLaunchMode(findAndroidLaunchModePreference(platformConfig)); + + manifest.setVersionName(platformConfig.version()) + .setVersionCode(platformConfig.android_versionCode() || default_versionCode(platformConfig.version())) + .setPackageId(pkg) + .setMinSdkVersion(platformConfig.getPreference('android-minSdkVersion', 'android')) + .setMaxSdkVersion(platformConfig.getPreference('android-maxSdkVersion', 'android')) + .setTargetSdkVersion(platformConfig.getPreference('android-targetSdkVersion', 'android')) + .write(); + + var javaPattern = path.join(locations.root, 'src', orig_pkg.replace(/\./g, '/'), '*.java'); + var java_files = shell.ls(javaPattern).filter(function(f) { + return shell.grep(/extends\s+CordovaActivity/g, f); + }); + + if (java_files.length === 0) { + throw new CordovaError('No Java files found which extend CordovaActivity.'); + } else if(java_files.length > 1) { + events.emit('log', 'Multiple candidate Java files (.java files which extend CordovaActivity) found. Guessing at the first one, ' + java_files[0]); + } + + var destFile = path.join(locations.root, 'src', pkg.replace(/\./g, '/'), path.basename(java_files[0])); + shell.mkdir('-p', path.dirname(destFile)); + shell.sed(/package [\w\.]*;/, 'package ' + pkg + ';', java_files[0]).to(destFile); + events.emit('verbose', 'Wrote out Android package name to "' + pkg + '"'); +} + + +// Consturct the default value for versionCode as +// PATCH + MINOR * 100 + MAJOR * 10000 +// see http://developer.android.com/tools/publishing/versioning.html +function default_versionCode(version) { + var nums = version.split('-')[0].split('.'); + var versionCode = 0; + if (+nums[0]) { + versionCode += +nums[0] * 10000; + } + if (+nums[1]) { + versionCode += +nums[1] * 100; + } + if (+nums[2]) { + versionCode += +nums[2]; + } + return versionCode; +} + +function copyImage(src, resourcesDir, density, name) { + var destFolder = path.join(resourcesDir, (density ? 'drawable-': 'drawable') + density); + var isNinePatch = !!/\.9\.png$/.exec(src); + var ninePatchName = name.replace(/\.png$/, '.9.png'); + + // default template does not have default asset for this density + if (!fs.existsSync(destFolder)) { + fs.mkdirSync(destFolder); + } + + var destFilePath = path.join(destFolder, isNinePatch ? ninePatchName : name); + events.emit('verbose', 'copying image from ' + src + ' to ' + destFilePath); + shell.cp('-f', src, destFilePath); +} + +function handleSplashes(projectConfig, platformRoot) { + var resources = projectConfig.getSplashScreens('android'); + // if there are "splash" elements in config.xml + if (resources.length > 0) { + deleteDefaultResourceAt(platformRoot, 'screen.png'); + events.emit('verbose', 'splash screens: ' + JSON.stringify(resources)); + + // The source paths for icons and splashes are relative to + // project's config.xml location, so we use it as base path. + var projectRoot = path.dirname(projectConfig.path); + var destination = path.join(platformRoot, 'res'); + + var hadMdpi = false; + resources.forEach(function (resource) { + if (!resource.density) { + return; + } + if (resource.density == 'mdpi') { + hadMdpi = true; + } + copyImage(path.join(projectRoot, resource.src), destination, resource.density, 'screen.png'); + }); + // There's no "default" drawable, so assume default == mdpi. + if (!hadMdpi && resources.defaultResource) { + copyImage(path.join(projectRoot, resources.defaultResource.src), destination, 'mdpi', 'screen.png'); + } + } +} + +function handleIcons(projectConfig, platformRoot) { + var icons = projectConfig.getIcons('android'); + + // if there are icon elements in config.xml + if (icons.length === 0) { + events.emit('verbose', 'This app does not have launcher icons defined'); + return; + } + + deleteDefaultResourceAt(platformRoot, 'icon.png'); + + var android_icons = {}; + var default_icon; + // http://developer.android.com/design/style/iconography.html + var sizeToDensityMap = { + 36: 'ldpi', + 48: 'mdpi', + 72: 'hdpi', + 96: 'xhdpi', + 144: 'xxhdpi', + 192: 'xxxhdpi' + }; + // find the best matching icon for a given density or size + // @output android_icons + var parseIcon = function(icon, icon_size) { + // do I have a platform icon for that density already + var density = icon.density || sizeToDensityMap[icon_size]; + if (!density) { + // invalid icon defition ( or unsupported size) + return; + } + var previous = android_icons[density]; + if (previous && previous.platform) { + return; + } + android_icons[density] = icon; + }; + + // iterate over all icon elements to find the default icon and call parseIcon + for (var i=0; i= 0; + if (!valid) { + // Note: warn, but leave the launch mode as developer wanted, in case the list of options changes in the future + events.emit('warn', 'Unrecognized value for AndroidLaunchMode preference: ' + + launchMode + '. Expected values are: ' + expectedValues.join(', ')); + } + + return launchMode; +} + +/** + * Queries ConfigParser object for the orientation value. Warns if + * global preference value is not supported by platform. + * + * @param {Object} platformConfig ConfigParser object + * + * @return {String} Global/platform-specific orientation in lower-case + * (or empty string if both are undefined). + */ +function findOrientationValue(platformConfig) { + + var ORIENTATION_DEFAULT = 'default'; + + var orientation = platformConfig.getPreference('orientation'); + if (!orientation) { + return ORIENTATION_DEFAULT; + } + + var GLOBAL_ORIENTATIONS = ['default', 'portrait','landscape']; + function isSupported(orientation) { + return GLOBAL_ORIENTATIONS.indexOf(orientation.toLowerCase()) >= 0; + } + + // Check if the given global orientation is supported + if (orientation && isSupported(orientation)) { + return orientation; + } + + events.emit('warn', 'Unsupported global orientation: ' + orientation + + '. Defaulting to value: ' + ORIENTATION_DEFAULT); + return ORIENTATION_DEFAULT; +} diff --git a/bin/update b/bin/update index 4000df27..861c8d04 100755 --- a/bin/update +++ b/bin/update @@ -1,35 +1,35 @@ -#!/usr/bin/env node - -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ -var path = require('path'); -var Api = require('./templates/cordova/Api'); -var args = require('nopt')({ - 'link': Boolean, - 'shared': Boolean, - 'help': Boolean -}); - -if (args.help || args.argv.remain.length === 0) { - console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'update')) + ' [--link]'); - console.log(' --link will use the CordovaLib project directly instead of making a copy.'); - process.exit(1); -} - -Api.updatePlatform(args.argv.remain[0], {link: (args.link || args.shared)}).done(); +#!/usr/bin/env node + +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +*/ +var path = require('path'); +var Api = require('./templates/cordova/Api'); +var args = require('nopt')({ + 'link': Boolean, + 'shared': Boolean, + 'help': Boolean +}); + +if (args.help || args.argv.remain.length === 0) { + console.log('Usage: ' + path.relative(process.cwd(), path.join(__dirname, 'update')) + ' [--link]'); + console.log(' --link will use the CordovaLib project directly instead of making a copy.'); + process.exit(1); +} + +Api.updatePlatform(args.argv.remain[0], {link: (args.link || args.shared)}).done(); diff --git a/package.json b/package.json index f49ca79c..de7e9aa6 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,46 @@ -{ - "name": "cordova-android", - "version": "5.0.0-dev", - "description": "cordova-android release", - "bin": { - "create": "bin/create" - }, - "main": "bin/templates/cordova/Api.js", - "repository": { - "type": "git", - "url": "https://git-wip-us.apache.org/repos/asf/cordova-android.git" - }, - "keywords": [ - "android", - "cordova", - "apache" - ], - "scripts": { - "test": "npm run jshint && jasmine-node --color spec", - "test-build": "rm -rf \"test create\" && node ./bin/create \"test create\" com.test.app 応用 && \"./test create/cordova/build\" && rm -rf \"test create\"", - "jshint": "node node_modules/jshint/bin/jshint bin && node node_modules/jshint/bin/jshint spec" - }, - "author": "Apache Software Foundation", - "license": "Apache version 2.0", - "dependencies": { - "cordova-common": "^0.1.0", - "elementtree": "^0.1.6", - "nopt": "^3.0.1", - "properties-parser": "^0.3.0", - "q": "^1.4.1", - "shelljs": "^0.5.3" - }, - "bundledDependencies": [ - "cordova-common", - "elementtree", - "nopt", - "properties-parser", - "q", - "shelljs" - ], - "devDependencies": { - "jasmine-node": "^1.14.5", - "jshint": "^2.6.0", - "promise-matchers": "~0" - } -} +{ + "name": "cordova-android", + "version": "5.0.0-dev", + "description": "cordova-android release", + "bin": { + "create": "bin/create" + }, + "main": "bin/templates/cordova/Api.js", + "repository": { + "type": "git", + "url": "https://git-wip-us.apache.org/repos/asf/cordova-android.git" + }, + "keywords": [ + "android", + "cordova", + "apache" + ], + "scripts": { + "test": "npm run jshint && jasmine-node --color spec", + "test-build": "rm -rf \"test create\" && node ./bin/create \"test create\" com.test.app 応用 && \"./test create/cordova/build\" && rm -rf \"test create\"", + "jshint": "node node_modules/jshint/bin/jshint bin && node node_modules/jshint/bin/jshint spec" + }, + "author": "Apache Software Foundation", + "license": "Apache version 2.0", + "dependencies": { + "cordova-common": "^0.1.0", + "elementtree": "^0.1.6", + "nopt": "^3.0.1", + "properties-parser": "^0.3.0", + "q": "^1.4.1", + "shelljs": "^0.5.3" + }, + "bundledDependencies": [ + "cordova-common", + "elementtree", + "nopt", + "properties-parser", + "q", + "shelljs" + ], + "devDependencies": { + "jasmine-node": "^1.14.5", + "jshint": "^2.6.0", + "promise-matchers": "~0" + } +}