From c5632490f25efd409472ca4727ee22b5bf0c763a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Fri, 14 Dec 2018 12:55:50 -0800 Subject: Refactor most of the sol-compiler methods into helper functions in utils and make resolver pluggable into them --- packages/sol-compiler/src/compiler.ts | 205 +++---------------------- packages/sol-compiler/src/utils/compiler.ts | 216 ++++++++++++++++++++++++++- packages/sol-compiler/src/utils/constants.ts | 3 + 3 files changed, 237 insertions(+), 187 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 85df8209e..45cbf527b 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -8,24 +8,24 @@ import { Resolver, URLResolver, } from '@0x/sol-resolver'; -import { fetchAsync, logUtils } from '@0x/utils'; -import chalk from 'chalk'; +import { logUtils } from '@0x/utils'; import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types'; -import * as ethUtil from 'ethereumjs-util'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; -import * as requireFromString from 'require-from-string'; import * as semver from 'semver'; import solc = require('solc'); import { compilerOptionsSchema } from './schemas/compiler_options_schema'; import { binPaths } from './solc/bin_paths'; import { + addHexPrefixToContractBytecode, + compile, createDirIfDoesNotExistAsync, getContractArtifactIfExistsAsync, - getNormalizedErrMsg, - parseDependencies, + getSolcAsync, + getSourcesWithDependencies, + getSourceTreeHash, parseSolidityVersionRange, } from './utils/compiler'; import { constants } from './utils/constants'; @@ -35,7 +35,6 @@ import { utils } from './utils/utils'; type TYPE_ALL_FILES_IDENTIFIER = '*'; const ALL_CONTRACTS_IDENTIFIER = '*'; const ALL_FILES_IDENTIFIER = '*'; -const SOLC_BIN_DIR = path.join(__dirname, '..', '..', 'solc_bin'); const DEFAULT_CONTRACTS_DIR = path.resolve('contracts'); const DEFAULT_ARTIFACTS_DIR = path.resolve('artifacts'); // Solc compiler settings cannot be configured from the commandline. @@ -82,49 +81,6 @@ export class Compiler { private readonly _artifactsDir: string; private readonly _solcVersionIfExists: string | undefined; private readonly _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER; - private static async _getSolcAsync( - solcVersion: string, - ): Promise<{ solcInstance: solc.SolcInstance; fullSolcVersion: string }> { - const fullSolcVersion = binPaths[solcVersion]; - if (_.isUndefined(fullSolcVersion)) { - throw new Error(`${solcVersion} is not a known compiler version`); - } - const compilerBinFilename = path.join(SOLC_BIN_DIR, fullSolcVersion); - let solcjs: string; - if (await fsWrapper.doesFileExistAsync(compilerBinFilename)) { - solcjs = (await fsWrapper.readFileAsync(compilerBinFilename)).toString(); - } else { - logUtils.warn(`Downloading ${fullSolcVersion}...`); - const url = `${constants.BASE_COMPILER_URL}${fullSolcVersion}`; - const response = await fetchAsync(url); - const SUCCESS_STATUS = 200; - if (response.status !== SUCCESS_STATUS) { - throw new Error(`Failed to load ${fullSolcVersion}`); - } - solcjs = await response.text(); - await fsWrapper.writeFileAsync(compilerBinFilename, solcjs); - } - if (solcjs.length === 0) { - throw new Error('No compiler available'); - } - const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename)); - return { solcInstance, fullSolcVersion }; - } - private static _addHexPrefixToContractBytecode(compiledContract: solc.StandardContractOutput): void { - if (!_.isUndefined(compiledContract.evm)) { - if (!_.isUndefined(compiledContract.evm.bytecode) && !_.isUndefined(compiledContract.evm.bytecode.object)) { - compiledContract.evm.bytecode.object = ethUtil.addHexPrefix(compiledContract.evm.bytecode.object); - } - if ( - !_.isUndefined(compiledContract.evm.deployedBytecode) && - !_.isUndefined(compiledContract.evm.deployedBytecode.object) - ) { - compiledContract.evm.deployedBytecode.object = ethUtil.addHexPrefix( - compiledContract.evm.deployedBytecode.object, - ); - } - } - } /** * Instantiates a new instance of the Compiler class. * @param opts Optional compiler options @@ -158,7 +114,7 @@ export class Compiler { */ public async compileAsync(): Promise { await createDirIfDoesNotExistAsync(this._artifactsDir); - await createDirIfDoesNotExistAsync(SOLC_BIN_DIR); + await createDirIfDoesNotExistAsync(constants.SOLC_BIN_DIR); await this._compileContractsAsync(this._getContractNamesToCompile(), true); } /** @@ -201,12 +157,14 @@ export class Compiler { for (const contractName of contractNames) { const contractSource = this._resolver.resolve(contractName); + const sourceTreeHashHex = getSourceTreeHash( + this._resolver, + path.join(this._contractsDir, contractSource.path), + ).toString('hex'); const contractData = { contractName, currentArtifactIfExists: await getContractArtifactIfExistsAsync(this._artifactsDir, contractName), - sourceTreeHashHex: `0x${this._getSourceTreeHash( - path.join(this._contractsDir, contractSource.path), - ).toString('hex')}`, + sourceTreeHashHex: `0x${sourceTreeHashHex}`, }; if (!this._shouldCompile(contractData)) { continue; @@ -244,9 +202,8 @@ export class Compiler { }) with Solidity v${solcVersion}...`, ); - const { solcInstance, fullSolcVersion } = await Compiler._getSolcAsync(solcVersion); - - const compilerOutput = this._compile(solcInstance, input.standardInput); + const { solcInstance, fullSolcVersion } = await getSolcAsync(solcVersion); + const compilerOutput = compile(this._resolver, solcInstance, input.standardInput); compilerOutputs.push(compilerOutput); for (const contractPath of input.contractsToCompile) { @@ -259,7 +216,7 @@ export class Compiler { ); } - Compiler._addHexPrefixToContractBytecode(compiledContract); + addHexPrefixToContractBytecode(compiledContract); if (shouldPersist) { await this._persistCompiledContractAsync( @@ -301,7 +258,11 @@ export class Compiler { // contains listings for for every contract compiled during the compiler invocation that compiled the contract // to be persisted, which could include many that are irrelevant to the contract at hand. So, gather up only // the relevant sources: - const { sourceCodes, sources } = this._getSourcesWithDependencies(contractPath, compilerOutput.sources); + const { sourceCodes, sources } = getSourcesWithDependencies( + this._resolver, + contractPath, + compilerOutput.sources, + ); const contractVersion: ContractVersionData = { compilerOutput: compiledContract, @@ -336,130 +297,4 @@ export class Compiler { await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); logUtils.warn(`${contractName} artifact saved!`); } - /** - * For the given @param contractPath, populates JSON objects to be used in the ContractVersionData interface's - * properties `sources` (source code file names mapped to ID numbers) and `sourceCodes` (source code content of - * contracts) for that contract. The source code pointed to by contractPath is read and parsed directly (via - * `this._resolver.resolve().source`), as are its imports, recursively. The ID numbers for @return `sources` are - * taken from the corresponding ID's in @param fullSources, and the content for @return sourceCodes is read from - * disk (via the aforementioned `resolver.source`). - */ - private _getSourcesWithDependencies( - contractPath: string, - fullSources: { [sourceName: string]: { id: number } }, - ): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } { - const sources = { [contractPath]: { id: fullSources[contractPath].id } }; - const sourceCodes = { [contractPath]: this._resolver.resolve(contractPath).source }; - this._recursivelyGatherDependencySources( - contractPath, - sourceCodes[contractPath], - fullSources, - sources, - sourceCodes, - ); - return { sourceCodes, sources }; - } - private _recursivelyGatherDependencySources( - contractPath: string, - contractSource: string, - fullSources: { [sourceName: string]: { id: number } }, - sourcesToAppendTo: { [sourceName: string]: { id: number } }, - sourceCodesToAppendTo: { [sourceName: string]: string }, - ): void { - const importStatementMatches = contractSource.match(/\nimport[^;]*;/g); - if (importStatementMatches === null) { - return; - } - for (const importStatementMatch of importStatementMatches) { - const importPathMatches = importStatementMatch.match(/\"([^\"]*)\"/); - if (importPathMatches === null || importPathMatches.length === 0) { - continue; - } - - let importPath = importPathMatches[1]; - // HACK(ablrow): We have, e.g.: - // - // importPath = "../../utils/LibBytes/LibBytes.sol" - // contractPath = "2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol" - // - // Resolver doesn't understand "../" so we want to pass - // "2.0.0/utils/LibBytes/LibBytes.sol" to resolver. - // - // This hack involves using path.resolve. But path.resolve returns - // absolute directories by default. We trick it into thinking that - // contractPath is a root directory by prepending a '/' and then - // removing the '/' the end. - // - // path.resolve("/a/b/c", ""../../d/e") === "/a/d/e" - // - const lastPathSeparatorPos = contractPath.lastIndexOf('/'); - const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1); - if (importPath.startsWith('.')) { - /** - * Some imports path are relative ("../Token.sol", "./Wallet.sol") - * while others are absolute ("Token.sol", "@0x/contracts/Wallet.sol") - * And we need to append the base path for relative imports. - */ - importPath = path.resolve(`/${contractFolder}`, importPath).replace('/', ''); - } - - if (_.isUndefined(sourcesToAppendTo[importPath])) { - sourcesToAppendTo[importPath] = { id: fullSources[importPath].id }; - sourceCodesToAppendTo[importPath] = this._resolver.resolve(importPath).source; - - this._recursivelyGatherDependencySources( - importPath, - this._resolver.resolve(importPath).source, - fullSources, - sourcesToAppendTo, - sourceCodesToAppendTo, - ); - } - } - } - private _compile(solcInstance: solc.SolcInstance, standardInput: solc.StandardInput): solc.StandardOutput { - const compiled: solc.StandardOutput = JSON.parse( - solcInstance.compileStandardWrapper(JSON.stringify(standardInput), importPath => { - const sourceCodeIfExists = this._resolver.resolve(importPath); - return { contents: sourceCodeIfExists.source }; - }), - ); - if (!_.isUndefined(compiled.errors)) { - const SOLIDITY_WARNING = 'warning'; - const errors = _.filter(compiled.errors, entry => entry.severity !== SOLIDITY_WARNING); - const warnings = _.filter(compiled.errors, entry => entry.severity === SOLIDITY_WARNING); - if (!_.isEmpty(errors)) { - errors.forEach(error => { - const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); - logUtils.warn(chalk.red(normalizedErrMsg)); - }); - throw new Error('Compilation errors encountered'); - } else { - warnings.forEach(warning => { - const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); - logUtils.warn(chalk.yellow(normalizedWarningMsg)); - }); - } - } - return compiled; - } - /** - * Gets the source tree hash for a file and its dependencies. - * @param fileName Name of contract file. - */ - private _getSourceTreeHash(importPath: string): Buffer { - const contractSource = this._resolver.resolve(importPath); - const dependencies = parseDependencies(contractSource); - const sourceHash = ethUtil.sha3(contractSource.source); - if (dependencies.length === 0) { - return sourceHash; - } else { - const dependencySourceTreeHashes = _.map(dependencies, (dependency: string) => - this._getSourceTreeHash(dependency), - ); - const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); - const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer); - return sourceTreeHash; - } - } } diff --git a/packages/sol-compiler/src/utils/compiler.ts b/packages/sol-compiler/src/utils/compiler.ts index cda67a414..034f72f8d 100644 --- a/packages/sol-compiler/src/utils/compiler.ts +++ b/packages/sol-compiler/src/utils/compiler.ts @@ -1,9 +1,16 @@ -import { ContractSource } from '@0x/sol-resolver'; -import { logUtils } from '@0x/utils'; +import { ContractSource, Resolver } from '@0x/sol-resolver'; +import { fetchAsync, logUtils } from '@0x/utils'; +import chalk from 'chalk'; import { ContractArtifact } from 'ethereum-types'; +import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import * as path from 'path'; +import * as requireFromString from 'require-from-string'; +import * as solc from 'solc'; +import { binPaths } from '../solc/bin_paths'; + +import { constants } from './constants'; import { fsWrapper } from './fs_wrapper'; /** @@ -106,3 +113,208 @@ export function parseDependencies(contractSource: ContractSource): string[] { }); return dependencies; } + +/** + * Compiles the contracts and prints errors/warnings + * @param resolver Resolver + * @param solcInstance Instance of a solc compiler + * @param standardInput Solidity standard JSON input + */ +export function compile( + resolver: Resolver, + solcInstance: solc.SolcInstance, + standardInput: solc.StandardInput, +): solc.StandardOutput { + const standardInputStr = JSON.stringify(standardInput); + const standardOutputStr = solcInstance.compileStandardWrapper(standardInputStr, importPath => { + const sourceCodeIfExists = resolver.resolve(importPath); + return { contents: sourceCodeIfExists.source }; + }); + const compiled: solc.StandardOutput = JSON.parse(standardOutputStr); + if (!_.isUndefined(compiled.errors)) { + printCompilationErrorsAndWarnings(compiled.errors); + } + return compiled; +} +/** + * Separates errors from warnings, formats the messages and prints them. Throws if there is any compilation error (not warning). + * @param solcErrors The errors field of standard JSON output that contains errors and warnings. + */ +function printCompilationErrorsAndWarnings(solcErrors: solc.SolcError[]): void { + const SOLIDITY_WARNING = 'warning'; + const errors = _.filter(solcErrors, entry => entry.severity !== SOLIDITY_WARNING); + const warnings = _.filter(solcErrors, entry => entry.severity === SOLIDITY_WARNING); + if (!_.isEmpty(errors)) { + errors.forEach(error => { + const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); + logUtils.warn(chalk.red(normalizedErrMsg)); + }); + throw new Error('Compilation errors encountered'); + } else { + warnings.forEach(warning => { + const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); + logUtils.warn(chalk.yellow(normalizedWarningMsg)); + }); + } +} + +/** + * Gets the source tree hash for a file and its dependencies. + * @param fileName Name of contract file. + */ +export function getSourceTreeHash(resolver: Resolver, importPath: string): Buffer { + const contractSource = resolver.resolve(importPath); + const dependencies = parseDependencies(contractSource); + const sourceHash = ethUtil.sha3(contractSource.source); + if (dependencies.length === 0) { + return sourceHash; + } else { + const dependencySourceTreeHashes = _.map(dependencies, (dependency: string) => + getSourceTreeHash(resolver, dependency), + ); + const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); + const sourceTreeHash = ethUtil.sha3(sourceTreeHashesBuffer); + return sourceTreeHash; + } +} + +/** + * For the given @param contractPath, populates JSON objects to be used in the ContractVersionData interface's + * properties `sources` (source code file names mapped to ID numbers) and `sourceCodes` (source code content of + * contracts) for that contract. The source code pointed to by contractPath is read and parsed directly (via + * `resolver.resolve().source`), as are its imports, recursively. The ID numbers for @return `sources` are + * taken from the corresponding ID's in @param fullSources, and the content for @return sourceCodes is read from + * disk (via the aforementioned `resolver.source`). + */ +export function getSourcesWithDependencies( + resolver: Resolver, + contractPath: string, + fullSources: { [sourceName: string]: { id: number } }, +): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } { + const sources = { [contractPath]: { id: fullSources[contractPath].id } }; + const sourceCodes = { [contractPath]: resolver.resolve(contractPath).source }; + recursivelyGatherDependencySources( + resolver, + contractPath, + sourceCodes[contractPath], + fullSources, + sources, + sourceCodes, + ); + return { sourceCodes, sources }; +} + +function recursivelyGatherDependencySources( + resolver: Resolver, + contractPath: string, + contractSource: string, + fullSources: { [sourceName: string]: { id: number } }, + sourcesToAppendTo: { [sourceName: string]: { id: number } }, + sourceCodesToAppendTo: { [sourceName: string]: string }, +): void { + const importStatementMatches = contractSource.match(/\nimport[^;]*;/g); + if (importStatementMatches === null) { + return; + } + for (const importStatementMatch of importStatementMatches) { + const importPathMatches = importStatementMatch.match(/\"([^\"]*)\"/); + if (importPathMatches === null || importPathMatches.length === 0) { + continue; + } + + let importPath = importPathMatches[1]; + // HACK(ablrow): We have, e.g.: + // + // importPath = "../../utils/LibBytes/LibBytes.sol" + // contractPath = "2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol" + // + // Resolver doesn't understand "../" so we want to pass + // "2.0.0/utils/LibBytes/LibBytes.sol" to resolver. + // + // This hack involves using path.resolve. But path.resolve returns + // absolute directories by default. We trick it into thinking that + // contractPath is a root directory by prepending a '/' and then + // removing the '/' the end. + // + // path.resolve("/a/b/c", ""../../d/e") === "/a/d/e" + // + const lastPathSeparatorPos = contractPath.lastIndexOf('/'); + const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1); + if (importPath.startsWith('.')) { + /** + * Some imports path are relative ("../Token.sol", "./Wallet.sol") + * while others are absolute ("Token.sol", "@0x/contracts/Wallet.sol") + * And we need to append the base path for relative imports. + */ + importPath = path.resolve(`/${contractFolder}`, importPath).replace('/', ''); + } + + if (_.isUndefined(sourcesToAppendTo[importPath])) { + sourcesToAppendTo[importPath] = { id: fullSources[importPath].id }; + sourceCodesToAppendTo[importPath] = resolver.resolve(importPath).source; + + recursivelyGatherDependencySources( + resolver, + importPath, + resolver.resolve(importPath).source, + fullSources, + sourcesToAppendTo, + sourceCodesToAppendTo, + ); + } + } +} + +/** + * Gets the solidity compiler instance and full version name. If the compiler is already cached - gets it from FS, + * otherwise - fetches it and caches it. + * @param solcVersion The compiler version. e.g. 0.5.0 + */ +export async function getSolcAsync( + solcVersion: string, +): Promise<{ solcInstance: solc.SolcInstance; fullSolcVersion: string }> { + const fullSolcVersion = binPaths[solcVersion]; + if (_.isUndefined(fullSolcVersion)) { + throw new Error(`${solcVersion} is not a known compiler version`); + } + const compilerBinFilename = path.join(constants.SOLC_BIN_DIR, fullSolcVersion); + let solcjs: string; + if (await fsWrapper.doesFileExistAsync(compilerBinFilename)) { + solcjs = (await fsWrapper.readFileAsync(compilerBinFilename)).toString(); + } else { + logUtils.warn(`Downloading ${fullSolcVersion}...`); + const url = `${constants.BASE_COMPILER_URL}${fullSolcVersion}`; + const response = await fetchAsync(url); + const SUCCESS_STATUS = 200; + if (response.status !== SUCCESS_STATUS) { + throw new Error(`Failed to load ${fullSolcVersion}`); + } + solcjs = await response.text(); + await fsWrapper.writeFileAsync(compilerBinFilename, solcjs); + } + if (solcjs.length === 0) { + throw new Error('No compiler available'); + } + const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename)); + return { solcInstance, fullSolcVersion }; +} + +/** + * Solidity compiler emits the bytecode without a 0x prefix for a hex. This function fixes it if bytecode is present. + * @param compiledContract The standard JSON output section for a contract. Geth modified in place. + */ +export function addHexPrefixToContractBytecode(compiledContract: solc.StandardContractOutput): void { + if (!_.isUndefined(compiledContract.evm)) { + if (!_.isUndefined(compiledContract.evm.bytecode) && !_.isUndefined(compiledContract.evm.bytecode.object)) { + compiledContract.evm.bytecode.object = ethUtil.addHexPrefix(compiledContract.evm.bytecode.object); + } + if ( + !_.isUndefined(compiledContract.evm.deployedBytecode) && + !_.isUndefined(compiledContract.evm.deployedBytecode.object) + ) { + compiledContract.evm.deployedBytecode.object = ethUtil.addHexPrefix( + compiledContract.evm.deployedBytecode.object, + ); + } + } +} diff --git a/packages/sol-compiler/src/utils/constants.ts b/packages/sol-compiler/src/utils/constants.ts index df2ddb3b2..433897f8a 100644 --- a/packages/sol-compiler/src/utils/constants.ts +++ b/packages/sol-compiler/src/utils/constants.ts @@ -1,5 +1,8 @@ +import * as path from 'path'; + export const constants = { SOLIDITY_FILE_EXTENSION: '.sol', BASE_COMPILER_URL: 'https://ethereum.github.io/solc-bin/bin/', LATEST_ARTIFACT_VERSION: '2.0.0', + SOLC_BIN_DIR: path.join(__dirname, '..', '..', 'solc_bin'), }; -- cgit v1.2.3 From 657b698e1eba7fde2322bb81547eba26491c0af4 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 14:11:08 +0100 Subject: Add sol-compiler watch mode --- packages/sol-compiler/CHANGELOG.json | 13 +++++++ packages/sol-compiler/package.json | 4 ++ packages/sol-compiler/src/cli.ts | 10 ++++- packages/sol-compiler/src/compiler.ts | 46 +++++++++++++++++++++++ packages/sol-compiler/src/utils/compiler.ts | 7 ++-- packages/sol-compiler/src/utils/types.ts | 9 +++++ packages/sol-compiler/test/compiler_utils_test.ts | 6 +-- 7 files changed, 88 insertions(+), 7 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/CHANGELOG.json b/packages/sol-compiler/CHANGELOG.json index 0a757f519..17b3f9edd 100644 --- a/packages/sol-compiler/CHANGELOG.json +++ b/packages/sol-compiler/CHANGELOG.json @@ -1,4 +1,17 @@ [ + { + "version": "2.0.0", + "changes": [ + { + "note": "Add sol-compiler watch mode with -w flag", + "pr": "TODO" + }, + { + "note": "Make error and warning colouring more visually pleasant and consistent with other compilers", + "pr": "TODO" + } + ] + }, { "version": "1.1.16", "changes": [ diff --git a/packages/sol-compiler/package.json b/packages/sol-compiler/package.json index 0ad620b1f..86167a603 100644 --- a/packages/sol-compiler/package.json +++ b/packages/sol-compiler/package.json @@ -44,7 +44,9 @@ "devDependencies": { "@0x/dev-utils": "^1.0.21", "@0x/tslint-config": "^2.0.0", + "@types/chokidar": "^1.7.5", "@types/mkdirp": "^0.5.2", + "@types/pluralize": "^0.0.29", "@types/require-from-string": "^1.2.0", "@types/semver": "^5.5.0", "chai": "^4.0.1", @@ -74,10 +76,12 @@ "@0x/web3-wrapper": "^3.2.1", "@types/yargs": "^11.0.0", "chalk": "^2.3.0", + "chokidar": "^2.0.4", "ethereum-types": "^1.1.4", "ethereumjs-util": "^5.1.1", "lodash": "^4.17.5", "mkdirp": "^0.5.1", + "pluralize": "^7.0.0", "require-from-string": "^2.0.1", "semver": "5.5.0", "solc": "^0.4.23", diff --git a/packages/sol-compiler/src/cli.ts b/packages/sol-compiler/src/cli.ts index 0a9db6e05..18cc68aaf 100644 --- a/packages/sol-compiler/src/cli.ts +++ b/packages/sol-compiler/src/cli.ts @@ -25,6 +25,10 @@ const SEPARATOR = ','; type: 'string', description: 'comma separated list of contracts to compile', }) + .option('watch', { + alias: 'w', + default: false, + }) .help().argv; const contracts = _.isUndefined(argv.contracts) ? undefined @@ -37,7 +41,11 @@ const SEPARATOR = ','; contracts, }; const compiler = new Compiler(opts); - await compiler.compileAsync(); + if (argv.watch) { + await compiler.watchAsync(); + } else { + await compiler.compileAsync(); + } })().catch(err => { logUtils.log(err); process.exit(1); diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 45cbf527b..17a1ce563 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -6,13 +6,17 @@ import { NPMResolver, RelativeFSResolver, Resolver, + SpyResolver, URLResolver, } from '@0x/sol-resolver'; import { logUtils } from '@0x/utils'; +import chalk from 'chalk'; +import * as chokidar from 'chokidar'; import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types'; import * as fs from 'fs'; import * as _ from 'lodash'; import * as path from 'path'; +import * as pluralize from 'pluralize'; import * as semver from 'semver'; import solc = require('solc'); @@ -30,6 +34,7 @@ import { } from './utils/compiler'; import { constants } from './utils/constants'; import { fsWrapper } from './utils/fs_wrapper'; +import { CompilationError } from './utils/types'; import { utils } from './utils/utils'; type TYPE_ALL_FILES_IDENTIFIER = '*'; @@ -129,6 +134,43 @@ export class Compiler { const promisedOutputs = this._compileContractsAsync(this._getContractNamesToCompile(), false); return promisedOutputs; } + public async watchAsync(): Promise { + console.clear(); // tslint:disable-line:no-console + logWithTime('Starting compilation in watch mode...'); + const watcher = chokidar.watch('^$', { ignored: /(^|[\/\\])\../ }); + const onFileChangedAsync = async () => { + watcher.unwatch('*'); // Stop watching + try { + await this.compileAsync(); + logWithTime('Found 0 errors. Watching for file changes.'); + } catch (err) { + if (err.typeName === 'CompilationError') { + logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); + } else { + logWithTime('Found errors. Watching for file changes.'); + } + } + + const pathsToWatch = this._getPathsToWatch(); + watcher.add(pathsToWatch); + }; + await onFileChangedAsync(); + watcher.on('change', (changedFilePath: string) => { + console.clear(); // tslint:disable-line:no-console + logWithTime('File change detected. Starting incremental compilation...'); + onFileChangedAsync(); + }); + } + private _getPathsToWatch(): string[] { + const contractNames = this._getContractNamesToCompile(); + const spyResolver = new SpyResolver(this._resolver); + for (const contractName of contractNames) { + const contractSource = spyResolver.resolve(contractName); + getSourceTreeHash(spyResolver, contractSource.path); + } + const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath)); + return pathsToWatch; + } private _getContractNamesToCompile(): string[] { let contractNamesToCompile; if (this._specifiedContracts === ALL_CONTRACTS_IDENTIFIER) { @@ -298,3 +340,7 @@ export class Compiler { logUtils.warn(`${contractName} artifact saved!`); } } + +function logWithTime(arg: string): void { + logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); +} diff --git a/packages/sol-compiler/src/utils/compiler.ts b/packages/sol-compiler/src/utils/compiler.ts index 034f72f8d..486d8bedd 100644 --- a/packages/sol-compiler/src/utils/compiler.ts +++ b/packages/sol-compiler/src/utils/compiler.ts @@ -12,6 +12,7 @@ import { binPaths } from '../solc/bin_paths'; import { constants } from './constants'; import { fsWrapper } from './fs_wrapper'; +import { CompilationError } from './types'; /** * Gets contract data on network or returns if an artifact does not exist. @@ -147,13 +148,13 @@ function printCompilationErrorsAndWarnings(solcErrors: solc.SolcError[]): void { if (!_.isEmpty(errors)) { errors.forEach(error => { const normalizedErrMsg = getNormalizedErrMsg(error.formattedMessage || error.message); - logUtils.warn(chalk.red(normalizedErrMsg)); + logUtils.log(chalk.red('error'), normalizedErrMsg); }); - throw new Error('Compilation errors encountered'); + throw new CompilationError(errors.length); } else { warnings.forEach(warning => { const normalizedWarningMsg = getNormalizedErrMsg(warning.formattedMessage || warning.message); - logUtils.warn(chalk.yellow(normalizedWarningMsg)); + logUtils.log(chalk.yellow('warning'), normalizedWarningMsg); }); } } diff --git a/packages/sol-compiler/src/utils/types.ts b/packages/sol-compiler/src/utils/types.ts index b211cfcbc..64328899d 100644 --- a/packages/sol-compiler/src/utils/types.ts +++ b/packages/sol-compiler/src/utils/types.ts @@ -29,3 +29,12 @@ export interface Token { } export type DoneCallback = (err?: Error) => void; + +export class CompilationError extends Error { + public errorsCount: number; + public typeName = 'CompilationError'; + constructor(errorsCount: number) { + super('Compilation errors encountered'); + this.errorsCount = errorsCount; + } +} diff --git a/packages/sol-compiler/test/compiler_utils_test.ts b/packages/sol-compiler/test/compiler_utils_test.ts index 4fe7b994e..b8c18110c 100644 --- a/packages/sol-compiler/test/compiler_utils_test.ts +++ b/packages/sol-compiler/test/compiler_utils_test.ts @@ -52,7 +52,7 @@ describe('Compiler utils', () => { const source = await fsWrapper.readFileAsync(path, { encoding: 'utf8', }); - const dependencies = parseDependencies({ source, path }); + const dependencies = parseDependencies({ source, path, absolutePath: path }); const expectedDependencies = [ 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', 'packages/sol-compiler/lib/test/fixtures/contracts/TokenTransferProxy.sol', @@ -68,7 +68,7 @@ describe('Compiler utils', () => { const source = await fsWrapper.readFileAsync(path, { encoding: 'utf8', }); - expect(parseDependencies({ source, path })).to.be.deep.equal([ + expect(parseDependencies({ source, path, absolutePath: path })).to.be.deep.equal([ 'zeppelin-solidity/contracts/ownership/Ownable.sol', 'zeppelin-solidity/contracts/token/ERC20/ERC20.sol', ]); @@ -77,7 +77,7 @@ describe('Compiler utils', () => { it.skip('correctly parses commented out dependencies', async () => { const path = ''; const source = `// import "./TokenTransferProxy.sol";`; - expect(parseDependencies({ path, source })).to.be.deep.equal([]); + expect(parseDependencies({ path, source, absolutePath: path })).to.be.deep.equal([]); }); }); }); -- cgit v1.2.3 From 56d48758d308f9450aaac2e986dd09efd8d479c0 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 14:19:35 +0100 Subject: Add PR numbers --- packages/sol-compiler/CHANGELOG.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/CHANGELOG.json b/packages/sol-compiler/CHANGELOG.json index 17b3f9edd..8548fd73f 100644 --- a/packages/sol-compiler/CHANGELOG.json +++ b/packages/sol-compiler/CHANGELOG.json @@ -4,11 +4,11 @@ "changes": [ { "note": "Add sol-compiler watch mode with -w flag", - "pr": "TODO" + "pr": 1461 }, { "note": "Make error and warning colouring more visually pleasant and consistent with other compilers", - "pr": "TODO" + "pr": 1461 } ] }, -- cgit v1.2.3 From 87d157b805465d76a8eb25cda84fb91dc064faec Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 14:32:59 +0100 Subject: Move logWithTime function to utils --- packages/sol-compiler/src/compiler.ts | 15 +++++---------- packages/sol-compiler/src/utils/utils.ts | 6 ++++++ 2 files changed, 11 insertions(+), 10 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 17a1ce563..986999254 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -10,7 +10,6 @@ import { URLResolver, } from '@0x/sol-resolver'; import { logUtils } from '@0x/utils'; -import chalk from 'chalk'; import * as chokidar from 'chokidar'; import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types'; import * as fs from 'fs'; @@ -136,18 +135,18 @@ export class Compiler { } public async watchAsync(): Promise { console.clear(); // tslint:disable-line:no-console - logWithTime('Starting compilation in watch mode...'); + utils.logWithTime('Starting compilation in watch mode...'); const watcher = chokidar.watch('^$', { ignored: /(^|[\/\\])\../ }); const onFileChangedAsync = async () => { watcher.unwatch('*'); // Stop watching try { await this.compileAsync(); - logWithTime('Found 0 errors. Watching for file changes.'); + utils.logWithTime('Found 0 errors. Watching for file changes.'); } catch (err) { if (err.typeName === 'CompilationError') { - logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); + utils.logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); } else { - logWithTime('Found errors. Watching for file changes.'); + utils.logWithTime('Found errors. Watching for file changes.'); } } @@ -157,7 +156,7 @@ export class Compiler { await onFileChangedAsync(); watcher.on('change', (changedFilePath: string) => { console.clear(); // tslint:disable-line:no-console - logWithTime('File change detected. Starting incremental compilation...'); + utils.logWithTime('File change detected. Starting incremental compilation...'); onFileChangedAsync(); }); } @@ -340,7 +339,3 @@ export class Compiler { logUtils.warn(`${contractName} artifact saved!`); } } - -function logWithTime(arg: string): void { - logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); -} diff --git a/packages/sol-compiler/src/utils/utils.ts b/packages/sol-compiler/src/utils/utils.ts index 4f2de2caa..538efc61a 100644 --- a/packages/sol-compiler/src/utils/utils.ts +++ b/packages/sol-compiler/src/utils/utils.ts @@ -1,6 +1,12 @@ +import { logUtils } from '@0x/utils'; +import chalk from 'chalk'; + export const utils = { stringifyWithFormatting(obj: any): string { const stringifiedObj = JSON.stringify(obj, null, '\t'); return stringifiedObj; }, + logWithTime(arg: string): void { + logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); + }, }; -- cgit v1.2.3 From 237014e823ad4fba7bea50bdf8ec9b5a5cc25918 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 15:06:11 +0100 Subject: Disable linter no a hanging promise with a comment --- packages/sol-compiler/src/compiler.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 986999254..b3531ab65 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -33,7 +33,6 @@ import { } from './utils/compiler'; import { constants } from './utils/constants'; import { fsWrapper } from './utils/fs_wrapper'; -import { CompilationError } from './utils/types'; import { utils } from './utils/utils'; type TYPE_ALL_FILES_IDENTIFIER = '*'; @@ -157,7 +156,9 @@ export class Compiler { watcher.on('change', (changedFilePath: string) => { console.clear(); // tslint:disable-line:no-console utils.logWithTime('File change detected. Starting incremental compilation...'); - onFileChangedAsync(); + // NOTE: We can't await it here because that's a callback. + // Instead we stop watching inside of it and start it again when we're finished. + onFileChangedAsync(); // tslint:disable-line no-floating-promises }); } private _getPathsToWatch(): string[] { -- cgit v1.2.3 From 69de1d05efcadc1a6e70e98a0e77f32e8976560e Mon Sep 17 00:00:00 2001 From: Fabio B Date: Wed, 19 Dec 2018 15:15:15 +0100 Subject: Update packages/sol-compiler/src/compiler.ts Co-Authored-By: LogvinovLeon --- packages/sol-compiler/src/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index b3531ab65..519712e20 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -297,7 +297,7 @@ export class Compiler { const compiledContract = compilerOutput.contracts[contractPath][contractName]; // need to gather sourceCodes for this artifact, but compilerOutput.sources (the list of contract modules) - // contains listings for for every contract compiled during the compiler invocation that compiled the contract + // contains listings for every contract compiled during the compiler invocation that compiled the contract // to be persisted, which could include many that are irrelevant to the contract at hand. So, gather up only // the relevant sources: const { sourceCodes, sources } = getSourcesWithDependencies( -- cgit v1.2.3 From 85be2fbf19ef937709f747b0c9169a2cf3e6697e Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 15:32:04 +0100 Subject: Move logWithTime to logUtils --- packages/sol-compiler/src/compiler.ts | 10 +++++----- packages/sol-compiler/src/utils/utils.ts | 6 ------ 2 files changed, 5 insertions(+), 11 deletions(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 519712e20..eca887ce9 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -134,18 +134,18 @@ export class Compiler { } public async watchAsync(): Promise { console.clear(); // tslint:disable-line:no-console - utils.logWithTime('Starting compilation in watch mode...'); + logUtils.logWithTime('Starting compilation in watch mode...'); const watcher = chokidar.watch('^$', { ignored: /(^|[\/\\])\../ }); const onFileChangedAsync = async () => { watcher.unwatch('*'); // Stop watching try { await this.compileAsync(); - utils.logWithTime('Found 0 errors. Watching for file changes.'); + logUtils.logWithTime('Found 0 errors. Watching for file changes.'); } catch (err) { if (err.typeName === 'CompilationError') { - utils.logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); + logUtils.logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); } else { - utils.logWithTime('Found errors. Watching for file changes.'); + logUtils.logWithTime('Found errors. Watching for file changes.'); } } @@ -155,7 +155,7 @@ export class Compiler { await onFileChangedAsync(); watcher.on('change', (changedFilePath: string) => { console.clear(); // tslint:disable-line:no-console - utils.logWithTime('File change detected. Starting incremental compilation...'); + logUtils.logWithTime('File change detected. Starting incremental compilation...'); // NOTE: We can't await it here because that's a callback. // Instead we stop watching inside of it and start it again when we're finished. onFileChangedAsync(); // tslint:disable-line no-floating-promises diff --git a/packages/sol-compiler/src/utils/utils.ts b/packages/sol-compiler/src/utils/utils.ts index 538efc61a..4f2de2caa 100644 --- a/packages/sol-compiler/src/utils/utils.ts +++ b/packages/sol-compiler/src/utils/utils.ts @@ -1,12 +1,6 @@ -import { logUtils } from '@0x/utils'; -import chalk from 'chalk'; - export const utils = { stringifyWithFormatting(obj: any): string { const stringifiedObj = JSON.stringify(obj, null, '\t'); return stringifiedObj; }, - logWithTime(arg: string): void { - logUtils.log(`[${chalk.gray(new Date().toLocaleTimeString())}] ${arg}`); - }, }; -- cgit v1.2.3 From 5c4a992b87aa4341fc768eb4865179f2d36abad2 Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 15:36:26 +0100 Subject: Add a NOTE comment --- packages/sol-compiler/src/compiler.ts | 3 +++ 1 file changed, 3 insertions(+) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index eca887ce9..793ab4f93 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -166,6 +166,9 @@ export class Compiler { const spyResolver = new SpyResolver(this._resolver); for (const contractName of contractNames) { const contractSource = spyResolver.resolve(contractName); + // NOTE: We ignore the return value here. We don't want to compute the source tree hash. + // We just want to call a SpyResolver on each contracts and it's dependencies and + // this is a convinient way to reuse the existing code that does that. getSourceTreeHash(spyResolver, contractSource.path); } const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath)); -- cgit v1.2.3 From 5656605355f201e36667cd054d0eed0a3ba0dbfe Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 15:39:43 +0100 Subject: Describe regexes --- packages/sol-compiler/src/compiler.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 793ab4f93..6b77a464d 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -135,7 +135,10 @@ export class Compiler { public async watchAsync(): Promise { console.clear(); // tslint:disable-line:no-console logUtils.logWithTime('Starting compilation in watch mode...'); - const watcher = chokidar.watch('^$', { ignored: /(^|[\/\\])\../ }); + const MATCH_NOTHING_REGEX = '^$'; + const IGNORE_DOT_FILES_REGEX = /(^|[\/\\])\../; + // Initially we watch nothing. We'll add the paths later. + const watcher = chokidar.watch(MATCH_NOTHING_REGEX, { ignored: IGNORE_DOT_FILES_REGEX }); const onFileChangedAsync = async () => { watcher.unwatch('*'); // Stop watching try { -- cgit v1.2.3 From 86a9375d047f78981ffba74d24543184b8ea089a Mon Sep 17 00:00:00 2001 From: Leonid Logvinov Date: Wed, 19 Dec 2018 15:40:27 +0100 Subject: Run prettier --- packages/sol-compiler/src/compiler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 6b77a464d..08ab97ba8 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -146,7 +146,9 @@ export class Compiler { logUtils.logWithTime('Found 0 errors. Watching for file changes.'); } catch (err) { if (err.typeName === 'CompilationError') { - logUtils.logWithTime(`Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`); + logUtils.logWithTime( + `Found ${err.errorsCount} ${pluralize('error', err.errorsCount)}. Watching for file changes.`, + ); } else { logUtils.logWithTime('Found errors. Watching for file changes.'); } -- cgit v1.2.3 From e886ef8c4b3d5f97fca6eb6decf3273fb401222d Mon Sep 17 00:00:00 2001 From: Fabio B Date: Wed, 19 Dec 2018 15:57:19 +0100 Subject: Update packages/sol-compiler/src/utils/compiler.ts Co-Authored-By: LogvinovLeon --- packages/sol-compiler/src/utils/compiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/utils/compiler.ts b/packages/sol-compiler/src/utils/compiler.ts index 486d8bedd..db308f2b5 100644 --- a/packages/sol-compiler/src/utils/compiler.ts +++ b/packages/sol-compiler/src/utils/compiler.ts @@ -224,7 +224,7 @@ function recursivelyGatherDependencySources( } let importPath = importPathMatches[1]; - // HACK(ablrow): We have, e.g.: + // HACK(albrow): We have, e.g.: // // importPath = "../../utils/LibBytes/LibBytes.sol" // contractPath = "2.0.0/protocol/AssetProxyOwner/AssetProxyOwner.sol" -- cgit v1.2.3 From d456710441f5d126c63ae31003ddfffb2654047a Mon Sep 17 00:00:00 2001 From: Fabio B Date: Wed, 19 Dec 2018 15:57:30 +0100 Subject: Update packages/sol-compiler/src/compiler.ts Co-Authored-By: LogvinovLeon --- packages/sol-compiler/src/compiler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'packages/sol-compiler') diff --git a/packages/sol-compiler/src/compiler.ts b/packages/sol-compiler/src/compiler.ts index 08ab97ba8..d38ccbf39 100644 --- a/packages/sol-compiler/src/compiler.ts +++ b/packages/sol-compiler/src/compiler.ts @@ -173,7 +173,8 @@ export class Compiler { const contractSource = spyResolver.resolve(contractName); // NOTE: We ignore the return value here. We don't want to compute the source tree hash. // We just want to call a SpyResolver on each contracts and it's dependencies and - // this is a convinient way to reuse the existing code that does that. + // this is a convenient way to reuse the existing code that does that. + // We can then get all the relevant paths from the `spyResolver` below. getSourceTreeHash(spyResolver, contractSource.path); } const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath)); -- cgit v1.2.3