hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
269 lines • 12 kB
JavaScript
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 { 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 debug from "debug";
import { NativeCompiler, SolcJsCompiler } from "./compiler.js";
const log = debug("hardhat:solidity:downloader");
const COMPILER_REPOSITORY_URL = "https://binaries.soliditylang.org";
// 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 var CompilerPlatform;
(function (CompilerPlatform) {
CompilerPlatform["LINUX"] = "linux-amd64";
CompilerPlatform["LINUX_ARM64"] = "linux-aarch64";
CompilerPlatform["WINDOWS"] = "windows-amd64";
CompilerPlatform["MACOS"] = "macosx-amd64";
CompilerPlatform["WASM"] = "wasm";
})(CompilerPlatform || (CompilerPlatform = {}));
/**
* Default implementation of CompilerDownloader.
*/
export class CompilerDownloaderImplementation {
static getCompilerPlatform() {
// 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;
}
}
#platform;
#compilersDir;
#downloadFunction;
#mutexCompiler = new MultiProcessMutex("compiler-download");
#mutexCompilerList = new MultiProcessMutex("compiler-download-list");
/**
* Use CompilerDownloader.getConcurrencySafeDownloader instead
*/
constructor(platform, compilersDir, downloadFunction = download) {
this.#platform = platform;
this.#compilersDir = compilersDir;
this.#downloadFunction = downloadFunction;
}
async updateCompilerListIfNeeded(versions) {
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);
}
}
});
}
async isCompilerDownloaded(version) {
const build = await this.#getCompilerBuild(version);
const downloadPath = this.#getCompilerBinaryPathFromBuild(build);
return exists(downloadPath);
}
async downloadCompiler(version) {
// Since only one process at a time can acquire the mutex, we avoid the risk of downloading the same compiler multiple times.
// This is because the mutex blocks access until a compiler has been fully downloaded, preventing any new process
// from checking whether that version of the compiler exists. Without mutex it might incorrectly
// return false, indicating that the compiler isn't present, even though it is currently being downloaded.
return this.#mutexCompiler.use(async () => {
const isCompilerDownloaded = await this.isCompilerDownloaded(version);
if (isCompilerDownloaded === true) {
return true;
}
const build = await this.#getCompilerBuild(version);
let downloadPath;
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 this.#postProcessCompilerDownload(build, downloadPath);
});
}
async getCompiler(version) {
const build = await this.#getCompilerBuild(version);
assertHardhatInvariant(build !== undefined, `Trying to get a compiler ${version} before it was downloaded`);
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) {
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() {
return path.join(this.#compilersDir, this.#platform, "list.json");
}
async #readCompilerList(listPath) {
return readJsonFile(listPath);
}
#getCompilerDownloadPathFromBuild(build) {
return path.join(this.#compilersDir, this.#platform, build.path);
}
#getCompilerBinaryPathFromBuild(build) {
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) {
return `${this.#getCompilerBinaryPathFromBuild(build)}.does.not.work`;
}
async #shouldDownloadCompilerList(versions) {
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;
}
}
return false;
}
async #downloadCompilerList() {
log(`Downloading compiler list for platform ${this.#platform}`);
let url;
if (this.#onLinuxArm()) {
url = `${LINUX_ARM64_REPOSITORY_URL}/list.json`;
}
else {
url = `${COMPILER_REPOSITORY_URL}/${this.#platform}/list.json`;
}
const downloadPath = this.#getCompilerListPath();
await this.#downloadFunction(url, downloadPath);
// If using the arm64 binary mirror, the list.json file has different information than the solc official mirror, so we complete it
if (this.#onLinuxArm()) {
const compilerList = await readJsonFile(downloadPath);
for (const build of compilerList.builds) {
build.path = `solc-v${build.version}`;
build.longVersion = build.version;
}
await writeJsonFile(downloadPath, compilerList);
}
}
#onLinuxArm() {
return this.#platform === CompilerPlatform.LINUX_ARM64;
}
async #downloadCompiler(build) {
let url;
if (this.#onLinuxArm()) {
url = `${LINUX_ARM64_REPOSITORY_URL}/${build.path}`;
}
else {
url = `${COMPILER_REPOSITORY_URL}/${this.#platform}/${build.path}`;
}
log(`Downloading compiler ${build.version} from ${url}`);
const downloadPath = this.#getCompilerDownloadPathFromBuild(build);
await this.#downloadFunction(url, downloadPath);
return downloadPath;
}
async #verifyCompilerDownload(build, downloadPath) {
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, downloadPath) {
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 { default: AdmZip } = await import("adm-zip");
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) {
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;
}
}
}
//# sourceMappingURL=downloader.js.map