hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
412 lines (358 loc) • 10.6 kB
text/typescript
import type {
BuildScope,
SolidityBuildSystem,
} from "../../../../types/solidity.js";
import type { NewTaskActionFunction } from "../../../../types/tasks.js";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import { resolveFromRoot } from "@nomicfoundation/hardhat-utils/path";
import { throwIfSolidityBuildFailed } from "../build-results.js";
import { isNpmRootPath } from "../build-system/root-paths-utils.js";
interface BuildActionArguments {
force: boolean;
files: string[];
quiet: boolean;
defaultBuildProfile: string;
noTests: boolean;
noContracts: boolean;
}
interface BuildActionResult {
contractRootPaths: string[];
testRootPaths: string[];
}
const buildAction: NewTaskActionFunction<BuildActionArguments> = async (
args: BuildActionArguments,
hre,
): Promise<BuildActionResult> => {
const buildProfile =
hre.globalOptions.buildProfile ?? args.defaultBuildProfile;
const files = normalizeRootPaths(args.files);
const partitionedFiles = await partitionRootPathsByScope(hre.solidity, files);
if (args.noContracts && partitionedFiles.contractRootPaths.length > 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.INCOMPATIBLE_FILES_WITH_BUILD_FLAGS,
{
files: partitionedFiles.contractRootPaths
.sort()
.map((f) => `- ${f}`)
.join("\n"),
},
);
}
if (args.noTests && partitionedFiles.testRootPaths.length > 0) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.INCOMPATIBLE_FILES_WITH_BUILD_FLAGS,
{
files: partitionedFiles.testRootPaths
.sort()
.map((f) => `- ${f}`)
.join("\n"),
},
);
}
if (hre.config.solidity.splitTestsCompilation) {
const contractRootPaths: string[] = [];
const testRootPaths: string[] = [];
const shouldBuildContracts =
!args.noContracts &&
(files.length === 0 || partitionedFiles.contractRootPaths.length > 0);
if (shouldBuildContracts) {
const contractBuildResults = await runSolidityBuild({
buildProfile,
files: partitionedFiles.contractRootPaths,
force: args.force,
isUnifiedModeOrScope: "contracts",
noContracts: args.noContracts,
noTests: args.noTests,
quiet: args.quiet,
solidity: hre.solidity,
});
assertHardhatInvariant(
contractBuildResults.testRootPaths.length === 0,
"The contracts scope should build no test in split test compilation mode",
);
contractRootPaths.push(...contractBuildResults.contractRootPaths);
testRootPaths.push(...contractBuildResults.testRootPaths);
}
const shouldBuildTests =
!args.noTests &&
(files.length === 0 || partitionedFiles.testRootPaths.length > 0);
if (shouldBuildTests) {
const testBuildResults = await runSolidityBuild({
buildProfile,
files: partitionedFiles.testRootPaths,
force: args.force,
isUnifiedModeOrScope: "tests",
noContracts: args.noContracts,
noTests: args.noTests,
quiet: args.quiet,
solidity: hre.solidity,
});
assertHardhatInvariant(
testBuildResults.contractRootPaths.length === 0,
"The tests scope should build no contract in split test compilation mode",
);
contractRootPaths.push(...testBuildResults.contractRootPaths);
testRootPaths.push(...testBuildResults.testRootPaths);
}
return { contractRootPaths, testRootPaths };
}
return runSolidityBuild({
buildProfile,
files,
force: args.force,
isUnifiedModeOrScope: true,
noContracts: args.noContracts,
noTests: args.noTests,
quiet: args.quiet,
solidity: hre.solidity,
});
};
/**
* Runs a solidity build for a scope/unified mode.
*
* Note: The files array should be pre-classified by scope if using split
* compilation. i.e. it should only include files of the scope being used.
*/
async function runSolidityBuild({
buildProfile,
files,
force,
isUnifiedModeOrScope,
noContracts,
noTests,
quiet,
solidity,
}: {
buildProfile: string;
files: string[];
force: boolean;
isUnifiedModeOrScope: true | BuildScope;
noContracts: boolean;
noTests: boolean;
quiet: boolean;
solidity: SolidityBuildSystem;
}): Promise<{ contractRootPaths: string[]; testRootPaths: string[] }> {
const scope =
isUnifiedModeOrScope === true ? "contracts" : isUnifiedModeOrScope;
const { isFullBuild, contractRootPaths, testRootPaths } =
await getRootsToBuild({
solidity,
isUnifiedModeOrScope,
files,
noTests,
noContracts,
});
// If there's nothing to build and this isn't a full build, we exit early.
// Full builds with no roots still need to run cleanup to remove stale
// artifacts.
if (
!isFullBuild &&
contractRootPaths.length === 0 &&
testRootPaths.length === 0
) {
return { contractRootPaths, testRootPaths };
}
const results = await solidity.build(
[...contractRootPaths, ...testRootPaths],
{
force,
buildProfile,
quiet,
scope,
},
);
throwIfSolidityBuildFailed(solidity, results);
// We use the result keys in case a hook added or removed root files
const builtRootPaths = [...results.keys()];
if (isFullBuild) {
await solidity.cleanupArtifacts(builtRootPaths, {
scope,
});
}
const preBuildRoots = new Set([...contractRootPaths, ...testRootPaths]);
if (
builtRootPaths.length === preBuildRoots.size &&
builtRootPaths.every((p) => preBuildRoots.has(p))
) {
return { contractRootPaths, testRootPaths };
}
return partitionRootPathsByScope(solidity, builtRootPaths);
}
/**
* Returns the files to build, classified by testRootPaths and
* contractRootPaths, and a boolean indicating if this represents a full build
* for the scope/unified build.
*
* Note: The files array should be pre-classified by scope if using split
* compilation. i.e. it should only include files of the scope being used.
*/
async function getRootsToBuild({
solidity,
isUnifiedModeOrScope,
files,
noTests,
noContracts,
}: {
solidity: SolidityBuildSystem;
isUnifiedModeOrScope: true | BuildScope;
files: string[];
noTests: boolean;
noContracts: boolean;
}): Promise<{
testRootPaths: string[];
contractRootPaths: string[];
isFullBuild: boolean;
}> {
if (isUnifiedModeOrScope === true) {
return getRootsToBuildInUnifiedMode({
files,
noContracts,
noTests,
solidity,
});
}
return getRootsToBuildForScope({
files,
scope: isUnifiedModeOrScope,
solidity,
});
}
/**
* Returns the root files to build in unified mode. While they are returned
* classified as contractRootPaths and testRootPaths, they are expected to be
* build together. It also returns a boolean indicating if this represents a
* full unified build.
*
* Note: The files array should be normalized already.
*/
async function getRootsToBuildInUnifiedMode({
files,
noContracts,
noTests,
solidity,
}: {
files: string[];
noContracts: boolean;
noTests: boolean;
solidity: SolidityBuildSystem;
}): Promise<{
testRootPaths: string[];
contractRootPaths: string[];
isFullBuild: boolean;
}> {
const isFullBuild = files.length === 0 && !noTests && !noContracts;
let rootFilePaths: string[];
if (isFullBuild) {
// In this mode, "contracts" also returns the tests
rootFilePaths = await solidity.getRootFilePaths({
scope: "contracts",
});
} else {
const allRoots =
files.length > 0
? files
: await solidity.getRootFilePaths({
scope: "contracts",
});
rootFilePaths = [];
for (const root of allRoots) {
if (isNpmRootPath(root)) {
// npm files are considered contract files, so we skip them if
// --no-contracts
if (!noContracts) {
rootFilePaths.push(root);
}
continue;
}
const scope = await solidity.getScope(root);
if (noTests && scope === "tests") {
continue;
}
if (noContracts && scope === "contracts") {
continue;
}
rootFilePaths.push(root);
}
}
const partitionedRootPaths = await partitionRootPathsByScope(
solidity,
rootFilePaths,
);
return {
isFullBuild,
...partitionedRootPaths,
};
}
/**
* Returns the root files to build for a certain scope, and a boolean indicating
* if it's a full build for that scope.
*
* Note: The files array should be pre-classified by scope if using split
* compilation. i.e. it should only include files of the scope being used.
*
* Note: One of the returned arrays is always empty, depending on the scope
* being used.
*/
async function getRootsToBuildForScope({
files,
scope,
solidity,
}: {
files: string[];
scope: BuildScope;
solidity: SolidityBuildSystem;
}): Promise<{
isFullBuild: boolean;
contractRootPaths: string[];
testRootPaths: string[];
}> {
const isFullBuild = files.length === 0;
const rootPaths = isFullBuild
? await solidity.getRootFilePaths({ scope })
: files; // This is safe because the files have already been partitioned by scope
if (scope === "contracts") {
return { isFullBuild, contractRootPaths: rootPaths, testRootPaths: [] };
}
return { isFullBuild, contractRootPaths: [], testRootPaths: rootPaths };
}
/**
* Partitions root paths by scope, as returned by `solidity.getScope(rootPath)`.
*/
async function partitionRootPathsByScope(
solidity: SolidityBuildSystem,
rootPaths: string[],
): Promise<{ contractRootPaths: string[]; testRootPaths: string[] }> {
const contractRootPaths: string[] = [];
const testRootPaths: string[] = [];
for (const rootPath of rootPaths) {
if (isNpmRootPath(rootPath)) {
contractRootPaths.push(rootPath);
continue;
}
const scope = await solidity.getScope(rootPath);
if (scope === "tests") {
testRootPaths.push(rootPath);
} else {
contractRootPaths.push(rootPath);
}
}
return { contractRootPaths, testRootPaths };
}
/**
* Normalizes the received root paths.
*
* If a file is an npm root path or absolute file path, it's returned as is.
* If it's a relative path it's resolved from the CWD.
*/
function normalizeRootPaths(files: string[]): string[] {
return files.map((f) => {
if (isNpmRootPath(f)) {
return f;
}
return resolveFromRoot(process.cwd(), f);
});
}
export default buildAction;