hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
198 lines (168 loc) • 6.47 kB
text/typescript
import type {
CompilerInput,
CompilerOutput,
} from "../../../../../types/solidity/compiler-io.js";
import { spawn } from "node:child_process";
import fsPromises from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { finished } from "node:stream/promises";
import { fileURLToPath, pathToFileURL } from "node:url";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import {
mkdtemp,
readJsonFileAsStream,
remove,
} from "@nomicfoundation/hardhat-utils/fs";
import { createNonClosingWriter } from "@nomicfoundation/hardhat-utils/stream";
import * as semver from "semver";
export interface Compiler {
readonly version: string;
readonly longVersion: string;
readonly compilerPath: string;
readonly isSolcJs: boolean;
compile(input: CompilerInput): Promise<CompilerOutput>;
}
/**
* Spawns a compilation process and returns its output.
*
* It pipes the stdout of the compilation process to a temporary file to avoid exceeding
* the maximum buffer size. It later reads the file as stream to parse it into a CompilerOutput.
*
* It also pipes the stderr of the compilation process to the stderr of the main process.
*
* @param command The compilation command to run
* @param args The arguments to pass to the compilation command
* @param input The solc input to pass to the compilation command on stdin
* @returns The compilation output
* @throws Error if the compilation process exits with a non-zero exit code.
* @throws HardhatInvariantError if the any of the io streams are null.
*/
async function spawnCompile(
command: string,
args: string[],
input: CompilerInput,
): Promise<CompilerOutput> {
// We create a temporary folder to store the output of the compiler in
// We use a random UUID to avoid collisions with other compilations
const tmpFolder = await mkdtemp("hardhat-solc-");
try {
return await new Promise(async (resolve, reject) => {
const stdoutPath = path.join(tmpFolder, "stdout.txt");
const stdoutFileHandle = await fsPromises.open(stdoutPath, "w");
const stdoutWriteStream = stdoutFileHandle.createWriteStream();
const subprocess = spawn(command, args);
assertHardhatInvariant(
subprocess.stdout !== null,
"process.stdout is null",
);
assertHardhatInvariant(
subprocess.stderr !== null,
"process.stderr is null",
);
const stdoutPipeline = subprocess.stdout.pipe(stdoutWriteStream);
// NOTE: Compiler warnings are NOT written to stder, they are returned via
// the `errors` field of the CompilerOutput instead
const stderrPipeline = subprocess.stderr.pipe(
createNonClosingWriter(process.stderr),
);
subprocess.on("close", async (code) => {
// We wait for the io pipelines to finish before resolving the compilation promise
await finished(stdoutPipeline);
await finished(stderrPipeline);
// Explicitly closing the file handle to fully release the underlying resources
await stdoutFileHandle.close();
if (code !== 0) {
return reject(new Error(`Subprocess exited with code ${code}`));
}
resolve(await readJsonFileAsStream(stdoutPath));
});
assertHardhatInvariant(
subprocess.stdin !== null,
"process.stdin is null",
);
subprocess.stdin.write(JSON.stringify(input));
subprocess.stdin.end();
});
} finally {
await remove(tmpFolder);
}
}
export class SolcJsCompiler implements Compiler {
public readonly isSolcJs = true;
constructor(
public readonly version: string,
public readonly longVersion: string,
public readonly compilerPath: string,
) {}
public async compile(input: CompilerInput): Promise<CompilerOutput> {
const scriptFileUrl = import.meta.resolve("./solcjs-runner.js");
const scriptPath = fileURLToPath(scriptFileUrl);
// If the script is a TypeScript file, we need to pass the --import tsx/esm
// which is available, as we are running the tests
const nodeOptions = scriptPath.endsWith(".ts")
? ["--import", import.meta.resolve("tsx/esm")]
: [];
const args = [...nodeOptions];
// NOTE(https://github.com/nodejs/node/issues/31710): We're using file URLs
// on Windows instead of path because only URLs with a scheme are supported
// by the default ESM loader there.
if (os.platform() === "win32") {
const compilerFileUrl = pathToFileURL(this.compilerPath);
// NOTE: The script path passed to a tsx/esm loader is an exception to the
// above rule since the tsx/esm loader doesn't support URLs with a scheme.
args.push(scriptPath);
args.push(compilerFileUrl.href);
} else {
args.push(scriptPath, this.compilerPath);
}
try {
return await spawnCompile(process.execPath, args, input);
} catch (e) {
ensureError(e);
// We pack any error encountered during the compilation process into a HardhatError
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.CANT_RUN_SOLCJS_COMPILER,
e,
);
}
}
}
export class NativeCompiler implements Compiler {
public readonly isSolcJs = false;
constructor(
public readonly version: string,
public readonly longVersion: string,
public readonly compilerPath: string,
) {}
public async compile(input: CompilerInput): Promise<CompilerOutput> {
const args = ["--standard-json"];
// Logic to make sure that solc default import callback is not being used.
// If solcVersion is not defined or <= 0.6.8, do not add extra args.
if (this.version !== undefined) {
if (semver.gte(this.version, "0.8.22")) {
// version >= 0.8.22
args.push("--no-import-callback");
} else if (semver.gte(this.version, "0.6.9")) {
// version >= 0.6.9
const tmpFolder = await mkdtemp("hardhat-solc-");
args.push(`--base-path`);
args.push(tmpFolder);
}
}
try {
return await spawnCompile(this.compilerPath, args, input);
} catch (e) {
ensureError(e);
// We pack any error encountered during the compilation process into a HardhatError
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.CANT_RUN_NATIVE_COMPILER,
e,
);
}
}
}