Merge pull request #2532 from 0xProject/feat/sol-compiler/support-0.6

sol-compiler: 0.6 support
This commit is contained in:
Lawrence Forman 2020-04-01 15:07:18 -04:00 committed by GitHub
commit 2086bfff25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 615 additions and 270 deletions

View File

@ -1,4 +1,13 @@
[
{
"version": "4.1.0",
"changes": [
{
"note": "Refactor + add solidity 0.6 support",
"pr": 2532
}
]
},
{
"timestamp": 1582623685,
"version": "4.0.8",

View File

@ -36,13 +36,16 @@ const SEPARATOR = ',';
: argv.contracts === DEFAULT_CONTRACTS_LIST
? DEFAULT_CONTRACTS_LIST
: argv.contracts.split(SEPARATOR);
const opts = {
contractsDir: argv.contractsDir,
artifactsDir: argv.artifactsDir,
contracts,
isOfflineMode: process.env.SOLC_OFFLINE ? true : undefined,
};
const compiler = new Compiler(opts);
const opts = _.omitBy(
{
contractsDir: argv.contractsDir,
artifactsDir: argv.artifactsDir,
contracts,
isOfflineMode: process.env.SOLC_OFFLINE ? true : undefined,
},
v => v === undefined,
);
const compiler = new Compiler(await Compiler.getCompilerOptionsAsync(opts));
if (argv.watch) {
await compiler.watchAsync();
} else {

View File

@ -1,5 +1,6 @@
import { assert } from '@0x/assert';
import {
ContractSource,
FallthroughResolver,
FSResolver,
NameResolver,
@ -10,7 +11,6 @@ import {
URLResolver,
} from '@0x/sol-resolver';
import { logUtils } from '@0x/utils';
import { execSync } from 'child_process';
import * as chokidar from 'chokidar';
import { CompilerOptions, ContractArtifact, ContractVersionData, StandardOutput } from 'ethereum-types';
import * as fs from 'fs';
@ -18,59 +18,46 @@ import * as _ from 'lodash';
import * as path from 'path';
import * as pluralize from 'pluralize';
import * as semver from 'semver';
import solc = require('solc');
import { StandardInput } from 'solc';
import { promisify } from 'util';
import { compilerOptionsSchema } from './schemas/compiler_options_schema';
import {
addHexPrefixToContractBytecode,
compileDockerAsync,
compileSolcJSAsync,
CompiledSources,
createDirIfDoesNotExistAsync,
getContractArtifactIfExistsAsync,
getDependencyNameToPackagePath,
getSolcJSAsync,
getSolcJSFromPath,
getSolcJSReleasesAsync,
getSolcJSVersionFromPath,
getSourcesWithDependencies,
getSourceTreeHash,
makeContractPathsRelative,
normalizeSolcVersion,
parseSolidityVersionRange,
printCompilationErrorsAndWarnings,
} from './utils/compiler';
import { constants } from './utils/constants';
import { fsWrapper } from './utils/fs_wrapper';
import { utils } from './utils/utils';
type TYPE_ALL_FILES_IDENTIFIER = '*';
const ALL_CONTRACTS_IDENTIFIER = '*';
const ALL_FILES_IDENTIFIER = '*';
const DEFAULT_CONTRACTS_DIR = path.resolve('contracts');
const DEFAULT_ARTIFACTS_DIR = path.resolve('artifacts');
const DEFAULT_USE_DOCKERISED_SOLC = false;
const DEFAULT_IS_OFFLINE_MODE = false;
const DEFAULT_SHOULD_SAVE_STANDARD_INPUT = false;
import { ContractContentsByPath, ImportPrefixRemappings, SolcWrapper } from './solc_wrapper';
import { SolcWrapperV04 } from './solc_wrapper_v04';
import { SolcWrapperV05 } from './solc_wrapper_v05';
import { SolcWrapperV06 } from './solc_wrapper_v06';
// Solc compiler settings cannot be configured from the commandline.
// If you need this configured, please create a `compiler.json` config file
// with your desired configurations.
const DEFAULT_COMPILER_SETTINGS: solc.CompilerSettings = {
optimizer: {
enabled: false,
},
outputSelection: {
[ALL_FILES_IDENTIFIER]: {
[ALL_CONTRACTS_IDENTIFIER]: ['abi', 'evm.bytecode.object'],
},
},
export type TYPE_ALL_FILES_IDENTIFIER = '*';
export const ALL_CONTRACTS_IDENTIFIER = '*';
export const ALL_FILES_IDENTIFIER = '*';
const DEFAULT_COMPILER_OPTS: CompilerOptions = {
contractsDir: path.resolve('contracts'),
artifactsDir: path.resolve('artifacts'),
contracts: ALL_CONTRACTS_IDENTIFIER as TYPE_ALL_FILES_IDENTIFIER,
useDockerisedSolc: false,
isOfflineMode: false,
shouldSaveStandardInput: false,
};
const CONFIG_FILE = 'compiler.json';
interface VersionToInputs {
[solcVersion: string]: {
standardInput: solc.StandardInput;
contractsToCompile: string[];
};
interface ContractsByVersion {
[solcVersion: string]: ContractContentsByPath;
}
interface ContractPathToData {
@ -83,60 +70,74 @@ interface ContractData {
contractName: string;
}
// tslint:disable no-non-null-assertion
/**
* The Compiler facilitates compiling Solidity smart contracts and saves the results
* to artifact files.
*/
export class Compiler {
private readonly _opts: CompilerOptions;
private readonly _resolver: Resolver;
private readonly _nameResolver: NameResolver;
private readonly _contractsDir: string;
private readonly _compilerSettings: solc.CompilerSettings;
private readonly _artifactsDir: string;
private readonly _solcVersionIfExists: string | undefined;
private readonly _specifiedContracts: string[] | TYPE_ALL_FILES_IDENTIFIER;
private readonly _useDockerisedSolc: boolean;
private readonly _isOfflineMode: boolean;
private readonly _shouldSaveStandardInput: boolean;
private readonly _solcWrappersByVersion: { [version: string]: SolcWrapper } = {};
public static async getCompilerOptionsAsync(
overrides: Partial<CompilerOptions> = {},
file: string = 'compiler.json',
): Promise<CompilerOptions> {
const fileConfig: CompilerOptions = (await promisify(fs.stat)(file)).isFile
? JSON.parse((await promisify(fs.readFile)(file, 'utf8')).toString())
: {};
assert.doesConformToSchema('compiler.json', fileConfig, compilerOptionsSchema);
return {
...fileConfig,
...overrides,
};
}
private static _createDefaultResolver(
contractsDir: string,
// tslint:disable-next-line: trailing-comma
...appendedResolvers: Resolver[]
): Resolver {
const resolver = new FallthroughResolver();
resolver.appendResolver(new URLResolver());
resolver.appendResolver(new NPMResolver(contractsDir));
resolver.appendResolver(new RelativeFSResolver(contractsDir));
resolver.appendResolver(new FSResolver());
for (const appendedResolver of appendedResolvers) {
resolver.appendResolver(appendedResolver);
}
return resolver;
}
/**
* Instantiates a new instance of the Compiler class.
* @param opts Optional compiler options
* @return An instance of the Compiler class.
*/
constructor(opts?: CompilerOptions) {
const passedOpts = opts || {};
assert.doesConformToSchema('opts', passedOpts, compilerOptionsSchema);
// TODO: Look for config file in parent directories if not found in current directory
const config: CompilerOptions = fs.existsSync(CONFIG_FILE)
? JSON.parse(fs.readFileSync(CONFIG_FILE).toString())
: {};
assert.doesConformToSchema('compiler.json', config, compilerOptionsSchema);
this._contractsDir = path.resolve(passedOpts.contractsDir || config.contractsDir || DEFAULT_CONTRACTS_DIR);
constructor(opts: CompilerOptions = {}) {
this._opts = { ...DEFAULT_COMPILER_OPTS, ...opts };
assert.doesConformToSchema('opts', this._opts, compilerOptionsSchema);
this._contractsDir = path.resolve(this._opts.contractsDir!);
this._solcVersionIfExists =
process.env.SOLCJS_PATH !== undefined
? getSolcJSVersionFromPath(process.env.SOLCJS_PATH)
: passedOpts.solcVersion || config.solcVersion;
this._compilerSettings = {
...DEFAULT_COMPILER_SETTINGS,
...config.compilerSettings,
...passedOpts.compilerSettings,
};
this._artifactsDir = passedOpts.artifactsDir || config.artifactsDir || DEFAULT_ARTIFACTS_DIR;
this._specifiedContracts = passedOpts.contracts || config.contracts || ALL_CONTRACTS_IDENTIFIER;
this._useDockerisedSolc =
passedOpts.useDockerisedSolc || config.useDockerisedSolc || DEFAULT_USE_DOCKERISED_SOLC;
this._isOfflineMode = passedOpts.isOfflineMode || config.isOfflineMode || DEFAULT_IS_OFFLINE_MODE;
this._shouldSaveStandardInput =
passedOpts.shouldSaveStandardInput || config.shouldSaveStandardInput || DEFAULT_SHOULD_SAVE_STANDARD_INPUT;
: this._opts.solcVersion;
this._artifactsDir = this._opts.artifactsDir!;
this._specifiedContracts = this._opts.contracts!;
this._isOfflineMode = this._opts.isOfflineMode!;
this._shouldSaveStandardInput = this._opts.shouldSaveStandardInput!;
this._nameResolver = new NameResolver(this._contractsDir);
const resolver = new FallthroughResolver();
resolver.appendResolver(new URLResolver());
resolver.appendResolver(new NPMResolver(this._contractsDir));
resolver.appendResolver(new RelativeFSResolver(this._contractsDir));
resolver.appendResolver(new FSResolver());
resolver.appendResolver(this._nameResolver);
this._resolver = resolver;
this._resolver = Compiler._createDefaultResolver(this._contractsDir, this._nameResolver);
}
/**
* Compiles selected Solidity files found in `contractsDir` and writes JSON artifacts to `artifactsDir`.
*/
@ -145,6 +146,7 @@ export class Compiler {
await createDirIfDoesNotExistAsync(constants.SOLC_BIN_DIR);
await this._compileContractsAsync(this.getContractNamesToCompile(), true);
}
/**
* Compiles Solidity files specified during instantiation, and returns the
* compiler output given by solc. Return value is an array of outputs:
@ -157,6 +159,10 @@ export class Compiler {
const promisedOutputs = this._compileContractsAsync(this.getContractNamesToCompile(), false);
return promisedOutputs;
}
/**
* Watch contracts in the current project directory and recompile on changes.
*/
public async watchAsync(): Promise<void> {
console.clear(); // tslint:disable-line:no-console
logUtils.logWithTime('Starting compilation in watch mode...');
@ -183,7 +189,7 @@ export class Compiler {
watcher.add(pathsToWatch);
};
await onFileChangedAsync();
watcher.on('change', (changedFilePath: string) => {
watcher.on('change', () => {
console.clear(); // tslint:disable-line:no-console
logUtils.logWithTime('File change detected. Starting incremental compilation...');
// NOTE: We can't await it here because that's a callback.
@ -191,6 +197,7 @@ export class Compiler {
onFileChangedAsync(); // tslint:disable-line no-floating-promises
});
}
/**
* Gets a list of contracts to compile.
*/
@ -206,6 +213,7 @@ export class Compiler {
}
return contractNamesToCompile;
}
private _getPathsToWatch(): string[] {
const contractNames = this.getContractNamesToCompile();
const spyResolver = new SpyResolver(this._resolver);
@ -220,6 +228,7 @@ export class Compiler {
const pathsToWatch = _.uniq(spyResolver.resolvedContractSources.map(cs => cs.absolutePath));
return pathsToWatch;
}
/**
* Compiles contracts, and, if `shouldPersist` is true, saves artifacts to artifactsDir.
* @param fileName Name of contract with '.sol' extension.
@ -227,13 +236,12 @@ export class Compiler {
*/
private async _compileContractsAsync(contractNames: string[], shouldPersist: boolean): Promise<StandardOutput[]> {
// batch input contracts together based on the version of the compiler that they require.
const versionToInputs: VersionToInputs = {};
const contractsByVersion: ContractsByVersion = {};
// map contract paths to data about them for later verification and persistence
const contractPathToData: ContractPathToData = {};
const solcJSReleases = await getSolcJSReleasesAsync(this._isOfflineMode);
const resolvedContractSources = [];
const resolvedContractSources: ContractSource[] = [];
for (const contractName of contractNames) {
const spyResolver = new SpyResolver(this._resolver);
const contractSource = spyResolver.resolve(contractName);
@ -246,161 +254,155 @@ export class Compiler {
if (!this._shouldCompile(contractData)) {
continue;
}
contractPathToData[contractSource.path] = contractData;
const solcVersion =
this._solcVersionIfExists === undefined
? semver.maxSatisfying(_.keys(solcJSReleases), parseSolidityVersionRange(contractSource.source))
: this._solcVersionIfExists;
if (solcVersion === null) {
contractPathToData[contractSource.absolutePath] = contractData;
let solcVersion: string | undefined;
if (this._solcVersionIfExists) {
solcVersion = this._solcVersionIfExists;
} else {
const solidityVersion = semver.maxSatisfying(
_.keys(solcJSReleases),
parseSolidityVersionRange(contractSource.source),
);
if (solidityVersion) {
solcVersion = normalizeSolcVersion(solcJSReleases[solidityVersion]);
}
}
if (solcVersion === undefined) {
throw new Error(
`Couldn't find any solidity version satisfying the constraint ${parseSolidityVersionRange(
contractSource.source,
)}`,
);
}
const isFirstContractWithThisVersion = versionToInputs[solcVersion] === undefined;
if (isFirstContractWithThisVersion) {
versionToInputs[solcVersion] = {
standardInput: {
language: 'Solidity',
sources: {},
settings: this._compilerSettings,
},
contractsToCompile: [],
};
}
// add input to the right version batch
for (const resolvedContractSource of spyResolver.resolvedContractSources) {
versionToInputs[solcVersion].standardInput.sources[resolvedContractSource.absolutePath] = {
content: resolvedContractSource.source,
};
contractsByVersion[solcVersion] = contractsByVersion[solcVersion] || {};
contractsByVersion[solcVersion][resolvedContractSource.absolutePath] = resolvedContractSource.source;
resolvedContractSources.push(resolvedContractSource);
}
resolvedContractSources.push(...spyResolver.resolvedContractSources);
versionToInputs[solcVersion].contractsToCompile.push(contractSource.path);
}
const dependencyNameToPath = getDependencyNameToPackagePath(resolvedContractSources);
const importRemappings = getDependencyNameToPackagePath(resolvedContractSources);
const versions = Object.keys(contractsByVersion);
const compilerOutputs: StandardOutput[] = [];
for (const solcVersion of _.keys(versionToInputs)) {
const input = versionToInputs[solcVersion];
logUtils.warn(
`Compiling ${input.contractsToCompile.length} contracts (${
input.contractsToCompile
}) with Solidity v${solcVersion}...`,
);
let compilerOutput;
let fullSolcVersion;
input.standardInput.settings.remappings = _.map(
dependencyNameToPath,
(dependencyPackagePath: string, dependencyName: string) => `${dependencyName}=${dependencyPackagePath}`,
);
if (this._useDockerisedSolc) {
const dockerCommand = `docker run ethereum/solc:${solcVersion} --version`;
const versionCommandOutput = execSync(dockerCommand).toString();
const versionCommandOutputParts = versionCommandOutput.split(' ');
fullSolcVersion = versionCommandOutputParts[versionCommandOutputParts.length - 1].trim();
compilerOutput = await compileDockerAsync(solcVersion, input.standardInput);
} else {
fullSolcVersion = solcJSReleases[solcVersion];
const solcInstance =
process.env.SOLCJS_PATH !== undefined
? getSolcJSFromPath(process.env.SOLCJS_PATH)
: await getSolcJSAsync(solcVersion, this._isOfflineMode);
compilerOutput = await compileSolcJSAsync(solcInstance, input.standardInput);
}
if (compilerOutput.errors !== undefined) {
printCompilationErrorsAndWarnings(compilerOutput.errors);
}
compilerOutput.sources = makeContractPathsRelative(
compilerOutput.sources,
this._contractsDir,
dependencyNameToPath,
);
compilerOutput.contracts = makeContractPathsRelative(
compilerOutput.contracts,
this._contractsDir,
dependencyNameToPath,
);
const compilationResults = await Promise.all(
versions.map(async solcVersion => {
const contracts = contractsByVersion[solcVersion];
logUtils.warn(
`Compiling ${Object.keys(contracts).length} contracts (${Object.keys(contracts).map(p =>
path.basename(p),
)}) with Solidity ${solcVersion}...`,
);
return this._getSolcWrapperForVersion(solcVersion).compileAsync(contracts, importRemappings);
}),
);
for (const contractPath of input.contractsToCompile) {
const contractName = contractPathToData[contractPath].contractName;
if (compilerOutput.contracts[contractPath] !== undefined) {
const compiledContract = compilerOutput.contracts[contractPath][contractName];
if (compiledContract === undefined) {
throw new Error(
`Contract ${contractName} not found in ${contractPath}. Please make sure your contract has the same name as it's file name`,
);
}
if (this._shouldSaveStandardInput) {
await fsWrapper.writeFileAsync(
`${this._artifactsDir}/${contractName}.input.json`,
utils.stringifyWithFormatting(input.standardInput),
);
}
addHexPrefixToContractBytecode(compiledContract);
}
if (shouldPersist) {
await this._persistCompiledContractAsync(
contractPath,
contractPathToData[contractPath].currentArtifactIfExists,
contractPathToData[contractPath].sourceTreeHashHex,
contractName,
fullSolcVersion,
compilerOutput,
if (shouldPersist) {
await Promise.all(
versions.map(async (solcVersion, i) => {
const compilationResult = compilationResults[i];
const contracts = contractsByVersion[solcVersion];
// tslint:disable-next-line: forin
await Promise.all(
Object.keys(contracts).map(async contractPath => {
const contractData = contractPathToData[contractPath];
if (contractData === undefined) {
return;
}
const { contractName } = contractData;
const compiledContract = compilationResult.output.contracts[contractPath][contractName];
if (compiledContract === undefined) {
throw new Error(
`Contract ${contractName} not found in ${contractPath}. Please make sure your contract has the same name as it's file name`,
);
}
await this._persistCompiledContractAsync(
contractPath,
contractPathToData[contractPath].currentArtifactIfExists,
contractPathToData[contractPath].sourceTreeHashHex,
contractName,
solcVersion,
contracts,
compilationResult.input,
compilationResult.output,
importRemappings,
);
}),
);
}
}
compilerOutputs.push(compilerOutput);
}),
);
}
return compilerOutputs;
return compilationResults.map(r => r.output);
}
private _shouldCompile(contractData: ContractData): boolean {
if (contractData.currentArtifactIfExists === undefined) {
return true;
} else {
const currentArtifact = contractData.currentArtifactIfExists as ContractArtifact;
const solc = this._getSolcWrapperForVersion(currentArtifact.compiler.version);
const isUserOnLatestVersion = currentArtifact.schemaVersion === constants.LATEST_ARTIFACT_VERSION;
const didCompilerSettingsChange = !_.isEqual(
_.omit(currentArtifact.compiler.settings, 'remappings'),
_.omit(this._compilerSettings, 'remappings'),
);
const didCompilerSettingsChange = solc.areCompilerSettingsDifferent(currentArtifact.compiler.settings);
const didSourceChange = currentArtifact.sourceTreeHashHex !== contractData.sourceTreeHashHex;
return !isUserOnLatestVersion || didCompilerSettingsChange || didSourceChange;
}
}
private _getSolcWrapperForVersion(solcVersion: string): SolcWrapper {
const normalizedVersion = normalizeSolcVersion(solcVersion);
return (
this._solcWrappersByVersion[normalizedVersion] ||
(this._solcWrappersByVersion[normalizedVersion] = this._createSolcInstance(normalizedVersion))
);
}
private _createSolcInstance(solcVersion: string): SolcWrapper {
if (solcVersion.startsWith('0.4.')) {
return new SolcWrapperV04(solcVersion, this._opts);
}
if (solcVersion.startsWith('0.5.')) {
return new SolcWrapperV05(solcVersion, this._opts);
}
if (solcVersion.startsWith('0.6')) {
return new SolcWrapperV06(solcVersion, this._opts);
}
throw new Error(`Missing Solc wrapper implementation for version ${solcVersion}`);
}
private async _persistCompiledContractAsync(
contractPath: string,
currentArtifactIfExists: ContractArtifact | void,
sourceTreeHashHex: string,
contractName: string,
fullSolcVersion: string,
compilerOutput: solc.StandardOutput,
solcVersion: string,
sourcesByPath: ContractContentsByPath,
compilerInput: StandardInput,
compilerOutput: StandardOutput,
importRemappings: ImportPrefixRemappings,
): Promise<void> {
const compiledContract = compilerOutput.contracts[contractPath][contractName];
// need to gather sourceCodes for this artifact, but compilerOutput.sources (the list of contract modules)
// 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(
this._resolver,
contractPath,
compilerOutput.sources,
);
const allSources: CompiledSources = {};
// tslint:disable-next-line: forin
for (const sourceContractPath in sourcesByPath) {
const content = sourcesByPath[sourceContractPath];
const { id } = compilerOutput.sources[sourceContractPath];
allSources[sourceContractPath] = { id, content };
}
const usedSources = getSourcesWithDependencies(contractPath, allSources, importRemappings);
const contractVersion: ContractVersionData = {
compilerOutput: compiledContract,
sources,
sourceCodes,
sourceTreeHashHex,
sources: _.mapValues(usedSources, ({ id }) => ({ id })),
sourceCodes: _.mapValues(usedSources, ({ content }) => content),
compiler: {
name: 'solc',
version: fullSolcVersion,
settings: this._compilerSettings,
version: solcVersion,
settings: compilerInput.settings,
},
};
@ -424,5 +426,20 @@ export class Compiler {
const currentArtifactPath = `${this._artifactsDir}/${contractName}.json`;
await fsWrapper.writeFileAsync(currentArtifactPath, artifactString);
logUtils.warn(`${contractName} artifact saved!`);
if (this._shouldSaveStandardInput) {
await fsWrapper.writeFileAsync(
`${this._artifactsDir}/${contractName}.input.json`,
utils.stringifyWithFormatting({
...compilerInput,
// Insert solcVersion into input.
settings: {
...compilerInput.settings,
version: solcVersion,
},
}),
);
logUtils.warn(`${contractName} input artifact saved!`);
}
}
}

View File

@ -0,0 +1,35 @@
import { StandardOutput } from 'ethereum-types';
import { StandardInput } from 'solc';
export interface ContractContentsByPath {
[path: string]: string;
}
export interface ImportPrefixRemappings {
[prefix: string]: string;
}
export interface CompilationResult {
input: StandardInput;
output: StandardOutput;
}
export abstract class SolcWrapper {
/**
* Get the solc version.
*/
public abstract get version(): string;
/**
* Check if the configured compiler settings is different from another.
*/
public abstract areCompilerSettingsDifferent(settings: any): boolean;
/**
* Compile contracts, returning standard input and output.
*/
public abstract compileAsync(
contractsByPath: ContractContentsByPath,
dependencies: ImportPrefixRemappings,
): Promise<CompilationResult>;
}

View File

@ -0,0 +1,3 @@
import { SolcWrapperV05 } from './solc_wrapper_v05';
export const SolcWrapperV04 = SolcWrapperV05;

View File

@ -0,0 +1,102 @@
import { CompilerOptions, StandardOutput } from 'ethereum-types';
import * as _ from 'lodash';
import solc = require('solc');
import {
addHexPrefixToContractBytecode,
compileDockerAsync,
compileSolcJSAsync,
getSolcJSAsync,
getSolidityVersionFromSolcVersion,
printCompilationErrorsAndWarnings,
} from './utils/compiler';
import { CompilationResult, ContractContentsByPath, ImportPrefixRemappings, SolcWrapper } from './solc_wrapper';
// Solc compiler settings cannot be configured from the commandline.
// If you need this configured, please create a `compiler.json` config file
// with your desired configurations.
export const DEFAULT_COMPILER_SETTINGS: solc.CompilerSettings = {
optimizer: {
enabled: false,
},
outputSelection: {
'*': {
'*': ['abi', 'evm.bytecode.object'],
},
},
};
// tslint:disable no-non-null-assertion
export class SolcWrapperV05 extends SolcWrapper {
protected readonly _compilerSettings: solc.CompilerSettings;
public static normalizeOutput(output: StandardOutput): StandardOutput {
const _output = _.cloneDeep(output);
// tslint:disable-next-line forin
for (const contractPath in _output.contracts) {
// tslint:disable-next-line forin
for (const contract of Object.values(_output.contracts[contractPath])) {
addHexPrefixToContractBytecode(contract);
}
}
return _output;
}
constructor(protected readonly _solcVersion: string, protected readonly _opts: CompilerOptions) {
super();
this._compilerSettings = {
...DEFAULT_COMPILER_SETTINGS,
..._opts.compilerSettings,
};
}
public get version(): string {
return this._solcVersion;
}
public get solidityVersion(): string {
return getSolidityVersionFromSolcVersion(this._solcVersion);
}
public areCompilerSettingsDifferent(settings: any): boolean {
return !_.isEqual(_.omit(settings, 'remappings'), _.omit(this._compilerSettings, 'remappings'));
}
public async compileAsync(
contractsByPath: ContractContentsByPath,
importRemappings: ImportPrefixRemappings,
): Promise<CompilationResult> {
const input: solc.StandardInput = {
language: 'Solidity',
sources: {},
settings: {
remappings: [],
...this._compilerSettings,
},
};
for (const [contractPath, contractContent] of Object.entries(contractsByPath)) {
input.sources[contractPath] = { content: contractContent };
}
for (const [prefix, _path] of Object.entries(importRemappings)) {
input.settings.remappings!.push(`${prefix}=${_path}`);
}
const output = await this._compileInputAsync(input);
if (output.errors !== undefined) {
printCompilationErrorsAndWarnings(output.errors);
}
return {
input,
output: SolcWrapperV05.normalizeOutput(output),
};
}
protected async _compileInputAsync(input: solc.StandardInput): Promise<StandardOutput> {
if (this._opts.useDockerisedSolc) {
return compileDockerAsync(this.solidityVersion, input);
}
const solcInstance = await getSolcJSAsync(this.solidityVersion, !!this._opts.isOfflineMode);
return compileSolcJSAsync(solcInstance, input);
}
}

View File

@ -0,0 +1,25 @@
import { CompilerOptions, StandardOutput } from 'ethereum-types';
import solc = require('solc');
import { compileSolcJSAsync, getSolcJSAsync } from './utils/compiler';
import { SolcWrapperV05 } from './solc_wrapper_v05';
// 0.6.x has a `compile()` function in lieu of `compileStandardWrapper`.
type SolcV06 = solc.SolcInstance & { compile(input: string): string };
export class SolcWrapperV06 extends SolcWrapperV05 {
constructor(solcVersion: string, opts: CompilerOptions) {
super(solcVersion, opts);
}
protected async _compileInputAsync(input: solc.StandardInput): Promise<StandardOutput> {
if (this._opts.useDockerisedSolc) {
return super._compileInputAsync(input);
}
// Shim the old `compileStandardWrapper` function.
const solcInstance = (await getSolcJSAsync(this.solidityVersion, !!this._opts.isOfflineMode)) as SolcV06;
solcInstance.compileStandardWrapper = solcInstance.compile;
return compileSolcJSAsync(solcInstance, input);
}
}

View File

@ -1,13 +1,14 @@
import { ContractSource, Resolver } from '@0x/sol-resolver';
import { fetchAsync, logUtils } from '@0x/utils';
import chalk from 'chalk';
import { execSync } from 'child_process';
import { exec, spawn } from 'child_process';
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 { promisify } from 'util';
import { constants } from './constants';
import { fsWrapper } from './fs_wrapper';
@ -152,18 +153,39 @@ export async function compileSolcJSAsync(
/**
* Compiles the contracts and prints errors/warnings
* @param solcVersion Version of a solc compiler
* @param solidityVersion Solidity version
* @param standardInput Solidity standard JSON input
*/
export async function compileDockerAsync(
solcVersion: string,
solidityVersion: string,
standardInput: solc.StandardInput,
): Promise<solc.StandardOutput> {
const standardInputStr = JSON.stringify(standardInput, null, 2);
const dockerCommand = `docker run -i -a stdin -a stdout -a stderr ethereum/solc:${solcVersion} solc --standard-json`;
const standardOutputStr = execSync(dockerCommand, { input: standardInputStr }).toString();
const compiled: solc.StandardOutput = JSON.parse(standardOutputStr);
return compiled;
// prettier-ignore
const dockerArgs = [
'run',
'-i',
'-a', 'stdin',
'-a', 'stdout',
'-a', 'stderr',
`ethereum/solc:${solidityVersion}`,
'solc', '--standard-json',
];
return new Promise<solc.StandardOutput>((accept, reject) => {
const p = spawn('docker', dockerArgs, { shell: true, stdio: ['pipe', 'inherit', 'inherit'] });
p.stdin.write(standardInputStr);
p.stdin.end();
let fullOutput = '';
p.stdout.on('data', (chunk: string) => {
fullOutput += chunk;
});
p.on('close', code => {
if (code !== 0) {
reject('Compilation failed');
}
accept(JSON.parse(fullOutput));
});
});
}
/**
@ -250,43 +272,60 @@ export function getSourceTreeHash(resolver: Resolver, importPath: string): Buffe
}
/**
* 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`).
* Mapping of absolute contract path to compilation ID and source code.
*/
export interface CompiledSources {
[sourcePath: string]: { id: number; content: string };
}
/**
* Contract sources by import path.
*/
export interface CompiledImports {
[importPath: string]: { id: number; content: string };
}
/**
* Recursively parses imports from sources starting from `contractPath`.
* @return Sources required by imports.
*/
export function getSourcesWithDependencies(
resolver: Resolver,
contractPath: string,
fullSources: { [sourceName: string]: { id: number } },
): { sourceCodes: { [sourceName: string]: string }; sources: { [sourceName: string]: { id: number } } } {
const sources = { [contractPath]: fullSources[contractPath] };
const sourceCodes = { [contractPath]: resolver.resolve(contractPath).source };
sourcesByAbsolutePath: CompiledSources,
importRemappings: { [prefix: string]: string },
): CompiledImports {
const compiledImports = { [`./${path.basename(contractPath)}`]: sourcesByAbsolutePath[contractPath] };
recursivelyGatherDependencySources(
resolver,
contractPath,
sourceCodes[contractPath],
fullSources,
sources,
sourceCodes,
path.dirname(contractPath),
sourcesByAbsolutePath,
importRemappings,
compiledImports,
);
return { sourceCodes, sources };
return compiledImports;
}
function recursivelyGatherDependencySources(
resolver: Resolver,
contractPath: string,
contractSource: string,
fullSources: { [sourceName: string]: { id: number } },
sourcesToAppendTo: { [sourceName: string]: { id: number } },
sourceCodesToAppendTo: { [sourceName: string]: string },
rootDir: string,
sourcesByAbsolutePath: CompiledSources,
importRemappings: { [prefix: string]: string },
compiledImports: CompiledImports,
visitedAbsolutePaths: { [absPath: string]: boolean } = {},
importRootDir?: string,
): void {
if (visitedAbsolutePaths[contractPath]) {
return;
} else {
visitedAbsolutePaths[contractPath] = true;
}
const contractSource = sourcesByAbsolutePath[contractPath].content;
const importStatementMatches = contractSource.match(/\nimport[^;]*;/g);
if (importStatementMatches === null) {
return;
}
const lastPathSeparatorPos = contractPath.lastIndexOf('/');
const contractFolder = lastPathSeparatorPos === -1 ? '' : contractPath.slice(0, lastPathSeparatorPos + 1);
for (const importStatementMatch of importStatementMatches) {
const importPathMatches = importStatementMatch.match(/\"([^\"]*)\"/);
if (importPathMatches === null || importPathMatches.length === 0) {
@ -294,59 +333,54 @@ function recursivelyGatherDependencySources(
}
let importPath = importPathMatches[1];
// HACK(albrow): 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);
let absPath = importPath;
let _importRootDir = importRootDir;
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 (sourcesToAppendTo[importPath] === undefined) {
sourcesToAppendTo[importPath] = { id: fullSources[importPath].id };
sourceCodesToAppendTo[importPath] = resolver.resolve(importPath).source;
recursivelyGatherDependencySources(
resolver,
importPath,
resolver.resolve(importPath).source,
fullSources,
sourcesToAppendTo,
sourceCodesToAppendTo,
);
absPath = path.join(contractFolder, importPath);
if (_importRootDir) {
// If there's an `_importRootDir`, we're in a package, so express
// the import path as within the package.
importPath = path.join(_importRootDir, importPath);
} else {
// Express relative imports paths as paths from the root directory.
importPath = path.relative(rootDir, absPath);
if (!importPath.startsWith('.')) {
importPath = `./${importPath}`;
}
}
} else {
for (const [prefix, replacement] of Object.entries(importRemappings)) {
if (importPath.startsWith(prefix)) {
absPath = `${replacement}${importPath.substr(prefix.length)}`;
_importRootDir = path.dirname(importPath);
break;
}
}
}
compiledImports[importPath] = sourcesByAbsolutePath[absPath];
recursivelyGatherDependencySources(
absPath,
rootDir,
sourcesByAbsolutePath,
importRemappings,
compiledImports,
visitedAbsolutePaths,
_importRootDir,
);
}
}
/**
* Gets the solidity compiler instance. 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
* @param solidityVersion The solidity version. e.g. 0.5.0
* @param isOfflineMode Offline mode flag
*/
export async function getSolcJSAsync(solcVersion: string, isOfflineMode: boolean): Promise<solc.SolcInstance> {
export async function getSolcJSAsync(solidityVersion: string, isOfflineMode: boolean): Promise<solc.SolcInstance> {
const solcJSReleases = await getSolcJSReleasesAsync(isOfflineMode);
const fullSolcVersion = solcJSReleases[solcVersion];
const fullSolcVersion = solcJSReleases[solidityVersion];
if (fullSolcVersion === undefined) {
throw new Error(`${solcVersion} is not a known compiler version`);
throw new Error(`${solidityVersion} is not a known compiler version`);
}
const compilerBinFilename = path.join(constants.SOLC_BIN_DIR, fullSolcVersion);
let solcjs: string;
@ -383,7 +417,7 @@ export function getSolcJSFromPath(modulePath: string): solc.SolcInstance {
* @param path The path to the solc module.
*/
export function getSolcJSVersionFromPath(modulePath: string): string {
return require(modulePath).version();
return normalizeSolcVersion(require(modulePath).version());
}
/**
@ -437,3 +471,47 @@ export function getDependencyNameToPackagePath(
});
return dependencyNameToPath;
}
/**
* Extract the solidity version (e.g., '0.5.9') from a solc version (e.g., `0.5.9+commit.34d3134f`).
*/
export function getSolidityVersionFromSolcVersion(solcVersion: string): string {
const m = /(\d+\.\d+\.\d+)\+commit\.[a-fA-F0-9]{8}/.exec(solcVersion);
if (!m) {
throw new Error(`Unable to parse solc version string "${solcVersion}"`);
}
return m[1];
}
/**
* Strips any extra characters before and after the version + commit hash of a solc version string.
*/
export function normalizeSolcVersion(fullSolcVersion: string): string {
const m = /\d+\.\d+\.\d+\+commit\.[a-fA-F0-9]{8}/.exec(fullSolcVersion);
if (!m) {
throw new Error(`Unable to parse solc version string "${fullSolcVersion}"`);
}
return m[0];
}
/**
* Gets the full version string of a dockerized solc.
*/
export async function getDockerFullSolcVersionAsync(solidityVersion: string): Promise<string> {
const dockerCommand = `docker run ethereum/solc:${solidityVersion} --version`;
const versionCommandOutput = (await promisify(exec)(dockerCommand)).stdout.toString();
const versionCommandOutputParts = versionCommandOutput.split(' ');
return normalizeSolcVersion(versionCommandOutputParts[versionCommandOutputParts.length - 1].trim());
}
/**
* Gets the full version string of a JS module solc.
*/
export async function getJSFullSolcVersionAsync(
solidityVersion: string,
isOfflineMode: boolean = false,
): Promise<string> {
return normalizeSolcVersion((await getSolcJSAsync(solidityVersion, isOfflineMode)).version());
}
// tslint:disable-next-line: max-file-line-count

View File

@ -1,19 +1,22 @@
import { join } from 'path';
import { hexUtils } from '@0x/utils';
import * as chai from 'chai';
import { CompilerOptions, ContractArtifact } from 'ethereum-types';
import 'mocha';
import { join } from 'path';
import { Compiler } from '../src/compiler';
import { fsWrapper } from '../src/utils/fs_wrapper';
import { exchange_binary } from './fixtures/exchange_bin';
import { v6_contract_binary } from './fixtures/v6_contract_bin';
import { chaiSetup } from './util/chai_setup';
import { constants } from './util/constants';
chaiSetup.configure();
const expect = chai.expect;
const METADATA_SIZE = 43;
describe('#Compiler', function(): void {
this.timeout(constants.timeoutMs); // tslint:disable-line:no-invalid-this
const artifactsDir = `${__dirname}/fixtures/artifacts`;
@ -41,14 +44,12 @@ describe('#Compiler', function(): void {
};
const exchangeArtifactString = await fsWrapper.readFileAsync(exchangeArtifactPath, opts);
const exchangeArtifact: ContractArtifact = JSON.parse(exchangeArtifactString);
// The last 43 bytes of the binaries are metadata which may not be equivalent
const metadataByteLength = 43;
const metadataHexLength = metadataByteLength * 2;
const unlinkedBinaryWithoutMetadata = exchangeArtifact.compilerOutput.evm.bytecode.object.slice(
2,
-metadataHexLength,
const unlinkedBinaryWithoutMetadata = hexUtils.slice(
exchangeArtifact.compilerOutput.evm.bytecode.object,
0,
-METADATA_SIZE,
);
const exchangeBinaryWithoutMetadata = exchange_binary.slice(0, -metadataHexLength);
const exchangeBinaryWithoutMetadata = hexUtils.slice(exchange_binary, 0, -METADATA_SIZE);
expect(unlinkedBinaryWithoutMetadata).to.equal(exchangeBinaryWithoutMetadata);
});
it("should throw when Whatever.sol doesn't contain a Whatever contract", async () => {
@ -114,4 +115,27 @@ describe('#Compiler', function(): void {
expect(artifact).to.equal('EmptyContract.json');
}
});
it('should compile a V0.6 contract', async () => {
compilerOpts.contracts = ['V6Contract'];
const artifactPath = `${artifactsDir}/V6Contract.json`;
if (fsWrapper.doesPathExistSync(artifactPath)) {
await fsWrapper.removeFileAsync(artifactPath);
}
await new Compiler(compilerOpts).compileAsync();
const opts = {
encoding: 'utf8',
};
const exchangeArtifactString = await fsWrapper.readFileAsync(artifactPath, opts);
const exchangeArtifact: ContractArtifact = JSON.parse(exchangeArtifactString);
const actualBinaryWithoutMetadata = hexUtils.slice(
exchangeArtifact.compilerOutput.evm.bytecode.object,
0,
-METADATA_SIZE,
);
const expectedBinaryWithoutMetadata = hexUtils.slice(v6_contract_binary, 0, -METADATA_SIZE);
expect(actualBinaryWithoutMetadata).to.eq(expectedBinaryWithoutMetadata);
});
});

View File

@ -0,0 +1,37 @@
/*
Copyright 2020 ZeroEx Intl.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
pragma solidity 0.6.4;
contract V6Contract {
uint256 private _privateNumber;
constructor(uint256 privateNumber) public {
_privateNumber = privateNumber;
}
fallback() external {
revert('nope');
}
receive() payable external {
// no-op
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
export const v6_contract_binary =
'0x6080604052348015600f57600080fd5b5060405161011238038061011283398181016040526020811015603157600080fd5b8101908080519060200190929190505050806000819055505060ba806100586000396000f3fe608060405236600a57005b348015601557600080fd5b506040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260048152602001807f6e6f70650000000000000000000000000000000000000000000000000000000081525060200191505060405180910390fdfea26469706673582212208084a572151fb41e40aa4d1197e387cba7b4f0cbd982e34682974038667b564764736f6c63430006040033';

View File

@ -1,4 +1,13 @@
[
{
"version": "5.1.0",
"changes": [
{
"note": "Add `version()` to `SolcInstance`",
"pr": 2532
}
]
},
{
"timestamp": 1581204851,
"version": "5.0.2",

View File

@ -105,6 +105,7 @@ declare module 'solc' {
findImports: (importPath: string) => ImportContents,
): CompilationResult;
compileStandardWrapper(input: string, findImports?: (importPath: string) => ImportContents): string;
version(): string;
}
export function loadRemoteVersion(
versionName: string,