CB-8484 Add signing flags to build and run scripts

Parameters for creating signed archives can be specified using command line or build.json file as part of the --buildConfig argument.
close #164
This commit is contained in:
Nikhil Khandelwal 2015-03-10 18:13:13 -07:00 committed by Andrew Grieve
parent 51adf81918
commit ad1c3d2438
2 changed files with 168 additions and 27 deletions

View File

@ -31,9 +31,12 @@ var shell = require('shelljs'),
var check_reqs = require('./check_reqs'); var check_reqs = require('./check_reqs');
var exec = require('./exec'); 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' + '# 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) { function findApks(directory) {
var ret = []; var ret = [];
@ -56,6 +59,14 @@ function sortFilesByDate(files) {
}).map(function(p) { return p.p; }); }).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) { function findOutputApksHelper(dir, build_type, arch) {
var ret = findApks(dir).filter(function(candidate) { var ret = findApks(dir).filter(function(candidate) {
// Need to choose between release and debug .apk. // Need to choose between release and debug .apk.
@ -121,16 +132,19 @@ function readProjectProperties() {
var builders = { var builders = {
ant: { ant: {
getArgs: function(cmd) { getArgs: function(cmd, opts) {
var args = [cmd, '-f', path.join(ROOT, 'build.xml')]; var args = [cmd, '-f', path.join(ROOT, 'build.xml')];
// custom_rules.xml is required for incremental builds. // custom_rules.xml is required for incremental builds.
if (hasCustomRules()) { if (hasCustomRules()) {
args.push('-Dout.dir=ant-build', '-Dgen.absolute.dir=ant-gen'); 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; return args;
}, },
prepEnv: function() { prepEnv: function(opts) {
return check_reqs.check_ant() return check_reqs.check_ant()
.then(function() { .then(function() {
// Copy in build.xml on each build so that: // Copy in build.xml on each build so that:
@ -142,7 +156,7 @@ var builders = {
var newData = buildTemplate.replace('PROJECT_NAME', extractProjectNameFromManifest(ROOT)); var newData = buildTemplate.replace('PROJECT_NAME', extractProjectNameFromManifest(ROOT));
fs.writeFileSync(path.join(projectPath, 'build.xml'), newData); fs.writeFileSync(path.join(projectPath, 'build.xml'), newData);
if (!fs.existsSync(path.join(projectPath, 'local.properties'))) { 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); writeBuildXml(ROOT);
@ -154,6 +168,14 @@ var builders = {
if (propertiesObj.systemLibs.length > 0) { 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.'); 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. * Builds the project with ant.
* Returns a promise. * Returns a promise.
*/ */
build: function(build_type) { build: function(opts) {
// Without our custom_rules.xml, we need to clean before building. // Without our custom_rules.xml, we need to clean before building.
var ret = Q(); var ret = Q();
if (!hasCustomRules()) { if (!hasCustomRules()) {
// clean will call check_ant() for us. // 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() return check_reqs.check_ant()
.then(function() { .then(function() {
console.log('Executing: ant ' + args.join(' '));
return spawn('ant', args); return spawn('ant', args);
}); });
}, },
clean: function() { clean: function(opts) {
var args = this.getArgs('clean'); var args = this.getArgs('clean', opts);
return check_reqs.check_ant() return check_reqs.check_ant()
.then(function() { .then(function() {
return spawn('ant', args); return spawn('ant', args);
@ -190,20 +213,20 @@ var builders = {
} }
}, },
gradle: { gradle: {
getArgs: function(cmd, arch, extraArgs) { getArgs: function(cmd, opts) {
if (cmd == 'release') { if (cmd == 'release') {
cmd = 'cdvBuildRelease'; cmd = 'cdvBuildRelease';
} else if (cmd == 'debug') { } else if (cmd == 'debug') {
cmd = 'cdvBuildDebug'; cmd = 'cdvBuildDebug';
} }
var args = [cmd, '-b', path.join(ROOT, 'build.gradle')]; var args = [cmd, '-b', path.join(ROOT, 'build.gradle')];
if (arch) { if (opts.arch) {
args.push('-PcdvBuildArch=' + arch); args.push('-PcdvBuildArch=' + opts.arch);
} }
// 10 seconds -> 6 seconds // 10 seconds -> 6 seconds
args.push('-Dorg.gradle.daemon=true'); 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): // Shaves another 100ms, but produces a "try at own risk" warning. Not worth it (yet):
// args.push('-Dorg.gradle.parallel=true'); // args.push('-Dorg.gradle.parallel=true');
return args; return args;
@ -268,7 +291,7 @@ var builders = {
fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle); fs.writeFileSync(path.join(projectPath, 'build.gradle'), buildGradle);
}, },
prepEnv: function() { prepEnv: function(opts) {
var self = this; var self = this;
return check_reqs.check_gradle() return check_reqs.check_gradle()
.then(function() { .then(function() {
@ -297,6 +320,14 @@ var builders = {
var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip'; var distributionUrl = 'distributionUrl=http\\://services.gradle.org/distributions/gradle-2.2.1-all.zip';
var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties'); var gradleWrapperPropertiesPath = path.join(projectPath, 'gradle', 'wrapper', 'gradle-wrapper.properties');
shell.sed('-i', distributionUrlRegex, distributionUrl, gradleWrapperPropertiesPath); 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. * Builds the project with gradle.
* Returns a promise. * Returns a promise.
*/ */
build: function(build_type, arch, extraArgs) { build: function(opts) {
var wrapper = path.join(ROOT, 'gradlew'); 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() { return Q().then(function() {
console.log('Running: ' + wrapper + ' ' + args.join(' ')); console.log('Running: ' + wrapper + ' ' + args.join(' '));
return spawn(wrapper, args); return spawn(wrapper, args);
}); });
}, },
clean: function(extraArgs) { clean: function(opts) {
var builder = this; var builder = this;
var wrapper = path.join(ROOT, 'gradlew'); var wrapper = path.join(ROOT, 'gradlew');
var args = builder.getArgs('clean', null, extraArgs); var args = builder.getArgs('clean', opts);
return Q().then(function() { return Q().then(function() {
console.log('Running: ' + wrapper + ' ' + args.join(' ')); console.log('Running: ' + wrapper + ' ' + args.join(' '));
return spawn(wrapper, args); 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) { function parseOpts(options, resolvedTarget) {
// Backwards-compatibility: Allow a single string argument // Backwards-compatibility: Allow a single string argument
if (typeof options == 'string') options = [options]; if (typeof options == 'string') options = [options];
@ -360,9 +395,16 @@ function parseOpts(options, resolvedTarget) {
var multiValueArgs = { var multiValueArgs = {
'versionCode': true, 'versionCode': true,
'minSdkVersion': 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 // Iterate through command line options
for (var i=0; options && (i < options.length); ++i) { for (var i=0; options && (i < options.length); ++i) {
if (/^--/.exec(options[i])) { if (/^--/.exec(options[i])) {
@ -402,6 +444,18 @@ function parseOpts(options, resolvedTarget) {
case 'gradleArg': case 'gradleArg':
ret.extraArgs.push(flagValue); ret.extraArgs.push(flagValue);
break; 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 : default :
console.warn('Build option --\'' + flagName + '\' not recognized (ignoring).'); 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; ret.arch = resolvedTarget && resolvedTarget.arch;
return ret; return ret;
@ -422,11 +506,18 @@ function parseOpts(options, resolvedTarget) {
module.exports.runClean = function(options) { module.exports.runClean = function(options) {
var opts = parseOpts(options); var opts = parseOpts(options);
var builder = builders[opts.buildMethod]; var builder = builders[opts.buildMethod];
return builder.prepEnv() return builder.prepEnv(opts)
.then(function() { .then(function() {
return builder.clean(opts.extraArgs); return builder.clean(opts);
}).then(function() { }).then(function() {
shell.rm('-rf', path.join(ROOT, 'out')); 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) { module.exports.run = function(options, optResolvedTarget) {
var opts = parseOpts(options, optResolvedTarget); var opts = parseOpts(options, optResolvedTarget);
var builder = builders[opts.buildMethod]; var builder = builders[opts.buildMethod];
return builder.prepEnv() return builder.prepEnv(opts)
.then(function() { .then(function() {
if (opts.prepEnv) { if (opts.prepEnv) {
console.log('Build file successfully prepared.'); console.log('Build file successfully prepared.');
return; return;
} }
return builder.build(opts.buildType, opts.arch, opts.extraArgs) return builder.build(opts)
.then(function() { .then(function() {
var apkPaths = builder.findOutputApks(opts.buildType, opts.arch); var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
console.log('Built the following apk(s):'); 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); 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() { 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('Flags:');
console.log(' \'--debug\': will build project in debug mode (default)'); console.log(' \'--debug\': will build project in debug mode (default)');
console.log(' \'--release\': will build project for release'); 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(' \'--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(' \'--minSdkVersion=#\': Override minSdkVersion for this build. Useful for uploading multiple APKs. Requires --gradle.');
console.log(' \'--gradleArg=<gradle command line arg>\': Extra args to pass to the gradle command. Use one flag per arg. Ex. --gradleArg=-PcdvBuildMultipleApks=true'); console.log(' \'--gradleArg=<gradle command line arg>\': 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=<path to 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); process.exit(0);
}; };

View File

@ -41,7 +41,7 @@ var path = require('path'),
var list = false; var list = false;
for (var i=2; i<args.length; i++) { for (var i=2; i<args.length; i++) {
if (/^--(debug|release|ant|gradle|nobuild|versionCode=|minSdkVersion=|gradleArg=)/.exec(args[i])) { if (build.isBuildFlag(args[i])) {
buildFlags.push(args[i]); buildFlags.push(args[i]);
} else if (args[i] == '--device') { } else if (args[i] == '--device') {
install_target = '--device'; install_target = '--device';