UNPKG

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
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, ); } } }