crytic_compile.platform.solc
Solc platform
1""" 2Solc platform 3""" 4import json 5import logging 6import os 7import re 8import shutil 9import subprocess 10from pathlib import Path 11from typing import TYPE_CHECKING, Dict, List, Optional, Union, Any 12 13from crytic_compile.compilation_unit import CompilationUnit 14from crytic_compile.compiler.compiler import CompilerVersion 15from crytic_compile.platform.abstract_platform import AbstractPlatform 16from crytic_compile.platform.exceptions import InvalidCompilation 17from crytic_compile.platform.types import Type 18from crytic_compile.utils.naming import ( 19 combine_filename_name, 20 convert_filename, 21 extract_filename, 22 extract_name, 23) 24 25# Cycle dependency 26from crytic_compile.utils.natspec import Natspec 27 28if TYPE_CHECKING: 29 from crytic_compile import CryticCompile 30 31LOGGER = logging.getLogger("CryticCompile") 32 33 34def _build_contract_data(compilation_unit: "CompilationUnit") -> Dict: 35 contracts = {} 36 37 libraries_to_update = compilation_unit.crytic_compile.libraries 38 39 for filename, source_unit in compilation_unit.source_units.items(): 40 for contract_name in source_unit.contracts_names: 41 libraries = source_unit.libraries_names_and_patterns(contract_name) 42 abi = str(source_unit.abi(contract_name)) 43 abi = abi.replace("'", '"') 44 abi = abi.replace("True", "true") 45 abi = abi.replace("False", "false") 46 abi = abi.replace(" ", "") 47 exported_name = combine_filename_name(filename.absolute, contract_name) 48 contracts[exported_name] = { 49 "srcmap": ";".join(source_unit.srcmap_init(contract_name)), 50 "srcmap-runtime": ";".join(source_unit.srcmap_runtime(contract_name)), 51 "abi": abi, 52 "bin": source_unit.bytecode_init(contract_name, libraries_to_update), 53 "bin-runtime": source_unit.bytecode_runtime(contract_name, libraries_to_update), 54 "userdoc": source_unit.natspec[contract_name].userdoc.export(), 55 "devdoc": source_unit.natspec[contract_name].devdoc.export(), 56 "libraries": dict(libraries) if libraries else {}, 57 } 58 return contracts 59 60 61def export_to_solc_from_compilation_unit( 62 compilation_unit: "CompilationUnit", key: str, export_dir: str 63) -> Optional[str]: 64 """Export the compilation unit to the standard solc output format. 65 The exported file will be $key.json 66 67 Args: 68 compilation_unit (CompilationUnit): Compilation unit to export 69 key (str): Filename Id 70 export_dir (str): Export directory 71 72 Returns: 73 Optional[str]: path to the file generated 74 """ 75 contracts = _build_contract_data(compilation_unit) 76 77 # Create additional informational objects. 78 sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()} 79 source_list = [x.absolute for x in compilation_unit.filenames] 80 81 # Create our root object to contain the contracts and other information. 82 output = {"sources": sources, "sourceList": source_list, "contracts": contracts} 83 84 # If we have an export directory specified, we output the JSON. 85 if export_dir: 86 if not os.path.exists(export_dir): 87 os.makedirs(export_dir) 88 path = os.path.join(export_dir, f"{key}.json") 89 90 with open(path, "w", encoding="utf8") as file_desc: 91 json.dump(output, file_desc) 92 return path 93 return None 94 95 96def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]: 97 """Export all the compilation units to the standard solc output format. 98 The files generated will be either 99 - combined_solc.json, if there is one compilation unit (echidna legacy) 100 - $key.json, where $key is the compilation unit identifiant 101 102 Args: 103 crytic_compile (CryticCompile): CryticCompile object to export 104 **kwargs: optional arguments. Used: "export_dir" 105 106 Returns: 107 List[str]: List of filenames generated 108 """ 109 # Obtain objects to represent each contract 110 export_dir = kwargs.get("export_dir", "crytic-export") 111 112 if len(crytic_compile.compilation_units) == 1: 113 compilation_unit = list(crytic_compile.compilation_units.values())[0] 114 path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir) 115 if path: 116 return [path] 117 return [] 118 119 paths = [] 120 for key, compilation_unit in crytic_compile.compilation_units.items(): 121 path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir) 122 if path: 123 paths.append(path) 124 return paths 125 126 127class Solc(AbstractPlatform): 128 """ 129 Solc platform 130 """ 131 132 NAME = "solc" 133 PROJECT_URL = "https://github.com/ethereum/solidity" 134 TYPE = Type.SOLC 135 136 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 137 """Run the compilation 138 139 Args: 140 crytic_compile (CryticCompile): Associated CryticCompile object 141 **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json" 142 143 Raises: 144 InvalidCompilation: If solc failed to run 145 """ 146 147 solc_working_dir = kwargs.get("solc_working_dir", None) 148 force_legacy_json = kwargs.get("solc_force_legacy_json", False) 149 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 150 151 targets_json = _get_targets_json(compilation_unit, self._target, **kwargs) 152 153 # there have been a couple of changes in solc starting from 0.8.x, 154 if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8): 155 raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards") 156 157 skip_filename = compilation_unit.compiler_version.version in [ 158 f"0.4.{x}" for x in range(0, 10) 159 ] 160 161 if "sources" in targets_json: 162 for path, info in targets_json["sources"].items(): 163 if skip_filename: 164 path = convert_filename( 165 self._target, 166 relative_to_short, 167 crytic_compile, 168 working_dir=solc_working_dir, 169 ) 170 else: 171 path = convert_filename( 172 path, relative_to_short, crytic_compile, working_dir=solc_working_dir 173 ) 174 source_unit = compilation_unit.create_source_unit(path) 175 source_unit.ast = info["AST"] 176 177 solc_handle_contracts( 178 targets_json, skip_filename, compilation_unit, self._target, solc_working_dir 179 ) 180 181 def clean(self, **_kwargs: str) -> None: 182 """Clean compilation artifacts 183 184 Args: 185 **_kwargs: unused. 186 """ 187 return 188 189 @staticmethod 190 def is_supported(target: str, **kwargs: str) -> bool: 191 """Check if the target is a Solidity file 192 193 Args: 194 target (str): path to the target 195 **kwargs: optional arguments. Not used 196 197 Returns: 198 bool: True if the target is a Solidity file 199 """ 200 return os.path.isfile(target) and target.endswith(".sol") 201 202 def is_dependency(self, _path: str) -> bool: 203 """Check if the path is a dependency (always false for direct solc) 204 205 Args: 206 _path (str): path to the target 207 208 Returns: 209 bool: True if the target is a dependency 210 """ 211 return False 212 213 def _guessed_tests(self) -> List[str]: 214 """Guess the potential unit tests commands (always empty for direct solc) 215 216 Returns: 217 List[str]: The guessed unit tests commands 218 """ 219 return [] 220 221 222def _get_targets_json(compilation_unit: "CompilationUnit", target: str, **kwargs: Any) -> Dict: 223 """Run the compilation, population the compilation info, and returns the json compilation artifacts 224 225 Args: 226 compilation_unit (CompilationUnit): Compilation unit 227 target (str): path to the solidity file 228 **kwargs: optional arguments. Used: "solc", "solc_disable_warnings", "solc_args", "solc_remaps", 229 "solc_solcs_bin", "solc_solcs_select", "solc_working_dir", "solc_force_legacy_json" 230 231 Returns: 232 Dict: Json of the compilation artifacts 233 """ 234 solc: str = kwargs.get("solc", "solc") 235 solc_disable_warnings: bool = kwargs.get("solc_disable_warnings", False) 236 solc_arguments: str = kwargs.get("solc_args", "") 237 solc_remaps: Optional[Union[str, List[str]]] = kwargs.get("solc_remaps", None) 238 # From config file, solcs is a dict (version -> path) 239 # From command line, solc is a list 240 # The guessing of version only works from config file 241 # This is to prevent too complex command line 242 solcs_path_: Optional[Union[str, Dict, List[str]]] = kwargs.get("solc_solcs_bin") 243 solcs_path: Optional[Union[Dict, List[str]]] = None 244 if solcs_path_: 245 if isinstance(solcs_path_, str): 246 solcs_path = solcs_path_.split(",") 247 else: 248 solcs_path = solcs_path_ 249 # solcs_env is always a list. It matches solc-select list 250 solcs_env = kwargs.get("solc_solcs_select") 251 solc_working_dir = kwargs.get("solc_working_dir", None) 252 force_legacy_json = kwargs.get("solc_force_legacy_json", False) 253 254 if solcs_path: 255 return _run_solcs_path( 256 compilation_unit, 257 target, 258 solcs_path, 259 solc_disable_warnings, 260 solc_arguments, 261 solc_remaps=solc_remaps, 262 working_dir=solc_working_dir, 263 force_legacy_json=force_legacy_json, 264 ) 265 266 if solcs_env: 267 solcs_env_list = solcs_env.split(",") 268 return _run_solcs_env( 269 compilation_unit, 270 target, 271 solc, 272 solc_disable_warnings, 273 solc_arguments, 274 solcs_env=solcs_env_list, 275 solc_remaps=solc_remaps, 276 working_dir=solc_working_dir, 277 force_legacy_json=force_legacy_json, 278 ) 279 280 return _run_solc( 281 compilation_unit, 282 target, 283 solc, 284 solc_disable_warnings, 285 solc_arguments, 286 solc_remaps=solc_remaps, 287 working_dir=solc_working_dir, 288 force_legacy_json=force_legacy_json, 289 ) 290 291 292def solc_handle_contracts( 293 targets_json: Dict, 294 skip_filename: bool, 295 compilation_unit: "CompilationUnit", 296 target: str, 297 solc_working_dir: Optional[str], 298) -> None: 299 """Populate the compilation unit from the compilation json artifacts 300 301 Args: 302 targets_json (Dict): Compilation artifacts 303 skip_filename (bool): If true, skip the filename (for solc <0.4.10) 304 compilation_unit (CompilationUnit): Associated compilation unit 305 target (str): Path to the target 306 solc_working_dir (Optional[str]): Working directory for running solc 307 """ 308 is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8) 309 310 if "contracts" in targets_json: 311 312 for original_contract_name, info in targets_json["contracts"].items(): 313 contract_name = extract_name(original_contract_name) 314 # for solc < 0.4.10 we cant retrieve the filename from the ast 315 if skip_filename: 316 filename = convert_filename( 317 target, 318 relative_to_short, 319 compilation_unit.crytic_compile, 320 working_dir=solc_working_dir, 321 ) 322 else: 323 filename = convert_filename( 324 extract_filename(original_contract_name), 325 relative_to_short, 326 compilation_unit.crytic_compile, 327 working_dir=solc_working_dir, 328 ) 329 330 source_unit = compilation_unit.create_source_unit(filename) 331 332 source_unit.add_contract_name(contract_name) 333 compilation_unit.filename_to_contracts[filename].add(contract_name) 334 source_unit.abis[contract_name] = ( 335 json.loads(info["abi"]) if not is_above_0_8 else info["abi"] 336 ) 337 source_unit.bytecodes_init[contract_name] = info["bin"] 338 source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] 339 source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") 340 source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") 341 userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"] 342 devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"] 343 natspec = Natspec(userdoc, devdoc) 344 source_unit.natspec[contract_name] = natspec 345 346 347def _is_at_or_above_minor_version(compilation_unit: "CompilationUnit", version: int) -> bool: 348 """Checks if the solc version is at or above(=newer) a given minor (0.x.0) version 349 350 Args: 351 compilation_unit (CompilationUnit): Associated compilation unit 352 version (int): version to check 353 354 Returns: 355 bool: True if the compilation unit version is above or equal to the provided version 356 """ 357 assert compilation_unit.compiler_version.version 358 return int(compilation_unit.compiler_version.version.split(".")[1]) >= version 359 360 361def get_version(solc: str, env: Optional[Dict[str, str]]) -> str: 362 """Obtains the version of the solc executable specified. 363 364 Args: 365 solc (str): The solc executable name to invoke. 366 env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable. 367 368 Raises: 369 InvalidCompilation: If solc failed to run 370 371 Returns: 372 str: Returns the version of the provided solc executable. 373 """ 374 375 cmd = [solc, "--version"] 376 LOGGER.info( 377 "'%s' running", 378 " ".join(cmd), 379 ) 380 try: 381 with subprocess.Popen( 382 cmd, 383 stdout=subprocess.PIPE, 384 stderr=subprocess.PIPE, 385 env=env, 386 executable=shutil.which(cmd[0]), 387 ) as process: 388 stdout_bytes, stderr_bytes = process.communicate() 389 stdout, stderr = ( 390 stdout_bytes.decode(errors="backslashreplace"), 391 stderr_bytes.decode(errors="backslashreplace"), 392 ) # convert bytestrings to unicode strings 393 version = re.findall(r"\d+\.\d+\.\d+", stdout) 394 if len(version) == 0: 395 raise InvalidCompilation( 396 f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}" 397 ) 398 return version[0] 399 except OSError as error: 400 # pylint: disable=raise-missing-from 401 raise InvalidCompilation(error) 402 403 404def is_optimized(solc_arguments: Optional[str]) -> bool: 405 """Check if optimization are used 406 407 Args: 408 solc_arguments (Optional[str]): Solc arguments to check 409 410 Returns: 411 bool: True if the optimization are enabled 412 """ 413 if solc_arguments: 414 return "--optimize" in solc_arguments 415 return False 416 417 418def _build_options(compiler_version: CompilerVersion, force_legacy_json: bool) -> str: 419 """ 420 Build the solc command line options 421 422 Args: 423 compiler_version (CompilerVersion): compiler version 424 force_legacy_json (bool): true if the legacy json must be used 425 426 Returns: 427 str: options to be passed to the CI 428 """ 429 old_04_versions = [f"0.4.{x}" for x in range(0, 12)] 430 # compact-format was introduced in 0.4.12 and made the default in solc 0.8.10 431 explicit_compact_format = ( 432 [f"0.4.{x}" for x in range(12, 27)] 433 + [f"0.5.{x}" for x in range(0, 18)] 434 + [f"0.6.{x}" for x in range(0, 13)] 435 + [f"0.7.{x}" for x in range(0, 7)] 436 + [f"0.8.{x}" for x in range(0, 10)] 437 ) 438 assert compiler_version.version 439 if compiler_version.version in old_04_versions or compiler_version.version.startswith("0.3"): 440 return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc" 441 if force_legacy_json: 442 return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes" 443 if compiler_version.version in explicit_compact_format: 444 return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes,compact-format" 445 446 return "abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes" 447 448 449# pylint: disable=too-many-arguments,too-many-locals,too-many-branches,too-many-statements 450def _run_solc( 451 compilation_unit: "CompilationUnit", 452 filename: str, 453 solc: str, 454 solc_disable_warnings: bool, 455 solc_arguments: Optional[str], 456 solc_remaps: Optional[Union[str, List[str]]] = None, 457 env: Optional[Dict] = None, 458 working_dir: Optional[Union[Path, str]] = None, 459 force_legacy_json: bool = False, 460) -> Dict: 461 """Run solc. 462 Ensure that crytic_compile.compiler_version is set prior calling _run_solc 463 464 Args: 465 compilation_unit (CompilationUnit): Associated compilation unit 466 filename (str): Solidity file to compile 467 solc (str): Solc binary 468 solc_disable_warnings (bool): If True, disable solc warnings 469 solc_arguments (Optional[str]): Additional solc cli arguments 470 solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None. 471 env (Optional[Dict]): Environment variable when solc is run. Defaults to None. 472 working_dir (Optional[Union[Path, str]]): Working directory when solc is run. Defaults to None. 473 force_legacy_json (bool): Force to use the legacy json format. Defaults to False. 474 475 Raises: 476 InvalidCompilation: If solc failed to run or file is not a solidity file 477 478 Returns: 479 Dict: Json compilation artifacts 480 """ 481 if not os.path.isfile(filename) and ( 482 not working_dir or not os.path.isfile(os.path.join(str(working_dir), filename)) 483 ): 484 if os.path.isdir(filename): 485 raise InvalidCompilation( 486 f"{filename} is a directory. Expected a Solidity file when not using a compilation framework." 487 ) 488 489 raise InvalidCompilation( 490 f"{filename} does not exist. Are you in the correct working directory?" 491 ) 492 493 if not filename.endswith(".sol"): 494 raise InvalidCompilation(f"{filename} is not the expected format '.sol'") 495 496 compilation_unit.compiler_version = CompilerVersion( 497 compiler="solc", version=get_version(solc, env), optimized=is_optimized(solc_arguments) 498 ) 499 500 compiler_version = compilation_unit.compiler_version 501 assert compiler_version 502 options = _build_options(compiler_version, force_legacy_json) 503 504 cmd = [solc] 505 if solc_remaps: 506 if isinstance(solc_remaps, str): 507 solc_remaps = solc_remaps.split(" ") 508 cmd += solc_remaps 509 cmd += [filename, "--combined-json", options] 510 if solc_arguments: 511 # To parse, we first split the string on each '--' 512 solc_args = solc_arguments.split("--") 513 # Split each argument on the first space found 514 # One solc option may have multiple argument sepparated with ' ' 515 # For example: --allow-paths /tmp . 516 # split() removes the delimiter, so we add it again 517 solc_args_ = [("--" + x).split(" ", 1) for x in solc_args if x] 518 # Flat the list of list 519 solc_args = [item.strip() for sublist in solc_args_ for item in sublist if item] 520 cmd += solc_args 521 522 additional_kwargs: Dict = {"cwd": working_dir} if working_dir else {} 523 if not compiler_version.version in [f"0.4.{x}" for x in range(0, 11)]: 524 # Add --allow-paths argument, if it isn't already specified 525 # We allow the CWD as well as the directory that contains the file 526 if "--allow-paths" not in cmd: 527 file_dir_start = os.path.normpath(os.path.dirname(filename)) 528 # Paths in the --allow-paths arg can't contain commas, since this is the delimeter 529 # Try using absolute path; if it contains a comma, try using relative path instead 530 file_dir = os.path.abspath(file_dir_start) 531 if "," in file_dir: 532 try: 533 file_dir = os.path.relpath(file_dir_start) 534 except ValueError: 535 # relpath can fail if, for example, we're on Windows and the directory is on a different drive than CWD 536 pass 537 538 # Even the relative path might have a comma in it, so we want to make sure first 539 if "," not in file_dir: 540 cmd += ["--allow-paths", ".," + file_dir] 541 else: 542 LOGGER.warning( 543 "Solc filepath contains a comma; omitting the --allow-paths argument. This may result in failed imports.\n" 544 ) 545 546 try: 547 LOGGER.info( 548 "'%s' running", 549 " ".join(cmd), 550 ) 551 # pylint: disable=consider-using-with 552 if env: 553 process = subprocess.Popen( 554 cmd, 555 stdout=subprocess.PIPE, 556 stderr=subprocess.PIPE, 557 executable=shutil.which(cmd[0]), 558 env=env, 559 **additional_kwargs, 560 ) 561 else: 562 process = subprocess.Popen( 563 cmd, 564 stdout=subprocess.PIPE, 565 stderr=subprocess.PIPE, 566 executable=shutil.which(cmd[0]), 567 **additional_kwargs, 568 ) 569 except OSError as error: 570 # pylint: disable=raise-missing-from 571 raise InvalidCompilation(error) 572 stdout_, stderr_ = process.communicate() 573 stdout, stderr = ( 574 stdout_.decode(encoding="utf-8", errors="ignore"), 575 stderr_.decode(encoding="utf-8", errors="ignore"), 576 ) # convert bytestrings to unicode strings 577 578 if stderr and (not solc_disable_warnings): 579 LOGGER.info("Compilation warnings/errors on %s:\n%s", filename, stderr) 580 581 try: 582 ret: Dict = json.loads(stdout) 583 return ret 584 except json.decoder.JSONDecodeError: 585 # pylint: disable=raise-missing-from 586 raise InvalidCompilation(f"Invalid solc compilation {stderr}") 587 588 589# pylint: disable=too-many-arguments 590def _run_solcs_path( 591 compilation_unit: "CompilationUnit", 592 filename: str, 593 solcs_path: Optional[Union[Dict, List[str]]], 594 solc_disable_warnings: bool, 595 solc_arguments: str, 596 solc_remaps: Optional[Union[str, List[str]]] = None, 597 env: Optional[Dict] = None, 598 working_dir: Optional[str] = None, 599 force_legacy_json: bool = False, 600) -> Dict: 601 """[summary] 602 603 Args: 604 compilation_unit (CompilationUnit): Associated compilation unit 605 filename (str): Solidity file to compile 606 solcs_path (Optional[Union[Dict, List[str]]]): List of solc binaries to try. If its a dict, in the form "version:path". 607 solc_disable_warnings (bool): If True, disable solc warnings 608 solc_arguments (str): Additional solc cli arguments 609 solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None. 610 env (Optional[Dict]): Environment variable when solc is run. Defaults to None. 611 working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None. 612 force_legacy_json (bool): Force to use the legacy json format. Defaults to False. 613 614 Raises: 615 InvalidCompilation: [description] 616 617 Returns: 618 Dict: Json compilation artifacts 619 """ 620 targets_json = None 621 if isinstance(solcs_path, dict): 622 guessed_solcs = _guess_solc(filename, working_dir) 623 compilation_errors = [] 624 for guessed_solc in guessed_solcs: 625 if not guessed_solc in solcs_path: 626 continue 627 try: 628 targets_json = _run_solc( 629 compilation_unit, 630 filename, 631 solcs_path[guessed_solc], 632 solc_disable_warnings, 633 solc_arguments, 634 solc_remaps=solc_remaps, 635 env=env, 636 working_dir=working_dir, 637 force_legacy_json=force_legacy_json, 638 ) 639 break 640 except InvalidCompilation: 641 pass 642 643 if not targets_json: 644 if isinstance(solcs_path, dict): 645 solc_bins: List[str] = list(solcs_path.values()) 646 elif solcs_path: 647 solc_bins = solcs_path 648 else: 649 solc_bins = [] 650 651 for solc_bin in solc_bins: 652 try: 653 targets_json = _run_solc( 654 compilation_unit, 655 filename, 656 solc_bin, 657 solc_disable_warnings, 658 solc_arguments, 659 solc_remaps=solc_remaps, 660 env=env, 661 working_dir=working_dir, 662 force_legacy_json=force_legacy_json, 663 ) 664 break 665 except InvalidCompilation as ic: 666 compilation_errors.append(solc_bin + ": " + ic.args[0]) 667 668 if not targets_json: 669 raise InvalidCompilation( 670 "Invalid solc compilation, none of the solc versions provided worked:\n" 671 + "\n".join(compilation_errors) 672 ) 673 674 return targets_json 675 676 677# pylint: disable=too-many-arguments 678def _run_solcs_env( 679 compilation_unit: "CompilationUnit", 680 filename: str, 681 solc: str, 682 solc_disable_warnings: bool, 683 solc_arguments: str, 684 solc_remaps: Optional[Union[List[str], str]] = None, 685 env: Optional[Dict] = None, 686 working_dir: Optional[str] = None, 687 solcs_env: Optional[List[str]] = None, 688 force_legacy_json: bool = False, 689) -> Dict: 690 """Run different solc based on environment variable 691 This is mostly a legacy function for old solc-select usages 692 693 Args: 694 compilation_unit (CompilationUnit): Associated compilation unit 695 filename (str): Solidity file to compile 696 solc (str): Solc binary 697 solc_disable_warnings (bool): If True, disable solc warnings 698 solc_arguments (str): Additional solc cli arguments 699 solc_remaps (Optional[Union[str, List[str]]], optional): Solc remaps. Can be a string where remap are separated with space, or list of str, or a list of. Defaults to None. 700 env (Optional[Dict], optional): Environment variable when solc is run. Defaults to None. 701 working_dir (Optional[Union[Path, str]], optional): Working directory when solc is run. Defaults to None. 702 solcs_env (Optional[List[str]]): List of solc env variable to try. Defaults to None. 703 force_legacy_json (bool): Force to use the legacy json format. Defaults to False. 704 705 Raises: 706 InvalidCompilation: If solc failed 707 708 Returns: 709 Dict: Json compilation artifacts 710 """ 711 env = dict(os.environ) if env is None else env 712 targets_json = None 713 guessed_solcs = _guess_solc(filename, working_dir) 714 compilation_errors = [] 715 for guessed_solc in guessed_solcs: 716 if solcs_env and not guessed_solc in solcs_env: 717 continue 718 try: 719 env["SOLC_VERSION"] = guessed_solc 720 targets_json = _run_solc( 721 compilation_unit, 722 filename, 723 solc, 724 solc_disable_warnings, 725 solc_arguments, 726 solc_remaps=solc_remaps, 727 env=env, 728 working_dir=working_dir, 729 force_legacy_json=force_legacy_json, 730 ) 731 break 732 except InvalidCompilation: 733 pass 734 735 if not targets_json: 736 solc_versions_env = solcs_env if solcs_env else [] 737 738 for version_env in solc_versions_env: 739 try: 740 env["SOLC_VERSION"] = version_env 741 targets_json = _run_solc( 742 compilation_unit, 743 filename, 744 solc, 745 solc_disable_warnings, 746 solc_arguments, 747 solc_remaps=solc_remaps, 748 env=env, 749 working_dir=working_dir, 750 force_legacy_json=force_legacy_json, 751 ) 752 break 753 except InvalidCompilation as ic: 754 compilation_errors.append(version_env + ": " + ic.args[0]) 755 756 if not targets_json: 757 raise InvalidCompilation( 758 "Invalid solc compilation, none of the solc versions provided worked:\n" 759 + "\n".join(compilation_errors) 760 ) 761 762 return targets_json 763 764 765PATTERN = re.compile(r"pragma solidity\s*(?:\^|>=|<=)?\s*(\d+\.\d+\.\d+)") 766 767 768def _guess_solc(target: str, solc_working_dir: Optional[str]) -> List[str]: 769 """Guess the Solidity version (look for "pragma solidity") 770 771 Args: 772 target (str): Solidity filename 773 solc_working_dir (Optional[str]): Working directory 774 775 Returns: 776 List[str]: List of potential solidity version 777 """ 778 if solc_working_dir: 779 target = os.path.join(solc_working_dir, target) 780 with open(target, encoding="utf8") as file_desc: 781 buf = file_desc.read() 782 return PATTERN.findall(buf) 783 784 785def relative_to_short(relative: Path) -> Path: 786 """Convert relative to short (does nothing for direct solc) 787 788 Args: 789 relative (Path): target 790 791 Returns: 792 Path: Converted path 793 """ 794 return relative
62def export_to_solc_from_compilation_unit( 63 compilation_unit: "CompilationUnit", key: str, export_dir: str 64) -> Optional[str]: 65 """Export the compilation unit to the standard solc output format. 66 The exported file will be $key.json 67 68 Args: 69 compilation_unit (CompilationUnit): Compilation unit to export 70 key (str): Filename Id 71 export_dir (str): Export directory 72 73 Returns: 74 Optional[str]: path to the file generated 75 """ 76 contracts = _build_contract_data(compilation_unit) 77 78 # Create additional informational objects. 79 sources = {filename: {"AST": ast} for (filename, ast) in compilation_unit.asts.items()} 80 source_list = [x.absolute for x in compilation_unit.filenames] 81 82 # Create our root object to contain the contracts and other information. 83 output = {"sources": sources, "sourceList": source_list, "contracts": contracts} 84 85 # If we have an export directory specified, we output the JSON. 86 if export_dir: 87 if not os.path.exists(export_dir): 88 os.makedirs(export_dir) 89 path = os.path.join(export_dir, f"{key}.json") 90 91 with open(path, "w", encoding="utf8") as file_desc: 92 json.dump(output, file_desc) 93 return path 94 return None
Export the compilation unit to the standard solc output format. The exported file will be $key.json
Args: compilation_unit (CompilationUnit): Compilation unit to export key (str): Filename Id export_dir (str): Export directory
Returns: Optional[str]: path to the file generated
97def export_to_solc(crytic_compile: "CryticCompile", **kwargs: str) -> List[str]: 98 """Export all the compilation units to the standard solc output format. 99 The files generated will be either 100 - combined_solc.json, if there is one compilation unit (echidna legacy) 101 - $key.json, where $key is the compilation unit identifiant 102 103 Args: 104 crytic_compile (CryticCompile): CryticCompile object to export 105 **kwargs: optional arguments. Used: "export_dir" 106 107 Returns: 108 List[str]: List of filenames generated 109 """ 110 # Obtain objects to represent each contract 111 export_dir = kwargs.get("export_dir", "crytic-export") 112 113 if len(crytic_compile.compilation_units) == 1: 114 compilation_unit = list(crytic_compile.compilation_units.values())[0] 115 path = export_to_solc_from_compilation_unit(compilation_unit, "combined_solc", export_dir) 116 if path: 117 return [path] 118 return [] 119 120 paths = [] 121 for key, compilation_unit in crytic_compile.compilation_units.items(): 122 path = export_to_solc_from_compilation_unit(compilation_unit, key, export_dir) 123 if path: 124 paths.append(path) 125 return paths
Export all the compilation units to the standard solc output format. The files generated will be either
- combined_solc.json, if there is one compilation unit (echidna legacy)
- $key.json, where $key is the compilation unit identifiant
Args: crytic_compile (CryticCompile): CryticCompile object to export **kwargs: optional arguments. Used: "export_dir"
Returns: List[str]: List of filenames generated
128class Solc(AbstractPlatform): 129 """ 130 Solc platform 131 """ 132 133 NAME = "solc" 134 PROJECT_URL = "https://github.com/ethereum/solidity" 135 TYPE = Type.SOLC 136 137 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 138 """Run the compilation 139 140 Args: 141 crytic_compile (CryticCompile): Associated CryticCompile object 142 **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json" 143 144 Raises: 145 InvalidCompilation: If solc failed to run 146 """ 147 148 solc_working_dir = kwargs.get("solc_working_dir", None) 149 force_legacy_json = kwargs.get("solc_force_legacy_json", False) 150 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 151 152 targets_json = _get_targets_json(compilation_unit, self._target, **kwargs) 153 154 # there have been a couple of changes in solc starting from 0.8.x, 155 if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8): 156 raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards") 157 158 skip_filename = compilation_unit.compiler_version.version in [ 159 f"0.4.{x}" for x in range(0, 10) 160 ] 161 162 if "sources" in targets_json: 163 for path, info in targets_json["sources"].items(): 164 if skip_filename: 165 path = convert_filename( 166 self._target, 167 relative_to_short, 168 crytic_compile, 169 working_dir=solc_working_dir, 170 ) 171 else: 172 path = convert_filename( 173 path, relative_to_short, crytic_compile, working_dir=solc_working_dir 174 ) 175 source_unit = compilation_unit.create_source_unit(path) 176 source_unit.ast = info["AST"] 177 178 solc_handle_contracts( 179 targets_json, skip_filename, compilation_unit, self._target, solc_working_dir 180 ) 181 182 def clean(self, **_kwargs: str) -> None: 183 """Clean compilation artifacts 184 185 Args: 186 **_kwargs: unused. 187 """ 188 return 189 190 @staticmethod 191 def is_supported(target: str, **kwargs: str) -> bool: 192 """Check if the target is a Solidity file 193 194 Args: 195 target (str): path to the target 196 **kwargs: optional arguments. Not used 197 198 Returns: 199 bool: True if the target is a Solidity file 200 """ 201 return os.path.isfile(target) and target.endswith(".sol") 202 203 def is_dependency(self, _path: str) -> bool: 204 """Check if the path is a dependency (always false for direct solc) 205 206 Args: 207 _path (str): path to the target 208 209 Returns: 210 bool: True if the target is a dependency 211 """ 212 return False 213 214 def _guessed_tests(self) -> List[str]: 215 """Guess the potential unit tests commands (always empty for direct solc) 216 217 Returns: 218 List[str]: The guessed unit tests commands 219 """ 220 return []
Solc platform
137 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 138 """Run the compilation 139 140 Args: 141 crytic_compile (CryticCompile): Associated CryticCompile object 142 **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json" 143 144 Raises: 145 InvalidCompilation: If solc failed to run 146 """ 147 148 solc_working_dir = kwargs.get("solc_working_dir", None) 149 force_legacy_json = kwargs.get("solc_force_legacy_json", False) 150 compilation_unit = CompilationUnit(crytic_compile, str(self._target)) 151 152 targets_json = _get_targets_json(compilation_unit, self._target, **kwargs) 153 154 # there have been a couple of changes in solc starting from 0.8.x, 155 if force_legacy_json and _is_at_or_above_minor_version(compilation_unit, 8): 156 raise InvalidCompilation("legacy JSON not supported from 0.8.x onwards") 157 158 skip_filename = compilation_unit.compiler_version.version in [ 159 f"0.4.{x}" for x in range(0, 10) 160 ] 161 162 if "sources" in targets_json: 163 for path, info in targets_json["sources"].items(): 164 if skip_filename: 165 path = convert_filename( 166 self._target, 167 relative_to_short, 168 crytic_compile, 169 working_dir=solc_working_dir, 170 ) 171 else: 172 path = convert_filename( 173 path, relative_to_short, crytic_compile, working_dir=solc_working_dir 174 ) 175 source_unit = compilation_unit.create_source_unit(path) 176 source_unit.ast = info["AST"] 177 178 solc_handle_contracts( 179 targets_json, skip_filename, compilation_unit, self._target, solc_working_dir 180 )
Run the compilation
Args: crytic_compile (CryticCompile): Associated CryticCompile object **kwargs: optional arguments. Used: "solc_working_dir", "solc_force_legacy_json"
Raises: InvalidCompilation: If solc failed to run
182 def clean(self, **_kwargs: str) -> None: 183 """Clean compilation artifacts 184 185 Args: 186 **_kwargs: unused. 187 """ 188 return
Clean compilation artifacts
Args: **_kwargs: unused.
190 @staticmethod 191 def is_supported(target: str, **kwargs: str) -> bool: 192 """Check if the target is a Solidity file 193 194 Args: 195 target (str): path to the target 196 **kwargs: optional arguments. Not used 197 198 Returns: 199 bool: True if the target is a Solidity file 200 """ 201 return os.path.isfile(target) and target.endswith(".sol")
Check if the target is a Solidity file
Args: target (str): path to the target **kwargs: optional arguments. Not used
Returns: bool: True if the target is a Solidity file
203 def is_dependency(self, _path: str) -> bool: 204 """Check if the path is a dependency (always false for direct solc) 205 206 Args: 207 _path (str): path to the target 208 209 Returns: 210 bool: True if the target is a dependency 211 """ 212 return False
Check if the path is a dependency (always false for direct solc)
Args: _path (str): path to the target
Returns: bool: True if the target is a dependency
293def solc_handle_contracts( 294 targets_json: Dict, 295 skip_filename: bool, 296 compilation_unit: "CompilationUnit", 297 target: str, 298 solc_working_dir: Optional[str], 299) -> None: 300 """Populate the compilation unit from the compilation json artifacts 301 302 Args: 303 targets_json (Dict): Compilation artifacts 304 skip_filename (bool): If true, skip the filename (for solc <0.4.10) 305 compilation_unit (CompilationUnit): Associated compilation unit 306 target (str): Path to the target 307 solc_working_dir (Optional[str]): Working directory for running solc 308 """ 309 is_above_0_8 = _is_at_or_above_minor_version(compilation_unit, 8) 310 311 if "contracts" in targets_json: 312 313 for original_contract_name, info in targets_json["contracts"].items(): 314 contract_name = extract_name(original_contract_name) 315 # for solc < 0.4.10 we cant retrieve the filename from the ast 316 if skip_filename: 317 filename = convert_filename( 318 target, 319 relative_to_short, 320 compilation_unit.crytic_compile, 321 working_dir=solc_working_dir, 322 ) 323 else: 324 filename = convert_filename( 325 extract_filename(original_contract_name), 326 relative_to_short, 327 compilation_unit.crytic_compile, 328 working_dir=solc_working_dir, 329 ) 330 331 source_unit = compilation_unit.create_source_unit(filename) 332 333 source_unit.add_contract_name(contract_name) 334 compilation_unit.filename_to_contracts[filename].add(contract_name) 335 source_unit.abis[contract_name] = ( 336 json.loads(info["abi"]) if not is_above_0_8 else info["abi"] 337 ) 338 source_unit.bytecodes_init[contract_name] = info["bin"] 339 source_unit.bytecodes_runtime[contract_name] = info["bin-runtime"] 340 source_unit.srcmaps_init[contract_name] = info["srcmap"].split(";") 341 source_unit.srcmaps_runtime[contract_name] = info["srcmap-runtime"].split(";") 342 userdoc = json.loads(info.get("userdoc", "{}")) if not is_above_0_8 else info["userdoc"] 343 devdoc = json.loads(info.get("devdoc", "{}")) if not is_above_0_8 else info["devdoc"] 344 natspec = Natspec(userdoc, devdoc) 345 source_unit.natspec[contract_name] = natspec
Populate the compilation unit from the compilation json artifacts
Args: targets_json (Dict): Compilation artifacts skip_filename (bool): If true, skip the filename (for solc <0.4.10) compilation_unit (CompilationUnit): Associated compilation unit target (str): Path to the target solc_working_dir (Optional[str]): Working directory for running solc
362def get_version(solc: str, env: Optional[Dict[str, str]]) -> str: 363 """Obtains the version of the solc executable specified. 364 365 Args: 366 solc (str): The solc executable name to invoke. 367 env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable. 368 369 Raises: 370 InvalidCompilation: If solc failed to run 371 372 Returns: 373 str: Returns the version of the provided solc executable. 374 """ 375 376 cmd = [solc, "--version"] 377 LOGGER.info( 378 "'%s' running", 379 " ".join(cmd), 380 ) 381 try: 382 with subprocess.Popen( 383 cmd, 384 stdout=subprocess.PIPE, 385 stderr=subprocess.PIPE, 386 env=env, 387 executable=shutil.which(cmd[0]), 388 ) as process: 389 stdout_bytes, stderr_bytes = process.communicate() 390 stdout, stderr = ( 391 stdout_bytes.decode(errors="backslashreplace"), 392 stderr_bytes.decode(errors="backslashreplace"), 393 ) # convert bytestrings to unicode strings 394 version = re.findall(r"\d+\.\d+\.\d+", stdout) 395 if len(version) == 0: 396 raise InvalidCompilation( 397 f"\nSolidity version not found:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}" 398 ) 399 return version[0] 400 except OSError as error: 401 # pylint: disable=raise-missing-from 402 raise InvalidCompilation(error)
Obtains the version of the solc executable specified.
Args: solc (str): The solc executable name to invoke. env (Optional[Dict[str, str]]): An optional environment key-value store which can be used when invoking the solc executable.
Raises: InvalidCompilation: If solc failed to run
Returns: str: Returns the version of the provided solc executable.
405def is_optimized(solc_arguments: Optional[str]) -> bool: 406 """Check if optimization are used 407 408 Args: 409 solc_arguments (Optional[str]): Solc arguments to check 410 411 Returns: 412 bool: True if the optimization are enabled 413 """ 414 if solc_arguments: 415 return "--optimize" in solc_arguments 416 return False
Check if optimization are used
Args: solc_arguments (Optional[str]): Solc arguments to check
Returns: bool: True if the optimization are enabled
786def relative_to_short(relative: Path) -> Path: 787 """Convert relative to short (does nothing for direct solc) 788 789 Args: 790 relative (Path): target 791 792 Returns: 793 Path: Converted path 794 """ 795 return relative
Convert relative to short (does nothing for direct solc)
Args: relative (Path): target
Returns: Path: Converted path