@haechi-labs/henesis-cli
Version:
🚀 Command Line Interface tool to Utilize henesis
488 lines • 17.9 kB
JavaScript
"use strict";
// COPYRIGHT for truffle team
/* eslint-disable */
const debug = require('debug')('compile:compilerSupplier');
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');
const request = require('request-promise');
const requireFromString = require('require-from-string');
const findCacheDir = require('find-cache-dir');
const originalRequire = require('original-require');
const solcWrap = require('solc/wrapper');
const ora = require('ora');
const semver = require('semver');
//------------------------------ Constructor/Config ------------------------------------------------
const defaultConfig = {
version: null,
versionsUrl: 'https://solc-bin.ethereum.org/bin/list.json',
compilerUrlRoot: 'https://solc-bin.ethereum.org/bin/',
dockerTagsUrl: 'https://registry.hub.docker.com/v2/repositories/ethereum/solc/tags/',
cache: true,
};
/**
* Constructor:
* @param {Object} _config (see `supplier.config`)
*/
function CompilerSupplier(_config) {
_config = _config || {};
this.config = Object.assign({}, defaultConfig, _config);
}
CompilerSupplier.prototype.cachePath = findCacheDir({
name: 'henesis',
cwd: __dirname,
create: true,
});
//----------------------------------- Interface ---------------------------------------------------
/**
* Load solcjs from four possible locations:
* - local node_modules (config.version = <undefined>)
* - absolute path to a local solc (config.version = <path>)
* - a solc cache (config.version = <version-string> && cache contains version)
* - a remote solc-bin source (config.version = <version-string> && version not cached)
*
* OR specify that solc.compile should wrap:
* - dockerized solc (config.version = "<image-name>" && config.docker: true)
* - native built solc (cofing.version = "native")
*
* @return {Module|Object} solc
*/
CompilerSupplier.prototype.load = function () {
const self = this;
const version = self.config.version;
const isNative = self.config.version === 'native';
return new Promise(accept => {
const useDocker = self.config.docker;
const useDefault = !version;
const useLocal = !useDefault && self.isLocal(version);
const useCached = !useDefault && self.versionIsCached(version);
const useNative = !useLocal && isNative;
const useRemote = !useNative;
if (useDocker)
return accept(self.getBuilt('docker'));
if (useNative)
return accept(self.getBuilt('native'));
if (useDefault)
return accept(self.getDefault());
if (useLocal)
return accept(self.getLocal(version));
if (useCached)
return accept(self.getCached(version));
if (useRemote)
return accept(self.getByUrl(version)); // Tries cache first, then remote.
});
};
/**
* Returns keys that can be used to specify which remote solc to fetch
* ```
* latestRelease: "0.4.21",
* releases: ["0.4.21", ...],
* prereleases: ["0.4.22-nightly.2018.4.10+commit.27385d6d", ...]
* ```
* @return {Object} See above
*/
CompilerSupplier.prototype.getReleases = function () {
return this.getVersions().then(list => {
// Prereleases
const prereleases = list.builds
.filter(build => build['prerelease'])
.map(build => build['longVersion']);
// Releases
const releases = Object.keys(list.releases);
return {
prereleases: prereleases,
releases: releases,
latestRelease: list.latestRelease,
};
});
};
/**
* Fetches the first page of docker tags for the the ethereum/solc image
* @return {Object} tags
*/
CompilerSupplier.prototype.getDockerTags = function () {
const self = this;
return request(self.config.dockerTagsUrl)
.then(list => JSON.parse(list).results.map(item => item.name))
.catch(err => {
throw self.errors('noRequest', self.config.dockerTagsUrl, err);
});
};
//------------------------------------ Getters -----------------------------------------------------
/**
* Gets solc from `node_modules`.`
* @return {Module} solc
*/
CompilerSupplier.prototype.getDefault = function () {
const compiler = require('solc');
this.removeListener();
return compiler;
};
/**
* Gets a cached solc from specified version.
* @param {String} version
* @return {Module}
*/
CompilerSupplier.prototype.getCached = function (version) {
const cachedCompilerFileNames = fs.readdirSync(this.cachePath);
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
? getMostRecentVersionOfCompiler(validVersions)
: validVersions[0];
return this.getFromCache(compilerFileName);
};
const getMostRecentVersionOfCompiler = versions => {
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');
};
/**
* Gets an npm installed solc from specified path.
* @param {String} localPath
* @return {Module}
*/
CompilerSupplier.prototype.getLocal = function (localPath) {
const self = this;
let compiler;
if (!path.isAbsolute(localPath)) {
localPath = path.resolve(process.cwd(), localPath);
}
try {
compiler = originalRequire(localPath);
self.removeListener();
}
catch (err) {
throw self.errors('noPath', localPath);
}
return compiler;
};
/**
* Fetches solc versions object from remote solc-bin. This includes an array of build
* objects with detailed version info, an array of release version numbers
* and their terminal url segment strings, and a latest version key with the
* same.
* @return {Object} versions
*/
CompilerSupplier.prototype.getVersions = function () {
const self = this;
const spinner = ora({
text: 'Fetching solc version list from solc-bin',
color: 'yellow',
}).start();
return request(self.config.versionsUrl)
.then(list => {
spinner.stop();
return JSON.parse(list);
})
.catch(err => {
spinner.stop();
throw self.errors('noRequest', self.config.versionsUrl, err);
});
};
/**
* Returns terminal url segment for `version` from the versions object
* generated by `getVersions`.
* @param {String} version ex: "0.4.1", "0.4.16-nightly.2017.8.9+commit.81887bc7"
* @param {Object} allVersions (see `getVersions`)
* @return {String} url ex: "soljson-v0.4.21+commit.dfe3193c.js"
*/
CompilerSupplier.prototype.getVersionUrlSegment = (version, allVersions) => {
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 = findNewestValidVersion(version, allVersions);
if (versionToUse)
return allVersions.releases[versionToUse];
return null;
};
const findNewestValidVersion = (version, allVersions) => {
if (!semver.validRange(version))
return null;
const satisfyingVersions = Object.keys(allVersions.releases)
.map(solcVersion => {
if (semver.satisfies(solcVersion, version))
return solcVersion;
})
.filter(solcVersion => solcVersion);
if (satisfyingVersions.length > 0) {
return satisfyingVersions.reduce((newestVersion, version) => {
return semver.gtr(version, newestVersion) ? version : newestVersion;
}, '0.0.0');
}
else {
return null;
}
};
/**
* Downloads solc specified by `version` after attempting retrieve it from cache on local machine,
* @param {String} version ex: "0.4.1", "0.4.16-nightly.2017.8.9+commit.81887bc7"
* @return {Module} solc
*/
CompilerSupplier.prototype.getByUrl = async function (version) {
const allVersions = await this.getVersions(this.config.versionsUrl);
const file = this.getVersionUrlSegment(version, allVersions);
if (!file)
throw this.errors('noVersion', version);
if (this.isCached(file))
return this.getFromCache(file);
const url = this.config.compilerUrlRoot + file;
const spinner = ora({
text: 'Downloading compiler',
color: 'red',
}).start();
try {
const response = await request.get(url);
spinner.stop();
this.addToCache(response, file);
return this.compilerFromString(response);
}
catch (error) {
spinner.stop();
throw this.errors('noRequest', url, error);
}
};
/**
* Makes solc.compile a wrapper to a child process invocation of dockerized solc
* or natively build solc. Also fetches a companion solcjs for the built js to parse imports
* @return {Object} solc output
*/
CompilerSupplier.prototype.getBuilt = function (buildType) {
let versionString;
let command;
switch (buildType) {
case 'native':
versionString = this.validateNative();
command = 'solc --standard-json';
break;
case 'docker':
versionString = this.validateDocker();
command =
'docker run -i ethereum/solc:' +
this.config.version +
' --standard-json';
break;
}
const commit = this.getCommitFromVersion(versionString);
return this.getByUrl(commit).then(solcjs => {
return {
compile: options => String(execSync(command, { input: options })),
version: () => versionString,
importsParser: solcjs,
};
});
};
//------------------------------------ Utils -------------------------------------------------------
/**
* Returns true if file exists or `localPath` is an absolute path.
* @param {String} localPath
* @return {Boolean}
*/
CompilerSupplier.prototype.isLocal = function (localPath) {
return fs.existsSync(localPath) || path.isAbsolute(localPath);
};
/**
* Returns a valid version name if compiler file is cached
* @param {String} version
* @return {Boolean}
*/
CompilerSupplier.prototype.versionIsCached = function (version) {
const cachedCompilerFileNames = fs.readdirSync(this.cachePath);
const cachedVersions = cachedCompilerFileNames.map(fileName => {
const match = fileName.match(/v\d+\.\d+\.\d+.*/);
if (match)
return match[0];
});
return cachedVersions.find(cachedVersion => semver.satisfies(cachedVersion, version));
};
/**
* Checks to make sure image is specified in the config, that docker exists and that
* the image exists locally. If the last condition isn't true, docker will try to pull
* it down and this breaks everything.
* @return {String} solc version string
* @throws {Error}
*/
CompilerSupplier.prototype.validateDocker = function () {
const image = this.config.version;
const fileName = image + '.version';
// Skip validation if they've validated for this image before.
if (this.isCached(fileName)) {
const cachePath = this.resolveCache(fileName);
return fs.readFileSync(cachePath, 'utf-8');
}
// Image specified
if (!image)
throw this.errors('noString', image);
// Docker exists locally
try {
execSync('docker -v');
}
catch (err) {
throw this.errors('noDocker');
}
// Image exists locally
try {
execSync('docker inspect --type=image ethereum/solc:' + image);
}
catch (err) {
throw this.errors('noImage', image);
}
// Get version & cache.
const version = execSync('docker run ethereum/solc:' + image + ' --version');
const normalized = this.normalizeVersion(version);
this.addToCache(normalized, fileName);
return normalized;
};
/**
* Checks to make sure image is specified in the config, that docker exists and that
* the image exists locally. If the last condition isn't true, docker will try to pull
* it down and this breaks everything.
* @return {String} solc version string
* @throws {Error}
*/
CompilerSupplier.prototype.validateNative = function () {
let version;
try {
version = execSync('solc --version');
}
catch (err) {
throw this.errors('noNative', null, err);
}
return this.normalizeVersion(version);
};
/**
* Extracts a commit key from the version info returned by native/docker solc.
* We use this to fetch a companion solcjs from solc-bin in order to parse imports
* correctly.
* @param {String} versionString version info from ex: `solc -v`
* @return {String} commit key, ex: commit.4cb486ee
*/
CompilerSupplier.prototype.getCommitFromVersion = function (versionString) {
return 'commit.' + versionString.match(/commit\.(.*?)\./)[1];
};
/**
* Converts shell exec'd solc version from buffer to string and strips out human readable
* description.
* @param {Buffer} version result of childprocess
* @return {String} normalized version string: e.g 0.4.22+commit.4cb486ee.Linux.g++
*/
CompilerSupplier.prototype.normalizeVersion = function (version) {
version = String(version);
return version.split(':')[1].trim();
};
/**
* Returns path to cached solc version
* @param {String} fileName ex: "soljson-v0.4.21+commit.dfe3193c.js"
* @return {String} path
*/
CompilerSupplier.prototype.resolveCache = function (fileName) {
const thunk = findCacheDir({ name: 'henesis', cwd: __dirname, thunk: true });
return thunk(fileName);
};
/**
* Returns true if `fileName` exists in the cache.
* @param {String} fileName ex: "soljson-v0.4.21+commit.dfe3193c.js"
* @return {Boolean}
*/
CompilerSupplier.prototype.isCached = function (fileName) {
const file = this.resolveCache(fileName);
return fs.existsSync(file);
};
/**
* Write to the cache at `config.cachePath`. Creates `cachePath` directory if
* does not exist.
* @param {String} code JS code string downloaded from solc-bin
* @param {String} fileName ex: "soljson-v0.4.21+commit.dfe3193c.js"
*/
CompilerSupplier.prototype.addToCache = function (code, fileName) {
if (!this.config.cache)
return;
const filePath = this.resolveCache(fileName);
fs.writeFileSync(filePath, code);
};
/**
* Retrieves usable solc module from cache
* @param {String} file ex: "soljson-v0.4.21+commit.dfe3193c.js"
* @return {Module} solc
*/
CompilerSupplier.prototype.getFromCache = function (fileName) {
const filePath = this.resolveCache(fileName);
const soljson = originalRequire(filePath);
debug('soljson %o', soljson);
const wrapped = solcWrap(soljson);
this.removeListener();
return wrapped;
};
/**
* Converts the JS code string obtained from solc-bin to usable node module.
* @param {String} code JS code
* @return {Module} solc
*/
CompilerSupplier.prototype.compilerFromString = function (code) {
const soljson = requireFromString(code);
const wrapped = solcWrap(soljson);
this.removeListener();
return wrapped;
};
/**
* Cleans up error listeners set (by solc?) when requiring it. (This code inherited from
* previous implementation, note to self - ask Tim about this)
*/
CompilerSupplier.prototype.removeListener = function () {
const listeners = process.listeners('uncaughtException');
const execeptionHandler = listeners[listeners.length - 1];
if (execeptionHandler) {
process.removeListener('uncaughtException', execeptionHandler);
}
};
/**
* Error formatter
* @param {String} kind descriptive key
* @param {String} input contextual info for error
* @param {Object} err [optional] additional error associated with this error
* @return {Error}
*/
CompilerSupplier.prototype.errors = function (kind, input, err) {
const info = 'Run `henesis compile --list` to see available versions.';
const kinds = {
noPath: 'Could not find compiler at: ' + input,
noVersion: `Could not find a compiler version matching ${input}. ` +
`Please ensure you are specifying a valid version, constraint or ` +
`build in the henesis config. ${info}`,
noRequest: 'Failed to complete request to: ' +
input +
'. Are you connected to the internet?\n\n' +
err,
noDocker: 'You are trying to run dockerized solc, but docker is not installed.',
noImage: 'Please pull ' + input + ' from docker before trying to compile with it.',
noNative: 'Could not execute local solc binary: ' + err,
// Lists
noString: '`compilers.solc.version` option must be a string specifying:\n' +
' - a path to a locally installed solcjs\n' +
" - a solc version (ex: '0.4.22')\n" +
" - a docker image name (ex: 'stable')\n" +
'Received: ' +
input +
' instead.',
};
return new Error(kinds[kind]);
};
module.exports = CompilerSupplier;
//# sourceMappingURL=compilerSupplier.js.map