hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
507 lines (420 loc) • 15.9 kB
text/typescript
import type { Compiler } from "../../../../../../src/types/solidity.js";
import { execFile } from "node:child_process";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import {
HardhatError,
assertHardhatInvariant,
} from "@nomicfoundation/hardhat-errors";
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/bytes";
import { sha256 } from "@nomicfoundation/hardhat-utils/crypto";
import { createDebug } from "@nomicfoundation/hardhat-utils/debug";
import { ensureError } from "@nomicfoundation/hardhat-utils/error";
import {
chmod,
createFile,
ensureDir,
exists,
readBinaryFile,
readJsonFile,
remove,
writeJsonFile,
} from "@nomicfoundation/hardhat-utils/fs";
import { getPrefixedHexString } from "@nomicfoundation/hardhat-utils/hex";
import { download } from "@nomicfoundation/hardhat-utils/request";
import { MultiProcessMutex } from "@nomicfoundation/hardhat-utils/synchronization";
import AdmZip from "adm-zip";
import { NativeCompiler, SolcJsCompiler } from "./compiler.js";
const log = createDebug(
"hardhat:core:solidity:build-system:compiler:downloader",
);
const COMPILER_REPOSITORY_URL = "https://binaries.soliditylang.org";
const DEFAULT_COMPILER_DOWNLOAD_RETRY_COUNT = 3;
const DEFAULT_COMPILER_DOWNLOAD_RETRY_DELAY_MS = 2000;
// We use a mirror of nikitastupin/solc because downloading directly from
// github has rate limiting issues
const LINUX_ARM64_REPOSITORY_URL =
"https://solc-linux-arm64-mirror.hardhat.org/linux/aarch64";
export enum CompilerPlatform {
LINUX = "linux-amd64",
LINUX_ARM64 = "linux-arm64",
WINDOWS = "windows-amd64",
MACOS = "macosx-amd64",
WASM = "wasm",
}
interface CompilerBuild {
path: string;
url?: string;
version: string;
longVersion: string;
sha256: string;
prerelease?: string;
}
interface CompilerList {
builds: CompilerBuild[];
releases: { [version: string]: string };
latestRelease: string;
}
/**
* A compiler downloader which must be specialized per-platform. It can't and
* shouldn't support multiple platforms at the same time.
*
* This is expected to be used like this:
* 1. First, the downloader is created for the given platform.
* 2. Then, call `downloader.updateCompilerListIfNeeded(versionsToUse)` to
* update the compiler list if one of the versions is not found.
* 3. Then, call `downloader.isCompilerDownloaded()` to check if the
* compiler is already downloaded.
* 4. If the compiler is not downloaded, call
* `downloader.downloadCompiler()` to download it.
* 5. Finally, call `downloader.getCompiler()` to get the compiler.
*
* Important things to note:
* 1. If a compiler version is not found, this downloader may fail.
* 1.1.1 If a user tries to download a new compiler before X amount of time
* has passed since its release, they may need to clean the cache, as
* indicated in the error messages.
*/
export interface CompilerDownloader {
/**
* Updates the compiler list if any of the versions is not found in the
* currently downloaded list, or if none has been downloaded yet.
*/
updateCompilerListIfNeeded(versions: Set<string>): Promise<void>;
/**
* Returns true if the compiler has been downloaded.
*
* This function access the filesystem, but doesn't modify it.
*/
isCompilerDownloaded(version: string): Promise<boolean>;
/**
* Downloads the compiler for a given version, which can later be obtained
* with getCompiler.
*
* @returns `true` if the compiler was downloaded and verified correctly,
* including validating the checksum and if the native compiler can be run.
*/
downloadCompiler(version: string): Promise<boolean>;
/**
* Returns the compiler, which MUST be downloaded before calling this function.
*
* Returns undefined if the compiler has been downloaded but can't be run.
*
* This function access the filesystem, but doesn't modify it.
*/
getCompiler(version: string): Promise<Compiler | undefined>;
}
/**
* Default implementation of CompilerDownloader.
*/
export class CompilerDownloaderImplementation implements CompilerDownloader {
public static getCompilerPlatform(): CompilerPlatform {
// TODO: This check is seriously wrong. It doesn't take into account
// the architecture nor the toolchain. This should check the triplet of
// system instead (see: https://wiki.osdev.org/Target_Triplet).
//
// The only reason this downloader works is that it validates if the
// binaries actually run.
//
// On top of that, AppleSilicon with Rosetta2 makes things even more
// complicated, as it allows x86 binaries to run on ARM, not on MacOS but
// on Linux Docker containers too!
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check -- Ignore other platforms
switch (os.platform()) {
case "win32":
return CompilerPlatform.WINDOWS;
case "linux":
if (os.arch() === "arm64") {
return CompilerPlatform.LINUX_ARM64;
} else {
return CompilerPlatform.LINUX;
}
case "darwin":
return CompilerPlatform.MACOS;
default:
return CompilerPlatform.WASM;
}
}
readonly #platform: CompilerPlatform;
readonly #compilersDir: string;
readonly #downloadFunction: typeof download;
readonly #mutexCompilerList: MultiProcessMutex;
constructor(
platform: CompilerPlatform,
compilersDir: string,
downloadFunction: typeof download = download,
) {
this.#platform = platform;
this.#compilersDir = compilersDir;
this.#mutexCompilerList = new MultiProcessMutex(
path.join(compilersDir, "compiler-download-list"),
);
this.#downloadFunction = downloadFunction;
}
public async updateCompilerListIfNeeded(
versions: Set<string>,
): Promise<void> {
await this.#mutexCompilerList.use(async () => {
if (await this.#shouldDownloadCompilerList(versions)) {
try {
log(
`Downloading the list of solc builds for platform ${this.#platform}`,
);
await this.#downloadCompilerList();
} catch (e) {
ensureError(e);
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.VERSION_LIST_DOWNLOAD_FAILED,
e,
);
}
}
});
}
public async isCompilerDownloaded(version: string): Promise<boolean> {
const build = await this.#getCompilerBuild(version);
const downloadPath = this.#getCompilerBinaryPathFromBuild(build);
return await exists(downloadPath);
}
public async downloadCompiler(version: string): Promise<boolean> {
// A per-version mutex ensures that only one process at a time can download a given compiler version,
// while still allowing different compiler versions to be downloaded in parallel.
// Without the mutex, a concurrent process might check whether a version exists, incorrectly
// find it missing (because another process is still downloading it), and start a redundant download.
const mutex = new MultiProcessMutex(
path.join(this.#compilersDir, `compiler-download-${version}`),
);
return await mutex.use(async () => {
const isCompilerDownloaded = await this.isCompilerDownloaded(version);
if (isCompilerDownloaded === true) {
return true;
}
const build = await this.#getCompilerBuild(version);
let downloadPath: string = "";
for (let i = 0; i <= DEFAULT_COMPILER_DOWNLOAD_RETRY_COUNT; i++) {
try {
downloadPath = await this.#downloadAndVerifyCompiler(build);
break;
} catch (e) {
if (i === DEFAULT_COMPILER_DOWNLOAD_RETRY_COUNT) {
ensureError(e);
throw e;
} else {
const attempt = i + 1;
log(
`Download or verification failed for solc ${version}, retrying (attempt ${attempt} of ${DEFAULT_COMPILER_DOWNLOAD_RETRY_COUNT})`,
);
await new Promise((resolve) =>
setTimeout(resolve, DEFAULT_COMPILER_DOWNLOAD_RETRY_DELAY_MS),
);
}
}
}
return await this.#postProcessCompilerDownload(build, downloadPath);
});
}
public async getCompiler(version: string): Promise<Compiler | undefined> {
const build = await this.#getCompilerBuild(version);
const compilerPath = this.#getCompilerBinaryPathFromBuild(build);
assertHardhatInvariant(
await exists(compilerPath),
`Trying to get a compiler ${version} before it was downloaded`,
);
if (await exists(this.#getCompilerDoesNotWorkFile(build))) {
return undefined;
}
if (this.#platform === CompilerPlatform.WASM) {
return new SolcJsCompiler(version, build.longVersion, compilerPath);
}
return new NativeCompiler(version, build.longVersion, compilerPath);
}
async #getCompilerBuild(version: string): Promise<CompilerBuild> {
const listPath = this.#getCompilerListPath();
assertHardhatInvariant(
await exists(listPath),
`Trying to get the compiler list for ${this.#platform} before it was downloaded`,
);
const list = await this.#readCompilerList(listPath);
const build = list.builds.find(
(b) => b.version === version && b.prerelease === undefined,
);
if (build === undefined) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.INVALID_SOLC_VERSION,
{
version,
},
);
}
return build;
}
#getCompilerListPath(): string {
return path.join(this.#compilersDir, this.#platform, "list.json");
}
async #readCompilerList(listPath: string): Promise<CompilerList> {
return await readJsonFile(listPath);
}
#getCompilerDownloadPathFromBuild(build: CompilerBuild): string {
return path.join(this.#compilersDir, this.#platform, build.path);
}
#getCompilerBinaryPathFromBuild(build: CompilerBuild): string {
const downloadPath = this.#getCompilerDownloadPathFromBuild(build);
if (
this.#platform !== CompilerPlatform.WINDOWS ||
!downloadPath.endsWith(".zip")
) {
return downloadPath;
}
return path.join(this.#compilersDir, build.version, "solc.exe");
}
#getCompilerDoesNotWorkFile(build: CompilerBuild): string {
return `${this.#getCompilerBinaryPathFromBuild(build)}.does.not.work`;
}
async #shouldDownloadCompilerList(versions: Set<string>): Promise<boolean> {
const listPath = this.#getCompilerListPath();
log(
`Checking if the compiler list for ${this.#platform} should be downloaded at ${listPath}`,
);
if (!(await exists(listPath))) {
return true;
}
const list = await this.#readCompilerList(listPath);
const listVersions = new Set(list.builds.map((b) => b.version));
for (const version of versions) {
if (!listVersions.has(version)) {
// TODO: We should also check if it wasn't downloaded soon ago
return true;
}
}
// download the list in case the cached list contains older ARM64 Linux builds without URL
return list.builds
.map((b) => b.path.startsWith("solc-v") && b.url === undefined)
.reduce((a, b) => a || b, false);
}
async #downloadCompilerList(): Promise<void> {
log(`Downloading compiler list for platform ${this.#platform}`);
const downloadPath = this.#getCompilerListPath();
// download hte official solc compiler list (now that ARM64 Linus is supported)
await this.#downloadFunction(
`${COMPILER_REPOSITORY_URL}/${this.#platform}/list.json`,
downloadPath,
);
// for Linux ARM64, we need to merge the official list with our custom builds
if (this.#platform === CompilerPlatform.LINUX_ARM64) {
// cache the official list since the file will be overwritten below
const officialCompilerList: CompilerList =
await readJsonFile(downloadPath);
await this.#downloadFunction(
`${LINUX_ARM64_REPOSITORY_URL}/list.json`,
downloadPath,
);
// add missing information and an explicit URL for download
const armLinuxcompilerList: CompilerList =
await readJsonFile(downloadPath);
for (const build of armLinuxcompilerList.builds) {
build.path = `solc-v${build.version}`;
build.url = LINUX_ARM64_REPOSITORY_URL;
build.longVersion = build.version;
}
// merge the official and custom lists
officialCompilerList.builds = officialCompilerList.builds.concat(
armLinuxcompilerList.builds,
);
officialCompilerList.releases = {
...officialCompilerList.releases,
...armLinuxcompilerList.releases,
};
await writeJsonFile(downloadPath, officialCompilerList);
}
}
async #downloadAndVerifyCompiler(build: CompilerBuild): Promise<string> {
let downloadPath: string = "";
try {
downloadPath = await this.#downloadCompiler(build);
} catch (e) {
ensureError(e);
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.DOWNLOAD_FAILED,
{
remoteVersion: build.longVersion,
},
e,
);
}
const verified = await this.#verifyCompilerDownload(build, downloadPath);
if (!verified) {
throw new HardhatError(
HardhatError.ERRORS.CORE.SOLIDITY.INVALID_DOWNLOAD,
{
remoteVersion: build.longVersion,
},
);
}
return downloadPath;
}
async #downloadCompiler(build: CompilerBuild): Promise<string> {
// use the explicit URL if available or the default solc download URL if not
const defaultUrl = `${COMPILER_REPOSITORY_URL}/${this.#platform}`;
const url = `${build.url ?? defaultUrl}/${build.path}`;
log(`Downloading compiler ${build.version} from ${url}`);
const downloadPath = this.#getCompilerDownloadPathFromBuild(build);
await this.#downloadFunction(url, downloadPath);
return downloadPath;
}
async #verifyCompilerDownload(
build: CompilerBuild,
downloadPath: string,
): Promise<boolean> {
const expectedSha = getPrefixedHexString(build.sha256);
const compiler = await readBinaryFile(downloadPath);
const compilerSha = bytesToHexString(await sha256(compiler));
if (expectedSha !== compilerSha) {
await remove(downloadPath);
return false;
}
return true;
}
async #postProcessCompilerDownload(
build: CompilerBuild,
downloadPath: string,
): Promise<boolean> {
if (this.#platform === CompilerPlatform.WASM) {
return true;
}
if (
this.#platform === CompilerPlatform.LINUX ||
this.#platform === CompilerPlatform.LINUX_ARM64 ||
this.#platform === CompilerPlatform.MACOS
) {
await chmod(downloadPath, 0o755);
} else if (
this.#platform === CompilerPlatform.WINDOWS &&
downloadPath.endsWith(".zip")
) {
// some window builds are zipped, some are not
const solcFolder = path.join(this.#compilersDir, build.version);
await ensureDir(solcFolder);
const zip = new AdmZip(downloadPath);
zip.extractAllTo(solcFolder);
}
log("Checking native solc binary");
const nativeSolcWorks = await this.#checkNativeSolc(build);
if (nativeSolcWorks) {
return true;
}
await createFile(this.#getCompilerDoesNotWorkFile(build));
return false;
}
async #checkNativeSolc(build: CompilerBuild): Promise<boolean> {
const solcPath = this.#getCompilerBinaryPathFromBuild(build);
const execFileP = promisify(execFile);
try {
await execFileP(solcPath, ["--version"]);
return true;
} catch {
log(`solc binary at ${solcPath} is not working`);
return false;
}
}
}