Skip to content

Commit ab339d4

Browse files
committed
prepare_report.py: Add support for switching between CLI and Standard JSON compiler interfaces
1 parent 170e193 commit ab339d4

File tree

4 files changed

+286
-47
lines changed

4 files changed

+286
-47
lines changed

scripts/bytecodecompare/prepare_report.py

Lines changed: 148 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,26 @@
33
import sys
44
import subprocess
55
import json
6+
import re
67
from argparse import ArgumentParser
78
from dataclasses import dataclass
9+
from enum import Enum
810
from glob import glob
911
from pathlib import Path
12+
from tempfile import TemporaryDirectory
1013
from typing import List, Optional, Tuple, Union
1114

1215

16+
CONTRACT_SEPARATOR_PATTERN = re.compile(r'^======= (?P<file_name>.+):(?P<contract_name>[^:]+) =======$', re.MULTILINE)
17+
BYTECODE_REGEX = re.compile(r'^Binary:\n(?P<bytecode>.*)$', re.MULTILINE)
18+
METADATA_REGEX = re.compile(r'^Metadata:\n(?P<metadata>\{.*\})$', re.MULTILINE)
19+
20+
21+
class CompilerInterface(Enum):
22+
CLI = 'cli'
23+
STANDARD_JSON = 'standard-json'
24+
25+
1326
@dataclass(frozen=True)
1427
class ContractReport:
1528
contract_name: str
@@ -72,60 +85,143 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str
7285
return file_report
7386

7487

75-
def prepare_compiler_input(compiler_path: Path, source_file_name: Path, optimize: bool) -> Tuple[List[str], str]:
76-
json_input: dict = {
77-
'language': 'Solidity',
78-
'sources': {
79-
str(source_file_name): {'content': load_source(source_file_name)}
80-
},
81-
'settings': {
82-
'optimizer': {'enabled': optimize},
83-
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
84-
'modelChecker': {'engine': 'none'},
85-
}
86-
}
88+
def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport:
89+
# re.split() returns a list containing the text between pattern occurrences but also inserts the
90+
# content of matched groups in between. It also never omits the empty elements so the number of
91+
# list items is predictable (3 per match + the text before the first match)
92+
output_segments = re.split(CONTRACT_SEPARATOR_PATTERN, cli_output)
93+
assert len(output_segments) % 3 == 1
8794

88-
command_line = [str(compiler_path), '--standard-json']
89-
compiler_input = json.dumps(json_input)
95+
if len(output_segments) == 1:
96+
return FileReport(file_name=source_file_name, contract_reports=None)
9097

91-
return (command_line, compiler_input)
98+
file_report = FileReport(file_name=source_file_name, contract_reports=[])
99+
for file_name, contract_name, contract_output in zip(output_segments[1::3], output_segments[2::3], output_segments[3::3]):
100+
bytecode_match = re.search(BYTECODE_REGEX, contract_output)
101+
metadata_match = re.search(METADATA_REGEX, contract_output)
102+
103+
assert file_report.contract_reports is not None
104+
file_report.contract_reports.append(ContractReport(
105+
contract_name=contract_name,
106+
file_name=Path(file_name),
107+
bytecode=bytecode_match['bytecode'] if bytecode_match is not None else None,
108+
metadata=metadata_match['metadata'] if metadata_match is not None else None,
109+
))
92110

111+
return file_report
93112

94-
def run_compiler(compiler_path: Path, source_file_name: Path, optimize: bool) -> FileReport:
95-
(command_line, compiler_input) = prepare_compiler_input(compiler_path, Path(Path(source_file_name).name), optimize)
96113

97-
process = subprocess.run(
98-
command_line,
99-
input=compiler_input,
100-
encoding='utf8',
101-
capture_output=True,
102-
check=False,
103-
)
114+
def prepare_compiler_input(
115+
compiler_path: Path,
116+
source_file_name: Path,
117+
optimize: bool,
118+
interface: CompilerInterface
119+
) -> Tuple[List[str], str]:
120+
121+
if interface == CompilerInterface.STANDARD_JSON:
122+
json_input: dict = {
123+
'language': 'Solidity',
124+
'sources': {
125+
str(source_file_name): {'content': load_source(source_file_name)}
126+
},
127+
'settings': {
128+
'optimizer': {'enabled': optimize},
129+
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
130+
'modelChecker': {'engine': 'none'},
131+
}
132+
}
133+
134+
command_line = [str(compiler_path), '--standard-json']
135+
compiler_input = json.dumps(json_input)
136+
else:
137+
assert interface == CompilerInterface.CLI
104138

105-
return parse_standard_json_output(Path(source_file_name), process.stdout)
139+
compiler_options = [str(source_file_name), '--bin', '--metadata', '--model-checker-engine', 'none']
140+
if optimize:
141+
compiler_options.append('--optimize')
106142

143+
command_line = [str(compiler_path)] + compiler_options
144+
compiler_input = load_source(source_file_name)
107145

108-
def generate_report(source_file_names: List[str], compiler_path: Path):
146+
return (command_line, compiler_input)
147+
148+
149+
def run_compiler(
150+
compiler_path: Path,
151+
source_file_name: Path,
152+
optimize: bool,
153+
interface: CompilerInterface,
154+
tmp_dir: Path
155+
) -> FileReport:
156+
157+
if interface == CompilerInterface.STANDARD_JSON:
158+
(command_line, compiler_input) = prepare_compiler_input(
159+
compiler_path,
160+
Path(Path(source_file_name).name),
161+
optimize,
162+
interface
163+
)
164+
165+
process = subprocess.run(
166+
command_line,
167+
input=compiler_input,
168+
encoding='utf8',
169+
capture_output=True,
170+
check=False,
171+
)
172+
173+
return parse_standard_json_output(Path(source_file_name), process.stdout)
174+
else:
175+
assert interface == CompilerInterface.CLI
176+
assert tmp_dir is not None
177+
178+
(command_line, compiler_input) = prepare_compiler_input(
179+
compiler_path.absolute(),
180+
Path(source_file_name.name),
181+
optimize,
182+
interface
183+
)
184+
185+
# Create a copy that we can use directly with the CLI interface
186+
modified_source_path = tmp_dir / source_file_name.name
187+
with open(modified_source_path, 'w') as modified_source_file:
188+
modified_source_file.write(compiler_input)
189+
190+
process = subprocess.run(
191+
command_line,
192+
cwd=tmp_dir,
193+
encoding='utf8',
194+
capture_output=True,
195+
check=False,
196+
)
197+
198+
return parse_cli_output(Path(source_file_name), process.stdout)
199+
200+
201+
def generate_report(source_file_names: List[str], compiler_path: Path, interface: CompilerInterface):
109202
with open('report.txt', mode='w', encoding='utf8', newline='\n') as report_file:
110203
for optimize in [False, True]:
111-
for source_file_name in sorted(source_file_names):
112-
try:
113-
report = run_compiler(Path(compiler_path), Path(source_file_name), optimize)
114-
report_file.write(report.format_report())
115-
except subprocess.CalledProcessError as exception:
116-
print(
117-
f"\n\nInterrupted by an exception while processing file '{source_file_name}' with optimize={optimize}\n\n",
118-
f"COMPILER STDOUT:\n{exception.stdout}\n"
119-
f"COMPILER STDERR:\n{exception.stderr}\n",
120-
file=sys.stderr
121-
)
122-
raise
123-
except:
124-
print(
125-
f"\n\nInterrupted by an exception while processing file '{source_file_name}' with optimize={optimize}\n",
126-
file=sys.stderr
127-
)
128-
raise
204+
with TemporaryDirectory(prefix='prepare_report-') as tmp_dir:
205+
for source_file_name in sorted(source_file_names):
206+
try:
207+
report = run_compiler(Path(compiler_path), Path(source_file_name), optimize, interface, Path(tmp_dir))
208+
report_file.write(report.format_report())
209+
except subprocess.CalledProcessError as exception:
210+
print(
211+
f"\n\nInterrupted by an exception while processing file "
212+
f"'{source_file_name}' with optimize={optimize}\n\n",
213+
f"COMPILER STDOUT:\n{exception.stdout}\n"
214+
f"COMPILER STDERR:\n{exception.stderr}\n",
215+
file=sys.stderr
216+
)
217+
raise
218+
except:
219+
print(
220+
f"\n\nInterrupted by an exception while processing file "
221+
f"'{source_file_name}' with optimize={optimize}\n",
222+
file=sys.stderr
223+
)
224+
raise
129225

130226

131227
def commandline_parser() -> ArgumentParser:
@@ -136,6 +232,13 @@ def commandline_parser() -> ArgumentParser:
136232

137233
parser = ArgumentParser(description=script_description)
138234
parser.add_argument(dest='compiler_path', help="Solidity compiler executable")
235+
parser.add_argument(
236+
'--interface',
237+
dest='interface',
238+
default=CompilerInterface.STANDARD_JSON.value,
239+
choices=[c.value for c in CompilerInterface],
240+
help="Compiler interface to use."
241+
)
139242
return parser;
140243

141244

@@ -144,4 +247,5 @@ def commandline_parser() -> ArgumentParser:
144247
generate_report(
145248
glob("*.sol"),
146249
Path(options.compiler_path),
250+
CompilerInterface(options.interface),
147251
)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
2+
--> syntaxTests/scoping/library_inherited2.sol
3+
4+
Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.0;"
5+
--> syntaxTests/scoping/library_inherited2.sol
6+
7+
8+
======= syntaxTests/scoping/library_inherited2.sol:A =======
9+
Binary:
10+
6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea264697066735822122086e727f29d40b264a19bbfcad38d64493dca4bab5dbba8c82ffdaae389d2bba064736f6c63430008000033
11+
Metadata:
12+
{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"A"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}
13+
14+
======= syntaxTests/scoping/library_inherited2.sol:B =======
15+
Binary:
16+
608060405234801561001057600080fd5b506101cc806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630423a13214610030575b600080fd5b61004a6004803603810190610045919061009d565b610060565b60405161005791906100d5565b60405180910390f35b600061006b82610072565b9050919050565b6000602a8261008191906100f0565b9050919050565b6000813590506100978161017f565b92915050565b6000602082840312156100af57600080fd5b60006100bd84828501610088565b91505092915050565b6100cf81610146565b82525050565b60006020820190506100ea60008301846100c6565b92915050565b60006100fb82610146565b915061010683610146565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0382111561013b5761013a610150565b5b828201905092915050565b6000819050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b61018881610146565b811461019357600080fd5b5056fea2646970667358221220104c345633313efe410492448844d96d78452c3044ce126b5e041b7fbeaa790064736f6c63430008000033
17+
Metadata:
18+
{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"uint256","name":"value","type":"uint256"}],"name":"bar","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"B"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}
19+
20+
======= syntaxTests/scoping/library_inherited2.sol:Lib =======
21+
Binary:
22+
60566050600b82828239805160001a6073146043577f4e487b7100000000000000000000000000000000000000000000000000000000600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea26469706673582212207f9515e2263fa71a7984707e2aefd82241fac15c497386ca798b526f14f8ba6664736f6c63430008000033
23+
Metadata:
24+
{"compiler":{"version":"0.8.0+commit.c7dfd78e"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"compilationTarget":{"syntaxTests/scoping/library_inherited2.sol":"Lib"},"evmVersion":"istanbul","libraries":{},"metadata":{"bytecodeHash":"ipfs"},"optimizer":{"enabled":false,"runs":200},"remappings":[]},"sources":{"syntaxTests/scoping/library_inherited2.sol":{"keccak256":"0xd0619f00638fdfea187368965615dbd599fead93dd14b6558725e85ec7011d96","urls":["bzz-raw://ec7af066be66a223f0d25ba3bf9ba6dc103e1a57531a66a38a5ca2b6ce172f55","dweb:/ipfs/QmW1NrqQNhnY1Tkgr3Z9oM8buCGLUJCJVCDTVejJTT5Vet"]}},"version":1}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
2+
--> syntaxTests/pragma/unknown_pragma.sol
3+
4+
Error: Unknown pragma "thisdoesntexist"
5+
--> syntaxTests/pragma/unknown_pragma.sol:1:1:
6+
|
7+
1 | pragma thisdoesntexist;
8+
| ^^^^^^^^^^^^^^^^^^^^^^^
9+
10+
Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.8.0;"
11+
--> syntaxTests/pragma/unknown_pragma.sol

0 commit comments

Comments
 (0)