diff --git a/.travis.yml b/.travis.yml index a994a80..a0eabb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ +dist: trusty sudo: required - -language: generic +language: node_js +node_js: + - "6" services: - docker @@ -9,17 +11,10 @@ before_install: - docker pull truffle/ci env: + - TEST=native - TEST=repo - TEST=upstream - TEST=scenario script: - - > - docker run -it --rm --name ${TEST} \ - -e TRAVIS_REPO_SLUG \ - -e TRAVIS_PULL_REQUEST \ - -e TRAVIS_PULL_REQUEST_SLUG \ - -e TRAVIS_PULL_REQUEST_BRANCH \ - -e TRAVIS_BRANCH \ - -e TEST \ - truffle/ci:latest run_tests + - npm run test:ci diff --git a/compilerProvider.js b/compilerProvider.js new file mode 100644 index 0000000..c7300f2 --- /dev/null +++ b/compilerProvider.js @@ -0,0 +1,455 @@ +const path = require('path'); +const fs = require('fs'); +const child = 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('./solcWrap.js'); + + +//------------------------------ Constructor/Config ------------------------------------------------ + +/** + * Constructor: + * @param {Object} _config (see `provider.config`) + */ +function CompilerProvider(_config){ + _config = _config || {}; + this.config = Object.assign(this.config, _config); +} + +/** + * Default configuration + * @type {Object} + */ +CompilerProvider.prototype.config = { + solc: 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, +} + + +CompilerProvider.prototype.cachePath = findCacheDir({ + name: 'truffle', + cwd: __dirname, + create: true, +}) + +//----------------------------------- Interface --------------------------------------------------- + +/** + * Load solcjs from four possible locations: + * - local node_modules (config.solc = ) + * - absolute path to a local solc (config.solc = ) + * - a solc cache (config.solc = && cache contains version) + * - a remote solc-bin source (config.solc = && version not cached) + * + * OR specify that solc.compileStandard should wrap: + * - dockerized solc (config.solc = "" && config.docker: true) + * - native built solc (cofing.solc = "native") + * + * @return {Module|Object} solc + */ +CompilerProvider.prototype.load = function(){ + const self = this; + const solc = self.config.solc; + const isNative = self.config.solc === 'native'; + + return new Promise((accept, reject) => { + const useDocker = self.config.docker; + const useDefault = !solc; + const useLocal = !useDefault && self.isLocal(solc); + 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(solc)); + if (useRemote) return accept(self.getByUrl(solc)); // 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 + */ +CompilerProvider.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 + */ +CompilerProvider.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', url, err)}); +} + + +//------------------------------------ Getters ----------------------------------------------------- + +/** + * Gets solc from `node_modules`.` + * @return {Module} solc + */ +CompilerProvider.prototype.getDefault = function(){ + const compiler = require('solc'); + this.removeListener(); + return compiler; +} + +/** + * Gets an npm installed solc from specified path. + * @param {String} localPath + * @return {Module} + */ +CompilerProvider.prototype.getLocal = function(localPath){ + const self = this; + let compiler; + + 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 + */ +CompilerProvider.prototype.getVersions = function(){ + const self = this; + + return request(self.config.versionsUrl) + .then(list => JSON.parse(list)) + .catch(err => {throw self.errors('noRequest', url, 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" + */ +CompilerProvider.prototype.getVersionUrlSegment = function(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']; + } + } + + 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 + */ +CompilerProvider.prototype.getByUrl = function(version){ + const self = this; + + return self + .getVersions(self.config.versionsUrl) + .then(allVersions => { + const file = self.getVersionUrlSegment(version, allVersions); + + if (!file) throw self.errors('noVersion', version); + + if (self.isCached(file)) return self.getFromCache(file); + + const url = self.config.compilerUrlRoot + file; + + return request + .get(url) + .then(response => { + self.addToCache(response, file); + return self.compilerFromString(response); + }) + .catch(err => { throw self.errors('noRequest', url, err)}); + }); +} + +/** + * Makes solc.compileStandard 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 + */ +CompilerProvider.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.solc + ' --standard-json'; + break; + } + + const commit = this.getCommitFromVersion(versionString); + + return this + .getByUrl(commit) + .then(solcjs => { + return { + compileStandard: (options) => String(child.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} + */ +CompilerProvider.prototype.isLocal = function(localPath){ + return fs.existsSync(localPath) || path.isAbsolute(localPath); +} + +/** + * 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} + */ +CompilerProvider.prototype.validateDocker = function(){ + const image = this.config.solc; + 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 { + child.execSync('docker -v'); + } catch(err){ + throw this.errors('noDocker'); + } + + // Image exists locally + try { + child.execSync('docker inspect --type=image ethereum/solc:' + image); + } catch(err){ + throw this.errors('noImage', image); + } + + // Get version & cache. + const version = child.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} + */ +CompilerProvider.prototype.validateNative = function(){ + let version; + try { + version = child.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 + */ +CompilerProvider.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++ + */ +CompilerProvider.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 + */ +CompilerProvider.prototype.resolveCache = function(fileName){ + const thunk = findCacheDir({name: 'truffle', 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} + */ +CompilerProvider.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" + */ +CompilerProvider.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 + */ +CompilerProvider.prototype.getFromCache = function(fileName){ + const filePath = this.resolveCache(fileName); + const soljson = originalRequire(filePath); + 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 + */ +CompilerProvider.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) + */ +CompilerProvider.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} + */ +CompilerProvider.prototype.errors = function(kind, input, err){ + const info = 'Run `truffle compile --list` to see available versions.' + + const kinds = { + + noPath: "Could not find compiler at: " + input, + noVersion: "Could not find compiler version:\n" + input + ".\n" + info, + noRequest: "Failed to complete request to: " + input + ".\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: "`compiler.solc` 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 = CompilerProvider; diff --git a/index.js b/index.js index dd8df98..41fe1c1 100644 --- a/index.js +++ b/index.js @@ -6,6 +6,7 @@ var fs = require("fs"); var async = require("async"); var Profiler = require("./profiler"); var CompileError = require("./compileerror"); +var CompilerProvider = require("./compilerProvider"); var expect = require("truffle-expect"); var find_contracts = require("truffle-contract-sources"); var Config = require("truffle-config"); @@ -31,25 +32,19 @@ var compile = function(sources, options, callback) { options.logger = console; } + var hasTargets = options.compilationTargets && options.compilationTargets.length; + expect.options(options, [ "contracts_directory", "solc" ]); - // Load solc module only when compilation is actually required. - var solc = require("solc"); - // Clean up after solc. - var listeners = process.listeners("uncaughtException"); - var solc_listener = listeners[listeners.length - 1]; - - if (solc_listener) { - process.removeListener("uncaughtException", solc_listener); - } // Ensure sources have operating system independent paths // i.e., convert backslashes to forward slashes; things like C: are left intact. var operatingSystemIndependentSources = {}; + var operatingSystemIndependentTargets = {}; var originalPathMappings = {}; Object.keys(sources).forEach(function(source) { @@ -65,30 +60,46 @@ var compile = function(sources, options, callback) { // Save the result operatingSystemIndependentSources[replacement] = sources[source]; + // Just substitute replacement for original in target case. It's + // a disposable subset of `sources` + if(hasTargets && options.compilationTargets.includes(source)){ + operatingSystemIndependentTargets[replacement] = sources[source]; + } + // Map the replacement back to the original source path. originalPathMappings[replacement] = source; }); + var defaultSelectors = { + "": [ + "legacyAST", + "ast" + ], + "*": [ + "abi", + "evm.bytecode.object", + "evm.bytecode.sourceMap", + "evm.deployedBytecode.object", + "evm.deployedBytecode.sourceMap" + ] + } + + // Specify compilation targets + // Each target uses defaultSelectors, defaulting to single target `*` if targets are unspecified + var outputSelection = {}; + var targets = operatingSystemIndependentTargets; + var targetPaths = Object.keys(targets); + + (targetPaths.length) + ? targetPaths.forEach(key => outputSelection[key] = defaultSelectors) + : outputSelection["*"] = defaultSelectors; + var solcStandardInput = { language: "Solidity", sources: {}, settings: { optimizer: options.solc.optimizer, - outputSelection: { - "*": { - "": [ - "legacyAST", - "ast" - ], - "*": [ - "abi", - "evm.bytecode.object", - "evm.bytecode.sourceMap", - "evm.deployedBytecode.object", - "evm.deployedBytecode.sourceMap" - ] - }, - } + outputSelection: outputSelection, } }; @@ -103,106 +114,116 @@ var compile = function(sources, options, callback) { } }); - var result = solc.compileStandard(JSON.stringify(solcStandardInput)); + // Load solc module only when compilation is actually required. + var provider = new CompilerProvider(options.compiler); - var standardOutput = JSON.parse(result); + provider.load().then(solc => { + var result = solc.compileStandard(JSON.stringify(solcStandardInput)); - var errors = standardOutput.errors || []; - var warnings = []; + var standardOutput = JSON.parse(result); - if (options.strict !== true) { - warnings = errors.filter(function(error) { - return error.severity == "warning"; - }); + var errors = standardOutput.errors || []; + var warnings = []; - errors = errors.filter(function(error) { - return error.severity != "warning"; - }); + if (options.strict !== true) { + warnings = errors.filter(function(error) { + return error.severity == "warning"; + }); - if (options.quiet !== true && warnings.length > 0) { - options.logger.log(OS.EOL + "Compilation warnings encountered:" + OS.EOL); - options.logger.log(warnings.map(function(warning) { - return warning.formattedMessage; - }).join()); + errors = errors.filter(function(error) { + return error.severity != "warning"; + }); + + if (options.quiet !== true && warnings.length > 0) { + options.logger.log(OS.EOL + "Compilation warnings encountered:" + OS.EOL); + options.logger.log(warnings.map(function(warning) { + return warning.formattedMessage; + }).join()); + } } - } - if (errors.length > 0) { - options.logger.log(""); - return callback(new CompileError(standardOutput.errors.map(function(error) { - return error.formattedMessage; - }).join())); - } + if (errors.length > 0) { + options.logger.log(""); + return callback(new CompileError(standardOutput.errors.map(function(error) { + return error.formattedMessage; + }).join())); + } - var contracts = standardOutput.contracts; + var contracts = standardOutput.contracts; - var files = []; - Object.keys(standardOutput.sources).forEach(function(filename) { - var source = standardOutput.sources[filename]; - files[source.id] = originalPathMappings[filename]; - }); + var files = []; + Object.keys(standardOutput.sources).forEach(function(filename) { + var source = standardOutput.sources[filename]; + files[source.id] = originalPathMappings[filename]; + }); - var returnVal = {}; - - // This block has comments in it as it's being prepared for solc > 0.4.10 - Object.keys(contracts).forEach(function(source_path) { - var files_contracts = contracts[source_path]; - - Object.keys(files_contracts).forEach(function(contract_name) { - var contract = files_contracts[contract_name]; - - var contract_definition = { - contract_name: contract_name, - sourcePath: originalPathMappings[source_path], // Save original source path, not modified ones - source: operatingSystemIndependentSources[source_path], - sourceMap: contract.evm.bytecode.sourceMap, - deployedSourceMap: contract.evm.deployedBytecode.sourceMap, - legacyAST: standardOutput.sources[source_path].legacyAST, - ast: standardOutput.sources[source_path].ast, - abi: contract.abi, - bytecode: "0x" + contract.evm.bytecode.object, - deployedBytecode: "0x" + contract.evm.deployedBytecode.object, - unlinked_binary: "0x" + contract.evm.bytecode.object, // deprecated - compiler: { - "name": "solc", - "version": solc.version() + var returnVal = {}; + + // This block has comments in it as it's being prepared for solc > 0.4.10 + Object.keys(contracts).forEach(function(source_path) { + var files_contracts = contracts[source_path]; + + Object.keys(files_contracts).forEach(function(contract_name) { + var contract = files_contracts[contract_name]; + + // All source will have a key, but only the compiled source will have + // the evm output. + if (!Object.keys(contract.evm).length) return; + + var contract_definition = { + contract_name: contract_name, + sourcePath: originalPathMappings[source_path], // Save original source path, not modified ones + source: operatingSystemIndependentSources[source_path], + sourceMap: contract.evm.bytecode.sourceMap, + deployedSourceMap: contract.evm.deployedBytecode.sourceMap, + legacyAST: standardOutput.sources[source_path].legacyAST, + ast: standardOutput.sources[source_path].ast, + abi: contract.abi, + bytecode: "0x" + contract.evm.bytecode.object, + deployedBytecode: "0x" + contract.evm.deployedBytecode.object, + unlinked_binary: "0x" + contract.evm.bytecode.object, // deprecated + compiler: { + "name": "solc", + "version": solc.version() + } } - } - // Reorder ABI so functions are listed in the order they appear - // in the source file. Solidity tests need to execute in their expected sequence. - contract_definition.abi = orderABI(contract_definition); + // Reorder ABI so functions are listed in the order they appear + // in the source file. Solidity tests need to execute in their expected sequence. + contract_definition.abi = orderABI(contract_definition); - // Go through the link references and replace them with older-style - // identifiers. We'll do this until we're ready to making a breaking - // change to this code. - Object.keys(contract.evm.bytecode.linkReferences).forEach(function(file_name) { - var fileLinks = contract.evm.bytecode.linkReferences[file_name]; + // Go through the link references and replace them with older-style + // identifiers. We'll do this until we're ready to making a breaking + // change to this code. + Object.keys(contract.evm.bytecode.linkReferences).forEach(function(file_name) { + var fileLinks = contract.evm.bytecode.linkReferences[file_name]; - Object.keys(fileLinks).forEach(function(library_name) { - var linkReferences = fileLinks[library_name] || []; + Object.keys(fileLinks).forEach(function(library_name) { + var linkReferences = fileLinks[library_name] || []; - contract_definition.bytecode = replaceLinkReferences(contract_definition.bytecode, linkReferences, library_name); - contract_definition.unlinked_binary = replaceLinkReferences(contract_definition.unlinked_binary, linkReferences, library_name); + contract_definition.bytecode = replaceLinkReferences(contract_definition.bytecode, linkReferences, library_name); + contract_definition.unlinked_binary = replaceLinkReferences(contract_definition.unlinked_binary, linkReferences, library_name); + }); }); - }); - // Now for the deployed bytecode - Object.keys(contract.evm.deployedBytecode.linkReferences).forEach(function(file_name) { - var fileLinks = contract.evm.deployedBytecode.linkReferences[file_name]; + // Now for the deployed bytecode + Object.keys(contract.evm.deployedBytecode.linkReferences).forEach(function(file_name) { + var fileLinks = contract.evm.deployedBytecode.linkReferences[file_name]; - Object.keys(fileLinks).forEach(function(library_name) { - var linkReferences = fileLinks[library_name] || []; + Object.keys(fileLinks).forEach(function(library_name) { + var linkReferences = fileLinks[library_name] || []; - contract_definition.deployedBytecode = replaceLinkReferences(contract_definition.deployedBytecode, linkReferences, library_name); + contract_definition.deployedBytecode = replaceLinkReferences(contract_definition.deployedBytecode, linkReferences, library_name); + }); }); - }); - returnVal[contract_name] = contract_definition; + returnVal[contract_name] = contract_definition; + }); }); - }); - callback(null, returnVal, files); + callback(null, returnVal, files); + }) + .catch(callback); }; function replaceLinkReferences(bytecode, linkReferences, libraryName) { @@ -289,8 +310,10 @@ function orderABI(contract){ // quiet: Boolean. Suppress output. Defaults to false. // strict: Boolean. Return compiler warnings as errors. Defaults to false. compile.all = function(options, callback) { - var self = this; + find_contracts(options.contracts_directory, function(err, files) { + if (err) return callback(err) + options.paths = files; compile.with_dependencies(options, callback); }); @@ -319,6 +342,8 @@ compile.necessary = function(options, callback) { }; compile.with_dependencies = function(options, callback) { + var self = this; + options.logger = options.logger || console; options.contracts_directory = options.contracts_directory || process.cwd(); @@ -331,26 +356,38 @@ compile.with_dependencies = function(options, callback) { var config = Config.default().merge(options); - var self = this; Profiler.required_sources(config.with({ paths: options.paths, base_path: options.contracts_directory, resolver: options.resolver - }), function(err, result) { + }), (err, allSources, required) => { if (err) return callback(err); - if (options.quiet != true) { - Object.keys(result).sort().forEach(function(import_path) { - var display_path = import_path; - if (path.isAbsolute(import_path)) { - display_path = "." + path.sep + path.relative(options.working_directory, import_path); - } - options.logger.log("Compiling " + display_path + "..."); - }); - } + var hasTargets = required.length; - compile(result, options, callback); + (hasTargets) + ? self.display(required, options) + : self.display(allSources, options); + + options.compilationTargets = required; + compile(allSources, options, callback); }); }; +compile.display = function(paths, options){ + if (options.quiet != true) { + if (!Array.isArray(paths)){ + paths = Object.keys(paths); + } + + paths.sort().forEach(contract => { + if (path.isAbsolute(contract)) { + contract = "." + path.sep + path.relative(options.working_directory, contract); + } + options.logger.log("Compiling " + contract + "..."); + }); + } +}; + +compile.CompilerProvider = CompilerProvider; module.exports = compile; diff --git a/package.json b/package.json index b8835fe..d18e863 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,11 @@ "async": "^2.1.4", "colors": "^1.1.2", "debug": "^3.1.0", - "graphlib": "^2.1.1", + "find-cache-dir": "^1.0.0", + "original-require": "^1.0.1", + "request": "^2.85.0", + "request-promise": "^4.2.2", + "require-from-string": "^2.0.2", "solc": "0.4.23", "truffle-config": "^1.0.4", "truffle-contract-sources": "^0.0.1", @@ -15,11 +19,21 @@ "truffle-expect": "^0.0.3" }, "devDependencies": { + "babel-core": "^6.26.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", "mocha": "^3.5.3", "truffle-resolver": "2.0.0" }, + "babel": { + "presets": [ + "env" + ] + }, "scripts": { - "test": "mocha" + "test": "mocha --invert --grep native --timeout 5000 --compilers js:babel-core/register --require babel-polyfill test", + "test:native": "mocha --grep native --timeout 5000 --compilers js:babel-core/register --require babel-polyfill test", + "test:ci": "./scripts/ci.sh" }, "repository": { "type": "git", diff --git a/parser.js b/parser.js index ec74935..42cddb6 100644 --- a/parser.js +++ b/parser.js @@ -1,103 +1,13 @@ var CompileError = require("./compileerror"); -var solc = require("solc"); var fs = require("fs"); var path = require("path"); -// Clean up after solc. -var listeners = process.listeners("uncaughtException"); -var solc_listener = listeners[listeners.length - 1]; - -if (solc_listener) { - process.removeListener("uncaughtException", solc_listener); -} - // Warning issued by a pre-release compiler version, ignored by this component. var preReleaseCompilerWarning = "This is a pre-release compiler version, please do not use it in production."; -var installedContractsDir = "installed_contracts" - module.exports = { - parse: function(body, fileName) { - // Here, we want a valid AST even if imports don't exist. The way to - // get around that is to tell the compiler, as they happen, that we - // have source for them (an empty file). - - var build_remappings = function() { - // Maps import paths to paths from EthPM installed contracts, so we can correctly solve imports - // e.g. "my_pkg/=installed_contracts/my_pkg/contracts/" - var remappings = []; - - if (fs.existsSync('ethpm.json')) { - ethpm = JSON.parse(fs.readFileSync('ethpm.json')); - for (pkg in ethpm.dependencies) { - remappings.push(pkg + "/=" + path.join(installedContractsDir, pkg, 'contracts', '/')); - } - } - - return remappings; - } - - var fileName = fileName || "ParsedContract.sol"; - - var remappings = build_remappings(); - - var solcStandardInput = { - language: "Solidity", - sources: { - [fileName]: { - content: body - } - }, - settings: { - remappings: remappings, - outputSelection: { - "*": { - "": [ - "ast" - ] - } - } - } - }; - - var output = solc.compileStandard(JSON.stringify(solcStandardInput), function(file_path) { - // Resolve dependency manually. - if (fs.existsSync(file_path)) { - contents = fs.readFileSync(file_path, {encoding: 'UTF-8'}); - } - else { - contents = "pragma solidity ^0.4.0;"; - } - return {contents: contents}; - }); - - output = JSON.parse(output); - - // Filter out the "pre-release compiler" warning, if present. - var errors = output.errors ? output.errors.filter(function(solidity_error) { - return solidity_error.message.indexOf(preReleaseCompilerWarning) < 0; - }) : []; - - // Filter out warnings. - var warnings = output.errors ? output.errors.filter(function(solidity_error) { - return solidity_error.severity == "warning"; - }) : []; - var errors = output.errors ? output.errors.filter(function(solidity_error) { - return solidity_error.severity != "warning"; - }) : []; - - if (errors.length > 0) { - throw new CompileError(errors[0].formattedMessage); - } - - return { - contracts: Object.keys(output.contracts[fileName]), - ast: output.sources[fileName].ast - }; - }, - // This needs to be fast! It is fast (as of this writing). Keep it fast! - parseImports: function(body) { + parseImports: function(body, solc) { var self = this; // WARNING: Kind of a hack (an expedient one). @@ -109,6 +19,9 @@ module.exports = { // statement right on the end; just to ensure it will error and we can parse // the imports speedily without doing extra work. + // If we're using docker/native, we'll still want to use solcjs to do this part. + if (solc.importsParser) solc = solc.importsParser; + // Helper to detect import errors with an easy regex. var importErrorKey = "TRUFFLE_IMPORT"; diff --git a/profiler.js b/profiler.js index 9ecc9c2..8e068f8 100644 --- a/profiler.js +++ b/profiler.js @@ -4,10 +4,9 @@ var path = require("path"); var async = require("async"); var fs = require("fs"); -var Graph = require("graphlib").Graph; -var isAcyclic = require("graphlib/lib/alg").isAcyclic; var Parser = require("./parser"); var CompileError = require("./compileerror"); +var CompilerProvider = require("./compilerProvider"); var expect = require("truffle-expect"); var find_contracts = require("truffle-contract-sources"); var debug = require("debug")("compile:profiler"); @@ -159,6 +158,8 @@ module.exports = { }); }, + // Returns the minimal set of sources to pass to solc as compilations targets, + // as well as the complete set of sources so solc can resolve the comp targets' imports. required_sources: function(options, callback) { var self = this; @@ -168,80 +169,168 @@ module.exports = { "resolver" ]); - var paths = this.convert_to_absolute_paths(options.paths, options.base_path); + var resolver = options.resolver; - function findRequiredSources(dependsGraph, done) { - var required = {}; + // Fetch the whole contract set + find_contracts(options.contracts_directory, (err, allPaths) => { + if(err) return callback(err); - function hasBeenTraversed(import_path) { - return required[import_path] != null; - } + // Solidity test files might have been injected. Include them in the known set. + options.paths.forEach(_path => { + if (!allPaths.includes(_path)) { + allPaths.push(_path) + } + }); - function include(import_path) { - //console.log("Including: " + file) + var updates = self.convert_to_absolute_paths(options.paths, options.base_path).sort(); + var allPaths = self.convert_to_absolute_paths(allPaths, options.base_path).sort(); - required[import_path] = dependsGraph.node(import_path); - } + var allSources = {}; + var compilationTargets = []; - function walk_down(import_path) { - if (hasBeenTraversed(import_path)) { - return; - } + // Load compiler + var provider = new CompilerProvider(options.compiler) + provider.load().then(solc => { - include(import_path); + // Get all the source code + self.resolveAllSources(resolver, allPaths, solc, (err, resolved) => { + if(err) return callback(err); - var dependencies = dependsGraph.successors(import_path); + // Generate hash of all sources including external packages - passed to solc inputs. + var resolvedPaths = Object.keys(resolved); + resolvedPaths.forEach(file => allSources[file] = resolved[file].body) - // console.log("At: " + import_path); - // console.log(" Dependencies: ", dependencies); + // Exit w/out minimizing if we've been asked to compile everything, or nothing. + if (self.listsEqual(options.paths, allPaths)){ + return callback(null, allSources, {}); + } else if (!options.paths.length){ + return callback(null, {}, {}); + } - if (dependencies.length > 0) { - dependencies.forEach(walk_down); - } - } + // Seed compilationTargets with known updates + updates.forEach(update => compilationTargets.push(update)); - function walk_from(import_path) { - if (hasBeenTraversed(import_path)) { - return; - } + // While there are updated files in the queue, we take each one + // and search the entire file corpus to find any sources that import it. + // Those sources are added to list of compilation targets as well as + // the update queue because their own ancestors need to be discovered. + async.whilst(() => updates.length > 0, updateFinished => { + var currentUpdate = updates.shift(); + var files = allPaths.slice(); - var ancestors = dependsGraph.predecessors(import_path); - var dependencies = dependsGraph.successors(import_path); + // While files: dequeue and inspect their imports + async.whilst(() => files.length > 0, fileFinished => { - // console.log("At: " + import_path); - // console.log(" Ancestors: ", ancestors); - // console.log(" Dependencies: ", dependencies); + var currentFile = files.shift(); - include(import_path); + // Ignore targets already selected. + if (compilationTargets.includes(currentFile)){ + return fileFinished(); + } - if (ancestors && ancestors.length > 0) { - ancestors.forEach(walk_from); - } + var imports; + try { + imports = self.getImports(currentFile, resolved[currentFile], solc); + } catch (err) { + err.message = "Error parsing " + currentFile + ": " + e.message; + return fileFinished(err); + } - if (dependencies && dependencies.length > 0) { - dependencies.forEach(walk_down); - } - } + // If file imports a compilation target, add it + // to list of updates and compilation targets + if (imports.includes(currentUpdate)){ + updates.push(currentFile); + compilationTargets.push(currentFile); + } + + fileFinished(); - paths.forEach(walk_from); + }, err => updateFinished(err)); + }, err => (err) ? callback(err) : callback(null, allSources, compilationTargets)) + }) + }).catch(callback) + }) + }, - done(null, required); + // Resolves sources in several async passes. For each resolved set it detects unknown + // imports from external packages and adds them to the set of files to resolve. + resolveAllSources: function(resolver, initialPaths, solc, callback){ + var self = this; + var mapping = {}; + var allPaths = initialPaths.slice(); + + function generateMapping(finished){ + var promises = []; + + // Dequeue all the known paths, generating resolver promises, + // We'll add paths if we discover external package imports. + while(allPaths.length){ + var file = allPaths.shift(); + + var promise = new Promise((accept, reject)=> { + resolver.resolve(file, null, (err, body, absolutePath, source) => { + (err) + ? reject(err) + : accept({ file: absolutePath, body: body, source: source }); + }); + }); + promises.push(promise); + }; + + // Resolve everything known and add it to the map, then inspect each file's + // imports and add those to the list of paths to resolve if we don't have it. + Promise.all(promises).then(results => { + + // Generate the sources mapping + results.forEach(item => mapping[item.file] = Object.assign({}, item)); + + // Queue unknown imports for the next resolver cycle + while(results.length){ + var result = results.shift(); + + // Inspect the imports + var imports; + try { + imports = self.getImports(result.file, result, solc); + } catch (err) { + err.message = "Error parsing " + result[file] + ": " + err.message; + return finished(err); + } + + // Detect unknown external packages / add them to the list of files to resolve + imports.forEach(item => (!mapping[item]) ? allPaths.push(item) : null) + }; + finished() + }).catch(err => { throw new Error(err) }); } - find_contracts(options.base_path, function(err, allPaths) { - if (err) return callback(err); + async.whilst( + () => allPaths.length, + generateMapping, + (err) => (err) ? callback(err) : callback(null, mapping) + ); + }, - // Include paths for Solidity .sols, specified in options. - allPaths = allPaths.concat(paths); + getImports: function(file, resolved, solc){ + var self = this; - self.dependency_graph(allPaths, options.resolver, function(err, dependsGraph) { - if (err) return callback(err); + var imports = Parser.parseImports(resolved.body, solc); - findRequiredSources(dependsGraph, callback); - }); + // Convert explicitly relative dependencies of modules back into module paths. + return imports.map(dependencyPath => { + return (self.isExplicitlyRelative(dependencyPath)) + ? resolved.source.resolve_dependency_path(file, dependencyPath) + : dependencyPath; }); }, + listsEqual: function(listA, listB){ + var a = listA.sort(); + var b = listB.sort(); + + return JSON.stringify(a) === JSON.stringify(b); + }, + convert_to_absolute_paths: function(paths, base) { var self = this; return paths.map(function(p) { @@ -259,135 +348,4 @@ module.exports = { isExplicitlyRelative: function(import_path) { return import_path.indexOf(".") == 0; }, - - dependency_graph: function(paths, resolver, callback) { - var self = this; - - // Iterate through all the contracts looking for libraries and building a dependency graph - var dependsGraph = new Graph(); - - var imports_cache = {}; - - // For the purposes of determining correct error messages. - // The second array item denotes which path imported the current path. - // In the case of the paths passed in, there was none. - paths = paths.map(function(p) { - return [p, null]; - }); - - async.whilst(function() { - return paths.length > 0; - }, function(finished) { - var current = paths.shift(); - var import_path = current[0]; - var imported_from = current[1]; - - if (dependsGraph.hasNode(import_path) && imports_cache[import_path] != null) { - return finished(); - } - - resolver.resolve(import_path, imported_from, function(err, resolved_body, resolved_path, source) { - if (err) return finished(err); - - if (dependsGraph.hasNode(resolved_path) && imports_cache[resolved_path] != null) { - return finished(); - } - - // Add the contract to the depends graph. - dependsGraph.setNode(resolved_path, resolved_body); - - var imports; - - try { - imports = Parser.parseImports(resolved_body); - } catch (e) { - e.message = "Error parsing " + import_path + ": " + e.message; - return finished(e); - } - - // Convert explicitly relative dependencies of modules - // back into module paths. We also use this loop to update - // the graph edges. - imports = imports.map(function(dependency_path) { - // Convert explicitly relative paths - if (self.isExplicitlyRelative(dependency_path)) { - dependency_path = source.resolve_dependency_path(import_path, dependency_path); - } - - // Update graph edges - if (!dependsGraph.hasEdge(import_path, dependency_path)) { - dependsGraph.setEdge(import_path, dependency_path); - } - - // Return an array that denotes a new import and the path it was imported from. - return [dependency_path, import_path]; - }); - - imports_cache[import_path] = imports; - - Array.prototype.push.apply(paths, imports); - - finished(); - }); - }, - function(err) { - if (err) return callback(err); - callback(null, dependsGraph) - }); - }, - - // Parse all source files in the directory and output the names of contracts and their source paths - // directory can either be a directory or array of files. - defined_contracts: function(directory, callback) { - function getFiles(callback) { - if (Array.isArray(directory)) { - callback(null, directory); - } else { - find_contracts(directory, callback); - } - } - - getFiles(function(err, files) { - if (err) return callback(err); - - var promises = files.map(function(file) { - return new Promise(function(accept, reject) { - fs.readFile(file, "utf8", function(err, body) { - if (err) return reject(err); - - var output; - - try { - output = Parser.parse(body); - } catch (e) { - e.message = "Error parsing " + file + ": " + e.message; - return reject(e); - } - - accept(output.contracts); - }); - }).then(function(contract_names) { - var returnVal = {}; - - contract_names.forEach(function(contract_name) { - returnVal[contract_name] = file; - }); - - return returnVal; - }); - }); - - Promise.all(promises).then(function(objects) { - var contract_source_paths = {}; - - objects.forEach(function(object) { - Object.keys(object).forEach(function(contract_name) { - contract_source_paths[contract_name] = object[contract_name]; - }); - }); - - callback(null, contract_source_paths); - }).catch(callback); - }); - } }; diff --git a/scripts/ci.sh b/scripts/ci.sh new file mode 100755 index 0000000..49ada75 --- /dev/null +++ b/scripts/ci.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -o errexit + +run_native_tests() { + sudo add-apt-repository --yes ppa:ethereum/ethereum + sudo apt-get update + sudo apt-get install solc + + docker pull ethereum/solc:0.4.22 + npm install + npm run test:native +} + +run_container_tests() { + docker run -it --rm --name ${TEST} \ + -e TRAVIS_REPO_SLUG \ + -e TRAVIS_PULL_REQUEST \ + -e TRAVIS_PULL_REQUEST_SLUG \ + -e TRAVIS_PULL_REQUEST_BRANCH \ + -e TRAVIS_BRANCH \ + -e TEST \ + truffle/ci:latest run_tests +} + +if [ "$TEST" == "native" ]; then + run_native_tests +else + run_container_tests +fi diff --git a/solcWrap.js b/solcWrap.js new file mode 100644 index 0000000..0d6504a --- /dev/null +++ b/solcWrap.js @@ -0,0 +1,90 @@ +/** + * This is a truncated version of soljs's wrapper for soljson.js (the solc js file available + * at solc-bin. Using this allows us to avoid requiring solc twice, cutting ~2s off the load time) + */ + +var assert = require('assert'); + +function solcWrap (soljson) { + var compileJSON = soljson.cwrap('compileJSON', 'string', ['string', 'number']); + var compileJSONMulti = null; + if ('_compileJSONMulti' in soljson) { + compileJSONMulti = soljson.cwrap('compileJSONMulti', 'string', ['string', 'number']); + } + var compileJSONCallback = null; + var compileStandard = null; + if (('_compileJSONCallback' in soljson) || ('_compileStandard' in soljson)) { + var copyString = function (str, ptr) { + var length = soljson.lengthBytesUTF8(str); + var buffer = soljson._malloc(length + 1); + soljson.stringToUTF8(str, buffer, length + 1); + soljson.setValue(ptr, buffer, '*'); + }; + var wrapCallback = function (callback) { + assert(typeof callback === 'function', 'Invalid callback specified.'); + return function (path, contents, error) { + var result = callback(soljson.Pointer_stringify(path)); + if (typeof result.contents === 'string') { + copyString(result.contents, contents); + } + if (typeof result.error === 'string') { + copyString(result.error, error); + } + }; + }; + + // This calls compile() with args || cb + var runWithReadCallback = function (readCallback, compile, args) { + if (readCallback === undefined) { + readCallback = function (path) { + return { + error: 'File import callback not supported' + }; + }; + } + var cb = soljson.Runtime.addFunction(wrapCallback(readCallback)); + var output; + try { + args.push(cb); + output = compile.apply(undefined, args); + } catch (e) { + soljson.Runtime.removeFunction(cb); + throw e; + } + soljson.Runtime.removeFunction(cb); + return output; + }; + + var compileInternal = soljson.cwrap( + 'compileJSONCallback', + 'string', + ['string', 'number', 'number'] + ); + + compileJSONCallback = function (input, optimize, readCallback) { + return runWithReadCallback(readCallback, compileInternal, [ input, optimize ]); + }; + + if ('_compileStandard' in soljson) { + + var compileStandardInternal = soljson.cwrap( + 'compileStandard', + 'string', + ['string', 'number'] + ); + + compileStandard = function (input, readCallback) { + return runWithReadCallback(readCallback, compileStandardInternal, [ input ]); + }; + } + } + + var version = soljson.cwrap('version', 'string', []); + + return { + compileStandard: compileStandard, + version: version, + }; +} + +module.exports = solcWrap; diff --git a/test/MyContract.sol b/test/mock/MyContract.sol similarity index 99% rename from test/MyContract.sol rename to test/mock/MyContract.sol index 05a5f91..0b67f68 100644 --- a/test/MyContract.sol +++ b/test/mock/MyContract.sol @@ -15,4 +15,4 @@ library SomeLibrary { interface SomeInterface { -} \ No newline at end of file +} diff --git a/test/mock/OldPragmaPin.sol b/test/mock/OldPragmaPin.sol new file mode 100644 index 0000000..04d28fb --- /dev/null +++ b/test/mock/OldPragmaPin.sol @@ -0,0 +1,9 @@ +pragma solidity 0.4.15; + +contract OldPragmaPin { + uint x; + + function OldPragmaPin() public { + x = 7; + } +} diff --git a/test/ShouldError.sol b/test/mock/ShouldError.sol similarity index 100% rename from test/ShouldError.sol rename to test/mock/ShouldError.sol diff --git a/test/ComplexOrdered.sol b/test/sources/ComplexOrdered.sol similarity index 95% rename from test/ComplexOrdered.sol rename to test/sources/ComplexOrdered.sol index 84e51d3..460e240 100644 --- a/test/ComplexOrdered.sol +++ b/test/sources/ComplexOrdered.sol @@ -16,4 +16,4 @@ contract ComplexOrdered is InheritA { function andThird() public pure {} } -contract Empty {} \ No newline at end of file +contract Empty {} diff --git a/test/InheritB.sol b/test/sources/InheritB.sol similarity index 100% rename from test/InheritB.sol rename to test/sources/InheritB.sol diff --git a/test/sources/NewPragma.sol b/test/sources/NewPragma.sol new file mode 100644 index 0000000..0e75f70 --- /dev/null +++ b/test/sources/NewPragma.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.4.21; + +contract NewPragma { + uint x; + + function NewPragma() public { + x = 5; + } +} diff --git a/test/sources/OldPragmaFloat.sol b/test/sources/OldPragmaFloat.sol new file mode 100644 index 0000000..7aadf72 --- /dev/null +++ b/test/sources/OldPragmaFloat.sol @@ -0,0 +1,9 @@ +pragma solidity ^0.4.15; + +contract OldPragmaFloat { + uint x; + + function OldPragmaFloat() public { + x = 7; + } +} diff --git a/test/SimpleOrdered.sol b/test/sources/SimpleOrdered.sol similarity index 100% rename from test/SimpleOrdered.sol rename to test/sources/SimpleOrdered.sol diff --git a/test/test_ordering.js b/test/test_ordering.js index e495f60..45c4c4b 100644 --- a/test/test_ordering.js +++ b/test/test_ordering.js @@ -6,15 +6,16 @@ var assert = require("assert"); describe("Compile", function() { this.timeout(5000); // solc - var orderedSource = null; - var emptySource = null; - var compileOptions = { contracts_directory: '', solc: ''}; + var simpleOrderedSource = null; + var complexOrderedSource = null; + var inheritedSource = null; + var compileOptions = { contracts_directory: '', solc: '', quiet: true}; describe("ABI Ordering", function(){ before("get code", function() { - simpleOrderedSource = fs.readFileSync(path.join(__dirname, "SimpleOrdered.sol"), "utf-8"); - complexOrderedSource = fs.readFileSync(path.join(__dirname, "ComplexOrdered.sol"), "utf-8"); - inheritedSource = fs.readFileSync(path.join(__dirname, "InheritB.sol"), "utf-8"); + simpleOrderedSource = fs.readFileSync(path.join(__dirname, "./sources/SimpleOrdered.sol"), "utf-8"); + complexOrderedSource = fs.readFileSync(path.join(__dirname, "./sources/ComplexOrdered.sol"), "utf-8"); + inheritedSource = fs.readFileSync(path.join(__dirname, "./sources/InheritB.sol"), "utf-8"); }); // Ordered.sol's methods are ordered semantically. @@ -35,7 +36,7 @@ describe("Compile", function() { assert.deepEqual(abi, alphabetic); }); - it("orders the simple ABI", function(){ + it("orders the simple ABI", function(done){ var expectedOrder = ['theFirst', 'second', 'andThird']; var sources = {}; sources["SimpleOrdered.sol"] = simpleOrderedSource; @@ -45,6 +46,7 @@ describe("Compile", function() { return item.name; }); assert.deepEqual(abi, expectedOrder); + done(); }) }); @@ -67,7 +69,7 @@ describe("Compile", function() { assert.deepEqual(abi, alphabetic); }); - it("orders the complex ABI", function(){ + it("orders the complex ABI", function(done){ var expectedOrder = ['LogB', 'LogA', 'LogD', 'LogC', 'theFirst', 'second', 'andThird']; var sources = {}; sources["ComplexOrdered.sol"] = complexOrderedSource; @@ -78,17 +80,20 @@ describe("Compile", function() { return item.name; }); assert.deepEqual(abi, expectedOrder); + done(); }) }); // Ported from `truffle-solidity-utils` - it("orders the ABI of a contract without functions", function(){ + it("orders the ABI of a contract without functions", function(done){ var sources = {}; + // ComplexOrdered.sol includes contract `Empty` sources["ComplexOrdered.sol"] = complexOrderedSource; sources["InheritB.sol"] = inheritedSource; Compile(sources, compileOptions, function(err, result){ assert.equal(result["Empty"].abi.length, 0); + done(); }) }) }) diff --git a/test/test_parser.js b/test/test_parser.js index b88ba8a..0b9b675 100644 --- a/test/test_parser.js +++ b/test/test_parser.js @@ -1,19 +1,24 @@ var fs = require("fs"); var path = require("path"); var Parser = require("../parser"); +var CompilerProvider = require("../compilerProvider") var assert = require("assert"); describe("Parser", function() { var source = null; var erroneousSource = null; + var solc; - before("get code", function() { - source = fs.readFileSync(path.join(__dirname, "MyContract.sol"), "utf-8"); - erroneousSource = fs.readFileSync(path.join(__dirname, "ShouldError.sol"), "utf-8"); + before("get code", async function() { + source = fs.readFileSync(path.join(__dirname, "./mock/MyContract.sol"), "utf-8"); + erroneousSource = fs.readFileSync(path.join(__dirname, "./mock/ShouldError.sol"), "utf-8"); + + const provider = new CompilerProvider(); + solc = await provider.load(); }); it("should return correct imports", function() { - var imports = Parser.parseImports(source); + var imports = Parser.parseImports(source, solc); // Note that this test is important because certain parts of the solidity // output cuts off path prefixes like "./" and "../../../". If we get the @@ -31,34 +36,7 @@ describe("Parser", function() { it("should throw an error when parsing imports if there's an actual parse error", function() { var error = null; try { - Parser.parseImports(erroneousSource); - } catch(e) { - error = e; - } - - if (!error) { - throw new Error("Expected a parse error but didn't get one!"); - } - - assert(error.message.indexOf("Expected pragma, import directive or contract") >= 0); - }); - - it("should return a full AST when parsed, even when dependencies don't exist", function() { - this.timeout(4000); - - var output = Parser.parse(source); - - assert.deepEqual(output.contracts, ["MyContract", "SomeInterface", "SomeLibrary"]); - assert(output.ast.nodes.length > 0); - - // The above assert means we at least got some kind of AST. - // Is there something we specifically need here? - }); - - it("should throw an error when parsing completely if there's an actual parse error", function() { - var error = null; - try { - Parser.parse(erroneousSource); + Parser.parseImports(erroneousSource, solc); } catch(e) { error = e; } diff --git a/test/test_provider.js b/test/test_provider.js new file mode 100644 index 0000000..dc0b7b2 --- /dev/null +++ b/test/test_provider.js @@ -0,0 +1,310 @@ +const fs = require("fs"); +const path = require("path"); +const solc = require("solc"); +const assert = require("assert"); +const findCacheDir = require('find-cache-dir'); +const Resolver = require('truffle-resolver'); +const compile = require("../index"); +const CompilerProvider = require('../compilerProvider'); + + +function waitSecond() { + return new Promise((resolve, reject) => setTimeout(() => resolve(), 1250)); +} + +describe('CompilerProvider', function(){ + let provider; + + describe('getters', function(){ + + before(() => provider = new CompilerProvider()); + + it('getVersions: should return versions object', async function(){ + const list = await provider.getVersions(); + assert(list.releases !== undefined); + }); + + it('getVersionUrlSegment: should return a JS file name', async function(){ + const list = await provider.getVersions(); + + let input = '0.4.21'; + let expected = 'soljson-v0.4.21+commit.dfe3193c.js'; + + let fileName = await provider.getVersionUrlSegment(input, list); + assert(fileName === expected, 'Should locate by version'); + + input = 'nightly.2018.4.12'; + expected = 'soljson-v0.4.22-nightly.2018.4.12+commit.c3dc67d0.js'; + + fileName = await provider.getVersionUrlSegment(input, list); + assert(fileName === expected, 'Should locate by nightly'); + + input = 'commit.c3dc67d0'; + expected = 'soljson-v0.4.22-nightly.2018.4.12+commit.c3dc67d0.js'; + + fileName = await provider.getVersionUrlSegment(input, list); + assert(fileName === expected, 'Should locate by commit'); + + input = '0.4.55.77-fantasy-solc'; + expected = null; + + fileName = await provider.getVersionUrlSegment(input, list); + assert(fileName === null, 'Should return null if not found') + }); + + it('getReleases: should return a `releases` object', async function(){ + const list = await provider.getVersions(); + const releases = await provider.getReleases(); + const firstSolc = "0.1.3-nightly.2015.9.25+commit.4457170" + + assert(releases.prereleases[0] === firstSolc, 'Should return prereleases'); + assert(releases.releases[0] === releases.latestRelease, 'Should return releases/latestRelease'); + }); + + it('lists available docker images [ @native ]', async function(){ + const list = await provider.getDockerTags(); + assert(Array.isArray(list)); + assert(typeof list[0] === 'string'); + }) + }); + + describe('integration', function(){ + this.timeout(40000); + let newPragmaSource; // ^0.4.21 + let oldPragmaPinSource; // 0.4.15 + let oldPragmaFloatSource; // ^0.4.15 + + const options = { + contracts_directory: '', + solc: '', + quiet: true, + }; + + before("get code", function() { + const newPragma = fs.readFileSync(path.join(__dirname, "./sources/NewPragma.sol"), "utf-8"); + const oldPragmaPin = fs.readFileSync(path.join(__dirname, "./mock/OldPragmaPin.sol"), "utf-8"); + const oldPragmaFloat = fs.readFileSync(path.join(__dirname, "./sources/OldPragmaFloat.sol"), "utf-8"); + + newPragmaSource = { "NewPragma.sol": newPragma}; + oldPragmaPinSource = { "OldPragmaPin.sol": oldPragmaPin}; + oldPragmaFloatSource = { "OldPragmaFloat.sol": oldPragmaFloat}; + }); + + + it('compiles w/ default solc if no compiler specified (float)', function(done){ + options.compiler = { cache: false }; + + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + assert(result['NewPragma'].contract_name === 'NewPragma'); + done(); + }); + }); + + it('compiles w/ remote solc when options specify release (pinned)', function(done){ + options.compiler = { + cache: false, + solc: "0.4.15" + }; + + compile(oldPragmaPinSource, options, (err, result) => { + if (err) return done(err); + + assert(result['OldPragmaPin'].contract_name === 'OldPragmaPin'); + done(); + }); + }); + + it('compiles w/ remote solc when options specify prerelease (float)', function(done){ + // An 0.4.16 prerelease for 0.4.15 + options.compiler = { + cache: false, + solc: "0.4.16-nightly.2017.8.9+commit.81887bc7" + }; + + compile(oldPragmaFloatSource, options, (err, result) => { + if (err) return done(err); + + assert(result['OldPragmaFloat'].contract_name === 'OldPragmaFloat'); + done(); + }); + }); + + it('errors when specified release does not exist', function(done){ + options.compiler = { + cache: false, + solc: "0.4.55.77-fantasy-solc" + }; + + compile(newPragmaSource, options, (err, result) => { + assert(err.message.includes('Could not find compiler version')); + done(); + }); + }); + + it('compiles w/ local path solc when options specify path', function(done){ + const pathToSolc = path.join(__dirname, "../node_modules/solc/index.js"); + + options.compiler = { + cache: false, + solc: pathToSolc + }; + + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + assert(result['NewPragma'].contract_name === 'NewPragma'); + done(); + }); + }); + + it('errors when specified path does not exist', function(done){ + const pathToSolc = path.join(__dirname, "../solidity-warehouse/solc/index.js"); + options.compiler = { + cache: false, + solc: pathToSolc + }; + + compile(newPragmaSource, options, (err, result) => { + assert(err.message.includes('Could not find compiler at:')); + done(); + }); + }); + + it('caches releases and uses them if available', function(done){ + let initialAccessTime; + let finalAccessTime; + + const thunk = findCacheDir({name: 'truffle', thunk: true}); + const expectedCache = thunk('soljson-v0.4.21+commit.dfe3193c.js'); + + // Delete if it's already there. + if (fs.existsSync(expectedCache)) fs.unlinkSync(expectedCache); + + options.compiler = { + cache: true, + solc: "0.4.21" + }; + + // Run compiler, expecting solc to be downloaded and cached. + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + assert(fs.existsSync(expectedCache), 'Should have cached compiler'); + + // Get cached solc access time + initialAccessTime = fs.statSync(expectedCache).atime.getTime() + + // Wait a second and recompile, verifying that the cached solc + // got accessed / ran ok. + waitSecond().then(() => { + + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + finalAccessTime = fs.statSync(expectedCache).atime.getTime() + + assert(result['NewPragma'].contract_name === 'NewPragma', 'Should have compiled'); + + // atime is not getting updated on read in CI. + if (!process.env.TEST){ + assert(initialAccessTime < finalAccessTime, "Should have used cached compiler"); + } + + done(); + }); + }).catch(done); + }); + }); + + describe('native / docker [ @native ]', function() { + + it('compiles with native solc', function(done){ + options.compiler = { + solc: "native" + }; + + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + assert(result['NewPragma'].compiler.version.includes('Linux.g++')); + assert(result['NewPragma'].contract_name === 'NewPragma', 'Should have compiled'); + done(); + }); + }); + + it('compiles with dockerized solc', function(done){ + options.compiler = { + solc: "0.4.22", + docker: true + }; + + const expectedVersion = '0.4.22+commit.4cb486ee.Linux.g++'; + + compile(newPragmaSource, options, (err, result) => { + if (err) return done(err); + + assert(result['NewPragma'].compiler.version === expectedVersion); + assert(result['NewPragma'].contract_name === 'NewPragma', 'Should have compiled'); + done(); + }); + }); + + it('resolves imports correctly when using built solc', function(done){ + const paths = []; + paths.push(path.join(__dirname, "./sources/ComplexOrdered.sol")); + paths.push(path.join(__dirname, "./sources/InheritB.sol")); + + let options = { + compiler : { + solc: "0.4.22", + docker: true + }, + quiet: true, + solc: '', + contracts_build_directory: path.join(__dirname, "./build"), + contracts_directory: path.join(__dirname, "./sources"), + working_directory: __dirname, + paths: paths + } + + options.resolver = new Resolver(options); + + compile.with_dependencies(options, (err, result) => { + if (err) return done(err); + + // This contract imports / inherits + assert(result['ComplexOrdered'].contract_name === 'ComplexOrdered', 'Should have compiled'); + done(); + }); + }) + + it('errors if running dockerized solc without specifying an image', function(done){ + options.compiler = { + solc: undefined, + docker: true + }; + + compile(newPragmaSource, options, (err, result) => { + assert(err.message.includes('option must be')); + done(); + }); + }) + + it('errors if running dockerized solc when image does not exist locally', function(done){ + const imageName = 'fantasySolc.7777555'; + + options.compiler = { + solc: imageName, + docker: true + }; + + compile(newPragmaSource, options, (err, result) => { + assert(err.message.includes(imageName)); + done(); + }); + }) + }); + }); +});