hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
385 lines (315 loc) • 11.6 kB
text/typescript
import path from "path";
import fsExtra from "fs-extra";
import debug from "debug";
import os from "os";
import { execFile } from "child_process";
import { promisify } from "util";
import { download } from "../../util/download";
import { assertHardhatInvariant, HardhatError } from "../../core/errors";
import { ERRORS } from "../../core/errors-list";
import { MultiProcessMutex } from "../../util/multi-process-mutex";
const log = debug("hardhat:core:solidity:downloader");
const COMPILER_REPOSITORY_URL = "https://binaries.soliditylang.org";
export enum CompilerPlatform {
LINUX = "linux-amd64",
WINDOWS = "windows-amd64",
MACOS = "macosx-amd64",
WASM = "wasm",
}
export interface Compiler {
version: string;
longVersion: string;
compilerPath: string;
isSolcJs: boolean;
}
interface CompilerBuild {
path: string;
version: string;
build: string;
longVersion: string;
keccak256: string;
urls: string[];
platform: CompilerPlatform;
}
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.
*/
export interface ICompilerDownloader {
/**
* 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.
*/
downloadCompiler(
version: string,
downloadStartedCb: (isCompilerDownloaded: boolean) => Promise<any>,
downloadEndedCb: (isCompilerDownloaded: boolean) => Promise<any>
): Promise<void>;
/**
* 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 ICompilerDownloader.
*
* Important things to note:
* 1. If a compiler version is not found, this downloader may fail.
* 1.1. It only re-downloads the list of compilers once every X time.
* 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 class CompilerDownloader implements ICompilerDownloader {
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.
switch (os.platform()) {
case "win32":
return CompilerPlatform.WINDOWS;
case "linux":
return CompilerPlatform.LINUX;
case "darwin":
return CompilerPlatform.MACOS;
default:
return CompilerPlatform.WASM;
}
}
private static _downloaderPerPlatform: Map<string, CompilerDownloader> =
new Map();
public static getConcurrencySafeDownloader(
platform: CompilerPlatform,
compilersDir: string
) {
const key = platform + compilersDir;
if (!this._downloaderPerPlatform.has(key)) {
this._downloaderPerPlatform.set(
key,
new CompilerDownloader(platform, compilersDir)
);
}
return this._downloaderPerPlatform.get(key)!;
}
public static defaultCompilerListCachePeriod = 3_600_00;
private readonly _mutex = new MultiProcessMutex("compiler-download");
/**
* Use CompilerDownloader.getConcurrencySafeDownloader instead
*/
constructor(
private readonly _platform: CompilerPlatform,
private readonly _compilersDir: string,
private readonly _compilerListCachePeriodMs = CompilerDownloader.defaultCompilerListCachePeriod,
private readonly _downloadFunction: typeof download = download
) {}
public async isCompilerDownloaded(version: string): Promise<boolean> {
const build = await this._getCompilerBuild(version);
if (build === undefined) {
return false;
}
const downloadPath = this._getCompilerBinaryPathFromBuild(build);
return fsExtra.pathExists(downloadPath);
}
public async downloadCompiler(
version: string,
downloadStartedCb: (isCompilerDownloaded: boolean) => Promise<any>,
downloadEndedCb: (isCompilerDownloaded: boolean) => Promise<any>
): Promise<void> {
// 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.
await this._mutex.use(async () => {
const isCompilerDownloaded = await this.isCompilerDownloaded(version);
if (isCompilerDownloaded === true) {
return;
}
await downloadStartedCb(isCompilerDownloaded);
let build = await this._getCompilerBuild(version);
if (build === undefined && (await this._shouldDownloadCompilerList())) {
try {
await this._downloadCompilerList();
} catch (e: any) {
throw new HardhatError(
ERRORS.SOLC.VERSION_LIST_DOWNLOAD_FAILED,
{},
e
);
}
build = await this._getCompilerBuild(version);
}
if (build === undefined) {
throw new HardhatError(ERRORS.SOLC.INVALID_VERSION, { version });
}
let downloadPath: string;
try {
downloadPath = await this._downloadCompiler(build);
} catch (e: any) {
throw new HardhatError(
ERRORS.SOLC.DOWNLOAD_FAILED,
{
remoteVersion: build.longVersion,
},
e
);
}
const verified = await this._verifyCompilerDownload(build, downloadPath);
if (!verified) {
throw new HardhatError(ERRORS.SOLC.INVALID_DOWNLOAD, {
remoteVersion: build.longVersion,
});
}
await this._postProcessCompilerDownload(build, downloadPath);
await downloadEndedCb(isCompilerDownloaded);
});
}
public async getCompiler(version: string): Promise<Compiler | undefined> {
const build = await this._getCompilerBuild(version);
assertHardhatInvariant(
build !== undefined,
"Trying to get a compiler before it was downloaded"
);
const compilerPath = this._getCompilerBinaryPathFromBuild(build);
assertHardhatInvariant(
await fsExtra.pathExists(compilerPath),
"Trying to get a compiler before it was downloaded"
);
if (await fsExtra.pathExists(this._getCompilerDoesntWorkFile(build))) {
return undefined;
}
return {
version,
longVersion: build.longVersion,
compilerPath,
isSolcJs: this._platform === CompilerPlatform.WASM,
};
}
private async _getCompilerBuild(
version: string
): Promise<CompilerBuild | undefined> {
const listPath = this._getCompilerListPath();
if (!(await fsExtra.pathExists(listPath))) {
return undefined;
}
const list = await this._readCompilerList(listPath);
return list.builds.find((b) => b.version === version);
}
private _getCompilerListPath(): string {
return path.join(this._compilersDir, this._platform, "list.json");
}
private async _readCompilerList(listPath: string): Promise<CompilerList> {
return fsExtra.readJSON(listPath);
}
private _getCompilerDownloadPathFromBuild(build: CompilerBuild): string {
return path.join(this._compilersDir, this._platform, build.path);
}
private _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");
}
private _getCompilerDoesntWorkFile(build: CompilerBuild): string {
return `${this._getCompilerBinaryPathFromBuild(build)}.does.not.work`;
}
private async _shouldDownloadCompilerList(): Promise<boolean> {
const listPath = this._getCompilerListPath();
if (!(await fsExtra.pathExists(listPath))) {
return true;
}
const stats = await fsExtra.stat(listPath);
const age = new Date().valueOf() - stats.ctimeMs;
return age > this._compilerListCachePeriodMs;
}
private async _downloadCompilerList(): Promise<void> {
log(`Downloading compiler list for platform ${this._platform}`);
const url = `${COMPILER_REPOSITORY_URL}/${this._platform}/list.json`;
const downloadPath = this._getCompilerListPath();
await this._downloadFunction(url, downloadPath);
}
private async _downloadCompiler(build: CompilerBuild): Promise<string> {
log(`Downloading compiler ${build.longVersion}`);
const url = `${COMPILER_REPOSITORY_URL}/${this._platform}/${build.path}`;
const downloadPath = this._getCompilerDownloadPathFromBuild(build);
await this._downloadFunction(url, downloadPath);
return downloadPath;
}
private async _verifyCompilerDownload(
build: CompilerBuild,
downloadPath: string
): Promise<boolean> {
const { bytesToHex } =
require("@nomicfoundation/ethereumjs-util") as typeof import("@nomicfoundation/ethereumjs-util");
const { keccak256 } = await import("../../util/keccak");
const expectedKeccak256 = build.keccak256;
const compiler = await fsExtra.readFile(downloadPath);
const compilerKeccak256 = bytesToHex(keccak256(compiler));
if (expectedKeccak256 !== compilerKeccak256) {
await fsExtra.unlink(downloadPath);
return false;
}
return true;
}
private async _postProcessCompilerDownload(
build: CompilerBuild,
downloadPath: string
): Promise<void> {
if (this._platform === CompilerPlatform.WASM) {
return;
}
if (
this._platform === CompilerPlatform.LINUX ||
this._platform === CompilerPlatform.MACOS
) {
fsExtra.chmodSync(downloadPath, 0o755);
} else if (
this._platform === CompilerPlatform.WINDOWS &&
downloadPath.endsWith(".zip")
) {
// some window builds are zipped, some are not
const AdmZip = require("adm-zip");
const solcFolder = path.join(this._compilersDir, build.version);
await fsExtra.ensureDir(solcFolder);
const zip = new AdmZip(downloadPath);
zip.extractAllTo(solcFolder);
}
log("Checking native solc binary");
const nativeSolcWorks = await this._checkNativeSolc(build);
if (nativeSolcWorks) {
return;
}
await fsExtra.createFile(this._getCompilerDoesntWorkFile(build));
}
private async _checkNativeSolc(build: CompilerBuild): Promise<boolean> {
const solcPath = this._getCompilerBinaryPathFromBuild(build);
const execFileP = promisify(execFile);
try {
await execFileP(solcPath, ["--version"]);
return true;
} catch {
return false;
}
}
}