Skip to content

Commit 0820688

Browse files
committed
fix(build:wasm): quote shell args and include message in toolchain detect (#990)
- quoteShellArg: with shell: true, execFileSync joins cmd+args into one shell string, so whitespace in paths (e.g. Windows 'C:\Users\First Last') gets re-split. Quote args containing whitespace with platform-appropriate quoting (cmd.exe double quotes vs POSIX single quotes). - Toolchain detection: include build.message in the combined string so Node-surfaced ENOENTs (e.g. 'spawn emcc ENOENT') trigger the missing toolchain banner. Also match on ENOENT as a trigger word. Impact: 2 functions changed, 2 affected
1 parent a4a21eb commit 0820688

1 file changed

Lines changed: 23 additions & 3 deletions

File tree

scripts/build-wasm.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,28 @@ function printBanner(title: string, lines: string[]): void {
3636
console.warn(bar);
3737
}
3838

39+
// With shell: true, execFileSync concatenates cmd + args into a single string
40+
// and hands it to the shell, so any whitespace in args (e.g. Windows paths like
41+
// `C:\Users\First Last\...`) gets re-split as separate tokens. Quote args that
42+
// contain whitespace so the shell treats them as one argument.
43+
function quoteShellArg(arg: string): string {
44+
if (arg.length === 0) return '""';
45+
if (!/\s/.test(arg)) return arg;
46+
if (process.platform === 'win32') {
47+
// cmd.exe: wrap in double quotes; escape any embedded double quotes.
48+
return `"${arg.replace(/"/g, '""')}"`;
49+
}
50+
// POSIX sh: wrap in single quotes; close/escape/reopen for embedded single quotes.
51+
return `'${arg.replace(/'/g, `'\\''`)}'`;
52+
}
53+
3954
function runCaptured(
4055
cmd: string,
4156
args: string[],
4257
cwd: string,
4358
): { ok: true; stdout: string } | { ok: false; stdout: string; stderr: string; message: string } {
4459
try {
45-
const stdout = execFileSync(cmd, args, {
60+
const stdout = execFileSync(cmd, args.map(quoteShellArg), {
4661
cwd,
4762
stdio: ['ignore', 'pipe', 'pipe'],
4863
shell: true,
@@ -229,8 +244,13 @@ for (const g of grammars) {
229244
const build = runCaptured('npx', ['tree-sitter', 'build', '--wasm', grammarDir], grammarsDir);
230245
if (!build.ok) {
231246
failed++;
232-
const combined = `${build.stderr}\n${build.stdout}`;
233-
if (/emcc|emscripten|docker/i.test(combined) && /not found|no such|cannot find|missing/i.test(combined)) {
247+
// Include build.message — Node.js surfaces ENOENT for spawned executables
248+
// (e.g. `spawn emcc ENOENT`) via the error message, not stderr.
249+
const combined = `${build.stderr}\n${build.stdout}\n${build.message}`;
250+
if (
251+
/emcc|emscripten|docker/i.test(combined) &&
252+
/not found|no such|cannot find|missing|ENOENT/i.test(combined)
253+
) {
234254
missingToolchain = true;
235255
}
236256
const detail = build.stderr.trim().split('\n').slice(-2).join(' | ') || build.message;

0 commit comments

Comments
 (0)