diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js index 75dc0c7b..42be23e6 100644 --- a/bin/templates/cordova/lib/build.js +++ b/bin/templates/cordova/lib/build.js @@ -31,9 +31,12 @@ var shell = require('shelljs'), var check_reqs = require('./check_reqs'); var exec = require('./exec'); -var LOCAL_PROPERTIES_TEMPLATE = + +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 -- YOUR CHANGES WILL BE ERASED!\n'; + '# Do not modify this file -- ' + MARKER + '\n'; function findApks(directory) { var ret = []; @@ -56,6 +59,14 @@ function sortFilesByDate(files) { }).map(function(p) { return p.p; }); } +function isAutoGenerated(file) { + if(fs.existsSync(file)) { + var fileContents = fs.readFileSync(file, 'utf8'); + return fileContents.indexOf(MARKER) > 0; + } + return false; +} + function findOutputApksHelper(dir, build_type, arch) { var ret = findApks(dir).filter(function(candidate) { // Need to choose between release and debug .apk. @@ -121,16 +132,19 @@ function readProjectProperties() { var builders = { ant: { - getArgs: function(cmd) { + getArgs: function(cmd, opts) { var args = [cmd, '-f', path.join(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(ROOT, opts.buildType + SIGNING_PROPERTIES)); + } return args; }, - prepEnv: function() { + prepEnv: function(opts) { return check_reqs.check_ant() .then(function() { // Copy in build.xml on each build so that: @@ -142,7 +156,7 @@ var builders = { var newData = buildTemplate.replace('PROJECT_NAME', extractProjectNameFromManifest(ROOT)); fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { - fs.writeFileSync(path.join(projectPath, 'local.properties'), LOCAL_PROPERTIES_TEMPLATE); + fs.writeFileSync(path.join(projectPath, 'local.properties'), TEMPLATE); } } writeBuildXml(ROOT); @@ -154,6 +168,14 @@ var builders = { if (propertiesObj.systemLibs.length > 0) { throw new Error('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(ROOT, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if(isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } }); }, @@ -161,23 +183,24 @@ var builders = { * Builds the project with ant. * Returns a promise. */ - build: function(build_type) { + 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(); + ret = this.clean(opts); } - var args = this.getArgs(build_type == 'debug' ? 'debug' : 'release'); + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); return check_reqs.check_ant() .then(function() { + console.log('Executing: ant ' + args.join(' ')); return spawn('ant', args); }); }, - clean: function() { - var args = this.getArgs('clean'); + clean: function(opts) { + var args = this.getArgs('clean', opts); return check_reqs.check_ant() .then(function() { return spawn('ant', args); @@ -190,20 +213,20 @@ var builders = { } }, gradle: { - getArgs: function(cmd, arch, extraArgs) { + getArgs: function(cmd, opts) { if (cmd == 'release') { cmd = 'cdvBuildRelease'; } else if (cmd == 'debug') { cmd = 'cdvBuildDebug'; } var args = [cmd, '-b', path.join(ROOT, 'build.gradle')]; - if (arch) { - args.push('-PcdvBuildArch=' + arch); + if (opts.arch) { + args.push('-PcdvBuildArch=' + opts.arch); } // 10 seconds -> 6 seconds args.push('-Dorg.gradle.daemon=true'); - args.push.apply(args, extraArgs); + 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; @@ -268,7 +291,7 @@ var builders = { fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle); }, - prepEnv: function() { + prepEnv: function(opts) { var self = this; return check_reqs.check_gradle() .then(function() { @@ -297,6 +320,14 @@ var builders = { var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties'); shell.sed('-i', distributionUrlRegex, distributionUrl, gradleWrapperPropertiesPath); + + var propertiesFile = opts.buildType + SIGNING_PROPERTIES; + var propertiesFilePath = path.join(ROOT, propertiesFile); + if (opts.packageInfo) { + fs.writeFileSync(propertiesFilePath, TEMPLATE + opts.packageInfo.toProperties()); + } else if (isAutoGenerated(propertiesFilePath)) { + shell.rm('-f', propertiesFilePath); + } }); }, @@ -304,19 +335,19 @@ var builders = { * Builds the project with gradle. * Returns a promise. */ - build: function(build_type, arch, extraArgs) { + build: function(opts) { var wrapper = path.join(ROOT, 'gradlew'); - var args = this.getArgs(build_type == 'debug' ? 'debug' : 'release', arch, extraArgs); + var args = this.getArgs(opts.buildType == 'debug' ? 'debug' : 'release', opts); return Q().then(function() { console.log('Running: ' + wrapper + ' ' + args.join(' ')); return spawn(wrapper, args); }); }, - clean: function(extraArgs) { + clean: function(opts) { var builder = this; var wrapper = path.join(ROOT, 'gradlew'); - var args = builder.getArgs('clean', null, extraArgs); + var args = builder.getArgs('clean', opts); return Q().then(function() { console.log('Running: ' + wrapper + ' ' + args.join(' ')); return spawn(wrapper, args); @@ -346,6 +377,10 @@ var builders = { } }; +module.exports.isBuildFlag = function(flag) { + return /^--(debug|release|ant|gradle|nobuild|versionCode=|minSdkVersion=|gradleArg=|keystore=|alias=|password=|storePassword=|keystoreType=|buildConfig=)/.exec(flag); +}; + function parseOpts(options, resolvedTarget) { // Backwards-compatibility: Allow a single string argument if (typeof options == 'string') options = [options]; @@ -360,9 +395,16 @@ function parseOpts(options, resolvedTarget) { var multiValueArgs = { 'versionCode': true, 'minSdkVersion': true, - 'gradleArg': true + 'gradleArg': true, + 'keystore' : true, + 'alias' : true, + 'password' : true, + 'storePassword' : true, + 'keystoreType' : true, + 'buildConfig' : true }; - + var packageArgs = {}; + var buildConfig; // Iterate through command line options for (var i=0; options && (i < options.length); ++i) { if (/^--/.exec(options[i])) { @@ -402,6 +444,18 @@ function parseOpts(options, resolvedTarget) { case 'gradleArg': ret.extraArgs.push(flagValue); break; + case 'keystore': + packageArgs.keystore = path.relative(ROOT, path.resolve(flagValue)); + break; + case 'alias': + case 'storePassword': + case 'password': + case 'keystoreType': + packageArgs[flagName] = flagValue; + break; + case 'buildConfig': + buildConfig = flagValue; + break; default : console.warn('Build option --\'' + flagName + '\' not recognized (ignoring).'); } @@ -410,6 +464,36 @@ function parseOpts(options, resolvedTarget) { } } + // 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) { + console.log(path.resolve(buildConfig)); + if (!fs.existsSync(buildConfig)) { + throw new Error('Specified build config file does not exist: ' + buildConfig); + } + console.log('Reading build config file: '+ 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.relative(ROOT, path.join(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) { + console.warn('\'keystore\' and \'alias\' need to be specified to generate a signed archive.'); + } + } ret.arch = resolvedTarget && resolvedTarget.arch; return ret; @@ -422,11 +506,18 @@ function parseOpts(options, resolvedTarget) { module.exports.runClean = function(options) { var opts = parseOpts(options); var builder = builders[opts.buildMethod]; - return builder.prepEnv() + return builder.prepEnv(opts) .then(function() { - return builder.clean(opts.extraArgs); + return builder.clean(opts); }).then(function() { shell.rm('-rf', path.join(ROOT, 'out')); + + ['debug', 'release'].forEach(function(config) { + var propertiesFilePath = path.join(ROOT, config + SIGNING_PROPERTIES); + if(isAutoGenerated(propertiesFilePath)){ + shell.rm('-f', propertiesFilePath); + } + }); }); }; @@ -437,13 +528,13 @@ module.exports.runClean = function(options) { module.exports.run = function(options, optResolvedTarget) { var opts = parseOpts(options, optResolvedTarget); var builder = builders[opts.buildMethod]; - return builder.prepEnv() + return builder.prepEnv(opts) .then(function() { if (opts.prepEnv) { console.log('Build file successfully prepared.'); return; } - return builder.build(opts.buildType, opts.arch, opts.extraArgs) + return builder.build(opts) .then(function() { var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); console.log('Built the following apk(s):'); @@ -530,8 +621,51 @@ module.exports.findBestApkForArchitecture = function(buildResults, arch) { 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(ROOT, 'cordova', 'build')) + ' [flags]'); + console.log('Usage: ' + path.relative(process.cwd(), path.join(ROOT, 'cordova', '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'); @@ -542,5 +676,12 @@ module.exports.help = function() { 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/run.js b/bin/templates/cordova/lib/run.js index d18c02e9..7f15448c 100644 --- a/bin/templates/cordova/lib/run.js +++ b/bin/templates/cordova/lib/run.js @@ -41,7 +41,7 @@ var path = require('path'), var list = false; for (var i=2; i