You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by ra...@apache.org on 2020/01/06 22:15:33 UTC

[cordova-android] branch master updated: chore: replace superspawn & child_process with execa (#862)

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

raphinesse 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 fd57909  chore: replace superspawn & child_process with execa (#862)
fd57909 is described below

commit fd57909730fd5a105ac93055a0dd31776079fc5b
Author: Raphael von der Grün <ra...@gmail.com>
AuthorDate: Mon Jan 6 23:15:22 2020 +0100

    chore: replace superspawn & child_process with execa (#862)
    
    * chore: added execa dependency
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in android_sdk
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in build
    
    * chore: execa - drop superspawn in check_reqs
    
    Plus: Remove useless trimming of execa output
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in emulator
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in device
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in run_java_unit_tests
    
    * chore: execa - drop superspawn in ProjectBuilder
    
    Co-authored-by: Raphael von der Grün <ra...@gmail.com>
    
    * chore: execa - drop superspawn in adb
    
    * chore: execa - drop superspawn in plugin.spec
    
    * chore: execa - replace child_process in log
    
    * chore: execa - replace child_process in check_reqs
    
    * chore: execa - replace child_process in emulator
    
    Co-authored-by: エリス <er...@users.noreply.github.com>
---
 bin/templates/cordova/lib/Adb.js                   | 16 +++---
 bin/templates/cordova/lib/android_sdk.js           |  6 +--
 bin/templates/cordova/lib/build.js                 |  6 +--
 .../cordova/lib/builders/ProjectBuilder.js         | 15 +++---
 bin/templates/cordova/lib/check_reqs.js            | 17 +++---
 bin/templates/cordova/lib/device.js                |  4 +-
 bin/templates/cordova/lib/emulator.js              | 51 +++++++++---------
 bin/templates/cordova/lib/log.js                   | 15 ++----
 package.json                                       |  1 +
 spec/e2e/plugin.spec.js                            |  5 +-
 spec/unit/Adb.spec.js                              | 34 ++++++------
 spec/unit/android_sdk.spec.js                      | 12 +++--
 spec/unit/builders/ProjectBuilder.spec.js          | 37 +++++++------
 spec/unit/device.spec.js                           | 12 ++---
 spec/unit/emulator.spec.js                         | 60 +++++++++++-----------
 test/run_java_unit_tests.js                        |  4 +-
 16 files changed, 138 insertions(+), 157 deletions(-)

diff --git a/bin/templates/cordova/lib/Adb.js b/bin/templates/cordova/lib/Adb.js
index b6ad8f1..4cebcba 100644
--- a/bin/templates/cordova/lib/Adb.js
+++ b/bin/templates/cordova/lib/Adb.js
@@ -19,8 +19,8 @@
 
 var Q = require('q');
 var os = require('os');
+var execa = require('execa');
 var events = require('cordova-common').events;
-var spawn = require('cordova-common').superspawn.spawn;
 var CordovaError = require('cordova-common').CordovaError;
 
 var Adb = {};
@@ -44,7 +44,7 @@ function isEmulator (line) {
  *   devices/emulators
  */
 Adb.devices = function (opts) {
-    return spawn('adb', ['devices'], { cwd: os.tmpdir() }).then(function (output) {
+    return execa('adb', ['devices'], { cwd: os.tmpdir() }).then(({ stdout: output }) => {
         return output.split('\n').filter(function (line) {
             // Filter out either real devices or emulators, depending on options
             return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line);
@@ -58,7 +58,7 @@ Adb.install = function (target, packagePath, opts) {
     events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
     var args = ['-s', target, 'install'];
     if (opts && opts.replace) args.push('-r');
-    return spawn('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(function (output) {
+    return execa('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(({ stdout: output }) => {
         // 'adb install' seems to always returns no error, even if installation fails
         // so we catching output to detect installation failure
         if (output.match(/Failure/)) {
@@ -77,24 +77,24 @@ Adb.install = function (target, packagePath, opts) {
 
 Adb.uninstall = function (target, packageId) {
     events.emit('verbose', 'Uninstalling package ' + packageId + ' from target ' + target + '...');
-    return spawn('adb', ['-s', target, 'uninstall', packageId], { cwd: os.tmpdir() });
+    return execa('adb', ['-s', target, 'uninstall', packageId], { cwd: os.tmpdir() }).then(({ stdout }) => stdout);
 };
 
 Adb.shell = function (target, shellCommand) {
     events.emit('verbose', 'Running adb shell command "' + shellCommand + '" on target ' + target + '...');
     var args = ['-s', target, 'shell'];
     shellCommand = shellCommand.split(/\s+/);
-    return spawn('adb', args.concat(shellCommand), { cwd: os.tmpdir() }).catch(function (output) {
+    return execa('adb', args.concat(shellCommand), { cwd: os.tmpdir() }).catch((error) => {
         return Q.reject(new CordovaError('Failed to execute shell command "' +
-            shellCommand + '"" on device: ' + output));
+            shellCommand + '"" on device: ' + error));
     });
 };
 
 Adb.start = function (target, activityName) {
     events.emit('verbose', 'Starting application "' + activityName + '" on target ' + target + '...');
-    return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch(function (output) {
+    return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch((error) => {
         return Q.reject(new CordovaError('Failed to start application "' +
-            activityName + '"" on device: ' + output));
+            activityName + '"" on device: ' + error));
     });
 };
 
diff --git a/bin/templates/cordova/lib/android_sdk.js b/bin/templates/cordova/lib/android_sdk.js
index 1c0ab20..9074847 100755
--- a/bin/templates/cordova/lib/android_sdk.js
+++ b/bin/templates/cordova/lib/android_sdk.js
@@ -17,7 +17,7 @@
        under the License.
 */
 
-var superspawn = require('cordova-common').superspawn;
+const execa = require('execa');
 
 var suffix_number_regex = /(\d+)$/;
 // Used for sorting Android targets, example strings to sort:
@@ -77,11 +77,11 @@ function parse_targets (output) {
 }
 
 module.exports.list_targets_with_android = function () {
-    return superspawn.spawn('android', ['list', 'target']).then(parse_targets);
+    return execa('android', ['list', 'target']).then(result => parse_targets(result.stdout));
 };
 
 module.exports.list_targets_with_avdmanager = function () {
-    return superspawn.spawn('avdmanager', ['list', 'target']).then(parse_targets);
+    return execa('avdmanager', ['list', 'target']).then(result => parse_targets(result.stdout));
 };
 
 module.exports.list_targets = function () {
diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js
index 0695539..c339901 100644
--- a/bin/templates/cordova/lib/build.js
+++ b/bin/templates/cordova/lib/build.js
@@ -28,7 +28,7 @@ var Adb = require('./Adb');
 
 var builders = require('./builders/builders');
 var events = require('cordova-common').events;
-var spawn = require('cordova-common').superspawn.spawn;
+const execa = require('execa');
 var CordovaError = require('cordova-common').CordovaError;
 var PackageType = require('./PackageType');
 
@@ -212,11 +212,11 @@ module.exports.detectArchitecture = function (target) {
             // Could probably find a x-platform version of killall, but I'm not actually
             // sure that this scenario even happens on non-OSX machines.
             events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.');
-            return spawn('killall', ['adb']).then(function () {
+            return execa('killall', ['adb']).then(function () {
                 return helper().then(null, function () {
                     // The double kill is sadly often necessary, at least on mac.
                     events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.');
-                    return spawn('killall', ['adb']).then(function () {
+                    return execa('killall', ['adb']).then(function () {
                         return helper().then(null, function () {
                             return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.'));
                         });
diff --git a/bin/templates/cordova/lib/builders/ProjectBuilder.js b/bin/templates/cordova/lib/builders/ProjectBuilder.js
index bb2a49a..335578d 100644
--- a/bin/templates/cordova/lib/builders/ProjectBuilder.js
+++ b/bin/templates/cordova/lib/builders/ProjectBuilder.js
@@ -17,11 +17,10 @@
        under the License.
 */
 
-var Q = require('q');
 var fs = require('fs');
 var path = require('path');
 var shell = require('shelljs');
-var spawn = require('cordova-common').superspawn.spawn;
+const execa = require('execa');
 var events = require('cordova-common').events;
 var CordovaError = require('cordova-common').CordovaError;
 var check_reqs = require('../check_reqs');
@@ -80,7 +79,7 @@ class ProjectBuilder {
         if (fs.existsSync(gradlePath)) {
             // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows
         } else {
-            return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
+            return execa(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' });
         }
     }
 
@@ -250,16 +249,16 @@ class ProjectBuilder {
         var wrapper = path.join(this.root, 'gradlew');
         var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts);
 
-        return spawn(wrapper, args, { stdio: 'inherit' })
+        return execa(wrapper, args, { stdio: 'inherit' })
             .catch(function (error) {
                 if (error.toString().indexOf('failed to find target with hash string') >= 0) {
                     return check_reqs.check_android_target(error).then(function () {
                         // If due to some odd reason - check_android_target succeeds
                         // we should still fail here.
-                        return Q.reject(error);
+                        throw error;
                     });
                 }
-                return Q.reject(error);
+                throw error;
             });
     }
 
@@ -267,9 +266,7 @@ class ProjectBuilder {
         var builder = this;
         var wrapper = path.join(this.root, 'gradlew');
         var args = builder.getArgs('clean', opts);
-        return Q().then(function () {
-            return spawn(wrapper, args, { stdio: 'inherit' });
-        })
+        return execa(wrapper, args, { stdio: 'inherit' })
             .then(function () {
                 shell.rm('-rf', path.join(builder.root, 'out'));
 
diff --git a/bin/templates/cordova/lib/check_reqs.js b/bin/templates/cordova/lib/check_reqs.js
index 6435e01..c3338e7 100644
--- a/bin/templates/cordova/lib/check_reqs.js
+++ b/bin/templates/cordova/lib/check_reqs.js
@@ -19,8 +19,8 @@
        under the License.
 */
 
+const execa = require('execa');
 var shelljs = require('shelljs');
-var child_process = require('child_process');
 var Q = require('q');
 var path = require('path');
 var fs = require('fs');
@@ -28,7 +28,6 @@ var os = require('os');
 var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..');
 var PROJECT_ROOT = path.join(__dirname, '..', '..');
 var CordovaError = require('cordova-common').CordovaError;
-var superspawn = require('cordova-common').superspawn;
 var android_sdk = require('./android_sdk');
 
 function forgivingWhichSync (cmd) {
@@ -71,7 +70,7 @@ module.exports.get_target = function () {
 
 // Returns a promise. Called only by build and clean commands.
 module.exports.check_ant = function () {
-    return superspawn.spawn('ant', ['-version']).then(function (output) {
+    return execa('ant', ['-version']).then(({ stdout: output }) => {
         // Parse Ant version from command output
         return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1];
     }).catch(function (err) {
@@ -89,7 +88,7 @@ module.exports.get_gradle_wrapper = function () {
     // OK, This hack only works on Windows, not on Mac OS or Linux.  We will be deleting this eventually!
     if (module.exports.isWindows()) {
 
-        var result = child_process.spawnSync(path.join(__dirname, 'getASPath.bat'));
+        var result = execa.sync(path.join(__dirname, 'getASPath.bat'));
         // console.log('result.stdout =' + result.stdout.toString());
         // console.log('result.stderr =' + result.stderr.toString());
 
@@ -157,8 +156,8 @@ module.exports.check_java = function () {
                 var find_java = '/usr/libexec/java_home';
                 var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.';
                 if (fs.existsSync(find_java)) {
-                    return superspawn.spawn(find_java).then(function (stdout) {
-                        process.env['JAVA_HOME'] = stdout.trim();
+                    return execa(find_java).then(({ stdout }) => {
+                        process.env['JAVA_HOME'] = stdout;
                     }).catch(function (err) {
                         if (err) {
                             throw new CordovaError(default_java_error_msg);
@@ -194,11 +193,9 @@ module.exports.check_java = function () {
             }
         }
     }).then(function () {
-        return Q.denodeify(child_process.exec)('javac -version')
-            .then(outputs => {
-                // outputs contains two entries: stdout and stderr
+        return execa('javac', ['-version'], { all: true })
+            .then(({ all: output }) => {
                 // Java <= 8 writes version info to stderr, Java >= 9 to stdout
-                const output = outputs.join('').trim();
                 const match = /javac\s+([\d.]+)/i.exec(output);
                 return match && match[1];
             }, () => {
diff --git a/bin/templates/cordova/lib/device.js b/bin/templates/cordova/lib/device.js
index 1559e9b..f0ea6d4 100644
--- a/bin/templates/cordova/lib/device.js
+++ b/bin/templates/cordova/lib/device.js
@@ -19,11 +19,11 @@
        under the License.
 */
 
+const execa = require('execa');
 var build = require('./build');
 var path = require('path');
 var Adb = require('./Adb');
 var AndroidManifest = require('./AndroidManifest');
-var spawn = require('cordova-common').superspawn.spawn;
 var CordovaError = require('cordova-common').CordovaError;
 var events = require('cordova-common').events;
 
@@ -37,7 +37,7 @@ module.exports.list = function (lookHarder) {
             // adb kill-server doesn't seem to do the trick.
             // Could probably find a x-platform version of killall, but I'm not actually
             // sure that this scenario even happens on non-OSX machines.
-            return spawn('killall', ['adb']).then(function () {
+            return execa('killall', ['adb']).then(function () {
                 events.emit('verbose', 'Restarting adb to see if more devices are detected.');
                 return Adb.devices();
             }, function () {
diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js
index fcdc170..b575052 100644
--- a/bin/templates/cordova/lib/emulator.js
+++ b/bin/templates/cordova/lib/emulator.js
@@ -19,6 +19,7 @@
        under the License.
 */
 
+const execa = require('execa');
 var android_versions = require('android-versions');
 var retry = require('./retry');
 var build = require('./build');
@@ -26,7 +27,6 @@ var path = require('path');
 var Adb = require('./Adb');
 var AndroidManifest = require('./AndroidManifest');
 var events = require('cordova-common').events;
-var superspawn = require('cordova-common').superspawn;
 var CordovaError = require('cordova-common').CordovaError;
 var shelljs = require('shelljs');
 var android_sdk = require('./android_sdk');
@@ -34,7 +34,6 @@ var check_reqs = require('./check_reqs');
 
 var os = require('os');
 var fs = require('fs');
-var child_process = require('child_process');
 
 // constants
 var ONE_SECOND = 1000; // in milliseconds
@@ -53,7 +52,7 @@ function forgivingWhichSync (cmd) {
 }
 
 module.exports.list_images_using_avdmanager = function () {
-    return superspawn.spawn('avdmanager', ['list', 'avd']).then(function (output) {
+    return execa('avdmanager', ['list', 'avd']).then(({ stdout: output }) => {
         var response = output.split('\n');
         var emulator_list = [];
         for (var i = 1; i < response.length; i++) {
@@ -113,7 +112,7 @@ module.exports.list_images_using_avdmanager = function () {
 };
 
 module.exports.list_images_using_android = function () {
-    return superspawn.spawn('android', ['list', 'avd']).then(function (output) {
+    return execa('android', ['list', 'avd']).then(({ stdout: output }) => {
         var response = output.split('\n');
         var emulator_list = [];
         for (var i = 1; i < response.length; i++) {
@@ -229,7 +228,7 @@ module.exports.list_started = function () {
 // Returns a promise.
 // TODO: we should remove this, there's a more robust method under android_sdk.js
 module.exports.list_targets = function () {
-    return superspawn.spawn('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(function (output) {
+    return execa('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(({ stdout: output }) => {
         var target_out = output.split('\n');
         var targets = [];
         for (var i = target_out.length; i >= 0; i--) {
@@ -294,8 +293,7 @@ module.exports.start = function (emulator_ID, boot_timeout) {
             var emulator_dir = path.dirname(shelljs.which('emulator'));
             var args = ['-avd', emulatorId, '-port', port];
             // Don't wait for it to finish, since the emulator will probably keep running for a long time.
-            child_process
-                .spawn('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
+            execa('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
                 .unref();
 
             // wait for emulator to start
@@ -387,22 +385,22 @@ module.exports.wait_for_boot = function (emulator_id, time_remaining) {
 module.exports.create_image = function (name, target) {
     console.log('Creating new avd named ' + name);
     if (target) {
-        return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
+        return execa('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
             console.error('ERROR : Failed to create emulator image : ');
             console.error(' Do you have the latest android targets including ' + target + '?');
-            console.error(error);
+            console.error(error.message);
         });
     } else {
         console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
         // TODO: there's a more robust method for finding targets in android_sdk.js
-        return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
+        return execa('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
             // TODO: This seems like another error case, even though it always happens.
             console.error('ERROR : Unable to create an avd emulator, no targets found.');
             console.error('Ensure you have targets available by running the "android" command');
             return Promise.reject(new CordovaError());
         }, function (error) {
             console.error('ERROR : Failed to create emulator image : ');
-            console.error(error);
+            console.error(error.message);
         });
     }
 };
@@ -474,24 +472,21 @@ module.exports.install = function (givenTarget, buildResults) {
             function adbInstallWithOptions (target, apk, opts) {
                 events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');
 
-                var command = 'adb -s ' + target + ' install -r "' + apk + '"';
-                return new Promise(function (resolve, reject) {
-                    child_process.exec(command, opts, function (err, stdout, stderr) {
-                        if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr));
-                        // adb does not return an error code even if installation fails. Instead it puts a specific
-                        // message to stdout, so we have to use RegExp matching to detect installation failure.
-                        else if (/Failure/.test(stdout)) {
-                            if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
-                                stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
-                                    ' or sign and deploy the unsigned apk manually using Android tools.';
-                            } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
-                                stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
-                                    '\nEither uninstall an app or increment the versionCode.';
-                            }
+                const args = ['-s', target, 'install', '-r', apk];
+                return execa('adb', args, opts).then(({ stdout }) => {
+                    // adb does not return an error code even if installation fails. Instead it puts a specific
+                    // message to stdout, so we have to use RegExp matching to detect installation failure.
+                    if (/Failure/.test(stdout)) {
+                        if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
+                            stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
+                                ' or sign and deploy the unsigned apk manually using Android tools.';
+                        } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
+                            stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
+                                '\nEither uninstall an app or increment the versionCode.';
+                        }
 
-                            reject(new CordovaError('Failed to install apk to emulator: ' + stdout));
-                        } else resolve(stdout);
-                    });
+                        throw new CordovaError('Failed to install apk to emulator: ' + stdout);
+                    }
                 });
             }
 
diff --git a/bin/templates/cordova/lib/log.js b/bin/templates/cordova/lib/log.js
index ec69f8c..db0ba41 100644
--- a/bin/templates/cordova/lib/log.js
+++ b/bin/templates/cordova/lib/log.js
@@ -21,8 +21,7 @@
 
 var path = require('path');
 var os = require('os');
-var Q = require('q');
-var child_process = require('child_process');
+var execa = require('execa');
 var ROOT = path.join(__dirname, '..', '..');
 
 /*
@@ -30,8 +29,7 @@ var ROOT = path.join(__dirname, '..', '..');
  * Returns a promise.
  */
 module.exports.run = function () {
-    var d = Q.defer();
-    var adb = child_process.spawn('adb', ['logcat'], { cwd: os.tmpdir() });
+    var adb = execa('adb', ['logcat'], { cwd: os.tmpdir(), stderr: 'inherit' });
 
     adb.stdout.on('data', function (data) {
         var lines = data ? data.toString().split('\n') : [];
@@ -39,14 +37,7 @@ module.exports.run = function () {
         console.log(out.join('\n'));
     });
 
-    adb.stderr.on('data', console.error);
-    adb.on('close', function (code) {
-        if (code > 0) {
-            d.reject('Failed to run logcat command.');
-        } else d.resolve();
-    });
-
-    return d.promise;
+    return adb;
 };
 
 module.exports.help = function () {
diff --git a/package.json b/package.json
index d151343..54feca9 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
     "android-versions": "^1.4.0",
     "compare-func": "^1.3.2",
     "cordova-common": "^3.2.0",
+    "execa": "^3.2.0",
     "nopt": "^4.0.1",
     "properties-parser": "^0.3.1",
     "q": "^1.5.1",
diff --git a/spec/e2e/plugin.spec.js b/spec/e2e/plugin.spec.js
index 509a072..30fc7b6 100644
--- a/spec/e2e/plugin.spec.js
+++ b/spec/e2e/plugin.spec.js
@@ -21,7 +21,8 @@ const os = require('os');
 const fs = require('fs');
 const path = require('path');
 const shell = require('shelljs');
-const { PluginInfoProvider, superspawn } = require('cordova-common');
+const execa = require('execa');
+const { PluginInfoProvider } = require('cordova-common');
 
 const createBin = path.join(__dirname, '../../bin/create');
 const fakePluginPath = path.join(__dirname, 'fixtures/cordova-plugin-fake');
@@ -44,7 +45,7 @@ describe('plugin add', function () {
         const pluginInfo = new PluginInfoProvider().get(fakePluginPath);
 
         return Promise.resolve()
-            .then(() => superspawn.spawn(createBin, [projectPath, projectid, projectname]))
+            .then(() => execa(createBin, [projectPath, projectid, projectname]))
             .then(() => {
                 const Api = require(path.join(projectPath, 'cordova/Api.js'));
                 return new Api('android', projectPath).addPlugin(pluginInfo);
diff --git a/spec/unit/Adb.spec.js b/spec/unit/Adb.spec.js
index 24e3cb4..593470b 100644
--- a/spec/unit/Adb.spec.js
+++ b/spec/unit/Adb.spec.js
@@ -33,12 +33,12 @@ emulator-5554\tdevice
     const downgradeError = 'adb: failed to install app.apk: Failure[INSTALL_FAILED_VERSION_DOWNGRADE]';
 
     let Adb;
-    let spawnSpy;
+    let execaSpy;
 
     beforeEach(() => {
         Adb = rewire('../../bin/templates/cordova/lib/Adb');
-        spawnSpy = jasmine.createSpy('spawn');
-        Adb.__set__('spawn', spawnSpy);
+        execaSpy = jasmine.createSpy('execa');
+        Adb.__set__('execa', execaSpy);
     });
 
     describe('isDevice', () => {
@@ -61,7 +61,7 @@ emulator-5554\tdevice
 
     describe('devices', () => {
         beforeEach(() => {
-            spawnSpy.and.returnValue(Promise.resolve(adbOutput));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: adbOutput }));
         });
 
         it('should return only devices if no options are specified', () => {
@@ -81,12 +81,12 @@ emulator-5554\tdevice
 
     describe('install', () => {
         beforeEach(() => {
-            spawnSpy.and.returnValue(Promise.resolve(''));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: '' }));
         });
 
         it('should target the passed device id to adb', () => {
             return Adb.install(deviceId).then(() => {
-                const args = spawnSpy.calls.argsFor(0);
+                const args = execaSpy.calls.argsFor(0);
                 expect(args[0]).toBe('adb');
 
                 const adbArgs = args[1].join(' ');
@@ -96,7 +96,7 @@ emulator-5554\tdevice
 
         it('should add the -r flag if opts.replace is set', () => {
             return Adb.install(deviceId, '', { replace: true }).then(() => {
-                const adbArgs = spawnSpy.calls.argsFor(0)[1];
+                const adbArgs = execaSpy.calls.argsFor(0)[1];
                 expect(adbArgs).toContain('-r');
             });
         });
@@ -105,13 +105,13 @@ emulator-5554\tdevice
             const packagePath = 'build/test/app.apk';
 
             return Adb.install(deviceId, packagePath).then(() => {
-                const adbArgs = spawnSpy.calls.argsFor(0)[1];
+                const adbArgs = execaSpy.calls.argsFor(0)[1];
                 expect(adbArgs).toContain(packagePath);
             });
         });
 
         it('should reject with a CordovaError if the adb output suggests a failure', () => {
-            spawnSpy.and.returnValue(Promise.resolve(alreadyExistsError));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: alreadyExistsError }));
 
             return Adb.install(deviceId, '').then(
                 () => fail('Unexpectedly resolved'),
@@ -124,7 +124,7 @@ emulator-5554\tdevice
         // The following two tests are somewhat brittle as they are dependent on the
         // exact message returned. But it is better to have them tested than not at all.
         it('should give a more specific error message if there is a certificate failure', () => {
-            spawnSpy.and.returnValue(Promise.resolve(certificateError));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: certificateError }));
 
             return Adb.install(deviceId, '').then(
                 () => fail('Unexpectedly resolved'),
@@ -136,7 +136,7 @@ emulator-5554\tdevice
         });
 
         it('should give a more specific error message if there is a downgrade error', () => {
-            spawnSpy.and.returnValue(Promise.resolve(downgradeError));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: downgradeError }));
 
             return Adb.install(deviceId, '').then(
                 () => fail('Unexpectedly resolved'),
@@ -151,10 +151,10 @@ emulator-5554\tdevice
     describe('uninstall', () => {
         it('should call adb uninstall with the correct arguments', () => {
             const packageId = 'io.cordova.test';
-            spawnSpy.and.returnValue(Promise.resolve(''));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: '' }));
 
             return Adb.uninstall(deviceId, packageId).then(() => {
-                const args = spawnSpy.calls.argsFor(0);
+                const args = execaSpy.calls.argsFor(0);
                 expect(args[0]).toBe('adb');
 
                 const adbArgs = args[1];
@@ -169,10 +169,10 @@ emulator-5554\tdevice
         const shellCommand = 'ls -l /sdcard';
 
         it('should run the passed command on the target device', () => {
-            spawnSpy.and.returnValue(Promise.resolve(''));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: '' }));
 
             return Adb.shell(deviceId, shellCommand).then(() => {
-                const args = spawnSpy.calls.argsFor(0);
+                const args = execaSpy.calls.argsFor(0);
                 expect(args[0]).toBe('adb');
 
                 const adbArgs = args[1].join(' ');
@@ -184,7 +184,7 @@ emulator-5554\tdevice
 
         it('should reject with a CordovaError on failure', () => {
             const errorMessage = 'shell error';
-            spawnSpy.and.returnValue(Promise.reject(errorMessage));
+            execaSpy.and.rejectWith(new Error(errorMessage));
 
             return Adb.shell(deviceId, shellCommand).then(
                 () => fail('Unexpectedly resolved'),
@@ -214,7 +214,7 @@ emulator-5554\tdevice
 
         it('should reject with a CordovaError on a shell error', () => {
             const errorMessage = 'Test Start error';
-            spyOn(Adb, 'shell').and.returnValue(Promise.reject(errorMessage));
+            spyOn(Adb, 'shell').and.rejectWith(new CordovaError(errorMessage));
 
             return Adb.start(deviceId, activityName).then(
                 () => fail('Unexpectedly resolved'),
diff --git a/spec/unit/android_sdk.spec.js b/spec/unit/android_sdk.spec.js
index 0b0f422..e5e0c32 100644
--- a/spec/unit/android_sdk.spec.js
+++ b/spec/unit/android_sdk.spec.js
@@ -17,16 +17,18 @@
     under the License.
 */
 
-const superspawn = require('cordova-common').superspawn;
 const fs = require('fs');
 const path = require('path');
 const rewire = require('rewire');
 
 describe('android_sdk', () => {
     let android_sdk;
+    let execaSpy;
 
     beforeEach(() => {
         android_sdk = rewire('../../bin/templates/cordova/lib/android_sdk');
+        execaSpy = jasmine.createSpy('execa');
+        android_sdk.__set__('execa', execaSpy);
     });
 
     describe('sort_by_largest_numerical_suffix', () => {
@@ -59,14 +61,14 @@ describe('android_sdk', () => {
 
     describe('list_targets_with_android', () => {
         it('should invoke `android` with the `list target` command and _not_ the `list targets` command, as the plural form is not supported in some Android SDK Tools versions', () => {
-            spyOn(superspawn, 'spawn').and.returnValue(new Promise(() => {}, () => {}));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: '' }));
             android_sdk.list_targets_with_android();
-            expect(superspawn.spawn).toHaveBeenCalledWith('android', ['list', 'target']);
+            expect(execaSpy).toHaveBeenCalledWith('android', ['list', 'target']);
         });
 
         it('should parse and return results from `android list targets` command', () => {
             const testTargets = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.2-android_list_targets.txt'), 'utf-8');
-            spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(testTargets));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: testTargets }));
 
             return android_sdk.list_targets_with_android().then(list => {
                 [ 'Google Inc.:Google APIs:23',
@@ -87,7 +89,7 @@ describe('android_sdk', () => {
     describe('list_targets_with_avdmanager', () => {
         it('should parse and return results from `avdmanager list target` command', () => {
             const testTargets = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.3-avdmanager_list_target.txt'), 'utf-8');
-            spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(testTargets));
+            execaSpy.and.returnValue(Promise.resolve({ stdout: testTargets }));
 
             return android_sdk.list_targets_with_avdmanager().then(list => {
                 expect(list).toContain('android-25');
diff --git a/spec/unit/builders/ProjectBuilder.spec.js b/spec/unit/builders/ProjectBuilder.spec.js
index 7104271..bc1960b 100644
--- a/spec/unit/builders/ProjectBuilder.spec.js
+++ b/spec/unit/builders/ProjectBuilder.spec.js
@@ -19,7 +19,6 @@
 
 const fs = require('fs');
 const path = require('path');
-const Q = require('q');
 const rewire = require('rewire');
 
 const CordovaError = require('cordova-common').CordovaError;
@@ -29,12 +28,12 @@ describe('ProjectBuilder', () => {
 
     let builder;
     let ProjectBuilder;
-    let spawnSpy;
+    let execaSpy;
 
     beforeEach(() => {
-        spawnSpy = jasmine.createSpy('spawn').and.returnValue(Q.defer().promise);
+        execaSpy = jasmine.createSpy('execa').and.returnValue(new Promise(() => {}));
         ProjectBuilder = rewire('../../../bin/templates/cordova/lib/builders/ProjectBuilder');
-        ProjectBuilder.__set__('spawn', spawnSpy);
+        ProjectBuilder.__set__('execa', execaSpy);
 
         builder = new ProjectBuilder(rootDir);
     });
@@ -120,13 +119,13 @@ describe('ProjectBuilder', () => {
         it('should run the provided gradle command if a gradle wrapper does not already exist', () => {
             spyOn(fs, 'existsSync').and.returnValue(false);
             builder.runGradleWrapper('/my/sweet/gradle');
-            expect(spawnSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
+            expect(execaSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
         });
 
         it('should do nothing if a gradle wrapper exists in the project directory', () => {
             spyOn(fs, 'existsSync').and.returnValue(true);
             builder.runGradleWrapper('/my/sweet/gradle');
-            expect(spawnSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
+            expect(execaSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object));
         });
     });
 
@@ -182,34 +181,34 @@ describe('ProjectBuilder', () => {
 
             builder.build({});
 
-            expect(spawnSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything());
+            expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything());
         });
 
         it('should reject if the spawn fails', () => {
-            const errorMessage = 'ERROR: Failed to spawn';
-            spawnSpy.and.returnValue(Q.reject(errorMessage));
+            const errorMessage = 'Test error';
+            execaSpy.and.rejectWith(new Error(errorMessage));
 
             return builder.build({}).then(
                 () => fail('Unexpectedly resolved'),
-                err => {
-                    expect(err).toBe(errorMessage);
+                error => {
+                    expect(error.message).toBe(errorMessage);
                 }
             );
         });
 
         it('should check the Android target if failed to find target', () => {
             const checkReqsSpy = jasmine.createSpyObj('check_reqs', ['check_android_target']);
-            const errorMessage = 'ERROR: failed to find target with hash string';
+            const testError = 'failed to find target with hash string';
 
             ProjectBuilder.__set__('check_reqs', checkReqsSpy);
-            checkReqsSpy.check_android_target.and.returnValue(Q.resolve());
-            spawnSpy.and.returnValue(Q.reject(errorMessage));
+            checkReqsSpy.check_android_target.and.returnValue(Promise.resolve());
+            execaSpy.and.rejectWith(testError);
 
             return builder.build({}).then(
                 () => fail('Unexpectedly resolved'),
-                err => {
-                    expect(checkReqsSpy.check_android_target).toHaveBeenCalledWith(errorMessage);
-                    expect(err).toBe(errorMessage);
+                error => {
+                    expect(checkReqsSpy.check_android_target).toHaveBeenCalledWith(testError);
+                    expect(error).toBe(testError);
                 }
             );
         });
@@ -222,7 +221,7 @@ describe('ProjectBuilder', () => {
             shellSpy = jasmine.createSpyObj('shell', ['rm']);
             ProjectBuilder.__set__('shell', shellSpy);
             spyOn(builder, 'getArgs');
-            spawnSpy.and.returnValue(Promise.resolve());
+            execaSpy.and.returnValue(Promise.resolve());
         });
 
         it('should get arguments for cleaning', () => {
@@ -238,7 +237,7 @@ describe('ProjectBuilder', () => {
             builder.getArgs.and.returnValue(gradleArgs);
 
             return builder.clean(opts).then(() => {
-                expect(spawnSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything());
+                expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything());
             });
         });
 
diff --git a/spec/unit/device.spec.js b/spec/unit/device.spec.js
index 27b6c62..d5adcaa 100644
--- a/spec/unit/device.spec.js
+++ b/spec/unit/device.spec.js
@@ -42,12 +42,12 @@ describe('device', () => {
         });
 
         it('should kill adb and try to get devices again if none are found the first time, and `lookHarder` is set', () => {
-            const spawnSpy = jasmine.createSpy('spawn').and.returnValue(Promise.resolve());
-            device.__set__('spawn', spawnSpy);
+            const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve());
+            device.__set__('execa', execaSpy);
             AdbSpy.devices.and.returnValues(Promise.resolve([]), Promise.resolve(DEVICE_LIST));
 
             return device.list(true).then(list => {
-                expect(spawnSpy).toHaveBeenCalledWith('killall', ['adb']);
+                expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']);
                 expect(list).toBe(DEVICE_LIST);
                 expect(AdbSpy.devices).toHaveBeenCalledTimes(2);
             });
@@ -55,12 +55,12 @@ describe('device', () => {
 
         it('should return the empty list if killing adb fails', () => {
             const emptyDevices = [];
-            const spawnSpy = jasmine.createSpy('spawn').and.returnValue(Promise.reject());
-            device.__set__('spawn', spawnSpy);
+            const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.reject());
+            device.__set__('execa', execaSpy);
             AdbSpy.devices.and.returnValues(Promise.resolve(emptyDevices));
 
             return device.list(true).then(list => {
-                expect(spawnSpy).toHaveBeenCalledWith('killall', ['adb']);
+                expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']);
                 expect(list).toBe(emptyDevices);
                 expect(AdbSpy.devices).toHaveBeenCalledTimes(1);
             });
diff --git a/spec/unit/emulator.spec.js b/spec/unit/emulator.spec.js
index 3f42fe7..8ae01e5 100644
--- a/spec/unit/emulator.spec.js
+++ b/spec/unit/emulator.spec.js
@@ -24,7 +24,6 @@ const shelljs = require('shelljs');
 
 const CordovaError = require('cordova-common').CordovaError;
 const check_reqs = require('../../bin/templates/cordova/lib/check_reqs');
-const superspawn = require('cordova-common').superspawn;
 
 describe('emulator', () => {
     const EMULATOR_LIST = ['emulator-5555', 'emulator-5556', 'emulator-5557'];
@@ -37,7 +36,9 @@ describe('emulator', () => {
     describe('list_images_using_avdmanager', () => {
         it('should properly parse details of SDK Tools 25.3.1 `avdmanager` output', () => {
             const avdList = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.3-avdmanager_list_avd.txt'), 'utf-8');
-            spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(avdList));
+
+            let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: avdList }));
+            emu.__set__('execa', execaSpy);
 
             return emu.list_images_using_avdmanager().then(list => {
                 expect(list).toBeDefined();
@@ -51,14 +52,18 @@ describe('emulator', () => {
 
     describe('list_images_using_android', () => {
         it('should invoke `android` with the `list avd` command and _not_ the `list avds` command, as the plural form is not supported in some Android SDK Tools versions', () => {
-            spyOn(superspawn, 'spawn').and.returnValue(new Promise(() => {}, () => {}));
+            let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: '' }));
+            emu.__set__('execa', execaSpy);
+
             emu.list_images_using_android();
-            expect(superspawn.spawn).toHaveBeenCalledWith('android', ['list', 'avd']);
+            expect(execaSpy).toHaveBeenCalledWith('android', ['list', 'avd']);
         });
 
         it('should properly parse details of SDK Tools pre-25.3.1 `android list avd` output', () => {
             const avdList = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.2-android_list_avd.txt'), 'utf-8');
-            spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(avdList));
+
+            let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: avdList }));
+            emu.__set__('execa', execaSpy);
 
             return emu.list_images_using_android().then(list => {
                 expect(list).toBeDefined();
@@ -249,7 +254,7 @@ describe('emulator', () => {
         let emulator;
         let AdbSpy;
         let checkReqsSpy;
-        let childProcessSpy;
+        let execaSpy;
         let shellJsSpy;
 
         beforeEach(() => {
@@ -268,9 +273,10 @@ describe('emulator', () => {
             checkReqsSpy = jasmine.createSpyObj('create_reqs', ['getAbsoluteAndroidCmd']);
             emu.__set__('check_reqs', checkReqsSpy);
 
-            childProcessSpy = jasmine.createSpyObj('child_process', ['spawn']);
-            childProcessSpy.spawn.and.returnValue(jasmine.createSpyObj('spawnFns', ['unref']));
-            emu.__set__('child_process', childProcessSpy);
+            execaSpy = jasmine.createSpy('execa').and.returnValue(
+                jasmine.createSpyObj('spawnFns', ['unref'])
+            );
+            emu.__set__('execa', execaSpy);
 
             spyOn(emu, 'get_available_port').and.returnValue(Promise.resolve(port));
             spyOn(emu, 'wait_for_emulator').and.returnValue(Promise.resolve('randomname'));
@@ -291,7 +297,7 @@ describe('emulator', () => {
             return emu.start().then(() => {
                 // This is the earliest part in the code where we can hook in and check
                 // the emulator that has been selected.
-                const spawnArgs = childProcessSpy.spawn.calls.argsFor(0);
+                const spawnArgs = execaSpy.calls.argsFor(0);
                 expect(spawnArgs[1]).toContain(emulator.name);
             });
         });
@@ -302,7 +308,7 @@ describe('emulator', () => {
             return emu.start(emulator.name).then(() => {
                 expect(emu.best_image).not.toHaveBeenCalled();
 
-                const spawnArgs = childProcessSpy.spawn.calls.argsFor(0);
+                const spawnArgs = execaSpy.calls.argsFor(0);
                 expect(spawnArgs[1]).toContain(emulator.name);
             });
         });
@@ -576,7 +582,7 @@ describe('emulator', () => {
         let AndroidManifestGetActivitySpy;
         let AdbSpy;
         let buildSpy;
-        let childProcessSpy;
+        let execaSpy;
         let target;
 
         beforeEach(() => {
@@ -597,9 +603,8 @@ describe('emulator', () => {
             AdbSpy.uninstall.and.returnValue(Promise.resolve());
             emu.__set__('Adb', AdbSpy);
 
-            childProcessSpy = jasmine.createSpyObj('child_process', ['exec']);
-            childProcessSpy.exec.and.callFake((cmd, opts, callback) => callback());
-            emu.__set__('child_process', childProcessSpy);
+            execaSpy = jasmine.createSpy('execa').and.resolveTo({});
+            emu.__set__('execa', execaSpy);
         });
 
         it('should get the full target object if only id is specified', () => {
@@ -613,7 +618,7 @@ describe('emulator', () => {
 
         it('should install to the passed target', () => {
             return emu.install(target, {}).then(() => {
-                const execCmd = childProcessSpy.exec.calls.argsFor(0)[0];
+                const execCmd = execaSpy.calls.argsFor(0)[1].join(' ');
                 expect(execCmd).toContain(`-s ${target.target} install`);
             });
         });
@@ -631,33 +636,26 @@ describe('emulator', () => {
             return emu.install(target, buildResults).then(() => {
                 expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch);
 
-                const execCmd = childProcessSpy.exec.calls.argsFor(0)[0];
-                expect(execCmd).toMatch(new RegExp(`install.*${apkPath}`));
+                const execCmd = execaSpy.calls.argsFor(0)[1].join(' ');
+                expect(execCmd).toContain(`install -r ${apkPath}`);
             });
         });
 
         it('should uninstall and reinstall app if failure is due to different certificates', () => {
-            let execAlreadyCalled;
-            childProcessSpy.exec.and.callFake((cmd, opts, callback) => {
-                if (!execAlreadyCalled) {
-                    execAlreadyCalled = true;
-                    callback(null, 'Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES');
-                } else {
-                    callback();
-                }
-            });
+            execaSpy.and.returnValues(
+                ...['Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES', '']
+                    .map(out => Promise.resolve({ stdout: out }))
+            );
 
             return emu.install(target, {}).then(() => {
-                expect(childProcessSpy.exec).toHaveBeenCalledTimes(2);
+                expect(execaSpy).toHaveBeenCalledTimes(2);
                 expect(AdbSpy.uninstall).toHaveBeenCalled();
             });
         });
 
         it('should throw any error not caused by different certificates', () => {
             const errorMsg = 'Failure: Failed to install';
-            childProcessSpy.exec.and.callFake((cmd, opts, callback) => {
-                callback(null, errorMsg);
-            });
+            execaSpy.and.resolveTo({ stdout: errorMsg });
 
             return emu.install(target, {}).then(
                 () => fail('Unexpectedly resolved'),
diff --git a/test/run_java_unit_tests.js b/test/run_java_unit_tests.js
index 3f99d28..6aa7219 100644
--- a/test/run_java_unit_tests.js
+++ b/test/run_java_unit_tests.js
@@ -21,7 +21,7 @@
 
 var Q = require('q');
 var path = require('path');
-var superspawn = require('cordova-common').superspawn;
+var execa = require('execa');
 var ProjectBuilder = require('../bin/templates/cordova/lib/builders/ProjectBuilder');
 
 Q.resolve()
@@ -42,7 +42,7 @@ process.on('unhandledRejection', err => {
 
 function gradlew () {
     const wrapperPath = path.join(__dirname, 'gradlew');
-    return superspawn.spawn(wrapperPath, Array.from(arguments), {
+    return execa(wrapperPath, Array.from(arguments), {
         stdio: 'inherit',
         cwd: __dirname
     });


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