hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
200 lines • 10.7 kB
JavaScript
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, } from "../../../../types/solidity.js";
import { DEFAULT_OUTPUT_SELECTION } from "../constants.js";
import { getEvmVersionFromSolcVersion } from "./solc-info.js";
export class CompilationJobImplementation {
dependencyGraph;
solcConfig;
solcLongVersion;
toolVersions;
#hooks;
// 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
#sharedContentHashes;
#buildId;
#solcInput;
constructor(dependencyGraph, solcConfig, solcLongVersion, hooks, sharedContentHashes = new Map(), toolVersions) {
this.dependencyGraph = dependencyGraph;
this.solcConfig = solcConfig;
this.solcLongVersion = solcLongVersion;
this.#hooks = hooks;
this.#sharedContentHashes = sharedContentHashes;
this.toolVersions = toolVersions;
}
async getSolcInput() {
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;
}
async getBuildId() {
if (this.#buildId === undefined) {
this.#buildId = await this.#computeBuildId();
}
return this.#buildId;
}
async #getFileContent(file) {
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() {
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(...DEFAULT_OUTPUT_SELECTION["*"][""]);
outputSelection["*"]["*"].push(...DEFAULT_OUTPUT_SELECTION["*"]["*"]);
const sources = {};
// 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 = {
...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)));
return {
language: "Solidity",
settings: sortedSettings,
sources,
};
}
#dedupeAndSortOutputSelection(outputSelection) {
const dedupedOutputSelection = {};
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() {
// NOTE: We type it this way so that this stop compiling if we ever change
// the format of the BuildInfo type.
const 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 compilerType = this.solcConfig.type;
// We normalize solcConfig.type to `undefined` so that "solc" and undefined
// produce the same hash, for backwards compatibility.
const normalizedSolcConfig = { ...this.solcConfig, type: undefined };
const preimageObject = {
format,
solcLongVersion: this.solcLongVersion,
smallerSolcInput,
solcConfig: normalizedSolcConfig,
userSourceNameMap: this.dependencyGraph.getRootsUserSourceNameMap(),
};
// Include compiler type in the preimage for non-solc types, so that
// different compiler types produce different build IDs.
if (compilerType !== undefined && compilerType !== "solc") {
preimageObject.compilerType = compilerType;
}
// Include tool versions in the preimage when present, so that
// different tool versions produce different build IDs.
if (this.toolVersions !== undefined) {
preimageObject.toolVersions = this.toolVersions;
}
const preimage = JSON.stringify(preimageObject);
const jobHash = await createNonCryptographicHashId(preimage);
const versionPart = this.solcConfig.version.replaceAll(".", "_");
// For non-solc compiler types, include the compiler type in the build ID.
// We keep the `solc-` prefix for all types to avoid breaking codepaths
// that look for it.
if (compilerType !== undefined && compilerType !== "solc") {
/* eslint-disable-next-line @typescript-eslint/restrict-template-expressions --
compilerType is `never` in the base type system (only "solc" is registered),
but plugins can extend SolidityCompilerConfigPerType to add new compiler types. */
return `solc-${versionPart}-${compilerType}-${jobHash}`;
}
return `solc-${versionPart}-${jobHash}`;
}
#getSourceContentHash(sourceName, text) {
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;
}
}
//# sourceMappingURL=compilation-job.js.map