hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
1,167 lines (973 loc) • 36.6 kB
text/typescript
import type { CompileCache } from "./cache.js";
import type { Compiler } from "./compiler/compiler.js";
import type { DependencyGraphImplementation } from "./dependency-graph.js";
import type { Artifact } from "../../../../types/artifacts.js";
import type { SolcConfig, SolidityConfig } from "../../../../types/config.js";
import type { HookManager } from "../../../../types/hooks.js";
import type {
SolidityBuildSystem,
BuildOptions,
CompilationJobCreationError,
FileBuildResult,
GetCompilationJobsOptions,
CompileBuildInfoOptions,
RunCompilationJobOptions,
GetCompilationJobsResult,
EmitArtifactsResult,
RunCompilationJobResult,
BuildScope,
} from "../../../../types/solidity/build-system.js";
import type { CompilationJob } from "../../../../types/solidity/compilation-job.js";
import type {
CompilerOutput,
CompilerOutputError,
} from "../../../../types/solidity/compiler-io.js";
import type { SolidityBuildInfo } from "../../../../types/solidity.js";
import os from "node:os";
import path from "node:path";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import {
exists,
ensureDir,
getAllDirectoriesMatching,
getAllFilesMatching,
move,
readJsonFile,
remove,
writeJsonFile,
writeJsonFileAsStream,
writeUtf8File,
} from "@nomicfoundation/hardhat-utils/fs";
import { shortenPath } from "@nomicfoundation/hardhat-utils/path";
import { createSpinner } from "@nomicfoundation/hardhat-utils/spinner";
import { pluralize } from "@nomicfoundation/hardhat-utils/string";
import chalk from "chalk";
import debug from "debug";
import pMap from "p-map";
import { FileBuildResultType } from "../../../../types/solidity/build-system.js";
import { DEFAULT_BUILD_PROFILE } from "../build-profiles.js";
import {
getArtifactsDeclarationFile,
getBuildInfo,
getBuildInfoOutput,
getContractArtifact,
getDuplicatedContractNamesDeclarationFile,
} from "./artifacts.js";
import { loadCache, saveCache } from "./cache.js";
import { CompilationJobImplementation } from "./compilation-job.js";
import { downloadConfiguredCompilers, getCompiler } from "./compiler/index.js";
import { buildDependencyGraph } from "./dependency-graph-building.js";
import { readSourceFileFactory } from "./read-source-file.js";
import {
formatRootPath,
isNpmParsedRootPath,
npmModuleToNpmRootPath,
parseRootPath,
} from "./root-paths-utils.js";
import { SolcConfigSelector } from "./solc-config-selection.js";
const log = debug("hardhat:core:solidity:build-system");
interface CompilationResult {
compilationJob: CompilationJob;
compilerOutput: CompilerOutput;
cached: boolean;
compiler: Compiler;
}
export interface SolidityBuildSystemOptions {
readonly solidityConfig: SolidityConfig;
readonly projectRoot: string;
readonly soliditySourcesPaths: string[];
readonly artifactsPath: string;
readonly cachePath: string;
readonly solidityTestsPath: string;
}
export class SolidityBuildSystemImplementation implements SolidityBuildSystem {
readonly #hooks: HookManager;
readonly #options: SolidityBuildSystemOptions;
#compileCache: CompileCache = {};
#downloadedCompilers = false;
constructor(hooks: HookManager, options: SolidityBuildSystemOptions) {
this.#hooks = hooks;
this.#options = options;
}
public async getScope(fsPath: string): Promise<BuildScope> {
if (
fsPath.startsWith(this.#options.solidityTestsPath) &&
fsPath.endsWith(".sol")
) {
return "tests";
}
for (const sourcesPath of this.#options.soliditySourcesPaths) {
if (fsPath.startsWith(sourcesPath) && fsPath.endsWith(".t.sol")) {
return "tests";
}
}
return "contracts";
}
public async getRootFilePaths(
options: { scope?: BuildScope } = {},
): Promise<string[]> {
const scope = options.scope ?? "contracts";
switch (scope) {
case "contracts":
const localFilesToCompile = (
await Promise.all(
this.#options.soliditySourcesPaths.map((dir) =>
getAllFilesMatching(
dir,
(f) => f.endsWith(".sol") && !f.endsWith(".t.sol"),
),
),
)
).flat(1);
const npmFilesToBuild =
this.#options.solidityConfig.npmFilesToBuild.map(
npmModuleToNpmRootPath,
);
return [...localFilesToCompile, ...npmFilesToBuild];
case "tests":
let rootFilePaths = (
await Promise.all([
getAllFilesMatching(this.#options.solidityTestsPath, (f) =>
f.endsWith(".sol"),
),
...this.#options.soliditySourcesPaths.map(async (dir) => {
return getAllFilesMatching(dir, (f) => f.endsWith(".t.sol"));
}),
])
).flat(1);
// NOTE: We remove duplicates in case there is an intersection between
// the tests.solidity paths and the sources paths
rootFilePaths = Array.from(new Set(rootFilePaths));
return rootFilePaths;
}
}
public async build(
rootFilePaths: string[],
_options?: BuildOptions,
): Promise<CompilationJobCreationError | Map<string, FileBuildResult>> {
const options: Required<BuildOptions> = {
buildProfile: DEFAULT_BUILD_PROFILE,
concurrency: Math.max(os.cpus().length - 1, 1),
force: false,
isolated: false,
quiet: false,
scope: "contracts",
..._options,
};
await this.#downloadConfiguredCompilers(options.quiet);
const { buildProfile } = this.#getBuildProfile(options.buildProfile);
const compilationJobsResult = await this.getCompilationJobs(
rootFilePaths,
options,
);
if ("reason" in compilationJobsResult) {
return compilationJobsResult;
}
const spinner = createSpinner({
text: `Compiling your Solidity ${options.scope}...`,
enabled: true,
});
spinner.start();
try {
const { compilationJobsPerFile, indexedIndividualJobs } =
compilationJobsResult;
const runnableCompilationJobs = [
...new Set(compilationJobsPerFile.values()),
];
// NOTE: We precompute the build ids in parallel here, which are cached
// internally in each compilation job
await Promise.all(
runnableCompilationJobs.map(async (runnableCompilationJob) =>
runnableCompilationJob.getBuildId(),
),
);
const results: CompilationResult[] = await pMap(
runnableCompilationJobs,
async (runnableCompilationJob) => {
const { output, compiler } = await this.runCompilationJob(
runnableCompilationJob,
options,
);
return {
compilationJob: runnableCompilationJob,
compilerOutput: output,
cached: false,
compiler,
};
},
{
concurrency: options.concurrency,
// An error when running the compiler is not a compilation failure, but
// a fatal failure trying to run it, so we just throw on the first error
stopOnError: true,
},
);
const uncachedResults = results.filter((result) => !result.cached);
const uncachedSuccessfulResults = uncachedResults.filter(
(result) => !this.#hasCompilationErrors(result.compilerOutput),
);
const isSuccessfulBuild =
uncachedResults.length === uncachedSuccessfulResults.length;
const contractArtifactsGeneratedByCompilationJob: Map<
CompilationJob,
ReadonlyMap<string, string[]>
> = new Map();
if (isSuccessfulBuild) {
log("Emitting artifacts of successful build");
await Promise.all(
results.map(async (compilationResult) => {
const emitArtifactsResult = await this.emitArtifacts(
compilationResult.compilationJob,
compilationResult.compilerOutput,
options,
);
const { artifactsPerFile } = emitArtifactsResult;
contractArtifactsGeneratedByCompilationJob.set(
compilationResult.compilationJob,
artifactsPerFile,
);
// Cache the results
await this.#cacheCompilationResult(
indexedIndividualJobs,
compilationResult,
emitArtifactsResult,
buildProfile.isolated,
options.scope,
);
}),
);
await saveCache(this.#options.cachePath, this.#compileCache);
}
spinner.stop();
const resultsMap: Map<string, FileBuildResult> = new Map();
for (const result of results) {
const contractArtifactsGenerated = isSuccessfulBuild
? contractArtifactsGeneratedByCompilationJob.get(
result.compilationJob,
)
: new Map();
assertHardhatInvariant(
contractArtifactsGenerated !== undefined,
"We emitted contract artifacts for all the jobs if the build was successful",
);
const errors = await Promise.all(
(result.compilerOutput.errors ?? []).map((error) =>
this.remapCompilerError(result.compilationJob, error, true),
),
);
this.#printSolcErrorsAndWarnings(errors);
const successfulResult = !this.#hasCompilationErrors(
result.compilerOutput,
);
for (const [
userSourceName,
root,
] of result.compilationJob.dependencyGraph.getRoots().entries()) {
if (!successfulResult) {
resultsMap.set(formatRootPath(userSourceName, root), {
type: FileBuildResultType.BUILD_FAILURE,
compilationJob: result.compilationJob,
errors,
});
continue;
}
if (result.cached) {
resultsMap.set(formatRootPath(userSourceName, root), {
type: FileBuildResultType.CACHE_HIT,
compilationJob: result.compilationJob,
contractArtifactsGenerated:
contractArtifactsGenerated.get(userSourceName) ?? [],
warnings: errors,
});
continue;
}
resultsMap.set(formatRootPath(userSourceName, root), {
type: FileBuildResultType.BUILD_SUCCESS,
compilationJob: result.compilationJob,
contractArtifactsGenerated:
contractArtifactsGenerated.get(userSourceName) ?? [],
warnings: errors,
});
}
}
if (!options.quiet) {
if (isSuccessfulBuild) {
await this.#printCompilationResult(runnableCompilationJobs, {
scope: options.scope,
});
}
}
return resultsMap;
} finally {
spinner.stop();
}
}
public async getCompilationJobs(
rootFilePaths: string[],
options?: GetCompilationJobsOptions,
): Promise<CompilationJobCreationError | GetCompilationJobsResult> {
await this.#downloadConfiguredCompilers(options?.quiet);
const dependencyGraph = await buildDependencyGraph(
rootFilePaths.toSorted(), // We sort them to have a deterministic order
this.#options.projectRoot,
readSourceFileFactory(this.#hooks),
);
const { buildProfileName, buildProfile } = this.#getBuildProfile(
options?.buildProfile,
);
log(`Using build profile ${buildProfileName}`);
const solcConfigSelector = new SolcConfigSelector(
buildProfileName,
buildProfile,
dependencyGraph,
);
let subgraphsWithConfig: Array<
[SolcConfig, DependencyGraphImplementation]
> = [];
for (const [rootFile, resolvedFile] of dependencyGraph.getRoots()) {
log(
`Building compilation job for root file ${rootFile} with input source name ${resolvedFile.inputSourceName} and user source name ${rootFile}`,
);
const subgraph = dependencyGraph.getSubgraph(rootFile);
const configOrError =
solcConfigSelector.selectBestSolcConfigForSingleRootGraph(subgraph);
if ("reason" in configOrError) {
return configOrError;
}
subgraphsWithConfig.push([configOrError, subgraph]);
}
// get longVersion and isWasm from the compiler for each version
const solcVersionToLongVersion = new Map<string, string>();
const versionIsWasm = new Map<string, boolean>();
for (const [solcConfig] of subgraphsWithConfig) {
let solcLongVersion = solcVersionToLongVersion.get(solcConfig.version);
if (solcLongVersion === undefined) {
const compiler = await getCompiler(solcConfig.version, {
preferWasm: buildProfile.preferWasm,
compilerPath: solcConfig.path,
});
solcLongVersion = compiler.longVersion;
solcVersionToLongVersion.set(solcConfig.version, solcLongVersion);
versionIsWasm.set(solcConfig.version, compiler.isSolcJs);
}
}
// build job for each root file. At this point subgraphsWithConfig are 1 root file each
const indexedIndividualJobs: Map<string, CompilationJob> = new Map();
const sharedContentHashes = new Map<string, string>();
await Promise.all(
subgraphsWithConfig.map(async ([config, subgraph]) => {
const solcLongVersion = solcVersionToLongVersion.get(config.version);
assertHardhatInvariant(
solcLongVersion !== undefined,
"solcLongVersion should not be undefined",
);
const individualJob = new CompilationJobImplementation(
subgraph,
config,
solcLongVersion,
this.#hooks,
sharedContentHashes,
);
await individualJob.getBuildId(); // precompute
assertHardhatInvariant(
subgraph.getRoots().size === 1,
"individual subgraph doesn't have exactly 1 root file",
);
const rootFilePath = Array.from(subgraph.getRoots().keys())[0];
indexedIndividualJobs.set(rootFilePath, individualJob);
}),
);
// Load the cache
this.#compileCache = await loadCache(this.#options.cachePath);
// Select which files to compile
const rootFilesToCompile: Set<string> = new Set();
const isolated = buildProfile.isolated;
for (const [rootFile, compilationJob] of indexedIndividualJobs.entries()) {
const jobHash = await compilationJob.getBuildId();
const cacheResult = this.#compileCache[rootFile];
const isWasm = versionIsWasm.get(compilationJob.solcConfig.version);
assertHardhatInvariant(
isWasm !== undefined,
`Version ${compilationJob.solcConfig.version} not present in isWasm map`,
);
// If there's no cache for the root file, or the compilation job changed, or using force flag, or isolated mode changed, compile it
if (
options?.force === true ||
cacheResult === undefined ||
cacheResult.jobHash !== jobHash ||
cacheResult.isolated !== isolated ||
cacheResult.wasm !== isWasm
) {
rootFilesToCompile.add(rootFile);
continue;
}
// If any of the emitted files are not present anymore, compile it
const {
artifactPaths,
buildInfoPath,
buildInfoOutputPath,
typeFilePath,
} = cacheResult;
for (const outputFilePath of [
...artifactPaths,
buildInfoPath,
buildInfoOutputPath,
typeFilePath,
]) {
// Type declaration file can be undefined (e.g. for solidity tests)
if (outputFilePath === undefined) {
continue;
}
if (!(await exists(outputFilePath))) {
rootFilesToCompile.add(rootFile);
break;
}
}
}
if (!isolated) {
// non-isolated mode
log(`Merging compilation jobs`);
const mergedSubgraphsByConfig: Map<
SolcConfig,
DependencyGraphImplementation
> = new Map();
// Note: This groups the subgraphs by solc config. It compares the configs
// based on reference, and not by deep equality. It misses some merging
// opportunities, but this is Hardhat v2's behavior and works well enough.
for (const [config, subgraph] of subgraphsWithConfig) {
assertHardhatInvariant(
subgraph.getRoots().size === 1,
"there should be only 1 root file on subgraph",
);
const rootFile = Array.from(subgraph.getRoots().keys())[0];
// Skip root files with cache hit (should not recompile)
if (!rootFilesToCompile.has(rootFile)) {
continue;
}
const mergedSubgraph = mergedSubgraphsByConfig.get(config);
if (mergedSubgraph === undefined) {
mergedSubgraphsByConfig.set(config, subgraph);
} else {
mergedSubgraphsByConfig.set(config, mergedSubgraph.merge(subgraph));
}
}
subgraphsWithConfig = [...mergedSubgraphsByConfig.entries()];
} else {
// isolated mode
subgraphsWithConfig = subgraphsWithConfig.filter(
([_config, subgraph]) => {
assertHardhatInvariant(
subgraph.getRoots().size === 1,
"there should be only 1 root file on subgraph",
);
const rootFile = Array.from(subgraph.getRoots().keys())[0];
return rootFilesToCompile.has(rootFile);
},
);
}
const compilationJobsPerFile = new Map<string, CompilationJob>();
for (const [solcConfig, subgraph] of subgraphsWithConfig) {
const solcLongVersion = solcVersionToLongVersion.get(solcConfig.version);
assertHardhatInvariant(
solcLongVersion !== undefined,
"solcLongVersion should not be undefined",
);
const runnableCompilationJob = new CompilationJobImplementation(
subgraph,
solcConfig,
solcLongVersion,
this.#hooks,
sharedContentHashes,
);
for (const [userSourceName, root] of subgraph.getRoots().entries()) {
compilationJobsPerFile.set(
formatRootPath(userSourceName, root),
runnableCompilationJob,
);
}
}
return { compilationJobsPerFile, indexedIndividualJobs };
}
#getBuildProfile(buildProfileName: string = DEFAULT_BUILD_PROFILE) {
const buildProfile =
this.#options.solidityConfig.profiles[buildProfileName];
if (buildProfile === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.BUILD_PROFILE_NOT_FOUND,
{
buildProfileName,
},
);
}
return { buildProfileName, buildProfile };
}
public async runCompilationJob(
runnableCompilationJob: CompilationJob,
options?: RunCompilationJobOptions,
): Promise<RunCompilationJobResult> {
await this.#downloadConfiguredCompilers(options?.quiet);
let numberOfFiles = 0;
for (const _ of runnableCompilationJob.dependencyGraph.getAllFiles()) {
numberOfFiles++;
}
const numberOfRootFiles =
runnableCompilationJob.dependencyGraph.getRoots().size;
const { buildProfile } = this.#getBuildProfile(options?.buildProfile);
const compiler = await getCompiler(
runnableCompilationJob.solcConfig.version,
{
preferWasm: buildProfile.preferWasm,
compilerPath: runnableCompilationJob.solcConfig.path,
},
);
log(
`Compiling ${numberOfRootFiles} root files and ${numberOfFiles - numberOfRootFiles} dependency files with solc ${runnableCompilationJob.solcConfig.version} using ${compiler.compilerPath}`,
);
assertHardhatInvariant(
runnableCompilationJob.solcLongVersion === compiler.longVersion,
"The long version of the compiler should match the long version of the compilation job",
);
const output = await compiler.compile(
await runnableCompilationJob.getSolcInput(),
);
return { output, compiler };
}
public async remapCompilerError(
runnableCompilationJob: CompilationJob,
error: CompilerOutputError,
shouldShortenPaths: boolean = false,
): Promise<CompilerOutputError> {
return {
type: error.type,
component: error.component,
message: error.message,
severity: error.severity,
errorCode: error.errorCode,
formattedMessage: error.formattedMessage?.replace(
/(-->\s+)([^\s:\n]+)/g,
(_match, prefix, inputSourceName) => {
const file =
runnableCompilationJob.dependencyGraph.getFileByInputSourceName(
inputSourceName,
);
if (file === undefined) {
return `${prefix}${inputSourceName}`;
}
const replacement = shouldShortenPaths
? shortenPath(file.fsPath)
: file.fsPath;
return `${prefix}${replacement}`;
},
),
};
}
public async emitArtifacts(
runnableCompilationJob: CompilationJob,
compilerOutput: CompilerOutput,
options: { scope?: BuildScope } = {},
): Promise<EmitArtifactsResult> {
const scope = options.scope ?? "contracts";
const artifactsPerFile = new Map<string, string[]>();
const typeFilePaths = new Map<string, string>();
const buildId = await runnableCompilationJob.getBuildId();
const artifactsDirectory = await this.getArtifactsDirectory(scope);
// We emit the artifacts for each root file, first emitting one artifact
// for each contract, and then one declaration file for the entire file,
// which defines their types and augments the ArtifactMap type.
for (const [userSourceName, root] of runnableCompilationJob.dependencyGraph
.getRoots()
.entries()) {
const fileFolder = path.join(artifactsDirectory, userSourceName);
// If the folder exists, we remove it first, as we don't want to leave
// any old artifacts there.
await remove(fileFolder);
const contracts = compilerOutput.contracts?.[root.inputSourceName];
const paths: string[] = [];
const artifacts: Artifact[] = [];
// This can be undefined if no contract is present in the source file
if (contracts !== undefined) {
for (const [contractName, contract] of Object.entries(contracts)) {
const contractArtifactPath = path.join(
fileFolder,
`${contractName}.json`,
);
const artifact = getContractArtifact(
buildId,
userSourceName,
root.inputSourceName,
contractName,
contract,
);
await writeUtf8File(
contractArtifactPath,
JSON.stringify(artifact, undefined, 2),
);
paths.push(contractArtifactPath);
artifacts.push(artifact);
}
}
artifactsPerFile.set(userSourceName, paths);
// Write the type declaration file, only for contracts
if (scope === "contracts") {
const artifactsDeclarationFilePath = path.join(
fileFolder,
"artifacts.d.ts",
);
typeFilePaths.set(userSourceName, artifactsDeclarationFilePath);
const artifactsDeclarationFile = getArtifactsDeclarationFile(artifacts);
await writeUtf8File(
artifactsDeclarationFilePath,
artifactsDeclarationFile,
);
}
}
// Once we have emitted all the contract artifacts and its declaration
// file, we emit the build info file and its output file.
const buildInfoId = buildId;
const buildInfoCacheDirPath = path.join(
this.#options.cachePath,
`build-info`,
);
await ensureDir(buildInfoCacheDirPath);
const buildInfoCachePath = path.join(
buildInfoCacheDirPath,
`${buildInfoId}.json`,
);
const buildInfoOutputCachePath = path.join(
buildInfoCacheDirPath,
`${buildInfoId}.output.json`,
);
// BuildInfo and BuildInfoOutput files are large, so we write them
// concurrently, and keep their lifetimes separated and small.
// NOTE: First, we write the build info file and its output to the cache
// directory. Once both are successfully written, we move them to the
// artifacts directory sequentially, ensuring the build info file is moved
// last. This approach minimizes the risk of having corrupted build info
// files in the artifacts directory and ensures other processes, like
// `hardhat node`, can safely monitor the build info file as an indicator
// for build completion.
await Promise.all([
(async () => {
const buildInfo = await getBuildInfo(runnableCompilationJob);
// TODO: Maybe formatting the build info is slow, but it's mostly
// strings, so it probably shouldn't be a problem.
await writeJsonFile(buildInfoCachePath, buildInfo);
})(),
(async () => {
const buildInfoOutput = await getBuildInfoOutput(
runnableCompilationJob,
compilerOutput,
);
// NOTE: We use writeJsonFileAsStream here because the build info output might exceed
// the maximum string length.
// TODO: Earlier in the build process, very similar files are created on disk by the
// Compiler. Instead of creating them again, we should consider copying/moving them.
// This would require changing the format of the build info output file.
await writeJsonFileAsStream(buildInfoOutputCachePath, buildInfoOutput);
})(),
]);
const buildInfoDirPath = path.join(artifactsDirectory, `build-info`);
await ensureDir(buildInfoDirPath);
const buildInfoPath = path.join(buildInfoDirPath, `${buildInfoId}.json`);
const buildInfoOutputPath = path.join(
buildInfoDirPath,
`${buildInfoId}.output.json`,
);
await move(buildInfoOutputCachePath, buildInfoOutputPath);
await move(buildInfoCachePath, buildInfoPath);
return {
artifactsPerFile,
buildInfoPath,
buildInfoOutputPath,
typeFilePaths,
};
}
public async getArtifactsDirectory(scope: BuildScope): Promise<string> {
return scope === "contracts"
? this.#options.artifactsPath
: path.join(this.#options.cachePath, "test-artifacts");
}
public async cleanupArtifacts(
rootFilePaths: string[],
options: { scope?: BuildScope } = {},
): Promise<void> {
log(`Cleaning up artifacts`);
const scope = options.scope ?? "contracts";
const artifactsDirectory = await this.getArtifactsDirectory(scope);
const userSourceNames = rootFilePaths.map((rootFilePath) => {
const parsed = parseRootPath(rootFilePath);
return isNpmParsedRootPath(parsed)
? parsed.npmPath
: toForwardSlash(
path.relative(this.#options.projectRoot, parsed.fsPath),
);
});
const userSourceNamesSet = new Set(userSourceNames);
for (const file of await getAllDirectoriesMatching(
artifactsDirectory,
(d) => d.endsWith(".sol"),
)) {
const relativePath = toForwardSlash(
path.relative(artifactsDirectory, file),
);
if (!userSourceNamesSet.has(relativePath)) {
await remove(file);
}
}
const buildInfosDir = path.join(artifactsDirectory, `build-info`);
// TODO: This logic is duplicated with respect to the artifacts manager
const artifactPaths = await getAllFilesMatching(
artifactsDirectory,
(p) =>
p.endsWith(".json") && // Only consider json files
// Ignore top level json files
p.indexOf(path.sep, artifactsDirectory.length + path.sep.length) !== -1,
(dir) => dir !== buildInfosDir,
);
const reachableBuildInfoIds = await Promise.all(
artifactPaths.map(async (artifactPath) => {
const artifact: Artifact = await readJsonFile(artifactPath);
return artifact.buildInfoId;
}),
);
const reachableBuildInfoIdsSet = new Set(
reachableBuildInfoIds.filter((id) => id !== undefined),
);
// Get all the reachable build info files
const buildInfoFiles = await getAllFilesMatching(buildInfosDir, (f) =>
f.startsWith(buildInfosDir),
);
for (const buildInfoFile of buildInfoFiles) {
const basename = path.basename(buildInfoFile);
const id = basename.substring(0, basename.indexOf("."));
if (!reachableBuildInfoIdsSet.has(id)) {
await remove(buildInfoFile);
}
}
// These steps only apply when compiling contracts
if (scope === "contracts") {
// Get duplicated contract names and write a top-level artifacts.d.ts file
const artifactNameCounts = new Map<string, number>();
for (const artifactPath of artifactPaths) {
const basename = path.basename(artifactPath);
const name = basename.substring(0, basename.indexOf("."));
const count = artifactNameCounts.get(name) ?? 0;
artifactNameCounts.set(name, count + 1);
}
const duplicatedNames = [...artifactNameCounts.entries()]
.filter(([_, count]) => count > 1)
.map(([name, _]) => name);
const duplicatedContractNamesDeclarationFilePath = path.join(
artifactsDirectory,
"artifacts.d.ts",
);
await writeUtf8File(
duplicatedContractNamesDeclarationFilePath,
getDuplicatedContractNamesDeclarationFile(duplicatedNames),
);
// Run the onCleanUpArtifacts hook
await this.#hooks.runHandlerChain(
"solidity",
"onCleanUpArtifacts",
[artifactPaths],
async () => {},
);
}
}
public async compileBuildInfo(
_buildInfo: SolidityBuildInfo,
_options?: CompileBuildInfoOptions,
): Promise<CompilerOutput> {
// TODO: Download the buildinfo compiler version
assertHardhatInvariant(false, "Method not implemented.");
}
async #downloadConfiguredCompilers(quiet = false): Promise<void> {
// TODO: For the alpha release, we always print this message
quiet = false;
if (this.#downloadedCompilers) {
return;
}
await downloadConfiguredCompilers(this.#getAllCompilerVersions(), quiet);
this.#downloadedCompilers = true;
}
#getAllCompilerVersions(): Set<string> {
return new Set(
Object.values(this.#options.solidityConfig.profiles)
.map((profile) => [
...profile.compilers.map((compiler) => compiler.version),
...Object.values(profile.overrides).map(
(override) => override.version,
),
])
.flat(1),
);
}
#isConsoleLogError(error: CompilerOutputError): boolean {
const message = error.message;
return (
error.type === "TypeError" &&
typeof message === "string" &&
message.includes("log") &&
message.includes("type(library console)")
);
}
#hasCompilationErrors(output: CompilerOutput): boolean {
return output.errors?.some((x: any) => x.severity === "error") ?? false;
}
/**
* This function returns a properly formatted Internal Compiler Error message.
*
* This is present due to a bug in Solidity. See: https://github.com/ethereum/solidity/issues/9926
*
* If the error is not an ICE, or if it's properly formatted, this function returns undefined.
*/
#getFormattedInternalCompilerErrorMessage(
error: CompilerOutputError,
): string | undefined {
if (error.formattedMessage?.trim() !== "InternalCompilerError:") {
return;
}
// We trim any final `:`, as we found some at the end of the error messages,
// and then trim just in case a blank space was left
return `${error.type}: ${error.message}`.replace(/[:\s]*$/g, "").trim();
}
async #cacheCompilationResult(
indexedIndividualJobs: Map<string, CompilationJob>,
result: CompilationResult,
emitArtifactsResult: EmitArtifactsResult,
isolated: boolean,
scope: BuildScope,
): Promise<void> {
const rootFilePaths = result.compilationJob.dependencyGraph
.getRoots()
.keys();
for (const rootFilePath of rootFilePaths) {
const individualJob = indexedIndividualJobs.get(rootFilePath);
assertHardhatInvariant(
individualJob !== undefined,
"Failed to get individual job from compiled job",
);
const artifactPaths =
emitArtifactsResult.artifactsPerFile.get(rootFilePath);
assertHardhatInvariant(
artifactPaths !== undefined,
`No artifacts found on map for ${rootFilePath}`,
);
const typeFilePath = emitArtifactsResult.typeFilePaths.get(rootFilePath);
// Type declaration file is not generated for solidity tests
assertHardhatInvariant(
scope === "tests" || typeFilePath !== undefined,
`No type file found on map for contract ${rootFilePath}`,
);
const jobHash = await individualJob.getBuildId();
this.#compileCache[rootFilePath] = {
jobHash,
isolated,
artifactPaths,
buildInfoPath: emitArtifactsResult.buildInfoPath,
buildInfoOutputPath: emitArtifactsResult.buildInfoOutputPath,
typeFilePath,
wasm: result.compiler.isSolcJs,
};
}
}
#printSolcErrorsAndWarnings(errors?: CompilerOutputError[]): void {
if (errors === undefined) {
return;
}
console.log();
for (const error of errors) {
if (error.severity === "error") {
const errorMessage: string =
this.#getFormattedInternalCompilerErrorMessage(error) ??
error.formattedMessage ??
error.message;
console.error(
errorMessage.replace(/^\w+:/, (t) => chalk.red.bold(t)).trimEnd() +
"\n",
);
} else {
console.warn(
(error.formattedMessage ?? error.message)
.replace(/^\w+:/, (t) => chalk.yellow.bold(t))
.trimEnd() + "\n",
);
}
}
const hasConsoleErrors: boolean = errors.some((e) =>
this.#isConsoleLogError(e),
);
if (hasConsoleErrors) {
console.error(
chalk.red(
`The console.log call you made isn't supported. See https://hardhat.org/console-log for the list of supported methods.`,
),
);
console.log();
}
}
async #printCompilationResult(
runnableCompilationJobs: CompilationJob[],
options: { scope: BuildScope },
) {
const jobsPerVersionAndEvmVersion = new Map<
string,
Map<string, CompilationJob[]>
>();
if (runnableCompilationJobs.length === 0) {
if (options.scope === "contracts") {
console.log("No contracts to compile");
} else {
console.log("No Solidity tests to compile");
}
return;
}
for (const job of runnableCompilationJobs) {
const solcVersion = job.solcConfig.version;
const solcInput = await job.getSolcInput();
const evmVersion =
solcInput.settings.evmVersion ??
`Check solc ${solcVersion}'s doc for its default evm version`;
let jobsPerVersion = jobsPerVersionAndEvmVersion.get(solcVersion);
if (jobsPerVersion === undefined) {
jobsPerVersion = new Map();
jobsPerVersionAndEvmVersion.set(solcVersion, jobsPerVersion);
}
let jobsPerEvmVersion = jobsPerVersion.get(evmVersion);
if (jobsPerEvmVersion === undefined) {
jobsPerEvmVersion = [];
jobsPerVersion.set(evmVersion, jobsPerEvmVersion);
}
jobsPerEvmVersion.push(job);
}
for (const solcVersion of [...jobsPerVersionAndEvmVersion.keys()].sort()) {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion --
This is a valid key, just sorted */
const jobsPerEvmVersion = jobsPerVersionAndEvmVersion.get(solcVersion)!;
for (const evmVersion of [...jobsPerEvmVersion.keys()].sort()) {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion --
This is a valid key, just sorted */
const jobs = jobsPerEvmVersion.get(evmVersion)!;
const rootFiles = jobs.reduce(
(count, job) => count + job.dependencyGraph.getRoots().size,
0,
);
console.log(
chalk.bold(
`Compiled ${rootFiles} Solidity ${pluralize(
options.scope === "contracts" ? "file" : "test file",
rootFiles,
)} with solc ${solcVersion}`,
),
`(evm target: ${evmVersion})`,
);
}
}
}
}
function toForwardSlash(str: string): string {
return str.split(/[\\\/]/).join(path.posix.sep);
}