Skip to content

Commit 04eebb6

Browse files
sz-piotrmarekkirejczyk
authored andcommitted
Compiler rewrite (#68)
* Move compilation sources to lib/compiler * Extract findInputs to a separate function * Extract findImports to separate function * Remove overrides from compiler * Move loadConfig into separate file * Remove the Compiler class * Remove solcjs custom saveOutput and extract buildCommand in nativeWrapper * Remove saving output from the Wrappers * Add formatErrors helper * Replace SolcjsWrapper with a higher order function * Replace DockerWrapper with a higher order function * Fix compilation output * Replace NativeWrapper with a higher order function * Move some logic to ImportMappingBuilder * Extract buildInputObject to a separate function * Extract buildSources to a separate function * Remove wrappers entirely * Prepare a 2.0.3 release * Fix linting errors
1 parent 98f8c5f commit 04eebb6

33 files changed

+465
-482
lines changed

bin/waffle

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
#!/usr/bin/env node
22

33
'use strict';
4-
const Waffle = require('../dist/compiler.js');
5-
Waffle.compile(process.argv[2]);
4+
const Waffle = require('../dist/compiler/compiler.js');
5+
Waffle
6+
.compileProject(process.argv[2])
7+
.catch(e => {
8+
console.error(e);
9+
process.exit(1);
10+
});

docs/release-notes/2.0.3.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Summary
2+
This release introduces TypeScript declaration files for custom matchers and
3+
a rewrite of the compiler.
4+
5+
## Type declarations for custom matchers
6+
You can now enjoy full type safety when writing your tests with waffle.
7+
8+
## Compiler rewrite
9+
While no external facing api is changing the internals of the compiler have been
10+
rewritten to enable further developments.

lib/compiler.ts

-106
This file was deleted.

lib/compiler/buildUitls.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function buildInputObject(sources: any, remappings?: any) {
2+
return {
3+
language: 'Solidity',
4+
sources,
5+
settings: {
6+
remappings,
7+
outputSelection: {'*': {'*': ['abi', 'evm.bytecode', 'evm.deployedBytecode']}}
8+
}
9+
};
10+
}
11+
12+
export function buildSources(inputs: string[], transform: (input: string) => string) {
13+
const sources: Record<string, { urls: string[] }> = {};
14+
for (const input of inputs) {
15+
sources[input.replace(/\\/g, '/')] = {urls: [transform(input)]};
16+
}
17+
return sources;
18+
}

lib/compiler/compileDocker.ts

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {join} from 'path';
2+
import ImportMappingBuilder from './importMappingBuilder';
3+
import {Config} from '../config/config';
4+
import {execSync} from 'child_process';
5+
import {buildInputObject, buildSources} from './buildUitls';
6+
7+
const CONTAINER_PATH = '/home/project';
8+
const NPM_PATH = '/home/npm';
9+
10+
export function compileDocker(config: Config) {
11+
return async function compile(sources: string[]) {
12+
const command = createBuildCommand(config);
13+
const input = JSON.stringify(buildInputJson(sources, config), null, 2);
14+
return JSON.parse(execSync(command, {input}).toString());
15+
};
16+
}
17+
18+
export function createBuildCommand(config: Config) {
19+
const configTag = config['docker-tag'];
20+
const tag = configTag ? `:${configTag}` : ':stable';
21+
const allowedPaths = `"${CONTAINER_PATH},${NPM_PATH}"`;
22+
return `docker run ${getVolumes(config)} -i -a stdin -a stdout ` +
23+
`ethereum/solc${tag} solc --standard-json --allow-paths ${allowedPaths}`;
24+
}
25+
26+
export function getVolumes(config: Config) {
27+
const hostPath = process.cwd();
28+
const hostNpmPath = join(hostPath, config.npmPath);
29+
return `-v ${hostPath}:${CONTAINER_PATH} -v ${hostNpmPath}:${NPM_PATH}`;
30+
}
31+
32+
export function buildInputJson(sources: string[], config: Config) {
33+
return buildInputObject(
34+
buildSources(sources, (input) => join(CONTAINER_PATH, input)),
35+
getMappings(sources, config)
36+
);
37+
}
38+
39+
function getMappings(sources: string[], config: Config) {
40+
const mappingBuilder = new ImportMappingBuilder(
41+
config.sourcesPath,
42+
config.npmPath,
43+
CONTAINER_PATH,
44+
NPM_PATH
45+
);
46+
return mappingBuilder.getMappings(sources);
47+
}

lib/compiler/compileNative.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {join, resolve} from 'path';
2+
import {execSync} from 'child_process';
3+
import {Config} from '../config/config';
4+
import ImportMappingBuilder from './importMappingBuilder';
5+
import {buildInputObject, buildSources} from './buildUitls';
6+
7+
export function compileNative(config: Config) {
8+
return async function compile(sources: string[]) {
9+
const command = createBuildCommand(config);
10+
const input = JSON.stringify(buildInputJson(sources, config), null, 2);
11+
return JSON.parse(execSync(command, {input}).toString());
12+
};
13+
}
14+
15+
export function createBuildCommand(config: Config) {
16+
const command = 'solc';
17+
const params = '--standard-json';
18+
const customAllowedPaths = (config.allowedPaths || []).map((path: string) => resolve(path));
19+
const allowedPaths = [resolve(config.sourcesPath), resolve(config.npmPath), ...customAllowedPaths];
20+
return `${command} ${params} --allow-paths ${allowedPaths.join(',')}`;
21+
}
22+
23+
function buildInputJson(sources: string[], config: Config) {
24+
return buildInputObject(
25+
buildSources(sources, (input) => join(process.cwd(), input)),
26+
getMappings(sources, config)
27+
);
28+
}
29+
30+
function getMappings(sources: string[], config: Config) {
31+
const mappingBuilder = new ImportMappingBuilder(config.sourcesPath, config.npmPath);
32+
return mappingBuilder.getMappings(sources);
33+
}

lib/compiler/compileSolcjs.ts

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import solc from 'solc';
2+
import {promisify} from 'util';
3+
import {readFileContent} from '../utils';
4+
import {Config} from '../config/config';
5+
import {buildInputObject} from './buildUitls';
6+
7+
const loadRemoteVersion = promisify(solc.loadRemoteVersion);
8+
9+
async function loadCompiler(config: Config) {
10+
if (config.solcVersion) {
11+
return loadRemoteVersion(config.solcVersion);
12+
} else {
13+
return solc;
14+
}
15+
}
16+
17+
export function compileSolcjs(config: Config) {
18+
return async function compile(sources: string[], findImports: (file: string) => any) {
19+
const solc = await loadCompiler(config);
20+
const inputs = findInputs(sources);
21+
const input = buildInputObject(convertInputs(inputs));
22+
const output = solc.compile(JSON.stringify(input), findImports);
23+
return JSON.parse(output);
24+
};
25+
}
26+
27+
function convertInputs(inputs: Record<string, any>) {
28+
const converted: Record<string, { content: string }> = {};
29+
Object.keys(inputs).map((key) => converted[key.replace(/\\/g, '/')] = {content: inputs[key]});
30+
return converted;
31+
}
32+
33+
export function findInputs(files: string[]) {
34+
return Object.assign({}, ...files.map((file) => ({
35+
[file]: readFileContent(file)
36+
})));
37+
}

lib/compiler/compiler.ts

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {Config} from '../config/config';
2+
import {isWarningMessage} from '../utils';
3+
import {getCompileFunction} from './getCompileFunction';
4+
import {findInputs} from './findInputs';
5+
import {findImports} from './findImports';
6+
import {loadConfig} from '../config/loadConfig';
7+
import {saveOutput} from './saveOutput';
8+
9+
export async function compileProject(configPath: string) {
10+
await compileAndSave(loadConfig(configPath));
11+
}
12+
13+
export async function compileAndSave(config: Config) {
14+
const output = await compile(config);
15+
await processOutput(output, config);
16+
}
17+
18+
export async function compile(config: Config) {
19+
return getCompileFunction(config)(
20+
findInputs(config.sourcesPath),
21+
findImports(config.npmPath)
22+
);
23+
}
24+
25+
async function processOutput(output: any, config: Config) {
26+
if (output.errors) {
27+
console.error(formatErrors(output.errors));
28+
}
29+
if (anyNonWarningErrors(output.errors)) {
30+
throw new Error('Compilation failed');
31+
} else {
32+
await saveOutput(output, config);
33+
}
34+
}
35+
36+
function anyNonWarningErrors(errors?: any[]) {
37+
return errors && !errors.every(isWarningMessage);
38+
}
39+
40+
function formatErrors(errors: any[]) {
41+
return errors.map(toFormattedMessage).join('\n');
42+
}
43+
44+
function toFormattedMessage(error: any) {
45+
return typeof error === 'string' ? error : error.formattedMessage;
46+
}

lib/compiler/findImports.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import {readFileContent} from '../utils';
4+
5+
export function findImports(libraryPath: string) {
6+
return (file: string) => {
7+
try {
8+
const libFile = path.join(libraryPath, file);
9+
if (fs.existsSync(file)) {
10+
return { contents: readFileContent(file) };
11+
} else if (fs.existsSync(libFile)) {
12+
return { contents: readFileContent(libFile) };
13+
} else {
14+
throw new Error(`File not found: ${file}`);
15+
}
16+
} catch (e) {
17+
return { error: e.message };
18+
}
19+
};
20+
}

lib/compiler/findInputs.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
4+
export function findInputs(sourcePath: string) {
5+
const stack = [sourcePath];
6+
const inputFiles: string[] = [];
7+
while (stack.length > 0) {
8+
const dir = stack.pop();
9+
const files = fs.readdirSync(dir);
10+
for (const file of files) {
11+
const filePath = path.join(dir, file);
12+
if (isDirectory(filePath)) {
13+
stack.push(filePath);
14+
} else if (file.endsWith('.sol')) {
15+
inputFiles.push(filePath);
16+
}
17+
}
18+
}
19+
return inputFiles;
20+
}
21+
22+
const isDirectory = (filePath: string) =>
23+
fs.existsSync(filePath) &&
24+
fs.statSync(filePath).isDirectory();

lib/compiler/getCompileFunction.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Config } from '../config/config';
2+
import {compileSolcjs} from './compileSolcjs';
3+
import {compileNative} from './compileNative';
4+
import {compileDocker} from './compileDocker';
5+
6+
export type CompileFunction = (
7+
sources: string[],
8+
findImports: (file: string) => any
9+
) => any;
10+
11+
export function getCompileFunction(config: Config): CompileFunction {
12+
if (config.compiler === 'native') {
13+
return compileNative(config);
14+
} else if (config.compiler === 'dockerized-solc') {
15+
return compileDocker(config);
16+
} else if (config.compiler === 'solcjs' || !config.compiler) {
17+
return compileSolcjs(config);
18+
}
19+
throw new Error(`Unknown compiler ${config.compiler}`);
20+
}

lib/wrappers/importMappingBuilder.ts renamed to lib/compiler/importMappingBuilder.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,11 @@ class ImportMappingBuilder {
6363
}
6464

6565
public getMappings(sources: string[]) {
66-
return falttenObjectArray(
66+
const mappings = falttenObjectArray(
6767
sources.map((path) =>
6868
this.getMappingForUnit(readFileContent(path), path)
6969
));
70+
return Object.entries(mappings).map(([key, value]) => `${key}=${value}`);
7071
}
7172
}
7273

0 commit comments

Comments
 (0)