Skip to content

Commit 22293c6

Browse files
authored
Merge pull request #544 from ethereum/solcjs-include-paths
[solcjs] `--include-path` option
2 parents 1ecce4b + 1584d30 commit 22293c6

File tree

10 files changed

+165
-20
lines changed

10 files changed

+165
-20
lines changed

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@ solcjs --help
2727

2828
To compile a contract that imports other contracts via relative paths:
2929
```bash
30-
solcjs --bin --base-path . ./MainContract.sol
30+
solcjs --bin --include-path node_modules/ --base-path . MainContract.sol
3131
```
32-
The option ``--base-path`` enables automatic loading of imports from the filesystem and
33-
takes a path as argument that contains the source files.
32+
Use the ``--base-path`` and ``--include-path`` options to describe the layout of your project.
33+
``--base-path`` represents the root of your own source tree while ``--include-path`` allows you to
34+
specify extra locations containing external code (e.g. libraries installed with a package manager).
35+
36+
Note: ensure that all the files you specify on the command line are located inside the base path or
37+
one of the include paths.
38+
The compiler refers to files from outside of these directories using absolute paths.
39+
Having absolute paths in contract metadata will result in your bytecode being reproducible only
40+
when it's placed in these exact absolute locations.
3441

3542
Note: this commandline interface is not compatible with `solc` provided by the Solidity compiler package and thus cannot be
3643
used in combination with an Ethereum client via the `eth.compile.solidity()` RPC method. Please refer to the

solcjs

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ program
3737
.option('--bin', 'Binary of the contracts in hex.')
3838
.option('--abi', 'ABI of the contracts.')
3939
.option('--standard-json', 'Turn on Standard JSON Input / Output mode.')
40-
.option('--base-path <path>', 'Automatically resolve all imports inside the given path.')
40+
.option('--base-path <path>', 'Root of the project source tree. ' +
41+
'The import callback will attempt to interpret all import paths as relative to this directory.'
42+
)
43+
.option('--include-path <path...>', 'Extra source directories available to the import callback. ' +
44+
'When using a package manager to install libraries, use this option to specify directories where packages are installed. ' +
45+
'Can be used multiple times to provide multiple locations.'
46+
)
4147
.option('-o, --output-dir <output-directory>', 'Output directory for the contracts.')
4248
.option('-p, --pretty-json', 'Pretty-print all JSON output.', false)
4349
.option('-v, --verbose', 'More detailed console output.', false);
@@ -54,16 +60,21 @@ function abort (msg) {
5460
}
5561

5662
function readFileCallback(sourcePath) {
57-
if (options.basePath)
58-
sourcePath = options.basePath + '/' + sourcePath;
59-
if (fs.existsSync(sourcePath)) {
60-
try {
61-
return { 'contents': fs.readFileSync(sourcePath).toString('utf8') }
62-
} catch (e) {
63-
return { error: 'Error reading ' + sourcePath + ': ' + e };
63+
const prefixes = [options.basePath ? options.basePath : ""].concat(
64+
options.includePath ? options.includePath : []
65+
);
66+
for (const prefix of prefixes) {
67+
const prefixedSourcePath = (prefix ? prefix + '/' : "") + sourcePath;
68+
69+
if (fs.existsSync(prefixedSourcePath)) {
70+
try {
71+
return {'contents': fs.readFileSync(prefixedSourcePath).toString('utf8')}
72+
} catch (e) {
73+
return {error: 'Error reading ' + prefixedSourcePath + ': ' + e};
74+
}
6475
}
65-
} else
66-
return { error: 'File not found at ' + sourcePath}
76+
}
77+
return {error: 'File not found inside the base path or any of the include paths.'}
6778
}
6879

6980
function withUnixPathSeparators(filePath) {
@@ -74,8 +85,13 @@ function withUnixPathSeparators(filePath) {
7485
return filePath.replace(/\\/g, "/");
7586
}
7687

77-
function stripBasePath(sourcePath) {
88+
function makeSourcePathRelativeIfPossible(sourcePath) {
7889
const absoluteBasePath = (options.basePath ? path.resolve(options.basePath) : path.resolve('.'));
90+
const absoluteIncludePaths = (
91+
options.includePath ?
92+
options.includePath.map((prefix) => { return path.resolve(prefix); }) :
93+
[]
94+
);
7995

8096
// Compared to base path stripping logic in solc this is much simpler because path.resolve()
8197
// handles symlinks correctly (does not resolve them except in work dir) and strips .. segments
@@ -84,13 +100,16 @@ function stripBasePath(sourcePath) {
84100
// Windows and UNC paths are not handled in a special way (at least on Linux). Finally, it has
85101
// very little test coverage so there might be more differences that we are just not aware of.
86102
const absoluteSourcePath = path.resolve(sourcePath);
87-
const relativeSourcePath = path.relative(absoluteBasePath, absoluteSourcePath);
88103

89-
if (relativeSourcePath.startsWith('../'))
90-
// Path can't be made relative without stepping outside of base path so return absolute one.
91-
return withUnixPathSeparators(absoluteSourcePath);
104+
for (const absolutePrefix of [absoluteBasePath].concat(absoluteIncludePaths)) {
105+
const relativeSourcePath = path.relative(absolutePrefix, absoluteSourcePath);
106+
107+
if (!relativeSourcePath.startsWith('../'))
108+
return withUnixPathSeparators(relativeSourcePath);
109+
}
92110

93-
return withUnixPathSeparators(relativeSourcePath);
111+
// File is not located inside base path or include paths so use its absolute path.
112+
return withUnixPathSeparators(absoluteSourcePath);
94113
}
95114

96115
function toFormattedJson(input) {
@@ -148,11 +167,22 @@ if (!(options.bin || options.abi)) {
148167
abort('Invalid option selected, must specify either --bin or --abi');
149168
}
150169

170+
if (!options.basePath && options.includePath && options.includePath.length > 0) {
171+
abort('--include-path option requires a non-empty base path.');
172+
}
173+
174+
if (options.includePath)
175+
for (const includePath of options.includePath)
176+
if (!includePath)
177+
abort('Empty values are not allowed in --include-path.');
178+
151179
var sources = {};
152180

153181
for (var i = 0; i < files.length; i++) {
154182
try {
155-
sources[stripBasePath(files[i])] = { content: fs.readFileSync(files[i]).toString() };
183+
sources[makeSourcePathRelativeIfPossible(files[i])] = {
184+
content: fs.readFileSync(files[i]).toString()
185+
};
156186
} catch (e) {
157187
abort('Error reading ' + files[i] + ': ' + e);
158188
}

test/cli.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,48 @@ tape('CLI', function (t) {
131131
spt.end();
132132
});
133133

134+
t.test('include paths', function (st) {
135+
const spt = spawn(
136+
st,
137+
'./solcjs --bin ' +
138+
'test/resources/importCallback/base/contractB.sol ' +
139+
'test/resources/importCallback/includeA/libY.sol ' +
140+
'./test/resources/importCallback/includeA//libY.sol ' +
141+
path.resolve('test/resources/importCallback/includeA/libY.sol') + ' ' +
142+
'--base-path test/resources/importCallback/base ' +
143+
'--include-path test/resources/importCallback/includeA ' +
144+
'--include-path ' + path.resolve('test/resources/importCallback/includeB/')
145+
);
146+
spt.stderr.empty();
147+
spt.succeeds();
148+
spt.end();
149+
});
150+
151+
t.test('include paths without base path', function (st) {
152+
const spt = spawn(
153+
st,
154+
'./solcjs --bin ' +
155+
'test/resources/importCallback/contractC.sol ' +
156+
'--include-path test/resources/importCallback/includeA'
157+
);
158+
spt.stderr.match(/--include-path option requires a non-empty base path\./);
159+
spt.fails();
160+
spt.end();
161+
});
162+
163+
t.test('empty include paths', function (st) {
164+
const spt = spawn(
165+
st,
166+
'./solcjs --bin ' +
167+
'test/resources/importCallback/contractC.sol ' +
168+
'--base-path test/resources/importCallback/base ' +
169+
'--include-path='
170+
);
171+
spt.stderr.match(/Empty values are not allowed in --include-path\./);
172+
spt.fails();
173+
spt.end();
174+
});
175+
134176
t.test('standard json', function (st) {
135177
var input = {
136178
'language': 'Solidity',
@@ -187,4 +229,34 @@ tape('CLI', function (t) {
187229
spt.end();
188230
});
189231
});
232+
233+
t.test('standard json include paths', function (st) {
234+
var input = {
235+
'language': 'Solidity',
236+
'sources': {
237+
'contractB.sol': {
238+
'content':
239+
'// SPDX-License-Identifier: GPL-3.0\n' +
240+
'pragma solidity >=0.0;\n' +
241+
'import "./contractA.sol";\n'
242+
}
243+
}
244+
};
245+
var spt = spawn(
246+
st,
247+
'./solcjs --standard-json ' +
248+
'--base-path test/resources/importCallback/base ' +
249+
'--include-path test/resources/importCallback/includeA ' +
250+
'--include-path ' + path.resolve('test/resources/importCallback/includeB/')
251+
);
252+
spt.stdin.setEncoding('utf-8');
253+
spt.stdin.write(JSON.stringify(input));
254+
spt.stdin.end();
255+
spt.stdin.on('finish', function () {
256+
spt.stderr.empty();
257+
spt.stdout.match(/{"sources":{"contractA.sol":{"id":0},"contractB.sol":{"id":1},"libX.sol":{"id":2},"libY.sol":{"id":3},"libZ.sol":{"id":4},"utils.sol":{"id":5}}}/);
258+
spt.succeeds();
259+
spt.end();
260+
});
261+
});
190262
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
import "libX.sol";
5+
import "libY.sol";
6+
import "libZ.sol";
7+
8+
contract A {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
import "./contractA.sol";
5+
6+
contract B {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
contract C {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
library X {}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
import "./utils.sol";
5+
6+
library Y {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
library Utils {}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// SPDX-License-Identifier: GPL-3.0
2+
pragma solidity >=0.0;
3+
4+
library Z {}

0 commit comments

Comments
 (0)