diff --git a/.circleci/config.yml b/.circleci/config.yml index a5265445..deeb4e9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -350,7 +350,7 @@ jobs: echo "contract C {}" > C.sol dist/solc.js C.sol --bin - [[ -f C_sol_C.bin ]] + [[ -f C.bin ]] - run: name: "CLI smoke test (package)" command: | @@ -362,7 +362,7 @@ jobs: echo "contract C {}" > C.sol npx solcjs C.sol --bin - [[ -f C_sol_C.bin ]] + [[ -f C.bin ]] solidity-solcjs-ext-test: docker: diff --git a/solc.ts b/solc.ts index 10dc8ed3..e54f9e4a 100755 --- a/solc.ts +++ b/solc.ts @@ -44,6 +44,7 @@ program 'When using a package manager to install libraries, use this option to specify directories where packages are installed. ' + 'Can be used multiple times to provide multiple locations.' ) + .option('--overwrite', 'If artifacts already exist on disk, overwrite them.', false) .option('-o, --output-dir ', 'Output directory for the contracts.') .option('-p, --pretty-json', 'Pretty-print all JSON output.', false) .option('-v, --verbose', 'More detailed console output.', false); @@ -224,26 +225,33 @@ if (!output) { fs.mkdirSync(destination, { recursive: true }); -function writeFile (file, content) { - file = path.join(destination, file); +function writeFile (file, extension, content) { + file = path.join(destination, `${file}.${extension}`); + + if (fs.existsSync(file) && !options.overwrite) { + throw new Error(`Refusing to overwrite existing file ${file} (use --overwrite to force).`); + } + fs.writeFile(file, content, function (err) { if (err) { - console.error('Failed to write ' + file + ': ' + err); + throw new Error(`Failed to write ${file}: ${err}`); } }); } for (const fileName in output.contracts) { for (const contractName in output.contracts[fileName]) { - let contractFileName = fileName + ':' + contractName; - contractFileName = contractFileName.replace(/[:./\\]/g, '_'); - - if (options.bin) { - writeFile(contractFileName + '.bin', output.contracts[fileName][contractName].evm.bytecode.object); - } + try { + if (options.bin) { + writeFile(contractName, 'bin', output.contracts[fileName][contractName].evm.bytecode.object); + } - if (options.abi) { - writeFile(contractFileName + '.abi', toFormattedJson(output.contracts[fileName][contractName].abi)); + if (options.abi) { + writeFile(contractName, 'abi', toFormattedJson(output.contracts[fileName][contractName].abi)); + } + } catch (err) { + console.error(err.message); + hasError = true; } } } diff --git a/test/cli.ts b/test/cli.ts index ab86e3f5..fcbdf81e 100644 --- a/test/cli.ts +++ b/test/cli.ts @@ -1,11 +1,21 @@ import tape from 'tape'; import spawn from 'tape-spawn'; +import rimraf from 'rimraf'; +import tmp from 'tmp'; +import fs from 'fs'; import * as path from 'path'; import solc from '../'; +const dist = path.resolve(__dirname, '..'); +const solcjs = path.join(dist, 'solc.js'); + +function cleanupArtifacts () { + rimraf.sync(`${dist}/*{.bin,.abi}`); +} + tape('CLI', function (t) { t.test('--version', function (st) { - const spt = spawn(st, './solc.js --version'); + const spt = spawn(st, `node ${solcjs} --version`); spt.stdout.match(solc.version() + '\n'); spt.stdout.match(/^\s*[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?\+commit\.[0-9a-f]+([a-zA-Z0-9.-]+)?\s*$/); spt.stderr.empty(); @@ -13,81 +23,87 @@ tape('CLI', function (t) { }); t.test('no parameters', function (st) { - const spt = spawn(st, './solc.js'); + const spt = spawn(st, `node ${solcjs}`); spt.stderr.match(/^Must provide a file/); spt.end(); }); t.test('no mode specified', function (st) { - const spt = spawn(st, './solc.js test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} test/resources/fixtureSmoke.sol`); spt.stderr.match(/^Invalid option selected/); spt.end(); }); t.test('--bin', function (st) { - const spt = spawn(st, './solc.js --bin test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --bin test/resources/fixtureSmoke.sol`); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('--bin --optimize', function (st) { - const spt = spawn(st, './solc.js --bin --optimize test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --bin --optimize test/resources/fixtureSmoke.sol`); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('--bin --optimize-runs 666', function (st) { - const spt = spawn(st, './solc.js --bin --optimize-runs 666 test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --bin --optimize-runs 666 test/resources/fixtureSmoke.sol`); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('--bin --optimize-runs not-a-number', function (st) { - const spt = spawn(st, './solc.js --bin --optimize-runs not-a-number test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --bin --optimize-runs not-a-number test/resources/fixtureSmoke.sol`); spt.stderr.match(/^error: option '--optimize-runs ' argument 'not-a-number' is invalid/); spt.end(); }); t.test('invalid file specified', function (st) { - const spt = spawn(st, './solc.js --bin test/fileNotFound.sol'); + const spt = spawn(st, `node ${solcjs} --bin test/fileNotFound.sol`); spt.stderr.match(/^Error reading /); spt.end(); }); t.test('incorrect source source', function (st) { - const spt = spawn(st, './solc.js --bin test/resources/fixtureIncorrectSource.sol'); + const spt = spawn(st, `node ${solcjs} --bin test/resources/fixtureIncorrectSource.sol`); spt.stderr.match(/SyntaxError: Invalid pragma "contract"/); spt.end(); }); t.test('--abi', function (st) { - const spt = spawn(st, './solc.js --abi test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --abi test/resources/fixtureSmoke.sol`); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('--bin --abi', function (st) { - const spt = spawn(st, './solc.js --bin --abi test/resources/fixtureSmoke.sol'); + const spt = spawn(st, `node ${solcjs} --bin --abi test/resources/fixtureSmoke.sol`); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('no base path', function (st) { const spt = spawn( st, - './solc.js --bin ' + - 'test/resources/importA.sol ' + - './test/resources//importA.sol ' + - path.resolve('test/resources/importA.sol') + `node ${solcjs} --bin \ + test/resources/importA.sol \ + ./test/resources//importA.sol \ + ${path.resolve('test/resources/importA.sol')}` ); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('relative base path', function (st) { @@ -96,65 +112,69 @@ tape('CLI', function (t) { // by the import callback when it appends the base path back. const spt = spawn( st, - './solc.js --bin --base-path test/resources ' + - 'test/resources/importA.sol ' + - './test/resources//importA.sol ' + - path.resolve('test/resources/importA.sol') + `node ${solcjs} --bin --base-path test/resources \ + test/resources/importA.sol \ + ./test/resources//importA.sol \ + ${path.resolve('test/resources/importA.sol')}` ); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('relative non canonical base path', function (st) { const spt = spawn( st, - './solc.js --bin --base-path ./test/resources ' + - 'test/resources/importA.sol ' + - './test/resources//importA.sol ' + - path.resolve('test/resources/importA.sol') + `node ${solcjs} --bin --base-path ./test/resources \ + test/resources/importA.sol \ + ./test/resources//importA.sol \ + ${path.resolve('test/resources/importA.sol')}` ); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('absolute base path', function (st) { const spt = spawn( st, - './solc.js --bin --base-path ' + path.resolve('test/resources') + ' ' + - 'test/resources/importA.sol ' + - './test/resources//importA.sol ' + - path.resolve('test/resources/importA.sol') + `node ${solcjs} --bin --base-path ${path.resolve('test/resources')} \ + test/resources/importA.sol \ + ./test/resources//importA.sol \ + ${path.resolve('test/resources/importA.sol')}` ); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('include paths', function (st) { const spt = spawn( st, - './solc.js --bin ' + - 'test/resources/importCallback/base/contractB.sol ' + - 'test/resources/importCallback/includeA/libY.sol ' + - './test/resources/importCallback/includeA//libY.sol ' + - path.resolve('test/resources/importCallback/includeA/libY.sol') + ' ' + - '--base-path test/resources/importCallback/base ' + - '--include-path test/resources/importCallback/includeA ' + - '--include-path ' + path.resolve('test/resources/importCallback/includeB/') + `node ${solcjs} --bin \ + test/resources/importCallback/base/contractB.sol \ + test/resources/importCallback/includeA/libY.sol \ + ./test/resources/importCallback/includeA//libY.sol \ + ${path.resolve('test/resources/importCallback/includeA/libY.sol')} \ + --base-path test/resources/importCallback/base \ + --include-path test/resources/importCallback/includeA \ + --include-path ${path.resolve('test/resources/importCallback/includeB/')}` ); spt.stderr.empty(); spt.succeeds(); spt.end(); + st.teardown(cleanupArtifacts); }); t.test('include paths without base path', function (st) { const spt = spawn( st, - './solc.js --bin ' + - 'test/resources/importCallback/contractC.sol ' + - '--include-path test/resources/importCallback/includeA' + `node ${solcjs} --bin \ + test/resources/importCallback/contractC.sol \ + --include-path test/resources/importCallback/includeA` ); spt.stderr.match(/--include-path option requires a non-empty base path\./); spt.fails(); @@ -164,10 +184,10 @@ tape('CLI', function (t) { t.test('empty include paths', function (st) { const spt = spawn( st, - './solc.js --bin ' + - 'test/resources/importCallback/contractC.sol ' + - '--base-path test/resources/importCallback/base ' + - '--include-path=' + `node ${solcjs} --bin \ + test/resources/importCallback/contractC.sol \ + --base-path test/resources/importCallback/base \ + --include-path=` ); spt.stderr.match(/Empty values are not allowed in --include-path\./); spt.fails(); @@ -190,7 +210,7 @@ tape('CLI', function (t) { } } }; - const spt = spawn(st, './solc.js --standard-json'); + const spt = spawn(st, `node ${solcjs} --standard-json`); spt.stdin.setEncoding('utf-8'); spt.stdin.write(JSON.stringify(input)); spt.stdin.end(); @@ -219,7 +239,7 @@ tape('CLI', function (t) { } } }; - const spt = spawn(st, './solc.js --standard-json --base-path test/resources'); + const spt = spawn(st, `node ${solcjs} --standard-json --base-path test/resources`); spt.stdin.setEncoding('utf-8'); spt.stdin.write(JSON.stringify(input)); spt.stdin.end(); @@ -245,10 +265,10 @@ tape('CLI', function (t) { }; const spt = spawn( st, - './solc.js --standard-json ' + - '--base-path test/resources/importCallback/base ' + - '--include-path test/resources/importCallback/includeA ' + - '--include-path ' + path.resolve('test/resources/importCallback/includeB/') + `node ${solcjs} --standard-json \ + --base-path test/resources/importCallback/base \ + --include-path test/resources/importCallback/includeA \ + --include-path ${path.resolve('test/resources/importCallback/includeB/')}` ); spt.stdin.setEncoding('utf-8'); spt.stdin.write(JSON.stringify(input)); @@ -260,4 +280,25 @@ tape('CLI', function (t) { spt.end(); }); }); + + t.test('attempt to overwrite without --overwrite flag', function (st) { + const cwd = tmp.dirSync({ unsafeCleanup: true }).name; + // create a fake C.bin to cause name collision + fs.openSync(`${cwd}/C.bin`, 'w'); + + const spt = spawn(st, `node ${solcjs} --bin ${dist}/test/resources/fixtureSmoke.sol`, { cwd }); + spt.stderr.match(/^Refusing to overwrite existing file C\.bin \(use --overwrite to force\)\./); + spt.end(); + }); + + t.test('--overwrite', function (st) { + const cwd = tmp.dirSync({ unsafeCleanup: true }).name; + // create a fake C.bin to cause name collision + fs.openSync(`${cwd}/C.bin`, 'w'); + + const spt = spawn(st, `node ${solcjs} --bin ${dist}/test/resources/fixtureSmoke.sol --overwrite`, { cwd }); + spt.stderr.empty(); + spt.succeeds(); + spt.end(); + }); });