Skip to content

Commit 4638d9e

Browse files
committed
prepare_report.py: Add support for switching between CLI and Standard JSON compiler interfaces
1 parent aef724c commit 4638d9e

File tree

4 files changed

+315
-55
lines changed

4 files changed

+315
-55
lines changed

scripts/bytecodecompare/prepare_report.py

Lines changed: 150 additions & 46 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
@@ -74,62 +87,145 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str
7487
return file_report
7588

7689

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

90-
command_line = [str(compiler_path), '--standard-json']
91-
compiler_input = json.dumps(json_input)
97+
if len(output_segments) == 1:
98+
return FileReport(file_name=source_file_name, contract_reports=None)
9299

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

113+
return file_report
95114

96-
def run_compiler(compiler_path: Path, source_file_name: Path, optimize: bool) -> FileReport:
97-
(command_line, compiler_input) = prepare_compiler_input(compiler_path, Path(Path(source_file_name).name), optimize)
98115

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

107-
return parse_standard_json_output(Path(source_file_name), process.stdout)
141+
compiler_options = [str(source_file_name), '--bin', '--metadata', '--model-checker-engine', 'none']
142+
if optimize:
143+
compiler_options.append('--optimize')
108144

145+
command_line = [str(compiler_path)] + compiler_options
146+
compiler_input = load_source(source_file_name)
109147

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

134230

135231
def commandline_parser() -> ArgumentParser:
@@ -140,6 +236,13 @@ def commandline_parser() -> ArgumentParser:
140236

141237
parser = ArgumentParser(description=script_description)
142238
parser.add_argument(dest='compiler_path', help="Solidity compiler executable")
239+
parser.add_argument(
240+
'--interface',
241+
dest='interface',
242+
default=CompilerInterface.STANDARD_JSON.value,
243+
choices=[c.value for c in CompilerInterface],
244+
help="Compiler interface to use."
245+
)
143246
return parser;
144247

145248

@@ -148,4 +251,5 @@ def commandline_parser() -> ArgumentParser:
148251
generate_report(
149252
glob("*.sol"),
150253
Path(options.compiler_path),
254+
CompilerInterface(options.interface),
151255
)
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)