@truffle/compile-solidity
Version:
Compiler helper and artifact manager for Solidity files
306 lines (269 loc) • 9.87 kB
text/typescript
import debugModule from "debug";
const debug = debugModule("compile:compilerSupplier");
import requireFromString from "require-from-string";
// must polyfill AbortController to use axios >=0.20.0, <=0.27.2 on node <= v14.x
import "../../polyfill";
import { default as axios, AxiosResponse } from "axios";
import semver from "semver";
import solcWrap from "solc/wrapper";
import { Cache } from "../Cache";
import { observeListeners } from "../observeListeners";
import { NoVersionError, CompilerFetchingError } from "../errors";
import { StrategyOptions } from "../types";
type SolidityCompilersList = {
builds: object[];
latestRelease: string;
releases: object;
};
export class VersionRange {
public config: StrategyOptions;
public cache: Cache;
constructor(options: StrategyOptions) {
const defaultConfig = {
compilerRoots: [
// this order of url root preference was recommended by Cameel from the
// Solidity team here -- https://github.com/trufflesuite/truffle/pull/5008
"https://binaries.soliditylang.org/emscripten-wasm32/",
"https://binaries.soliditylang.org/emscripten-asmjs/",
"https://solc-bin.ethereum.org/bin/",
"https://ethereum.github.io/solc-bin/bin/",
"https://relay.trufflesuite.com/solc/emscripten-wasm32/",
"https://relay.trufflesuite.com/solc/emscripten-asmjs/"
]
};
this.config = Object.assign({}, defaultConfig, options);
this.cache = new Cache();
}
async load(versionRange: string) {
const rangeIsSingleVersion = semver.valid(versionRange);
if (rangeIsSingleVersion && (await this.versionIsCached(versionRange))) {
return await this.getCachedSolcByVersionRange(versionRange);
}
try {
return await this.getSolcFromCacheOrUrl(versionRange);
} catch (error) {
if (error.message.includes("Failed to complete request")) {
return await this.getSatisfyingVersionFromCache(versionRange);
}
throw error;
}
}
async list(index = 0) {
if (index >= this.config.compilerRoots!.length) {
throw new Error(
`Failed to fetch the list of Solidity compilers from the following ` +
`sources: ${this.config.compilerRoots}. Make sure you are connected ` +
`to the internet.`
);
}
let data: SolidityCompilersList;
try {
const attemptNumber = index + 1;
data = await this.getSolcVersionsForSource(
this.config.compilerRoots![index],
attemptNumber
);
} catch (error) {
if (error.message.includes("Failed to fetch compiler list at")) {
return await this.list(index + 1);
}
throw error;
}
const { latestRelease } = data;
const prereleases = data.builds
.filter(build => build["prerelease"])
.map(build => build["longVersion"]);
// ensure releases are listed in descending order
const releases = semver.rsort(Object.keys(data.releases));
return {
prereleases,
releases,
latestRelease
};
}
compilerFromString(code: string) {
const listeners = observeListeners();
try {
const soljson = requireFromString(code);
return solcWrap(soljson);
} finally {
listeners.cleanup();
}
}
findNewestValidVersion(
version: string,
allVersions: SolidityCompilersList
): string | null {
return semver.maxSatisfying(
Object.keys(allVersions.releases || {}),
version
);
}
async getCachedSolcByFileName(fileName: string) {
const listeners = observeListeners();
try {
const soljson = await this.cache.loadFile(fileName);
debug("soljson %o", soljson);
return this.compilerFromString(soljson);
} finally {
listeners.cleanup();
}
}
// Range can also be a single version specification like "0.5.0"
async getCachedSolcByVersionRange(version: string) {
const cachedCompilerFileNames = await this.cache.list();
const validVersions = cachedCompilerFileNames.filter(fileName => {
const match = fileName.match(/v\d+\.\d+\.\d+.*/);
if (match) return semver.satisfies(match[0], version);
});
const multipleValidVersions = validVersions.length > 1;
const compilerFileName = multipleValidVersions
? this.getMostRecentVersionOfCompiler(validVersions)
: validVersions[0];
return await this.getCachedSolcByFileName(compilerFileName);
}
async getCachedSolcFileName(commit: string) {
const cachedCompilerFileNames = await this.cache.list();
return cachedCompilerFileNames.find(fileName => {
return fileName.includes(commit);
});
}
getMostRecentVersionOfCompiler(versions: string[]) {
return versions.reduce((mostRecentVersionFileName, fileName) => {
const match = fileName.match(/v\d+\.\d+\.\d+.*/);
const mostRecentVersionMatch =
mostRecentVersionFileName.match(/v\d+\.\d+\.\d+.*/);
return semver.gtr(match![0], mostRecentVersionMatch![0])
? fileName
: mostRecentVersionFileName;
}, "-v0.0.0+commit");
}
async getSatisfyingVersionFromCache(versionRange: string) {
if (await this.versionIsCached(versionRange)) {
return await this.getCachedSolcByVersionRange(versionRange);
}
throw new NoVersionError(versionRange);
}
async getAndCacheSolcByUrl(fileName: string, index: number) {
const { events, compilerRoots } = this.config;
const url = `${compilerRoots![index].replace(/\/+$/, "")}/${fileName}`;
events.emit("downloadCompiler:start", {
attemptNumber: index + 1
});
let response: AxiosResponse;
try {
response = await axios.get(url, { maxRedirects: 50 });
} catch (error) {
events.emit("downloadCompiler:fail");
throw error;
}
events.emit("downloadCompiler:succeed");
try {
await this.cache.add(response.data, fileName);
} catch (error) {
if (error.message.includes("EACCES: permission denied")) {
const warningMessage =
"There was an error attempting to save the compiler to disk. " +
"The current user likely does not have sufficient permissions to " +
"write to disk in Truffle's compiler cache directory. See the error" +
`printed below for more information about this directory.\n${error}`;
console.warn(warningMessage);
}
}
return this.compilerFromString(response.data);
}
async getSolcFromCacheOrUrl(versionConstraint: string, index: number = 0) {
// go through all sources (compilerRoots) trying to locate a
// suitable version of the Solidity compiler
const { compilerRoots, events } = this.config;
if (!compilerRoots || compilerRoots.length === 0) {
events.emit("fetchSolcList:fail");
throw new NoUrlError();
}
if (index >= compilerRoots.length) {
throw new CompilerFetchingError(compilerRoots);
}
let allVersionsForSource: SolidityCompilersList,
versionToUse: string | null;
try {
const attemptNumber = index + 1;
allVersionsForSource = await this.getSolcVersionsForSource(
compilerRoots[index],
attemptNumber
);
const isVersionRange = !semver.valid(versionConstraint);
versionToUse = isVersionRange
? this.findNewestValidVersion(versionConstraint, allVersionsForSource)
: versionConstraint;
if (versionToUse === null) {
throw new Error("No valid version found for source.");
}
const fileName = this.getSolcVersionFileName(
versionToUse,
allVersionsForSource
);
if (!fileName) throw new NoVersionError(versionToUse);
if (await this.cache.has(fileName)) {
return await this.getCachedSolcByFileName(fileName);
}
return await this.getAndCacheSolcByUrl(fileName, index);
} catch (error) {
const attemptNumber = index + 1;
return await this.getSolcFromCacheOrUrl(versionConstraint, attemptNumber);
}
}
async getSolcVersionsForSource(
urlRoot: string,
attemptNumber: number
): Promise<SolidityCompilersList> {
const { events } = this.config;
events.emit("fetchSolcList:start", { attemptNumber });
// trim trailing slashes from compilerRoot
const url = `${urlRoot.replace(/\/+$/, "")}/list.json`;
try {
const response = await axios.get(url, { maxRedirects: 50 });
events.emit("fetchSolcList:succeed");
return response.data;
} catch (error) {
events.emit("fetchSolcList:fail");
throw new Error(`Failed to fetch compiler list at ${url}`);
}
}
getSolcVersionFileName(
version: string,
allVersions: SolidityCompilersList
): string | null {
if (allVersions.releases[version]) return allVersions.releases[version];
const isPrerelease =
version.includes("nightly") || version.includes("commit");
if (isPrerelease) {
for (let build of allVersions.builds) {
const exists =
build["prerelease"] === version ||
build["build"] === version ||
build["longVersion"] === version;
if (exists) return build["path"];
}
}
const versionToUse = this.findNewestValidVersion(version, allVersions);
if (versionToUse) return allVersions.releases[versionToUse];
return null;
}
async versionIsCached(version: string) {
const cachedCompilerFileNames = await this.cache.list();
const cachedVersions = cachedCompilerFileNames
.map(fileName => {
const match = fileName.match(/v\d+\.\d+\.\d+.*/);
if (match) return match[0];
})
.filter((version): version is string => !!version);
return cachedVersions.find(cachedVersion =>
semver.satisfies(cachedVersion, version)
);
}
}
export class NoUrlError extends Error {
constructor() {
super("compiler root URL missing");
}
}