mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
prepare_report.py: Add support for switching between CLI and Standard JSON compiler interfaces
This commit is contained in:
@@ -3,13 +3,26 @@
|
||||
import sys
|
||||
import subprocess
|
||||
import json
|
||||
import re
|
||||
from argparse import ArgumentParser
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from glob import glob
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
|
||||
CONTRACT_SEPARATOR_PATTERN = re.compile(r'^======= (?P<file_name>.+):(?P<contract_name>[^:]+) =======$', re.MULTILINE)
|
||||
BYTECODE_REGEX = re.compile(r'^Binary:\n(?P<bytecode>.*)$', re.MULTILINE)
|
||||
METADATA_REGEX = re.compile(r'^Metadata:\n(?P<metadata>\{.*\})$', re.MULTILINE)
|
||||
|
||||
|
||||
class CompilerInterface(Enum):
|
||||
CLI = 'cli'
|
||||
STANDARD_JSON = 'standard-json'
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContractReport:
|
||||
contract_name: str
|
||||
@@ -74,62 +87,145 @@ def parse_standard_json_output(source_file_name: Path, standard_json_output: str
|
||||
return file_report
|
||||
|
||||
|
||||
def prepare_compiler_input(compiler_path: Path, source_file_name: Path, optimize: bool) -> Tuple[List[str], str]:
|
||||
json_input: dict = {
|
||||
'language': 'Solidity',
|
||||
'sources': {
|
||||
str(source_file_name): {'content': load_source(source_file_name)}
|
||||
},
|
||||
'settings': {
|
||||
'optimizer': {'enabled': optimize},
|
||||
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
|
||||
'modelChecker': {'engine': 'none'},
|
||||
}
|
||||
}
|
||||
def parse_cli_output(source_file_name: Path, cli_output: str) -> FileReport:
|
||||
# re.split() returns a list containing the text between pattern occurrences but also inserts the
|
||||
# content of matched groups in between. It also never omits the empty elements so the number of
|
||||
# list items is predictable (3 per match + the text before the first match)
|
||||
output_segments = re.split(CONTRACT_SEPARATOR_PATTERN, cli_output)
|
||||
assert len(output_segments) % 3 == 1
|
||||
|
||||
command_line = [str(compiler_path), '--standard-json']
|
||||
compiler_input = json.dumps(json_input)
|
||||
if len(output_segments) == 1:
|
||||
return FileReport(file_name=source_file_name, contract_reports=None)
|
||||
|
||||
file_report = FileReport(file_name=source_file_name, contract_reports=[])
|
||||
for file_name, contract_name, contract_output in zip(output_segments[1::3], output_segments[2::3], output_segments[3::3]):
|
||||
bytecode_match = re.search(BYTECODE_REGEX, contract_output)
|
||||
metadata_match = re.search(METADATA_REGEX, contract_output)
|
||||
|
||||
assert file_report.contract_reports is not None
|
||||
file_report.contract_reports.append(ContractReport(
|
||||
contract_name=contract_name,
|
||||
file_name=Path(file_name),
|
||||
bytecode=bytecode_match['bytecode'] if bytecode_match is not None else None,
|
||||
metadata=metadata_match['metadata'] if metadata_match is not None else None,
|
||||
))
|
||||
|
||||
return file_report
|
||||
|
||||
|
||||
def prepare_compiler_input(
|
||||
compiler_path: Path,
|
||||
source_file_name: Path,
|
||||
optimize: bool,
|
||||
interface: CompilerInterface
|
||||
) -> Tuple[List[str], str]:
|
||||
|
||||
if interface == CompilerInterface.STANDARD_JSON:
|
||||
json_input: dict = {
|
||||
'language': 'Solidity',
|
||||
'sources': {
|
||||
str(source_file_name): {'content': load_source(source_file_name)}
|
||||
},
|
||||
'settings': {
|
||||
'optimizer': {'enabled': optimize},
|
||||
'outputSelection': {'*': {'*': ['evm.bytecode.object', 'metadata']}},
|
||||
'modelChecker': {'engine': 'none'},
|
||||
}
|
||||
}
|
||||
|
||||
command_line = [str(compiler_path), '--standard-json']
|
||||
compiler_input = json.dumps(json_input)
|
||||
else:
|
||||
assert interface == CompilerInterface.CLI
|
||||
|
||||
compiler_options = [str(source_file_name), '--bin', '--metadata', '--model-checker-engine', 'none']
|
||||
if optimize:
|
||||
compiler_options.append('--optimize')
|
||||
|
||||
command_line = [str(compiler_path)] + compiler_options
|
||||
compiler_input = load_source(source_file_name)
|
||||
|
||||
return (command_line, compiler_input)
|
||||
|
||||
|
||||
def run_compiler(compiler_path: Path, source_file_name: Path, optimize: bool) -> FileReport:
|
||||
(command_line, compiler_input) = prepare_compiler_input(compiler_path, Path(Path(source_file_name).name), optimize)
|
||||
def run_compiler(
|
||||
compiler_path: Path,
|
||||
source_file_name: Path,
|
||||
optimize: bool,
|
||||
interface: CompilerInterface,
|
||||
tmp_dir: Path,
|
||||
) -> FileReport:
|
||||
|
||||
process = subprocess.run(
|
||||
command_line,
|
||||
input=compiler_input,
|
||||
encoding='utf8',
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
if interface == CompilerInterface.STANDARD_JSON:
|
||||
(command_line, compiler_input) = prepare_compiler_input(
|
||||
compiler_path,
|
||||
Path(source_file_name.name),
|
||||
optimize,
|
||||
interface
|
||||
)
|
||||
|
||||
return parse_standard_json_output(Path(source_file_name), process.stdout)
|
||||
process = subprocess.run(
|
||||
command_line,
|
||||
input=compiler_input,
|
||||
encoding='utf8',
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
return parse_standard_json_output(Path(source_file_name), process.stdout)
|
||||
else:
|
||||
assert interface == CompilerInterface.CLI
|
||||
assert tmp_dir is not None
|
||||
|
||||
(command_line, compiler_input) = prepare_compiler_input(
|
||||
compiler_path.absolute(),
|
||||
Path(source_file_name.name),
|
||||
optimize,
|
||||
interface
|
||||
)
|
||||
|
||||
# Create a copy that we can use directly with the CLI interface
|
||||
modified_source_path = tmp_dir / source_file_name.name
|
||||
# NOTE: newline='' disables newline conversion.
|
||||
# We want the file exactly as is because changing even a single byte in the source affects metadata.
|
||||
with open(modified_source_path, 'w', encoding='utf8', newline='') as modified_source_file:
|
||||
modified_source_file.write(compiler_input)
|
||||
|
||||
process = subprocess.run(
|
||||
command_line,
|
||||
cwd=tmp_dir,
|
||||
encoding='utf8',
|
||||
capture_output=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
return parse_cli_output(Path(source_file_name), process.stdout)
|
||||
|
||||
|
||||
def generate_report(source_file_names: List[str], compiler_path: Path):
|
||||
def generate_report(source_file_names: List[str], compiler_path: Path, interface: CompilerInterface):
|
||||
with open('report.txt', mode='w', encoding='utf8', newline='\n') as report_file:
|
||||
for optimize in [False, True]:
|
||||
for source_file_name in sorted(source_file_names):
|
||||
try:
|
||||
report = run_compiler(Path(compiler_path), Path(source_file_name), optimize)
|
||||
report_file.write(report.format_report())
|
||||
except subprocess.CalledProcessError as exception:
|
||||
print(
|
||||
f"\n\nInterrupted by an exception while processing file "
|
||||
f"'{source_file_name}' with optimize={optimize}\n\n"
|
||||
f"COMPILER STDOUT:\n{exception.stdout}\n"
|
||||
f"COMPILER STDERR:\n{exception.stderr}\n",
|
||||
file=sys.stderr
|
||||
)
|
||||
raise
|
||||
except:
|
||||
print(
|
||||
f"\n\nInterrupted by an exception while processing file "
|
||||
f"'{source_file_name}' with optimize={optimize}\n",
|
||||
file=sys.stderr
|
||||
)
|
||||
raise
|
||||
with TemporaryDirectory(prefix='prepare_report-') as tmp_dir:
|
||||
for source_file_name in sorted(source_file_names):
|
||||
try:
|
||||
report = run_compiler(compiler_path, Path(source_file_name), optimize, interface, Path(tmp_dir))
|
||||
report_file.write(report.format_report())
|
||||
except subprocess.CalledProcessError as exception:
|
||||
print(
|
||||
f"\n\nInterrupted by an exception while processing file "
|
||||
f"'{source_file_name}' with optimize={optimize}\n\n"
|
||||
f"COMPILER STDOUT:\n{exception.stdout}\n"
|
||||
f"COMPILER STDERR:\n{exception.stderr}\n",
|
||||
file=sys.stderr
|
||||
)
|
||||
raise
|
||||
except:
|
||||
print(
|
||||
f"\n\nInterrupted by an exception while processing file "
|
||||
f"'{source_file_name}' with optimize={optimize}\n",
|
||||
file=sys.stderr
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def commandline_parser() -> ArgumentParser:
|
||||
@@ -140,6 +236,13 @@ def commandline_parser() -> ArgumentParser:
|
||||
|
||||
parser = ArgumentParser(description=script_description)
|
||||
parser.add_argument(dest='compiler_path', help="Solidity compiler executable")
|
||||
parser.add_argument(
|
||||
'--interface',
|
||||
dest='interface',
|
||||
default=CompilerInterface.STANDARD_JSON.value,
|
||||
choices=[c.value for c in CompilerInterface],
|
||||
help="Compiler interface to use."
|
||||
)
|
||||
return parser;
|
||||
|
||||
|
||||
@@ -148,4 +251,5 @@ if __name__ == "__main__":
|
||||
generate_report(
|
||||
glob("*.sol"),
|
||||
Path(options.compiler_path),
|
||||
CompilerInterface(options.interface),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user