You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by br...@apache.org on 2019/08/08 16:53:19 UTC

[cordova-android] branch master updated: feat: Build app bundles (.aab files) (#764)

This is an automated email from the ASF dual-hosted git repository.

brodybits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cordova-android.git


The following commit(s) were added to refs/heads/master by this push:
     new bd1697d  feat: Build app bundles (.aab files) (#764)
bd1697d is described below

commit bd1697dbd27411d8b12d9ad0902babaaae327efa
Author: Norman Breau <no...@normanbreau.com>
AuthorDate: Thu Aug 8 13:53:10 2019 -0300

    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 <no...@normanbreau.com>
    
    Co-authored-by: Norman Breau <no...@normanbreau.com>
    Co-authored-by: Chris Brody <ch...@brody.consulting>
---
 bin/templates/cordova/Api.js                       |  4 +-
 bin/templates/cordova/lib/PackageType.js           | 25 ++++++++
 bin/templates/cordova/lib/build.js                 | 53 +++++++++++++---
 .../cordova/lib/builders/ProjectBuilder.js         | 73 ++++++++++++++++++----
 bin/templates/cordova/lib/run.js                   |  9 +++
 spec/unit/builders/ProjectBuilder.spec.js          | 28 +++++++++
 spec/unit/run.spec.js                              | 12 ++++
 7 files changed, 182 insertions(+), 22 deletions(-)

diff --git a/bin/templates/cordova/Api.js b/bin/templates/cordova/Api.js
index 30b1fbe..dd4a209 100644
--- a/bin/templates/cordova/Api.js
+++ b/bin/templates/cordova/Api.js
@@ -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, '')
             };
         });
     });
diff --git a/bin/templates/cordova/lib/PackageType.js b/bin/templates/cordova/lib/PackageType.js
new file mode 100644
index 0000000..fd129f1
--- /dev/null
+++ b/bin/templates/cordova/lib/PackageType.js
@@ -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;
diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js
index dec218e..0695539 100644
--- a/bin/templates/cordova/lib/build.js
+++ b/bin/templates/cordova/lib/build.js
@@ -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)');
diff --git a/bin/templates/cordova/lib/builders/ProjectBuilder.js b/bin/templates/cordova/lib/builders/ProjectBuilder.js
index f2a0066..35ce906 100644
--- a/bin/templates/cordova/lib/builders/ProjectBuilder.js
+++ b/bin/templates/cordova/lib/builders/ProjectBuilder.js
@@ -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) {
-        if (cmd === 'release') {
-            cmd = 'cdvBuildRelease';
-        } else if (cmd === 'debug') {
-            cmd = 'cdvBuildDebug';
-        }
+        let args;
+        if (opts.packageType === PackageType.BUNDLE) {
+            let buildCmd;
+            if (cmd === 'release') {
+                buildCmd = ':app:bundleRelease';
+            } else if (cmd === 'debug') {
+                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';
+            }
 
-        if (opts.arch) {
-            args.push('-PcdvBuildArch=' + opts.arch);
-        }
+            args = [buildCmd, '-b', path.join(this.root, 'build.gradle')];
 
-        args.push.apply(args, opts.extraArgs);
+            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;
 }
diff --git a/bin/templates/cordova/lib/run.js b/bin/templates/cordova/lib/run.js
index 221467b..f1c70f2 100644
--- a/bin/templates/cordova/lib/run.js
+++ b/bin/templates/cordova/lib/run.js
@@ -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) {
diff --git a/spec/unit/builders/ProjectBuilder.spec.js b/spec/unit/builders/ProjectBuilder.spec.js
index 6dfd05f..44769b5 100644
--- a/spec/unit/builders/ProjectBuilder.spec.js
+++ b/spec/unit/builders/ProjectBuilder.spec.js
@@ -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 });
diff --git a/spec/unit/run.spec.js b/spec/unit/run.spec.js
index 61a843c..76081eb 100644
--- a/spec/unit/run.spec.js
+++ b/spec/unit/run.spec.js
@@ -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', () => {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cordova.apache.org
For additional commands, e-mail: commits-help@cordova.apache.org