mirror of
https://github.com/apache/cordova-android.git
synced 2025-01-31 17:32:51 +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);
|
||||
}).then(function (buildResults) {
|
||||
// Cast build result to array of build artifacts
|
||||
return buildResults.apkPaths.map(function (apkPath) {
|
||||
return buildResults.paths.map(function (apkPath) {
|
||||
return {
|
||||
buildType: buildResults.buildType,
|
||||
buildMethod: buildResults.buildMethod,
|
||||
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 spawn = require('cordova-common').superspawn.spawn;
|
||||
var CordovaError = require('cordova-common').CordovaError;
|
||||
var PackageType = require('./PackageType');
|
||||
|
||||
module.exports.parseBuildOptions = parseOpts;
|
||||
function parseOpts (options, resolvedTarget, projectRoot) {
|
||||
@ -45,7 +46,8 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
||||
alias: String,
|
||||
storePassword: String,
|
||||
password: String,
|
||||
keystoreType: String
|
||||
keystoreType: String,
|
||||
packageType: String
|
||||
}, {}, options.argv, 0);
|
||||
|
||||
// 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)); }
|
||||
|
||||
['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (flagName) {
|
||||
['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].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.
|
||||
// Command line arguments have precedence over build config.
|
||||
if (buildConfig) {
|
||||
if (!fs.existsSync(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);
|
||||
}
|
||||
|
||||
['alias', 'storePassword', 'password', 'keystoreType'].forEach(function (key) {
|
||||
['alias', 'storePassword', 'password', 'keystoreType', 'packageType'].forEach(function (key) {
|
||||
packageArgs[key] = packageArgs[key] || androidInfo[key];
|
||||
});
|
||||
}
|
||||
@ -105,11 +107,38 @@ function parseOpts (options, resolvedTarget, projectRoot) {
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -148,10 +177,17 @@ module.exports.run = function (options, optResolvedTarget) {
|
||||
return;
|
||||
}
|
||||
return builder.build(opts).then(function () {
|
||||
var apkPaths = builder.findOutputApks(opts.buildType, opts.arch);
|
||||
events.emit('log', 'Built the following apk(s): \n\t' + apkPaths.join('\n\t'));
|
||||
var paths;
|
||||
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 {
|
||||
apkPaths: apkPaths,
|
||||
paths: paths,
|
||||
buildType: opts.buildType
|
||||
};
|
||||
});
|
||||
@ -273,6 +309,7 @@ module.exports.help = function () {
|
||||
console.log(' \'--maxSdkVersion=#\': Override maxSdkVersion for this build. (Not Recommended)');
|
||||
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(' \'--packageType=<apk|bundle>\': Builds an APK or a bundle');
|
||||
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)');
|
||||
|
@ -27,6 +27,7 @@ var spawn = require('cordova-common').superspawn.spawn;
|
||||
var events = require('cordova-common').events;
|
||||
var CordovaError = require('cordova-common').CordovaError;
|
||||
var check_reqs = require('../check_reqs');
|
||||
var PackageType = require('../PackageType');
|
||||
const compareFunc = require('compare-func');
|
||||
|
||||
const MARKER = 'YOUR CHANGES WILL BE ERASED!';
|
||||
@ -38,23 +39,37 @@ const TEMPLATE =
|
||||
class ProjectBuilder {
|
||||
constructor (rootDirectory) {
|
||||
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) {
|
||||
let args;
|
||||
if (opts.packageType === PackageType.BUNDLE) {
|
||||
let buildCmd;
|
||||
if (cmd === 'release') {
|
||||
cmd = 'cdvBuildRelease';
|
||||
buildCmd = ':app:bundleRelease';
|
||||
} else if (cmd === 'debug') {
|
||||
cmd = 'cdvBuildDebug';
|
||||
buildCmd = ':app:bundleDebug';
|
||||
}
|
||||
|
||||
let args = [cmd, '-b', path.join(this.root, 'build.gradle')];
|
||||
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);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
@ -288,7 +303,11 @@ class ProjectBuilder {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -359,6 +378,36 @@ function findOutputApksHelper (dir, build_type, arch) {
|
||||
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) {
|
||||
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 device = require('./device');
|
||||
var Q = require('q');
|
||||
var PackageType = require('./PackageType');
|
||||
var events = require('cordova-common').events;
|
||||
|
||||
function getInstallTarget (runOptions) {
|
||||
@ -104,6 +105,14 @@ module.exports.run = function (runOptions) {
|
||||
return new Promise((resolve) => {
|
||||
const builder = require('./builders/builders').getBuilder();
|
||||
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));
|
||||
}).then(function (buildResults) {
|
||||
if (resolvedTarget && resolvedTarget.isEmulator) {
|
||||
|
@ -66,6 +66,34 @@ describe('ProjectBuilder', () => {
|
||||
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', () => {
|
||||
const arch = 'unittest';
|
||||
const args = builder.getArgs('debug', { arch });
|
||||
|
@ -196,6 +196,18 @@ describe('run', () => {
|
||||
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', () => {
|
||||
|
Loading…
Reference in New Issue
Block a user