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
@@ -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 \n Interrupted 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 \n Interrupted 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 \n Interrupted 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 \n Interrupted by an exception while processing file "
221+ f"'{ source_file_name } ' with optimize={ optimize } \n " ,
222+ file = sys .stderr
223+ )
224+ raise
129225
130226
131227def 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 )
0 commit comments