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