crytic_compile.platform.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

NAME: str = 'Brownie'
PROJECT_URL: str = 'https://github.com/iamdefinitelyahuman/brownie'
TYPE: crytic_compile.platform.types.Type = <Type.BROWNIE: 9>
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