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

View File

@ -41,7 +41,7 @@ var path = require('path'),
var list = false;
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]);
} else if (args[i] == '--device') {
install_target = '--device';