UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

263 lines (226 loc) 9.84 kB
import type { DependencyGraphImplementation } from "./dependency-graph.js"; import type { BuildInfo } from "../../../../types/artifacts.js"; import type { SolcConfig } from "../../../../types/config.js"; import type { HookManager } from "../../../../types/hooks.js"; import type { CompilationJob } from "../../../../types/solidity/compilation-job.js"; import type { CompilerInput } from "../../../../types/solidity/compiler-io.js"; import type { DependencyGraph } from "../../../../types/solidity/dependency-graph.js"; import { createHash } from "node:crypto"; import { HardhatError } from "@nomicfoundation/hardhat-errors"; import { createNonCryptographicHashId } from "@nomicfoundation/hardhat-utils/crypto"; import { deepClone } from "@nomicfoundation/hardhat-utils/lang"; import { ResolvedFileType, type ResolvedFile, } from "../../../../types/solidity.js"; import { getEvmVersionFromSolcVersion } from "./solc-info.js"; export class CompilationJobImplementation implements CompilationJob { public readonly dependencyGraph: DependencyGraph; public readonly solcConfig: SolcConfig; public readonly solcLongVersion: string; readonly #hooks: HookManager; // This map is shared across compilation jobs and is meant to store content hashes of source files // It is used to speed up the hashing of compilation jobs readonly #sharedContentHashes: Map<string, string>; #buildId: string | undefined; #solcInput: CompilerInput | undefined; constructor( dependencyGraph: DependencyGraphImplementation, solcConfig: SolcConfig, solcLongVersion: string, hooks: HookManager, sharedContentHashes: Map<string, string> = new Map(), ) { this.dependencyGraph = dependencyGraph; this.solcConfig = solcConfig; this.solcLongVersion = solcLongVersion; this.#hooks = hooks; this.#sharedContentHashes = sharedContentHashes; } public async getSolcInput(): Promise<CompilerInput> { if (this.#solcInput === undefined) { const solcInput = await this.#buildSolcInput(); // NOTE: We run the solc input via the hook handler chain to allow plugins // to modify it before it is passed to solc. Originally, we use it to // insert the coverage.sol file into the solc input sources when coverage // feature is enabled. this.#solcInput = await this.#hooks.runHandlerChain( "solidity", "preprocessSolcInputBeforeBuilding", [solcInput], async (_context, nextSolcInput) => { return nextSolcInput; }, ); } return this.#solcInput; } public async getBuildId(): Promise<string> { if (this.#buildId === undefined) { this.#buildId = await this.#computeBuildId(); } return this.#buildId; } async #getFileContent(file: ResolvedFile): Promise<string> { switch (file.type) { case ResolvedFileType.NPM_PACKAGE_FILE: // NOTE: We currently don't allow custom npm package file preprocessing // because we don't have a use case for it yet. return file.content.text; case ResolvedFileType.PROJECT_FILE: const solcVersion = this.solcConfig.version; // NOTE: We run the project file content via the hook handler chain to allow // plugins to modify it before it is passed to solc. Originally, we use it to // instrument the project file content when coverage feature is enabled. // We pass some additional data via the chain - i.e. the input source name and solc // version - but we expect any handlers to pass them on as-is without modification. return this.#hooks.runHandlerChain( "solidity", "preprocessProjectFileBeforeBuilding", [file.inputSourceName, file.fsPath, file.content.text, solcVersion], async ( _context, nextInputSourceName, nextFsPath, nextFileContent, nextSolcVersion, ) => { for (const [paramName, expectedParamValue, actualParamValue] of [ ["inputSourceName", file.inputSourceName, nextInputSourceName], ["fsPath", file.fsPath, nextFsPath], ["solcVersion", solcVersion, nextSolcVersion], ]) { if (expectedParamValue !== actualParamValue) { throw new HardhatError( HardhatError.ERRORS.CORE.HOOKS.UNEXPECTED_HOOK_PARAM_MODIFICATION, { hookCategoryName: "solidity", hookName: "preprocessProjectFileBeforeBuilding", paramName, }, ); } } return nextFileContent; }, ); } } async #buildSolcInput(): Promise<CompilerInput> { const settings = this.solcConfig.settings; // Ideally we would be more selective with the output selection, so that // we only ask solc to compile the root files. // Unfortunately, solc may need to generate bytecode of contracts/libraries // from other files (e.g. new Foo()), and it won't output its bytecode if // it's not asked for. This would prevent EDR from doing any runtime // analysis. const outputSelection = await deepClone(settings.outputSelection ?? {}); outputSelection["*"] ??= {}; outputSelection["*"][""] ??= []; outputSelection["*"]["*"] ??= []; outputSelection["*"][""].push("ast"); outputSelection["*"]["*"].push( "abi", "evm.bytecode", "evm.deployedBytecode", "evm.methodIdentifiers", "metadata", ); const sources: { [sourceName: string]: { content: string } } = {}; // we sort the files so that we always get the same compilation input const resolvedFiles = [...this.dependencyGraph.getAllFiles()].sort((a, b) => a.inputSourceName.localeCompare(b.inputSourceName), ); for (const file of resolvedFiles) { const content = await this.#getFileContent(file); sources[file.inputSourceName] = { content, }; } const resolvedSettings: CompilerInput["settings"] = { ...settings, evmVersion: settings.evmVersion ?? getEvmVersionFromSolcVersion(this.solcConfig.version), outputSelection: this.#dedupeAndSortOutputSelection(outputSelection), remappings: this.dependencyGraph.getAllRemappings(), }; /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We just sort the same object to make the builds more consistent */ const sortedSettings = Object.fromEntries( Object.entries(resolvedSettings).sort(([keyA], [keyB]) => keyA.localeCompare(keyB), ), ) as CompilerInput["settings"]; return { language: "Solidity", settings: sortedSettings, sources, }; } #dedupeAndSortOutputSelection( outputSelection: CompilerInput["settings"]["outputSelection"], ): CompilerInput["settings"]["outputSelection"] { const dedupedOutputSelection: CompilerInput["settings"]["outputSelection"] = {}; for (const sourceName of Object.keys(outputSelection).sort()) { dedupedOutputSelection[sourceName] = {}; const contracts = outputSelection[sourceName]; for (const contractName of Object.keys(contracts).sort()) { const selectors = contracts[contractName]; dedupedOutputSelection[sourceName][contractName] = Array.from( new Set(selectors), ).sort(); } } return dedupedOutputSelection; } async #computeBuildId(): Promise<string> { // NOTE: We type it this way so that this stop compiling if we ever change // the format of the BuildInfo type. const format: BuildInfo["_format"] = "hh3-sol-build-info-1"; const solcInput = await this.getSolcInput(); const smallerSolcInput = { ...solcInput }; // We replace the source files content with their hashes for speeding up the build id computation smallerSolcInput.sources = Object.fromEntries( Object.entries(solcInput.sources).map(([sourceName, _source]) => [ sourceName, { content: this.#getSourceContentHash(sourceName, _source.content) }, ]), ); // EXTREMELY IMPORTANT: The preimage should include **all** the information // that makes this compilation job unique from the point of view of Hardhat. // // Note that we can have multiple compilation jobs that are equivalent from // the point of view of solc, but not for Hardhat. (e.g. same input, // config, version, but different root files). // // Also note that we include the build info format here. Technically, this // violates the encapsulation of this class a bit. We could leave that // field out, and then recompute the BuildInfo id based on the compilation // job id and the BuildInfo format. We add it here instead to keep both // ids the same, and as a small performance optimization. // // Changing this shouldn't be taken lightly, as it makes reproducing // builds pretty difficult when upgrading Hardhat between versions that // change it. const preimage = JSON.stringify({ format, solcLongVersion: this.solcLongVersion, smallerSolcInput, solcConfig: this.solcConfig, userSourceNameMap: this.dependencyGraph.getRootsUserSourceNameMap(), }); const jobHash = await createNonCryptographicHashId(preimage); return `solc-${this.solcConfig.version.replaceAll(".", "_")}-${jobHash}`; } #getSourceContentHash(sourceName: string, text: string): any { let hash = this.#sharedContentHashes.get(sourceName); if (hash !== undefined) { return hash; } hash = createHash("sha1").update(text).digest("hex"); this.#sharedContentHashes.set(sourceName, hash); return hash; } }