@@ -6,7 +6,6 @@ import childProcess from 'node:child_process';
66import fs from 'node:fs/promises' ;
77import path from 'node:path' ;
88import { performance } from 'node:perf_hooks' ;
9- import util from 'node:util' ;
109
1110import {
1211 autoninjaPyPath ,
@@ -16,7 +15,65 @@ import {
1615 vpython3ExecutablePath ,
1716} from './devtools_paths.js' ;
1817
19- const execFile = util . promisify ( childProcess . execFile ) ;
18+ /**
19+ * Errors returned from `spawn()` will have additional `stderr` and `stdout`
20+ * properties, similar to what we'd get from `child_process.execFile()`.
21+ */
22+ class SpawnError extends Error {
23+ /**
24+ * Constructor for errors generated from `spawn()`.
25+ *
26+ * @param {string } message - The actual error message.
27+ * @param {string } stderr - The child process' error output.
28+ * @param {string } stdout - The child process' regular output.
29+ */
30+ constructor ( message , stderr , stdout ) {
31+ super ( message ) ;
32+ this . stderr = stderr ;
33+ this . stdout = stdout ;
34+ }
35+ }
36+
37+ /**
38+ * Promisified wrapper around `child_process.spawn()`.
39+ *
40+ * In addition to forwarding the `options` to `child_process.spawn()`, it'll also
41+ * set the `shell` option to `true`, to ensure that on Windows we can correctly
42+ * invoke `.bat` files (necessary for the Python3 wrapper script).
43+ *
44+ * @param {string } command - The command to run.
45+ * @param {Array<string> } args - List of string arguments to pass to the `command`.
46+ * @param {Object } options - Passed directly to `child_process.spawn()`.
47+ * @returns {Promise<{stdout: string, stderr: string}> }
48+ */
49+ function spawn ( command , args , options = { } ) {
50+ return new Promise ( ( resolve , reject ) => {
51+ const child = childProcess . spawn ( command , args , { ...options , shell : true } ) ;
52+
53+ let stdout = '' ;
54+ let stderr = '' ;
55+
56+ child . stdout . on ( 'data' , data => {
57+ stdout += data . toString ( ) ;
58+ } ) ;
59+
60+ child . stderr . on ( 'data' , data => {
61+ stderr += data . toString ( ) ;
62+ } ) ;
63+
64+ child . on ( 'exit' , ( code , signal ) => {
65+ if ( signal ) {
66+ reject ( new SpawnError ( `Process terminated due to signal ${ signal } ` , stderr , stdout ) ) ;
67+ } else if ( code ) {
68+ reject ( new SpawnError ( `Process exited with code ${ code } ` , stderr , stdout ) ) ;
69+ } else {
70+ resolve ( { stdout, stderr} ) ;
71+ }
72+ } ) ;
73+
74+ child . on ( 'error' , reject ) ;
75+ } ) ;
76+ }
2077
2178/**
2279 * Representation of the feature set that is configured for Chrome. This
@@ -188,7 +245,7 @@ export class BuildError extends Error {
188245 * Constructs a new `BuildError` with the given parameters.
189246 *
190247 * @param step - the build step that failed.
191- * @param options - additional options for the `BuildError`.
248+ * @param { object } options - additional options for the `BuildError`.
192249 * @param options.cause - the actual cause for the build error.
193250 * @param options.outDir - the absolute path to the `target` out directory.
194251 * @param options.target - the target relative to `//out`.
@@ -225,7 +282,7 @@ export async function prepareBuild(target) {
225282 try {
226283 const gnExe = vpython3ExecutablePath ( ) ;
227284 const gnArgs = [ gnPyPath ( ) , '-q' , 'gen' , outDir ] ;
228- await execFile ( gnExe , gnArgs ) ;
285+ await spawn ( gnExe , gnArgs ) ;
229286 } catch ( cause ) {
230287 throw new BuildError ( BuildStep . GN , { cause, outDir, target} ) ;
231288 }
@@ -246,7 +303,7 @@ function gnArgsForTarget(target) {
246303 const cwd = rootPath ( ) ;
247304 const gnExe = vpython3ExecutablePath ( ) ;
248305 const gnArgs = [ gnPyPath ( ) , '-q' , 'args' , outDir , '--json' , '--list' , '--short' ] ;
249- const { stdout} = await execFile ( gnExe , gnArgs , { cwd} ) ;
306+ const { stdout} = await spawn ( gnExe , gnArgs , { cwd} ) ;
250307 return new Map ( JSON . parse ( stdout ) . map ( arg => [ arg . name , arg . current ?. value ?? arg . default ?. value ] ) ) ;
251308 } catch {
252309 return new Map ( ) ;
@@ -273,7 +330,7 @@ function gnRefsForTarget(target, filename) {
273330 const outDir = path . join ( rootPath ( ) , 'out' , target ) ;
274331 const gnExe = vpython3ExecutablePath ( ) ;
275332 const gnArgs = [ gnPyPath ( ) , 'refs' , outDir , '--as=output' , filename ] ;
276- const { stdout} = await execFile ( gnExe , gnArgs , { cwd} ) ;
333+ const { stdout} = await spawn ( gnExe , gnArgs , { cwd} ) ;
277334 return stdout . trim ( ) . split ( '\n' ) ;
278335 } ) ( ) ;
279336 gnRefsPerTarget . set ( filename , gnRef ) ;
@@ -325,7 +382,7 @@ export async function build(target, signal, filenames) {
325382 try {
326383 const autoninjaExe = vpython3ExecutablePath ( ) ;
327384 const autoninjaArgs = [ autoninjaPyPath ( ) , '-C' , outDir , ...buildTargets ] ;
328- await execFile ( autoninjaExe , autoninjaArgs , { signal} ) ;
385+ await spawn ( autoninjaExe , autoninjaArgs , { shell : true , signal} ) ;
329386 } catch ( cause ) {
330387 if ( cause . name === 'AbortError' ) {
331388 throw cause ;
0 commit comments