Skip to content

Commit 46da544

Browse files
authored
Merge pull request #6535 from NomicFoundation/fix-compilation-in-subprocess
Fix Solidity compilation in subprocesses
2 parents a8ad44c + ae7fbc0 commit 46da544

2 files changed

Lines changed: 91 additions & 47 deletions

File tree

.changeset/loud-kids-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hardhat": patch
3+
---
4+
5+
Fix a few potential errors that could happen when compiling Solidity in a subprocess

packages/hardhat-core/src/internal/solidity/compiler/index.ts

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as fs from "fs";
33
import os from "node:os";
44
import path from "node:path";
55
import * as semver from "semver";
6+
import { ExecFileOptions } from "node:child_process";
67
import { CompilerInput, CompilerOutput } from "../../../types";
78
import { HardhatError } from "../../core/errors";
89
import { ERRORS } from "../../core/errors-list";
@@ -17,32 +18,21 @@ export class Compiler implements ICompiler {
1718
public async compile(input: CompilerInput) {
1819
const scriptPath = path.join(__dirname, "./solcjs-runner.js");
1920

20-
const output: string = await new Promise((resolve, reject) => {
21-
try {
22-
const subprocess = execFile(
23-
process.execPath,
24-
[scriptPath, this._pathToSolcJs],
25-
{
26-
maxBuffer: 1024 * 1024 * 500,
27-
},
28-
(err, stdout) => {
29-
if (err !== null) {
30-
return reject(err);
31-
}
32-
resolve(stdout);
33-
}
34-
);
35-
36-
subprocess.stdin!.write(JSON.stringify(input));
37-
subprocess.stdin!.end();
38-
} catch (e: any) {
39-
throw new HardhatError(
40-
ERRORS.SOLC.SOLCJS_ERROR,
41-
{ error: e.message },
42-
e
43-
);
44-
}
45-
});
21+
let output: string;
22+
try {
23+
const { stdout } = await execFileWithInput(
24+
process.execPath,
25+
[scriptPath, this._pathToSolcJs],
26+
JSON.stringify(input),
27+
{
28+
maxBuffer: 1024 * 1024 * 500,
29+
}
30+
);
31+
32+
output = stdout;
33+
} catch (e: any) {
34+
throw new HardhatError(ERRORS.SOLC.SOLCJS_ERROR, {}, e);
35+
}
4636

4737
return JSON.parse(output);
4838
}
@@ -69,29 +59,78 @@ export class NativeCompiler implements ICompiler {
6959
}
7060
}
7161

72-
const output: string = await new Promise((resolve, reject) => {
73-
try {
74-
const process = execFile(
75-
this._pathToSolc,
76-
args,
77-
{
78-
maxBuffer: 1024 * 1024 * 500,
79-
},
80-
(err, stdout) => {
81-
if (err !== null) {
82-
return reject(err);
83-
}
84-
resolve(stdout);
85-
}
86-
);
62+
let output: string;
63+
try {
64+
const { stdout } = await execFileWithInput(
65+
this._pathToSolc,
66+
args,
67+
JSON.stringify(input),
68+
{
69+
maxBuffer: 1024 * 1024 * 500,
70+
}
71+
);
8772

88-
process.stdin!.write(JSON.stringify(input));
89-
process.stdin!.end();
90-
} catch (e: any) {
91-
throw new HardhatError(ERRORS.SOLC.CANT_RUN_NATIVE_COMPILER, {}, e);
92-
}
93-
});
73+
output = stdout;
74+
} catch (e: any) {
75+
throw new HardhatError(ERRORS.SOLC.CANT_RUN_NATIVE_COMPILER, {}, e);
76+
}
9477

9578
return JSON.parse(output);
9679
}
9780
}
81+
82+
/**
83+
* Executes a command using execFile, writes provided input to stdin,
84+
* and returns a Promise that resolves with stdout and stderr.
85+
*
86+
* @param {string} file - The file to execute.
87+
* @param {readonly string[]} args - The arguments to pass to the file.
88+
* @param {ExecFileOptions} options - The options to pass to the exec function.
89+
* @returns {Promise<{stdout: string, stderr: string}>}
90+
*/
91+
export async function execFileWithInput(
92+
file: string,
93+
args: readonly string[],
94+
input: string,
95+
options: ExecFileOptions = {}
96+
): Promise<{ stdout: string; stderr: string }> {
97+
return new Promise((resolve, reject) => {
98+
const child = execFile(file, args, options, (error, stdout, stderr) => {
99+
// `error` is any execution error. e.g. command not found, non-zero exit code, etc.
100+
if (error !== null) {
101+
reject(error);
102+
} else {
103+
resolve({ stdout, stderr });
104+
}
105+
});
106+
107+
// This could be triggered if node fails to spawn the child process
108+
child.on("error", (err) => {
109+
reject(err);
110+
});
111+
112+
const stdin = child.stdin;
113+
114+
if (stdin !== null) {
115+
stdin.on("error", (err) => {
116+
// This captures EPIPE error
117+
reject(err);
118+
});
119+
120+
child.once("spawn", () => {
121+
if (!stdin.writable || child.killed) {
122+
return reject(new Error("Failed to write to unwritable stdin"));
123+
}
124+
125+
stdin.write(input, (error) => {
126+
if (error !== null && error !== undefined) {
127+
reject(error);
128+
}
129+
stdin.end();
130+
});
131+
});
132+
} else {
133+
reject(new Error("No stdin on child process"));
134+
}
135+
});
136+
}

0 commit comments

Comments
 (0)