From 77ab5296b69f04cbf4539a18439b7a17b20f4d07 Mon Sep 17 00:00:00 2001 From: Christian Petrov Date: Thu, 17 Aug 2017 20:07:22 +0200 Subject: [PATCH] Fix build on Windows for user paths with spaces Fix incorrect command and argument handling on Windows by spawning the command in a shell and by wrapping command and arguments containing spaces in quotes. See https://github.com/nodejs/node/issues/5060 Fix #25 Change-Id: Ieb32892946e9a779b67b5842f4f58d18be847b0f --- src/helpers/proc.js | 30 ++++++++++++++++--- test/proc.test.js | 70 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 test/proc.test.js diff --git a/src/helpers/proc.js b/src/helpers/proc.js index 877dcfb..cf84fbd 100644 --- a/src/helpers/proc.js +++ b/src/helpers/proc.js @@ -1,15 +1,37 @@ const os = require('os'); -const {spawnSync} = require('child_process'); +const proc = require('child_process'); const log = require('./log'); function execSync(cmd, args, opts = {}) { - let cmdName = os.platform() === 'win32' ? cmd + '.cmd' : cmd; - log.command([cmdName, ...args].join(' '), opts.cwd); - const ps = spawnSync(cmdName, args, Object.assign({stdio: 'inherit'}, opts)); + let normalizedCmd = normalizeCommand(cmd); + let normalizedArgs = normalizeArguments(args); + log.command([normalizedCmd, ...normalizedArgs].join(' '), opts.cwd); + const ps = proc.spawnSync(normalizedCmd, normalizedArgs, Object.assign({ + stdio: 'inherit', + shell: isWindows() + }, opts)); if (ps.status !== 0) { throw new Error(`The command ${cmd} exited with ${ps.status}`); } return ps; } +function normalizeArguments(args) { + if (isWindows()) { + return args.map(arg => arg.indexOf(' ') >= 0 ? `"${arg}"` : arg); + } + return args; +} + +function normalizeCommand(cmd) { + if (isWindows()) { + return cmd.indexOf(' ') >= 0 ? `"${cmd}"` : cmd; + } + return cmd; +} + +function isWindows() { + return os.platform() === 'win32'; +} + module.exports = {execSync}; diff --git a/test/proc.test.js b/test/proc.test.js new file mode 100644 index 0000000..030d9d9 --- /dev/null +++ b/test/proc.test.js @@ -0,0 +1,70 @@ +const os = require('os'); +const childProcess = require('child_process'); +const proc = require('../src/helpers/proc'); +const log = require('../src/helpers/log'); +const {expect, stub, restore, match} = require('./test'); + +describe('proc', function() { + + describe('execSync', function() { + + let status, platform; + + beforeEach(function() { + status = 0; + platform = 'linux'; + stub(log, 'command'); + stub(os, 'platform').callsFake(() => platform); + stub(childProcess, 'spawnSync').callsFake(() => ({status})); + }); + + afterEach(restore); + + it('spawns command with options', function() { + proc.execSync('foo', ['ba r', 'bak'], {option: 'value'}); + + expect(childProcess.spawnSync).to.have.been.calledWithMatch('foo', ['ba r', 'bak'], { + stdio: 'inherit', + shell: false, + option: 'value' + }); + }); + + it('runs command inside of a shell on Windows', function() { + platform = 'win32'; + + proc.execSync('foo', ['bar'], {option: 'value'}); + + expect(childProcess.spawnSync).to.have.been.calledWithMatch(match.any, match.any, { + shell: true + }); + }); + + it('throws an error when process exits with non 0 status', function() { + status = 123; + + expect(() => { + proc.execSync('foo', ['bar'], {option: 'value'}); + }).to.throw('The command foo exited with 123'); + }); + + it('normalizes command on Windows', function() { + platform = 'win32'; + + proc.execSync('fo o', ['bar'], {option: 'value'}); + + expect(childProcess.spawnSync).to.have.been.calledWithMatch('"fo o"'); + }); + + it('normalizes arguments on Windows', function() { + platform = 'win32'; + + proc.execSync('foo', ['bar', 'ba k'], {option: 'value'}); + + expect(childProcess.spawnSync).to.have.been.calledWithMatch(match.any, ['bar', '"ba k"']); + }); + + }); + +}); +