33import sys
44import subprocess
55import json
6+ import re
67from argparse import ArgumentParser
78from dataclasses import dataclass
9+ from enum import Enum
810from glob import glob
911from pathlib import Path
12+ from tempfile import TemporaryDirectory
1013from 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 )
1427class 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 \n Interrupted 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 \n Interrupted 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 \n Interrupted 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 \n Interrupted by an exception while processing file "
225+ f"'{ source_file_name } ' with optimize={ optimize } \n " ,
226+ file = sys .stderr
227+ )
228+ raise
133229
134230
135231def 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 )
0 commit comments