From 61fc3346c2fe2adc33dfe84aa50780d61e10efdf Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Tue, 3 Apr 2018 17:39:55 -0700 Subject: Updated deployer to accept a list of contract directories as input. Contract directories are namespaced to a void clashes. Also in this commit is a fix for overloading contract functions. --- packages/deployer/src/cli.ts | 37 +++++++++--- packages/deployer/src/compiler.ts | 102 +++++++++++++++++++++----------- packages/deployer/src/utils/compiler.ts | 65 +++++++++++++++++--- packages/deployer/src/utils/contract.ts | 4 +- packages/deployer/src/utils/encoder.ts | 4 +- packages/deployer/src/utils/types.ts | 19 +++++- 6 files changed, 172 insertions(+), 59 deletions(-) (limited to 'packages/deployer/src') diff --git a/packages/deployer/src/cli.ts b/packages/deployer/src/cli.ts index d1bd645b3..3d69925a8 100644 --- a/packages/deployer/src/cli.ts +++ b/packages/deployer/src/cli.ts @@ -11,7 +11,7 @@ import * as yargs from 'yargs'; import { commands } from './commands'; import { constants } from './utils/constants'; import { consoleReporter } from './utils/error_reporter'; -import { CliOptions, CompilerOptions, DeployerOptions } from './utils/types'; +import { CliOptions, CompilerOptions, ContractDirectory, DeployerOptions } from './utils/types'; const DEFAULT_OPTIMIZER_ENABLED = false; const DEFAULT_CONTRACTS_DIR = path.resolve('src/contracts'); @@ -27,7 +27,7 @@ const DEFAULT_CONTRACTS_LIST = '*'; */ async function onCompileCommandAsync(argv: CliOptions): Promise { const opts: CompilerOptions = { - contractsDir: argv.contractsDir, + contractDirs: getContractDirectoriesFromList(argv.contractDirs), networkId: argv.networkId, optimizerEnabled: argv.shouldOptimize, artifactsDir: argv.artifactsDir, @@ -45,7 +45,7 @@ async function onDeployCommandAsync(argv: CliOptions): Promise { const web3Wrapper = new Web3Wrapper(web3Provider); const networkId = await web3Wrapper.getNetworkIdAsync(); const compilerOpts: CompilerOptions = { - contractsDir: argv.contractsDir, + contractDirs: getContractDirectoriesFromList(argv.contractsDir), networkId, optimizerEnabled: argv.shouldOptimize, artifactsDir: argv.artifactsDir, @@ -67,6 +67,29 @@ async function onDeployCommandAsync(argv: CliOptions): Promise { const deployerArgs = deployerArgsString.split(','); await commands.deployAsync(argv.contract as string, deployerArgs, deployerOpts); } +/** + * Creates a set of contracts to compile. + * @param contractDirectoriesList Comma separated list of contract directories + * @return Set of contract directories + */ +function getContractDirectoriesFromList(contractDirectoriesList: string): Set { + const directories = new Set(); + const possiblyNamespacedDirectories = contractDirectoriesList.split(','); + _.forEach(possiblyNamespacedDirectories, namespacedDirectory => { + const directoryComponents = namespacedDirectory.split(':'); + if (directoryComponents.length === 1) { + const directory = { namespace: '', path: directoryComponents[0] }; + directories.add(directory); + } else if (directoryComponents.length === 2) { + const directory = { namespace: directoryComponents[0], path: directoryComponents[1] }; + directories.add(directory); + } else { + throw new Error(`Unable to parse contracts directory: '${namespacedDirectory}'`); + } + }); + + return directories; +} /** * Creates a set of contracts to compile. * @param contracts Comma separated list of contracts to compile @@ -78,8 +101,7 @@ function getContractsSetFromList(contracts: string): Set { } const contractsArray = contracts.split(','); _.forEach(contractsArray, contractName => { - const fileName = `${contractName}${constants.SOLIDITY_FILE_EXTENSION}`; - specifiedContracts.add(fileName); + specifiedContracts.add(contractName); }); return specifiedContracts; } @@ -104,10 +126,11 @@ function deployCommandBuilder(yargsInstance: any) { (() => { const identityCommandBuilder = _.identity; return yargs - .option('contracts-dir', { + .option('contract-dirs', { type: 'string', default: DEFAULT_CONTRACTS_DIR, - description: 'path of contracts directory to compile', + description: + "comma separated list of contract directories.\nTo avoid filename clashes, directories should be prefixed with a namespace as follows: 'namespace:/path/to/dir'.", }) .option('network-id', { type: 'number', diff --git a/packages/deployer/src/compiler.ts b/packages/deployer/src/compiler.ts index ba360cb57..beaaab141 100644 --- a/packages/deployer/src/compiler.ts +++ b/packages/deployer/src/compiler.ts @@ -1,4 +1,4 @@ -import { ContractAbi } from '@0xproject/types'; +import { AbiType, ContractAbi, MethodAbi } from '@0xproject/types'; import { logUtils, promisify } from '@0xproject/utils'; import * as ethUtil from 'ethereumjs-util'; import * as fs from 'fs'; @@ -11,6 +11,8 @@ import solc = require('solc'); import { binPaths } from './solc/bin_paths'; import { + constructContractId, + constructUniqueSourceFileId, createDirIfDoesNotExistAsync, findImportIfExist, getContractArtifactIfExistsAsync, @@ -23,11 +25,14 @@ import { fsWrapper } from './utils/fs_wrapper'; import { CompilerOptions, ContractArtifact, + ContractDirectory, + ContractIds, ContractNetworkData, ContractNetworks, - ContractSourceData, + ContractSourceDataByFileId, ContractSources, ContractSpecificSourceData, + FunctionNameToSeenCount, } from './utils/types'; import { utils } from './utils/utils'; @@ -39,20 +44,22 @@ const SOLC_BIN_DIR = path.join(__dirname, '..', '..', 'solc_bin'); * to artifact files. */ export class Compiler { - private _contractsDir: string; + private _contractDirs: Set; private _networkId: number; private _optimizerEnabled: boolean; private _artifactsDir: string; // This get's set in the beggining of `compileAsync` function. It's not called from a constructor, but it's the only public method of that class and could as well be. private _contractSources!: ContractSources; private _specifiedContracts: Set = new Set(); - private _contractSourceData: ContractSourceData = {}; + private _contractSourceDataByFileId: ContractSourceDataByFileId = {}; + /** * Recursively retrieves Solidity source code from directory. * @param dirPath Directory to search. - * @return Mapping of contract fileName to contract source. + * @param contractBaseDir Base contracts directory of search tree. + * @return Mapping of sourceFilePath to the contract source. */ - private static async _getContractSourcesAsync(dirPath: string): Promise { + private static async _getContractSourcesAsync(dirPath: string, contractBaseDir: string): Promise { let dirContents: string[] = []; try { dirContents = await fsWrapper.readdirAsync(dirPath); @@ -68,14 +75,15 @@ export class Compiler { encoding: 'utf8', }; const source = await fsWrapper.readFileAsync(contentPath, opts); - sources[fileName] = source; - logUtils.log(`Reading ${fileName} source...`); + const sourceFilePath = contentPath.slice(contractBaseDir.length); + sources[sourceFilePath] = source; + logUtils.log(`Reading ${sourceFilePath} source...`); } catch (err) { logUtils.log(`Could not find file at ${contentPath}`); } } else { try { - const nestedSources = await Compiler._getContractSourcesAsync(contentPath); + const nestedSources = await Compiler._getContractSourcesAsync(contentPath, contractBaseDir); sources = { ...sources, ...nestedSources, @@ -93,7 +101,7 @@ export class Compiler { * @return An instance of the Compiler class. */ constructor(opts: CompilerOptions) { - this._contractsDir = opts.contractsDir; + this._contractDirs = opts.contractDirs; this._networkId = opts.networkId; this._optimizerEnabled = opts.optimizerEnabled; this._artifactsDir = opts.artifactsDir; @@ -105,25 +113,47 @@ export class Compiler { public async compileAsync(): Promise { await createDirIfDoesNotExistAsync(this._artifactsDir); await createDirIfDoesNotExistAsync(SOLC_BIN_DIR); - this._contractSources = await Compiler._getContractSourcesAsync(this._contractsDir); + this._contractSources = {}; + const contractIds: ContractIds = {}; + const contractDirs = Array.from(this._contractDirs.values()); + for (const contractDir of contractDirs) { + const sources = await Compiler._getContractSourcesAsync(contractDir.path, contractDir.path); + _.forIn(sources, (source, sourceFilePath) => { + const sourceFileId = constructUniqueSourceFileId(contractDir.namespace, sourceFilePath); + // Record the file's source and data + if (!_.isUndefined(this._contractSources[sourceFileId])) { + throw new Error(`Found duplicate source files with ID '${sourceFileId}'`); + } + this._contractSources[sourceFileId] = source; + // Create a mapping between the contract id and its source file id + const contractId = constructContractId(contractDir.namespace, sourceFilePath); + if (!_.isUndefined(contractIds[contractId])) { + throw new Error(`Found duplicate contract with ID '${contractId}'`); + } + contractIds[contractId] = sourceFileId; + }); + } _.forIn(this._contractSources, this._setContractSpecificSourceData.bind(this)); - const fileNames = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER) - ? _.keys(this._contractSources) + const specifiedContractIds = this._specifiedContracts.has(ALL_CONTRACTS_IDENTIFIER) + ? _.keys(contractIds) : Array.from(this._specifiedContracts.values()); - for (const fileName of fileNames) { - await this._compileContractAsync(fileName); - } + await Promise.all( + _.map(specifiedContractIds, async contractId => this._compileContractAsync(contractIds[contractId])), + ); } /** * Compiles contract and saves artifact to artifactsDir. - * @param fileName Name of contract with '.sol' extension. + * @param sourceFileId Unique ID of the source file. */ - private async _compileContractAsync(fileName: string): Promise { + private async _compileContractAsync(sourceFileId: string): Promise { if (_.isUndefined(this._contractSources)) { throw new Error('Contract sources not yet initialized'); } - const contractSpecificSourceData = this._contractSourceData[fileName]; - const currentArtifactIfExists = await getContractArtifactIfExistsAsync(this._artifactsDir, fileName); + if (_.isUndefined(this._contractSourceDataByFileId[sourceFileId])) { + throw new Error(`Contract source for ${sourceFileId} not yet initialized`); + } + const contractSpecificSourceData = this._contractSourceDataByFileId[sourceFileId]; + const currentArtifactIfExists = await getContractArtifactIfExistsAsync(this._artifactsDir, sourceFileId); const sourceHash = `0x${contractSpecificSourceData.sourceHash.toString('hex')}`; const sourceTreeHash = `0x${contractSpecificSourceData.sourceTreeHash.toString('hex')}`; @@ -162,16 +192,17 @@ export class Compiler { } const solcInstance = solc.setupMethods(requireFromString(solcjs, compilerBinFilename)); - logUtils.log(`Compiling ${fileName} with Solidity v${solcVersion}...`); - const source = this._contractSources[fileName]; + logUtils.log(`Compiling ${sourceFileId} with Solidity v${solcVersion}...`); + const source = this._contractSources[sourceFileId]; const input = { - [fileName]: source, + [sourceFileId]: source, }; const sourcesToCompile = { sources: input, }; + const compiled = solcInstance.compile(sourcesToCompile, Number(this._optimizerEnabled), importPath => - findImportIfExist(this._contractSources, importPath), + findImportIfExist(this._contractSources, sourceFileId, importPath), ); if (!_.isUndefined(compiled.errors)) { @@ -193,11 +224,11 @@ export class Compiler { }); } } - const contractName = path.basename(fileName, constants.SOLIDITY_FILE_EXTENSION); - const contractIdentifier = `${fileName}:${contractName}`; + const contractName = path.basename(sourceFileId, constants.SOLIDITY_FILE_EXTENSION); + const contractIdentifier = `${sourceFileId}:${contractName}`; if (_.isUndefined(compiled.contracts[contractIdentifier])) { throw new Error( - `Contract ${contractName} not found in ${fileName}. Please make sure your contract has the same name as it's file name`, + `Contract ${contractName} not found in ${sourceFileId}. Please make sure your contract has the same name as it's file name`, ); } const abi: ContractAbi = JSON.parse(compiled.contracts[contractIdentifier].interface); @@ -207,6 +238,7 @@ export class Compiler { const sourceMapRuntime = compiled.contracts[contractIdentifier].srcmapRuntime; const sources = _.keys(compiled.sources); const updated_at = Date.now(); + const contractNetworkData: ContractNetworkData = { solc_version: solcVersion, keccak256: sourceHash, @@ -243,28 +275,30 @@ export class Compiler { const artifactString = utils.stringifyWithFormatting(newArtifact); const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`; await fsWrapper.writeFileAsync(currentArtifactPath, artifactString); - logUtils.log(`${fileName} artifact saved!`); + logUtils.log(`${sourceFileId} artifact saved!`); } /** * Gets contract dependendencies and keccak256 hash from source. * @param source Source code of contract. + * @param fileId FileId of the contract source file. * @return Object with contract dependencies and keccak256 hash of source. */ - private _setContractSpecificSourceData(source: string, fileName: string): void { - if (!_.isUndefined(this._contractSourceData[fileName])) { + private _setContractSpecificSourceData(source: string, fileId: string): void { + if (!_.isUndefined(this._contractSourceDataByFileId[fileId])) { return; } const sourceHash = ethUtil.sha3(source); const solcVersionRange = parseSolidityVersionRange(source); - const dependencies = parseDependencies(source); - const sourceTreeHash = this._getSourceTreeHash(fileName, sourceHash, dependencies); - this._contractSourceData[fileName] = { + const dependencies = parseDependencies(source, fileId); + const sourceTreeHash = this._getSourceTreeHash(fileId, sourceHash, dependencies); + this._contractSourceDataByFileId[fileId] = { dependencies, solcVersionRange, sourceHash, sourceTreeHash, }; } + /** * Gets the source tree hash for a file and its dependencies. * @param fileName Name of contract file. @@ -276,7 +310,7 @@ export class Compiler { const dependencySourceTreeHashes = _.map(dependencies, dependency => { const source = this._contractSources[dependency]; this._setContractSpecificSourceData(source, dependency); - const sourceData = this._contractSourceData[dependency]; + const sourceData = this._contractSourceDataByFileId[dependency]; return this._getSourceTreeHash(dependency, sourceData.sourceHash, sourceData.dependencies); }); const sourceTreeHashesBuffer = Buffer.concat([sourceHash, ...dependencySourceTreeHashes]); diff --git a/packages/deployer/src/utils/compiler.ts b/packages/deployer/src/utils/compiler.ts index d5137d394..600495693 100644 --- a/packages/deployer/src/utils/compiler.ts +++ b/packages/deployer/src/utils/compiler.ts @@ -1,3 +1,4 @@ +import { AbiType, ContractAbi, MethodAbi } from '@0xproject/types'; import { logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import * as path from 'path'; @@ -5,8 +6,48 @@ import * as solc from 'solc'; import { constants } from './constants'; import { fsWrapper } from './fs_wrapper'; -import { ContractArtifact, ContractSources } from './types'; +import { ContractArtifact, ContractSources, FunctionNameToSeenCount } from './types'; +/** + * Constructs a system-wide unique identifier for a source file. + * @param directoryNamespace Namespace of the source file's root contract directory. + * @param sourceFilePath Path to a source file, relative to contractBaseDir. + * @return sourceFileId A system-wide unique identifier for the source file. + */ +export function constructUniqueSourceFileId(directoryNamespace: string, sourceFilePath: string): string { + const namespacePrefix = !_.isEmpty(directoryNamespace) ? `/${directoryNamespace}` : ''; + const sourceFilePathNoLeadingSlash = sourceFilePath.replace(/^\/+/g, ''); + const sourceFileId = `${namespacePrefix}/${sourceFilePathNoLeadingSlash}`; + return sourceFileId; +} +/** + * Constructs a system-wide unique identifier for a dependency file. + * @param dependencyFilePath Path from a sourceFile to a dependency. + * @param contractBaseDir Base contracts directory of search tree. + * @return sourceFileId A system-wide unique identifier for the source file. + */ +export function constructDependencyFileId(dependencyFilePath: string, sourceFilePath: string): string { + if (_.startsWith(dependencyFilePath, '/')) { + // Path of the form /namespace/path/to/dependency.sol + return dependencyFilePath; + } else { + // Dependency is relative to the source file: ./dependency.sol, ../../some/path/dependency.sol, etc. + // Join the two paths to construct a valid source file id: /namespace/path/to/dependency.sol + return path.join(path.dirname(sourceFilePath), dependencyFilePath); + } +} +/** + * Constructs a system-wide unique identifier for a contract. + * @param directoryNamespace Namespace of the source file's root contract directory. + * @param sourceFilePath Path to a source file, relative to contractBaseDir. + * @return sourceFileId A system-wide unique identifier for contract. + */ +export function constructContractId(directoryNamespace: string, sourceFilePath: string): string { + const namespacePrefix = !_.isEmpty(directoryNamespace) ? `${directoryNamespace}:` : ''; + const sourceFileName = path.basename(sourceFilePath, constants.SOLIDITY_FILE_EXTENSION); + const contractId = `${namespacePrefix}${sourceFileName}`; + return contractId; +} /** * Gets contract data on network or returns if an artifact does not exist. * @param artifactsDir Path to the artifacts directory. @@ -82,9 +123,10 @@ export function getNormalizedErrMsg(errMsg: string): string { /** * Parses the contract source code and extracts the dendencies * @param source Contract source code + * @param sourceFilePath File path of the source code. * @return List of dependendencies */ -export function parseDependencies(source: string): string[] { +export function parseDependencies(source: string, sourceFileId: string): string[] { // TODO: Use a proper parser const IMPORT_REGEX = /(import\s)/; const DEPENDENCY_PATH_REGEX = /"([^"]+)"/; // Source: https://github.com/BlockChainCompany/soljitsu/blob/master/lib/shared.js @@ -95,8 +137,8 @@ export function parseDependencies(source: string): string[] { const dependencyMatch = line.match(DEPENDENCY_PATH_REGEX); if (!_.isNull(dependencyMatch)) { const dependencyPath = dependencyMatch[1]; - const basenName = path.basename(dependencyPath); - dependencies.push(basenName); + const dependencyId = constructDependencyFileId(dependencyPath, sourceFileId); + dependencies.push(dependencyId); } } }); @@ -107,14 +149,19 @@ export function parseDependencies(source: string): string[] { * Callback to resolve dependencies with `solc.compile`. * Throws error if contractSources not yet initialized. * @param contractSources Source codes of contracts. - * @param importPath Path to an imported dependency. + * @param sourceFileId ID of the source file. + * @param importPath Path of dependency source file. * @return Import contents object containing source code of dependency. */ -export function findImportIfExist(contractSources: ContractSources, importPath: string): solc.ImportContents { - const fileName = path.basename(importPath); - const source = contractSources[fileName]; +export function findImportIfExist( + contractSources: ContractSources, + sourceFileId: string, + importPath: string, +): solc.ImportContents { + const dependencyFileId = constructDependencyFileId(importPath, sourceFileId); + const source = contractSources[dependencyFileId]; if (_.isUndefined(source)) { - throw new Error(`Contract source not found for ${fileName}`); + throw new Error(`Contract source not found for ${dependencyFileId}`); } const importContents: solc.ImportContents = { contents: source, diff --git a/packages/deployer/src/utils/contract.ts b/packages/deployer/src/utils/contract.ts index 9b7baac11..e8dd5218a 100644 --- a/packages/deployer/src/utils/contract.ts +++ b/packages/deployer/src/utils/contract.ts @@ -1,11 +1,9 @@ import { schemas, SchemaValidator } from '@0xproject/json-schemas'; -import { ContractAbi, EventAbi, FunctionAbi, MethodAbi, TxData } from '@0xproject/types'; +import { AbiType, ContractAbi, EventAbi, FunctionAbi, MethodAbi, TxData } from '@0xproject/types'; import { promisify } from '@0xproject/utils'; import * as _ from 'lodash'; import * as Web3 from 'web3'; -import { AbiType } from './types'; - export class Contract implements Web3.ContractInstance { public address: string; public abi: ContractAbi; diff --git a/packages/deployer/src/utils/encoder.ts b/packages/deployer/src/utils/encoder.ts index 4f62662e1..806efbbca 100644 --- a/packages/deployer/src/utils/encoder.ts +++ b/packages/deployer/src/utils/encoder.ts @@ -1,9 +1,7 @@ -import { AbiDefinition, ContractAbi, DataItem } from '@0xproject/types'; +import { AbiDefinition, AbiType, ContractAbi, DataItem } from '@0xproject/types'; import * as _ from 'lodash'; import * as web3Abi from 'web3-eth-abi'; -import { AbiType } from './types'; - export const encoder = { encodeConstructorArgsFromAbi(args: any[], abi: ContractAbi): string { const constructorTypes: string[] = []; diff --git a/packages/deployer/src/utils/types.ts b/packages/deployer/src/utils/types.ts index 7d131f5ce..08cab37b2 100644 --- a/packages/deployer/src/utils/types.ts +++ b/packages/deployer/src/utils/types.ts @@ -18,6 +18,11 @@ export interface ContractNetworks { [key: number]: ContractNetworkData; } +export interface ContractDirectory { + path: string; + namespace: string; +} + export interface ContractNetworkData { solc_version: string; optimizer_enabled: boolean; @@ -40,7 +45,7 @@ export interface SolcErrors { export interface CliOptions extends yargs.Arguments { artifactsDir: string; - contractsDir: string; + contractDirs: string; jsonrpcUrl: string; networkId: number; shouldOptimize: boolean; @@ -51,7 +56,7 @@ export interface CliOptions extends yargs.Arguments { } export interface CompilerOptions { - contractsDir: string; + contractDirs: Set; networkId: number; optimizerEnabled: boolean; artifactsDir: string; @@ -78,7 +83,11 @@ export interface ContractSources { [key: string]: string; } -export interface ContractSourceData { +export interface ContractIds { + [key: string]: string; +} + +export interface ContractSourceDataByFileId { [key: string]: ContractSpecificSourceData; } @@ -98,4 +107,8 @@ export interface Token { swarmHash: string; } +export interface FunctionNameToSeenCount { + [key: string]: number; +} + export type DoneCallback = (err?: Error) => void; -- cgit v1.2.3