mirror of
https://github.com/apache/cordova-android.git
synced 2025-02-07 23:03:11 +08:00
feat: Build app bundles (.aab files) (#764)
* (android) Added android bundle support with some corrected tests added bundle specific output * with --packageType flag to have consistency with cordova-ios * warn about missing required signing params only if at least one signing param is present * produce error on run if packageType = bundle * added comments relating to shelljs as suggested * unit test case added by @brodybits - Chris Brody * Filled in error message and unit test spec Primary author: @breautek - Norman Breau <norman@normanbreau.com> Co-authored-by: Norman Breau <norman@normanbreau.com> Co-authored-by: Chris Brody <chris@brody.consulting>
This commit is contained in:
parent
b3b8690bbd
commit
bd1697dbd2
4
bin/templates/cordova/Api.js
vendored
4
bin/templates/cordova/Api.js
vendored
@ -302,12 +302,12 @@ Api.prototype.build = function (buildOptions) {
|
|||||||
return require('./lib/build').run.call(self, buildOptions);
|
return require('./lib/build').run.call(self, buildOptions);
|
||||||
}).then(function (buildResults) {
|
}).then(function (buildResults) {
|
||||||
// Cast build result to array of build artifacts
|
// Cast build result to array of build artifacts
|
||||||
return buildResults.apkPaths.map(function (apkPath) {
|
return buildResults.paths.map(function (apkPath) {
|
||||||
return {
|
return {
|
||||||
buildType: buildResults.buildType,
|
buildType: buildResults.buildType,
|
||||||
buildMethod: buildResults.buildMethod,
|
buildMethod: buildResults.buildMethod,
|
||||||
path: apkPath,
|
path: apkPath,
|
||||||
type: 'apk'
|
type: path.extname(apkPath).replace(/\./g, '')
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
25
bin/templates/cordova/lib/PackageType.js
vendored
Normal file
25
bin/templates/cordova/lib/PackageType.js
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const PackageType = {
|
||||||
|
APK: 'apk',
|
||||||
|
BUNDLE: 'bundle'
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = PackageType;
|
53
bin/templates/cordova/lib/build.js
vendored
53
bin/templates/cordova/lib/build.js
vendored
@ -30,6 +30,7 @@ var builders = require('./builders/builders');
|
|||||||
var events = require('cordova-common').events;
|
var events = require('cordova-common').events;
|
||||||
var spawn = require('cordova-common').superspawn.spawn;
|
var spawn = require('cordova-common').superspawn.spawn;
|
||||||
var CordovaError = require('cordova-common').CordovaError;
|
var CordovaError = require('cordova-common').CordovaError;
|
||||||
|
var PackageType = require('./PackageType');
|
||||||
|
|
||||||
module.exports.parseBuildOptions = parseOpts;
|
module.exports.parseBuildOptions = parseOpts;
|
||||||
function parseOpts (options, resolvedTarget, projectRoot) {
|
function parseOpts (options, resolvedTarget, projectRoot) {
|
||||||
@ -45,7 +46,8 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
|||||||
alias: String,
|
alias: String,
|
||||||
storePassword: String,
|
storePassword: String,
|
||||||
password: String,
|
password: String,
|
||||||
keystoreType: String
|
keystoreType: String,
|
||||||
|
packageType: String
|
||||||
}, {}, options.argv, 0);
|
}, {}, options.argv, 0);
|
||||||
|
|
||||||
// Android Studio Build method is the default
|
// Android Studio Build method is the default
|
||||||
@ -68,14 +70,14 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
|||||||
|
|
||||||
if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }
|
if (options.argv.keystore) { packageArgs.keystore = path.relative(projectRoot, path.resolve(options.argv.keystore)); }
|
||||||
|
|
||||||
['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (flagName) {
|
['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].forEach(function (flagName) {
|
||||||
if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
|
if (options.argv[flagName]) { packageArgs[flagName] = options.argv[flagName]; }
|
||||||
});
|
});
|
||||||
|
|
||||||
var buildConfig = options.buildConfig;
|
var buildConfig = options.buildConfig;
|
||||||
|
|
||||||
// If some values are not specified as command line arguments - use build config to supplement them.
|
// If some values are not specified as command line arguments - use build config to supplement them.
|
||||||
// Command line arguemnts have precedence over build config.
|
// Command line arguments have precedence over build config.
|
||||||
if (buildConfig) {
|
if (buildConfig) {
|
||||||
if (!fs.existsSync(buildConfig)) {
|
if (!fs.existsSync(buildConfig)) {
|
||||||
throw new Error('Specified build config file does not exist: ' + buildConfig);
|
throw new Error('Specified build config file does not exist: ' + buildConfig);
|
||||||
@ -93,7 +95,7 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
|||||||
events.emit('log', 'Reading the keystore from: ' + packageArgs.keystore);
|
events.emit('log', 'Reading the keystore from: ' + packageArgs.keystore);
|
||||||
}
|
}
|
||||||
|
|
||||||
['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (key) {
|
['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].forEach(function (key) {
|
||||||
packageArgs[key] = packageArgs[key] || androidInfo[key];
|
packageArgs[key] = packageArgs[key] || androidInfo[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -105,11 +107,38 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ret.packageInfo) {
|
if (!ret.packageInfo) {
|
||||||
if (Object.keys(packageArgs).length > 0) {
|
// The following loop is to decide whether to print a warning about generating a signed archive
|
||||||
|
// We only want to produce a warning if they are using a config property that is related to signing, but
|
||||||
|
// missing the required properties for signing. We don't want to produce a warning if they are simply
|
||||||
|
// using a build property that isn't related to signing, such as --packageType
|
||||||
|
let shouldWarn = false;
|
||||||
|
const signingKeys = ['keystore', 'alias', 'storePassword', 'password', 'keystoreType'];
|
||||||
|
|
||||||
|
for (let key in packageArgs) {
|
||||||
|
if (!shouldWarn && signingKeys.indexOf(key) > -1) {
|
||||||
|
// If we enter this condition, we have a key used for signing a build,
|
||||||
|
// but we are missing some required signing properties
|
||||||
|
shouldWarn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldWarn) {
|
||||||
events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.');
|
events.emit('warn', '\'keystore\' and \'alias\' need to be specified to generate a signed archive.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (packageArgs.packageType) {
|
||||||
|
const VALID_PACKAGE_TYPES = [PackageType.APK, PackageType.BUNDLE];
|
||||||
|
if (VALID_PACKAGE_TYPES.indexOf(packageArgs.packageType) === -1) {
|
||||||
|
events.emit('warn', '"' + packageArgs.packageType + '" is an invalid packageType. Valid values are: ' + VALID_PACKAGE_TYPES.join(', ') + '\nDefaulting packageType to ' + PackageType.APK);
|
||||||
|
ret.packageType = PackageType.APK;
|
||||||
|
} else {
|
||||||
|
ret.packageType = packageArgs.packageType;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ret.packageType = PackageType.APK;
|
||||||
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,10 +177,17 @@ module.exports.run = function (options, optResolvedTarget) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return builder.build(opts).then(function () {
|
return builder.build(opts).then(function () {
|
||||||
var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
|
var paths;
|
||||||
events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t'));
|
if (opts.packageType === PackageType.BUNDLE) {
|
||||||
|
paths = builder.findOutputBundles(opts.buildType);
|
||||||
|
events.emit('log', 'Built the following bundle(s): \n\t' + paths.join('\n\t'));
|
||||||
|
} else {
|
||||||
|
paths = builder.findOutputApks(opts.buildType, opts.arch);
|
||||||
|
events.emit('log', 'Built the following apk(s): \n\t' + paths.join('\n\t'));
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apkPaths: apkPaths,
|
paths: paths,
|
||||||
buildType: opts.buildType
|
buildType: opts.buildType
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -273,6 +309,7 @@ module.exports.help = function () {
|
|||||||
console.log(' \'--maxSdkVersion=#\': Override maxSdkVersion for this build. (Not Recommended)');
|
console.log(' \'--maxSdkVersion=#\': Override maxSdkVersion for this build. (Not Recommended)');
|
||||||
console.log(' \'--targetSdkVersion=#\': Override targetSdkVersion for this build.');
|
console.log(' \'--targetSdkVersion=#\': Override targetSdkVersion for this build.');
|
||||||
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(' \'--packageType=<apk|bundle>\': Builds an APK or a bundle');
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('Signed APK flags (overwrites debug/release-signing.proprties) :');
|
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(' \'--keystore=<path to keystore>\': Key store used to build a signed archive. (Required)');
|
||||||
|
@ -27,6 +27,7 @@ var spawn = require('cordova-common').superspawn.spawn;
|
|||||||
var events = require('cordova-common').events;
|
var events = require('cordova-common').events;
|
||||||
var CordovaError = require('cordova-common').CordovaError;
|
var CordovaError = require('cordova-common').CordovaError;
|
||||||
var check_reqs = require('../check_reqs');
|
var check_reqs = require('../check_reqs');
|
||||||
|
var PackageType = require('../PackageType');
|
||||||
const compareFunc = require('compare-func');
|
const compareFunc = require('compare-func');
|
||||||
|
|
||||||
const MARKER = 'YOUR CHANGES WILL BE ERASED!';
|
const MARKER = 'YOUR CHANGES WILL BE ERASED!';
|
||||||
@ -38,24 +39,38 @@ const TEMPLATE =
|
|||||||
class ProjectBuilder {
|
class ProjectBuilder {
|
||||||
constructor (rootDirectory) {
|
constructor (rootDirectory) {
|
||||||
this.root = rootDirectory || path.resolve(__dirname, '../../..');
|
this.root = rootDirectory || path.resolve(__dirname, '../../..');
|
||||||
this.binDir = path.join(this.root, 'app', 'build', 'outputs', 'apk');
|
this.apkDir = path.join(this.root, 'app', 'build', 'outputs', 'apk');
|
||||||
|
this.aabDir = path.join(this.root, 'app', 'build', 'outputs', 'bundle');
|
||||||
}
|
}
|
||||||
|
|
||||||
getArgs (cmd, opts) {
|
getArgs (cmd, opts) {
|
||||||
if (cmd === 'release') {
|
let args;
|
||||||
cmd = 'cdvBuildRelease';
|
if (opts.packageType === PackageType.BUNDLE) {
|
||||||
} else if (cmd === 'debug') {
|
let buildCmd;
|
||||||
cmd = 'cdvBuildDebug';
|
if (cmd === 'release') {
|
||||||
|
buildCmd = ':app:bundleRelease';
|
||||||
|
} else if (cmd === 'debug') {
|
||||||
|
buildCmd = ':app:bundleDebug';
|
||||||
|
}
|
||||||
|
|
||||||
|
args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
|
||||||
|
} else {
|
||||||
|
let buildCmd;
|
||||||
|
if (cmd === 'release') {
|
||||||
|
buildCmd = 'cdvBuildRelease';
|
||||||
|
} else if (cmd === 'debug') {
|
||||||
|
buildCmd = 'cdvBuildDebug';
|
||||||
|
}
|
||||||
|
|
||||||
|
args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
|
||||||
|
|
||||||
|
if (opts.arch) {
|
||||||
|
args.push('-PcdvBuildArch=' + opts.arch);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push.apply(args, opts.extraArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = [cmd, '-b', path.join(this.root, 'build.gradle')];
|
|
||||||
|
|
||||||
if (opts.arch) {
|
|
||||||
args.push('-PcdvBuildArch=' + opts.arch);
|
|
||||||
}
|
|
||||||
|
|
||||||
args.push.apply(args, opts.extraArgs);
|
|
||||||
|
|
||||||
return args;
|
return args;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,7 +303,11 @@ class ProjectBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findOutputApks (build_type, arch) {
|
findOutputApks (build_type, arch) {
|
||||||
return findOutputApksHelper(this.binDir, build_type, arch).sort(apkSorter);
|
return findOutputApksHelper(this.apkDir, build_type, arch).sort(apkSorter);
|
||||||
|
}
|
||||||
|
|
||||||
|
findOutputBundles (build_type) {
|
||||||
|
return findOutputBundlesHelper(this.aabDir, build_type);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchBuildResults (build_type, arch) {
|
fetchBuildResults (build_type, arch) {
|
||||||
@ -359,6 +378,36 @@ function findOutputApksHelper (dir, build_type, arch) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This method was a copy of findOutputApksHelper and modified to look for bundles
|
||||||
|
// While replacing shell with fs-extra, it might be a good idea to see if we can
|
||||||
|
// generalise these findOutput methods.
|
||||||
|
function findOutputBundlesHelper (dir, build_type) {
|
||||||
|
// This is an unused variable that was copied from findOutputApksHelper
|
||||||
|
// we are pretty sure it was meant to reset shell.config.silent back to
|
||||||
|
// the original value. However shell is planned to be replaced,
|
||||||
|
// it was left as is to avoid unintended consequences.
|
||||||
|
const shellSilent = shell.config.silent;
|
||||||
|
shell.config.silent = true;
|
||||||
|
|
||||||
|
// list directory recursively
|
||||||
|
const ret = shell.ls('-R', dir).map(function (file) {
|
||||||
|
return path.join(dir, file); // ls does not include base directory
|
||||||
|
}).filter(function (file) {
|
||||||
|
return file.match(/\.aab?$/i); // find all bundles
|
||||||
|
}).filter(function (candidate) {
|
||||||
|
// Need to choose between release and debug bundle.
|
||||||
|
if (build_type === 'debug') {
|
||||||
|
return /debug/.exec(candidate);
|
||||||
|
}
|
||||||
|
if (build_type === 'release') {
|
||||||
|
return /release/.exec(candidate);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
function isAutoGenerated (file) {
|
function isAutoGenerated (file) {
|
||||||
return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0;
|
return fs.existsSync(file) && fs.readFileSync(file, 'utf8').indexOf(MARKER) > 0;
|
||||||
}
|
}
|
||||||
|
9
bin/templates/cordova/lib/run.js
vendored
9
bin/templates/cordova/lib/run.js
vendored
@ -23,6 +23,7 @@ var path = require('path');
|
|||||||
var emulator = require('./emulator');
|
var emulator = require('./emulator');
|
||||||
var device = require('./device');
|
var device = require('./device');
|
||||||
var Q = require('q');
|
var Q = require('q');
|
||||||
|
var PackageType = require('./PackageType');
|
||||||
var events = require('cordova-common').events;
|
var events = require('cordova-common').events;
|
||||||
|
|
||||||
function getInstallTarget (runOptions) {
|
function getInstallTarget (runOptions) {
|
||||||
@ -104,6 +105,14 @@ module.exports.run = function (runOptions) {
|
|||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const builder = require('./builders/builders').getBuilder();
|
const builder = require('./builders/builders').getBuilder();
|
||||||
const buildOptions = require('./build').parseBuildOptions(runOptions, null, self.root);
|
const buildOptions = require('./build').parseBuildOptions(runOptions, null, self.root);
|
||||||
|
|
||||||
|
// Android app bundles cannot be deployed directly to the device
|
||||||
|
if (buildOptions.packageType === PackageType.BUNDLE) {
|
||||||
|
const packageTypeErrorMessage = 'Package type "bundle" is not supported during cordova run.';
|
||||||
|
events.emit('error', packageTypeErrorMessage);
|
||||||
|
throw packageTypeErrorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
resolve(builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
|
resolve(builder.fetchBuildResults(buildOptions.buildType, buildOptions.arch));
|
||||||
}).then(function (buildResults) {
|
}).then(function (buildResults) {
|
||||||
if (resolvedTarget && resolvedTarget.isEmulator) {
|
if (resolvedTarget && resolvedTarget.isEmulator) {
|
||||||
|
@ -66,6 +66,34 @@ describe('ProjectBuilder', () => {
|
|||||||
expect(args[0]).toBe('cdvBuildDebug');
|
expect(args[0]).toBe('cdvBuildDebug');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set apk release', () => {
|
||||||
|
const args = builder.getArgs('release', {
|
||||||
|
packageType: 'apk'
|
||||||
|
});
|
||||||
|
expect(args[0]).withContext(args).toBe('cdvBuildRelease');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set apk debug', () => {
|
||||||
|
const args = builder.getArgs('debug', {
|
||||||
|
packageType: 'apk'
|
||||||
|
});
|
||||||
|
expect(args[0]).withContext(args).toBe('cdvBuildDebug');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set bundle release', () => {
|
||||||
|
const args = builder.getArgs('release', {
|
||||||
|
packageType: 'bundle'
|
||||||
|
});
|
||||||
|
expect(args[0]).withContext(args).toBe(':app:bundleRelease');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set bundle debug', () => {
|
||||||
|
const args = builder.getArgs('debug', {
|
||||||
|
packageType: 'bundle'
|
||||||
|
});
|
||||||
|
expect(args[0]).withContext(args).toBe(':app:bundleDebug');
|
||||||
|
});
|
||||||
|
|
||||||
it('should add architecture if it is passed', () => {
|
it('should add architecture if it is passed', () => {
|
||||||
const arch = 'unittest';
|
const arch = 'unittest';
|
||||||
const args = builder.getArgs('debug', { arch });
|
const args = builder.getArgs('debug', { arch });
|
||||||
|
@ -196,6 +196,18 @@ describe('run', () => {
|
|||||||
expect(emulatorSpyObj.install).toHaveBeenCalledWith(emulatorTarget, { apkPaths: [], buildType: 'debug' });
|
expect(emulatorSpyObj.install).toHaveBeenCalledWith(emulatorTarget, { apkPaths: [], buildType: 'debug' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should fail with the error message if --packageType=bundle setting is used', () => {
|
||||||
|
const deviceList = ['testDevice1', 'testDevice2'];
|
||||||
|
getInstallTargetSpy.and.returnValue(null);
|
||||||
|
|
||||||
|
deviceSpyObj.list.and.returnValue(Promise.resolve(deviceList));
|
||||||
|
|
||||||
|
return run.run({ argv: ['--packageType=bundle'] }).then(
|
||||||
|
() => fail('Expected error to be thrown'),
|
||||||
|
err => expect(err).toContain('Package type "bundle" is not supported during cordova run.')
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('help', () => {
|
describe('help', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user