diff options
Diffstat (limited to 'packages/deployer/src/compiler.ts')
-rw-r--r-- | packages/deployer/src/compiler.ts | 215 |
1 files changed, 143 insertions, 72 deletions
diff --git a/packages/deployer/src/compiler.ts b/packages/deployer/src/compiler.ts index 2b0b81c44..149ca5d6d 100644 --- a/packages/deployer/src/compiler.ts +++ b/packages/deployer/src/compiler.ts @@ -5,32 +5,39 @@ import solc = require('solc'); import * as Web3 from 'web3'; import { binPaths } from './solc/bin_paths'; +import { constants } from './utils/constants'; import { fsWrapper } from './utils/fs_wrapper'; import { CompilerOptions, ContractArtifact, - ContractData, + ContractNetworkData, ContractNetworks, + ContractSourceData, ContractSources, + ContractSpecificSourceData, ImportContents, } from './utils/types'; import { utils } from './utils/utils'; -const SOLIDITY_FILE_EXTENSION = '.sol'; const ALL_CONTRACTS_IDENTIFIER = '*'; +const SOLIDITY_VERSION_REGEX = /(?:solidity\s\^?)(\d+\.\d+\.\d+)/; +const SOLIDITY_FILE_EXTENSION_REGEX = /(.*\.sol)/; +const IMPORT_REGEX = /(import\s)/; +const DEPENDENCY_PATH_REGEX = /"([^"]+)"/; // Source: https://github.com/BlockChainCompany/soljitsu/blob/master/lib/shared.js export class Compiler { private _contractsDir: string; private _networkId: number; private _optimizerEnabled: number; private _artifactsDir: string; - private _contractSourcesIfExists?: ContractSources; - private _solcErrors: Set<string>; - private _specifiedContracts: Set<string>; + private _contractSources?: ContractSources; + private _solcErrors: Set<string> = new Set(); + private _specifiedContracts: Set<string> = new Set(); + private _contractSourceData: ContractSourceData = {}; /** * Recursively retrieves Solidity source code from directory. * @param dirPath Directory to search. - * @return Mapping of contract name to contract source. + * @return Mapping of contract fileName to contract source. */ private static async _getContractSourcesAsync(dirPath: string): Promise<ContractSources> { let dirContents: string[] = []; @@ -40,15 +47,16 @@ export class Compiler { throw new Error(`No directory found at ${dirPath}`); } let sources: ContractSources = {}; - for (const name of dirContents) { - const contentPath = `${dirPath}/${name}`; - if (path.extname(name) === SOLIDITY_FILE_EXTENSION) { + for (const fileName of dirContents) { + const contentPath = `${dirPath}/${fileName}`; + if (path.extname(fileName) === constants.SOLIDITY_FILE_EXTENSION) { try { const opts = { encoding: 'utf8', }; - sources[name] = await fsWrapper.readFileAsync(contentPath, opts); - utils.consoleLog(`Reading ${name} source...`); + const source = await fsWrapper.readFileAsync(contentPath, opts); + sources[fileName] = source; + utils.consoleLog(`Reading ${fileName} source...`); } catch (err) { utils.consoleLog(`Could not find file at ${contentPath}`); } @@ -60,19 +68,46 @@ export class Compiler { ...nestedSources, }; } catch (err) { - utils.consoleLog(`${contentPath} is not a directory or ${SOLIDITY_FILE_EXTENSION} file`); + utils.consoleLog(`${contentPath} is not a directory or ${constants.SOLIDITY_FILE_EXTENSION} file`); } } } return sources; } /** + * Gets contract dependendencies and keccak256 hash from source. + * @param source Source code of contract. + * @return Object with contract dependencies and keccak256 hash of source. + */ + private static _getContractSpecificSourceData(source: string): ContractSpecificSourceData { + const dependencies: string[] = []; + const sourceHash = ethUtil.sha3(source); + const solcVersion = Compiler._parseSolidityVersion(source); + const contractSpecificSourceData: ContractSpecificSourceData = { + dependencies, + solcVersion, + sourceHash, + }; + const lines = source.split('\n'); + _.forEach(lines, line => { + if (!_.isNull(line.match(IMPORT_REGEX))) { + const dependencyMatch = line.match(DEPENDENCY_PATH_REGEX); + if (!_.isNull(dependencyMatch)) { + const dependencyPath = dependencyMatch[1]; + const fileName = path.basename(dependencyPath); + contractSpecificSourceData.dependencies.push(fileName); + } + } + }); + return contractSpecificSourceData; + } + /** * Searches Solidity source code for compiler version. * @param source Source code of contract. * @return Solc compiler version. */ private static _parseSolidityVersion(source: string): string { - const solcVersionMatch = source.match(/(?:solidity\s\^?)([0-9]{1,2}[.][0-9]{1,2}[.][0-9]{1,2})/); + const solcVersionMatch = source.match(SOLIDITY_VERSION_REGEX); if (_.isNull(solcVersionMatch)) { throw new Error('Could not find Solidity version in source'); } @@ -88,7 +123,7 @@ export class Compiler { * @return The error message with directories truncated from the contract path. */ private static _getNormalizedErrMsg(errMsg: string): string { - const errPathMatch = errMsg.match(/(.*\.sol)/); + const errPathMatch = errMsg.match(SOLIDITY_FILE_EXTENSION_REGEX); if (_.isNull(errPathMatch)) { throw new Error('Could not find a path in error message'); } @@ -107,7 +142,6 @@ export class Compiler { this._networkId = opts.networkId; this._optimizerEnabled = opts.optimizerEnabled; this._artifactsDir = opts.artifactsDir; - this._solcErrors = new Set(); this._specifiedContracts = opts.specifiedContracts; } /** @@ -115,68 +149,52 @@ export class Compiler { */ public async compileAllAsync(): Promise<void> { await this._createArtifactsDirIfDoesNotExistAsync(); - this._contractSourcesIfExists = await Compiler._getContractSourcesAsync(this._contractsDir); - const contractBaseNames = _.keys(this._contractSourcesIfExists); - const compiledContractPromises = _.map(contractBaseNames, async (contractBaseName: string): Promise<void> => { - return this._compileContractAsync(contractBaseName); + this._contractSources = await Compiler._getContractSourcesAsync(this._contractsDir); + _.forIn(this._contractSources, (source, fileName) => { + this._contractSourceData[fileName] = Compiler._getContractSpecificSourceData(source); }); - await Promise.all(compiledContractPromises); - + const fileNames = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER) + ? _.keys(this._contractSources) + : Array.from(this._specifiedContracts.values()); + _.forEach(fileNames, fileName => { + this._setSourceTreeHash(fileName); + }); + await Promise.all(_.map(fileNames, async fileName => this._compileContractAsync(fileName))); this._solcErrors.forEach(errMsg => { utils.consoleLog(errMsg); }); } /** * Compiles contract and saves artifact to artifactsDir. - * @param contractBaseName Name of contract with '.sol' extension. + * @param fileName Name of contract with '.sol' extension. */ - private async _compileContractAsync(contractBaseName: string): Promise<void> { - if (_.isUndefined(this._contractSourcesIfExists)) { + private async _compileContractAsync(fileName: string): Promise<void> { + if (_.isUndefined(this._contractSources)) { throw new Error('Contract sources not yet initialized'); } + const contractSpecificSourceData = this._contractSourceData[fileName]; + const currentArtifactIfExists = (await this._getContractArtifactIfExistsAsync(fileName)) as ContractArtifact; + const sourceHash = `0x${contractSpecificSourceData.sourceHash.toString('hex')}`; + const sourceTreeHash = `0x${contractSpecificSourceData.sourceTreeHashIfExists.toString('hex')}`; - const source = this._contractSourcesIfExists[contractBaseName]; - const contractName = path.basename(contractBaseName, SOLIDITY_FILE_EXTENSION); - const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`; - const sourceHash = `0x${ethUtil.sha3(source).toString('hex')}`; - const isContractSpecified = - this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER) || this._specifiedContracts.has(contractName); - - let currentArtifactString: string; - let currentArtifact: ContractArtifact; - let oldNetworks: ContractNetworks; - let shouldCompile: boolean; - try { - const opts = { - encoding: 'utf8', - }; - currentArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); - currentArtifact = JSON.parse(currentArtifactString); - oldNetworks = currentArtifact.networks; - const oldNetwork: ContractData = oldNetworks[this._networkId]; - shouldCompile = - (_.isUndefined(oldNetwork) || - oldNetwork.keccak256 !== sourceHash || - oldNetwork.optimizer_enabled !== this._optimizerEnabled) && - isContractSpecified; - } catch (err) { - shouldCompile = isContractSpecified; - } - + const shouldCompile = + _.isUndefined(currentArtifactIfExists) || + currentArtifactIfExists.networks[this._networkId].optimizer_enabled !== this._optimizerEnabled || + currentArtifactIfExists.networks[this._networkId].source_tree_hash !== sourceTreeHash; if (!shouldCompile) { return; } - const input = { - [contractBaseName]: source, - }; - const solcVersion = Compiler._parseSolidityVersion(source); - const fullSolcVersion = binPaths[solcVersion]; + const fullSolcVersion = binPaths[contractSpecificSourceData.solcVersion]; const solcBinPath = `./solc/solc_bin/${fullSolcVersion}`; const solcBin = require(solcBinPath); const solcInstance = solc.setupMethods(solcBin); - utils.consoleLog(`Compiling ${contractBaseName}...`); + utils.consoleLog(`Compiling ${fileName}...`); + const source = this._contractSources[fileName]; + const input = { + [fileName]: source, + }; const sourcesToCompile = { sources: input, }; @@ -187,19 +205,21 @@ export class Compiler { ); if (!_.isUndefined(compiled.errors)) { - _.each(compiled.errors, errMsg => { + _.forEach(compiled.errors, errMsg => { const normalizedErrMsg = Compiler._getNormalizedErrMsg(errMsg); this._solcErrors.add(normalizedErrMsg); }); } - const contractIdentifier = `${contractBaseName}:${contractName}`; + const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION); + const contractIdentifier = `${fileName}:${contractName}`; const abi: Web3.ContractAbi = JSON.parse(compiled.contracts[contractIdentifier].interface); const unlinked_binary = `0x${compiled.contracts[contractIdentifier].bytecode}`; const updated_at = Date.now(); - const contractData: ContractData = { - solc_version: solcVersion, + const contractNetworkData: ContractNetworkData = { + solc_version: contractSpecificSourceData.solcVersion, keccak256: sourceHash, + source_tree_hash: sourceTreeHash, optimizer_enabled: this._optimizerEnabled, abi, unlinked_binary, @@ -207,26 +227,56 @@ export class Compiler { }; let newArtifact: ContractArtifact; - if (!_.isUndefined(currentArtifactString)) { + if (!_.isUndefined(currentArtifactIfExists)) { newArtifact = { - ...currentArtifact, + ...currentArtifactIfExists, networks: { - ...oldNetworks, - [this._networkId]: contractData, + ...currentArtifactIfExists.networks, + [this._networkId]: contractNetworkData, }, }; } else { newArtifact = { contract_name: contractName, networks: { - [this._networkId]: contractData, + [this._networkId]: contractNetworkData, }, }; } const artifactString = utils.stringifyWithFormatting(newArtifact); + const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`; await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); - utils.consoleLog(`${contractBaseName} artifact saved!`); + utils.consoleLog(`${fileName} artifact saved!`); + } + /** + * Sets the source tree hash for a file and its dependencies. + * @param fileName Name of contract file. + */ + private _setSourceTreeHash(fileName: string): void { + const contractSpecificSourceData = this._contractSourceData[fileName]; + if (_.isUndefined(contractSpecificSourceData)) { + throw new Error(`Contract data for ${fileName} not yet set`); + } + if (_.isUndefined(contractSpecificSourceData.sourceTreeHashIfExists)) { + const dependencies = contractSpecificSourceData.dependencies; + if (dependencies.length === 0) { + contractSpecificSourceData.sourceTreeHashIfExists = contractSpecificSourceData.sourceHash; + } else { + _.forEach(dependencies, dependency => { + this._setSourceTreeHash(dependency); + }); + const dependencySourceTreeHashes = _.map( + dependencies, + dependency => this._contractSourceData[dependency].sourceTreeHashIfExists, + ); + const sourceTreeHashesBuffer = Buffer.concat([ + contractSpecificSourceData.sourceHash, + ...dependencySourceTreeHashes, + ]); + contractSpecificSourceData.sourceTreeHashIfExists = ethUtil.sha3(sourceTreeHashesBuffer); + } + } } /** * Callback to resolve dependencies with `solc.compile`. @@ -235,11 +285,11 @@ export class Compiler { * @return Import contents object containing source code of dependency. */ private _findImportsIfSourcesExist(importPath: string): ImportContents { - if (_.isUndefined(this._contractSourcesIfExists)) { - throw new Error('Contract sources not yet initialized'); + const fileName = path.basename(importPath); + const source = this._contractSources[fileName]; + if (_.isUndefined(source)) { + throw new Error(`Contract source not found for ${fileName}`); } - const contractBaseName = path.basename(importPath); - const source = this._contractSourcesIfExists[contractBaseName]; const importContents: ImportContents = { contents: source, }; @@ -254,4 +304,25 @@ export class Compiler { await fsWrapper.mkdirAsync(this._artifactsDir); } } + /** + * Gets contract data on network or returns if an artifact does not exist. + * @param fileName Name of contract file. + * @return Contract data on network or undefined. + */ + private async _getContractArtifactIfExistsAsync(fileName: string): Promise<ContractArtifact | void> { + let contractArtifact; + const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION); + const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`; + try { + const opts = { + encoding: 'utf8', + }; + const contractArtifactString = await fsWrapper.readFileAsync(currentArtifactPath, opts); + contractArtifact = JSON.parse(contractArtifactString); + return contractArtifact; + } catch (err) { + utils.consoleLog(`Artifact for ${fileName} does not exist`); + return undefined; + } + } } |