crytic_compile.platform.brownie
Brownie platform. https://github.com/iamdefinitelyahuman/brownie
1""" 2Brownie platform. https://github.com/iamdefinitelyahuman/brownie 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 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 Filename, convert_filename 19 20# Cycle dependency 21from crytic_compile.utils.natspec import Natspec 22 23if TYPE_CHECKING: 24 from crytic_compile import CryticCompile 25 26LOGGER = logging.getLogger("CryticCompile") 27 28 29class Brownie(AbstractPlatform): 30 """ 31 Brownie class 32 """ 33 34 NAME = "Brownie" 35 PROJECT_URL = "https://github.com/iamdefinitelyahuman/brownie" 36 TYPE = Type.BROWNIE 37 38 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 39 """Run the compilation 40 41 Args: 42 crytic_compile (CryticCompile): Associated CryticCompile object 43 **kwargs: optional arguments. Used "brownie_ignore_compile", "ignore_compile" 44 45 Raises: 46 InvalidCompilation: If brownie failed to run 47 """ 48 base_build_directory = _get_build_dir_from_config(self._target) or "build" 49 build_directory = Path(base_build_directory, "contracts") 50 brownie_ignore_compile = kwargs.get("brownie_ignore_compile", False) or kwargs.get( 51 "ignore_compile", False 52 ) 53 54 base_cmd = ["brownie"] 55 56 if not brownie_ignore_compile: 57 cmd = base_cmd + ["compile"] 58 LOGGER.info( 59 "'%s' running", 60 " ".join(cmd), 61 ) 62 try: 63 with subprocess.Popen( 64 cmd, 65 stdout=subprocess.PIPE, 66 stderr=subprocess.PIPE, 67 cwd=self._target, 68 executable=shutil.which(cmd[0]), 69 ) as process: 70 stdout_bytes, stderr_bytes = process.communicate() 71 stdout, stderr = ( 72 stdout_bytes.decode(errors="backslashreplace"), 73 stderr_bytes.decode(errors="backslashreplace"), 74 ) # convert bytestrings to unicode strings 75 76 LOGGER.info(stdout) 77 if stderr: 78 LOGGER.error(stderr) 79 80 except OSError as error: 81 # pylint: disable=raise-missing-from 82 raise InvalidCompilation(error) 83 84 if not os.path.isdir(os.path.join(self._target, build_directory)): 85 raise InvalidCompilation("`brownie compile` failed. Can you run it?") 86 87 filenames = list(Path(self._target, build_directory).rglob("*.json")) 88 89 _iterate_over_files(crytic_compile, Path(self._target), filenames) 90 91 def clean(self, **_kwargs: str) -> None: 92 # brownie does not offer a way to clean a project 93 pass 94 95 @staticmethod 96 def is_supported(target: str, **kwargs: str) -> bool: 97 """Check if the target is a brownie project 98 99 Args: 100 target (str): path to the target 101 **kwargs: optional arguments. Used "brownie_ignore" 102 103 Returns: 104 bool: True if the target is a brownie project 105 """ 106 brownie_ignore = kwargs.get("brownie_ignore", False) 107 if brownie_ignore: 108 return False 109 # < 1.1.0: brownie-config.json 110 # >= 1.1.0: brownie-config.yaml 111 return ( 112 os.path.isfile(os.path.join(target, "brownie-config.json")) 113 or os.path.isfile(os.path.join(target, "brownie-config.yaml")) 114 or os.path.isfile(os.path.join(target, "brownie-config.yml")) 115 ) 116 117 def is_dependency(self, _path: str) -> bool: 118 """Check if the path is a dependency (not supported for brownie) 119 120 Args: 121 _path (str): path to the target 122 123 Returns: 124 bool: True if the target is a dependency 125 """ 126 return False 127 128 def _guessed_tests(self) -> List[str]: 129 """Guess the potential unit tests commands 130 131 Returns: 132 List[str]: The guessed unit tests commands 133 """ 134 return ["brownie test"] 135 136 137# pylint: disable=too-many-locals 138def _iterate_over_files( 139 crytic_compile: "CryticCompile", target: Path, filenames: List[Path] 140) -> None: 141 """Iterates over the files and populates the information into the CryticCompile object 142 143 Args: 144 crytic_compile (CryticCompile): associated cryticCompile object 145 target (Path): path to the target 146 filenames (List[Path]): List of files to iterate over 147 """ 148 optimized = None 149 compiler = "solc" 150 version = None 151 152 compilation_unit = CompilationUnit(crytic_compile, str(target)) 153 154 for original_filename in filenames: 155 with open(original_filename, encoding="utf8") as f_file: 156 target_loaded: Dict = json.load(f_file) 157 158 if "ast" not in target_loaded: 159 continue 160 161 if optimized is None: 162 # Old brownie 163 if compiler in target_loaded: 164 compiler_d: Dict = target_loaded["compiler"] 165 optimized = compiler_d.get("optimize", False) 166 version = _get_version(compiler_d) 167 if "compiler" in target_loaded: 168 compiler_d = target_loaded["compiler"] 169 optimized = compiler_d.get("optimize", False) 170 version = _get_version(compiler_d) 171 172 # Filter out vyper files 173 if "absolutePath" not in target_loaded["ast"]: 174 continue 175 176 filename_txt = target_loaded["ast"]["absolutePath"] 177 filename: Filename = convert_filename( 178 filename_txt, _relative_to_short, crytic_compile, working_dir=target 179 ) 180 181 source_unit = compilation_unit.create_source_unit(filename) 182 183 source_unit.ast = target_loaded["ast"] 184 contract_name = target_loaded["contractName"] 185 186 compilation_unit.filename_to_contracts[filename].add(contract_name) 187 188 source_unit.add_contract_name(contract_name) 189 source_unit.abis[contract_name] = target_loaded["abi"] 190 source_unit.bytecodes_init[contract_name] = target_loaded["bytecode"].replace("0x", "") 191 source_unit.bytecodes_runtime[contract_name] = target_loaded[ 192 "deployedBytecode" 193 ].replace("0x", "") 194 source_unit.srcmaps_init[contract_name] = target_loaded["sourceMap"].split(";") 195 source_unit.srcmaps_runtime[contract_name] = target_loaded["deployedSourceMap"].split( 196 ";" 197 ) 198 199 userdoc = target_loaded.get("userdoc", {}) 200 devdoc = target_loaded.get("devdoc", {}) 201 natspec = Natspec(userdoc, devdoc) 202 source_unit.natspec[contract_name] = natspec 203 204 compilation_unit.compiler_version = CompilerVersion( 205 compiler=compiler, version=version, optimized=optimized 206 ) 207 208 209def _get_build_dir_from_config(target: str) -> Optional[str]: 210 config = Path(target, "brownie-config.yml") 211 if not config.exists(): 212 config = Path(target, "brownie-config.yaml") 213 if not config.exists(): 214 return None 215 216 with open(config, "r", encoding="utf8") as config_f: 217 config_buffer = config_f.readlines() 218 # config is a yaml file 219 # use regex because we don't have a yaml parser 220 for line in config_buffer: 221 match = re.search(r"build: (.*)$", line) 222 if match: 223 return match.groups()[0] 224 return None 225 226 227def _get_version(compiler: Dict) -> str: 228 """Parse the compiler version 229 230 Args: 231 compiler (Dict): dictionary from the json 232 233 Returns: 234 str: Compiler version 235 """ 236 version = compiler.get("version", "") 237 if "Version:" in version: 238 version = version.split("Version:")[1].strip() 239 version = version[0 : version.find("+")] # TODO handle not "+" not found 240 return version 241 242 243def _relative_to_short(relative: Path) -> Path: 244 """Translate relative path to short (do nothing for brownie) 245 246 Args: 247 relative (Path): path to the target 248 249 Returns: 250 Path: Translated path 251 """ 252 return relative
LOGGER =
<Logger CryticCompile (WARNING)>
30class Brownie(AbstractPlatform): 31 """ 32 Brownie class 33 """ 34 35 NAME = "Brownie" 36 PROJECT_URL = "https://github.com/iamdefinitelyahuman/brownie" 37 TYPE = Type.BROWNIE 38 39 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 40 """Run the compilation 41 42 Args: 43 crytic_compile (CryticCompile): Associated CryticCompile object 44 **kwargs: optional arguments. Used "brownie_ignore_compile", "ignore_compile" 45 46 Raises: 47 InvalidCompilation: If brownie failed to run 48 """ 49 base_build_directory = _get_build_dir_from_config(self._target) or "build" 50 build_directory = Path(base_build_directory, "contracts") 51 brownie_ignore_compile = kwargs.get("brownie_ignore_compile", False) or kwargs.get( 52 "ignore_compile", False 53 ) 54 55 base_cmd = ["brownie"] 56 57 if not brownie_ignore_compile: 58 cmd = base_cmd + ["compile"] 59 LOGGER.info( 60 "'%s' running", 61 " ".join(cmd), 62 ) 63 try: 64 with subprocess.Popen( 65 cmd, 66 stdout=subprocess.PIPE, 67 stderr=subprocess.PIPE, 68 cwd=self._target, 69 executable=shutil.which(cmd[0]), 70 ) as process: 71 stdout_bytes, stderr_bytes = process.communicate() 72 stdout, stderr = ( 73 stdout_bytes.decode(errors="backslashreplace"), 74 stderr_bytes.decode(errors="backslashreplace"), 75 ) # convert bytestrings to unicode strings 76 77 LOGGER.info(stdout) 78 if stderr: 79 LOGGER.error(stderr) 80 81 except OSError as error: 82 # pylint: disable=raise-missing-from 83 raise InvalidCompilation(error) 84 85 if not os.path.isdir(os.path.join(self._target, build_directory)): 86 raise InvalidCompilation("`brownie compile` failed. Can you run it?") 87 88 filenames = list(Path(self._target, build_directory).rglob("*.json")) 89 90 _iterate_over_files(crytic_compile, Path(self._target), filenames) 91 92 def clean(self, **_kwargs: str) -> None: 93 # brownie does not offer a way to clean a project 94 pass 95 96 @staticmethod 97 def is_supported(target: str, **kwargs: str) -> bool: 98 """Check if the target is a brownie project 99 100 Args: 101 target (str): path to the target 102 **kwargs: optional arguments. Used "brownie_ignore" 103 104 Returns: 105 bool: True if the target is a brownie project 106 """ 107 brownie_ignore = kwargs.get("brownie_ignore", False) 108 if brownie_ignore: 109 return False 110 # < 1.1.0: brownie-config.json 111 # >= 1.1.0: brownie-config.yaml 112 return ( 113 os.path.isfile(os.path.join(target, "brownie-config.json")) 114 or os.path.isfile(os.path.join(target, "brownie-config.yaml")) 115 or os.path.isfile(os.path.join(target, "brownie-config.yml")) 116 ) 117 118 def is_dependency(self, _path: str) -> bool: 119 """Check if the path is a dependency (not supported for brownie) 120 121 Args: 122 _path (str): path to the target 123 124 Returns: 125 bool: True if the target is a dependency 126 """ 127 return False 128 129 def _guessed_tests(self) -> List[str]: 130 """Guess the potential unit tests commands 131 132 Returns: 133 List[str]: The guessed unit tests commands 134 """ 135 return ["brownie test"]
Brownie class
def
compile( self, crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> None:
39 def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: 40 """Run the compilation 41 42 Args: 43 crytic_compile (CryticCompile): Associated CryticCompile object 44 **kwargs: optional arguments. Used "brownie_ignore_compile", "ignore_compile" 45 46 Raises: 47 InvalidCompilation: If brownie failed to run 48 """ 49 base_build_directory = _get_build_dir_from_config(self._target) or "build" 50 build_directory = Path(base_build_directory, "contracts") 51 brownie_ignore_compile = kwargs.get("brownie_ignore_compile", False) or kwargs.get( 52 "ignore_compile", False 53 ) 54 55 base_cmd = ["brownie"] 56 57 if not brownie_ignore_compile: 58 cmd = base_cmd + ["compile"] 59 LOGGER.info( 60 "'%s' running", 61 " ".join(cmd), 62 ) 63 try: 64 with subprocess.Popen( 65 cmd, 66 stdout=subprocess.PIPE, 67 stderr=subprocess.PIPE, 68 cwd=self._target, 69 executable=shutil.which(cmd[0]), 70 ) as process: 71 stdout_bytes, stderr_bytes = process.communicate() 72 stdout, stderr = ( 73 stdout_bytes.decode(errors="backslashreplace"), 74 stderr_bytes.decode(errors="backslashreplace"), 75 ) # convert bytestrings to unicode strings 76 77 LOGGER.info(stdout) 78 if stderr: 79 LOGGER.error(stderr) 80 81 except OSError as error: 82 # pylint: disable=raise-missing-from 83 raise InvalidCompilation(error) 84 85 if not os.path.isdir(os.path.join(self._target, build_directory)): 86 raise InvalidCompilation("`brownie compile` failed. Can you run it?") 87 88 filenames = list(Path(self._target, build_directory).rglob("*.json")) 89 90 _iterate_over_files(crytic_compile, Path(self._target), filenames)
Run the compilation
Args: crytic_compile (CryticCompile): Associated CryticCompile object **kwargs: optional arguments. Used "brownie_ignore_compile", "ignore_compile"
Raises: InvalidCompilation: If brownie failed to run
def
clean(self, **_kwargs: str) -> None:
92 def clean(self, **_kwargs: str) -> None: 93 # brownie does not offer a way to clean a project 94 pass
Clean compilation artifacts
Args: **kwargs: optional arguments.
@staticmethod
def
is_supported(target: str, **kwargs: str) -> bool:
96 @staticmethod 97 def is_supported(target: str, **kwargs: str) -> bool: 98 """Check if the target is a brownie project 99 100 Args: 101 target (str): path to the target 102 **kwargs: optional arguments. Used "brownie_ignore" 103 104 Returns: 105 bool: True if the target is a brownie project 106 """ 107 brownie_ignore = kwargs.get("brownie_ignore", False) 108 if brownie_ignore: 109 return False 110 # < 1.1.0: brownie-config.json 111 # >= 1.1.0: brownie-config.yaml 112 return ( 113 os.path.isfile(os.path.join(target, "brownie-config.json")) 114 or os.path.isfile(os.path.join(target, "brownie-config.yaml")) 115 or os.path.isfile(os.path.join(target, "brownie-config.yml")) 116 )
Check if the target is a brownie project
Args: target (str): path to the target **kwargs: optional arguments. Used "brownie_ignore"
Returns: bool: True if the target is a brownie project
def
is_dependency(self, _path: str) -> bool:
118 def is_dependency(self, _path: str) -> bool: 119 """Check if the path is a dependency (not supported for brownie) 120 121 Args: 122 _path (str): path to the target 123 124 Returns: 125 bool: True if the target is a dependency 126 """ 127 return False
Check if the path is a dependency (not supported for brownie)
Args: _path (str): path to the target
Returns: bool: True if the target is a dependency