crytic_compile.platform.foundry

Foundry platform

  1"""
  2Foundry platform
  3"""
  4import logging
  5import subprocess
  6from pathlib import Path
  7from typing import TYPE_CHECKING, List, Optional, TypeVar, Union
  8
  9import json
 10
 11from crytic_compile.platform.abstract_platform import AbstractPlatform, PlatformConfig
 12from crytic_compile.platform.types import Type
 13from crytic_compile.platform.hardhat import hardhat_like_parsing
 14from crytic_compile.utils.subprocess import run
 15
 16# Handle cycle
 17if TYPE_CHECKING:
 18    from crytic_compile import CryticCompile
 19
 20T = TypeVar("T")
 21
 22LOGGER = logging.getLogger("CryticCompile")
 23
 24
 25class Foundry(AbstractPlatform):
 26    """
 27    Foundry platform
 28    """
 29
 30    NAME = "Foundry"
 31    PROJECT_URL = "https://github.com/foundry-rs/foundry"
 32    TYPE = Type.FOUNDRY
 33
 34    def __init__(self, target: str, **_kwargs: str):
 35        super().__init__(target, **_kwargs)
 36
 37        project_root = Foundry.locate_project_root(target)
 38        # if we are initializing this, it is indeed a foundry project and thus has a root path
 39        assert project_root is not None
 40        self._project_root: Path = project_root
 41
 42    # pylint: disable=too-many-locals,too-many-statements,too-many-branches
 43    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
 44        """Compile
 45
 46        Args:
 47            crytic_compile (CryticCompile): CryticCompile object to populate
 48            **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory"
 49
 50        """
 51
 52        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
 53            "ignore_compile", False
 54        )
 55
 56        foundry_config = None
 57
 58        if ignore_compile:
 59            LOGGER.info(
 60                "--ignore-compile used, if something goes wrong, consider removing the ignore compile flag"
 61            )
 62        else:
 63            compilation_command = [
 64                "forge",
 65                "build",
 66                "--build-info",
 67            ]
 68
 69            targeted_build = not self._project_root.samefile(self._target)
 70            if targeted_build:
 71                compilation_command += [
 72                    str(Path(self._target).resolve().relative_to(self._project_root))
 73                ]
 74
 75            compile_all = kwargs.get("foundry_compile_all", False)
 76
 77            foundry_config = self.config(self._project_root)
 78
 79            if not targeted_build and not compile_all and foundry_config:
 80                compilation_command += [
 81                    "--skip",
 82                    f"./{foundry_config.tests_path}/**",
 83                    f"./{foundry_config.scripts_path}/**",
 84                    "--force",
 85                ]
 86
 87            run(
 88                compilation_command,
 89                cwd=self._project_root,
 90            )
 91
 92        out_directory_detected = foundry_config.out_path if foundry_config else "out"
 93        out_directory_config = kwargs.get("foundry_out_directory", None)
 94        out_directory = out_directory_config if out_directory_config else out_directory_detected
 95
 96        build_directory = Path(
 97            self._project_root,
 98            out_directory,
 99            "build-info",
100        )
101
102        hardhat_like_parsing(
103            crytic_compile, str(self._target), build_directory, str(self._project_root)
104        )
105
106    def clean(self, **kwargs: str) -> None:
107        """Clean compilation artifacts
108
109        Args:
110            **kwargs: optional arguments.
111        """
112
113        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
114            "ignore_compile", False
115        )
116
117        if ignore_compile:
118            return
119
120        run(["forge", "clean"], cwd=self._project_root)
121
122    @staticmethod
123    def locate_project_root(file_or_dir: str) -> Optional[Path]:
124        """Determine the project root (if the target is a Foundry project)
125
126        Foundry projects are detected through the presence of their
127        configuration file. See the following for reference:
128
129        https://github.com/foundry-rs/foundry/blob/6983a938580a1eb25d9dbd61eb8cad8cd137a86d/crates/config/README.md#foundrytoml
130
131        Args:
132            file_or_dir (str): path to the target
133
134        Returns:
135            Optional[Path]: path to the project root, if found
136        """
137
138        target = Path(file_or_dir).resolve()
139
140        # if the target is a directory, see if it has a foundry config
141        if target.is_dir() and (target / "foundry.toml").is_file():
142            return target
143
144        # if the target is a file, it might be a specific contract
145        # within a foundry project. Look in parent directories for a
146        # config file
147        for p in target.parents:
148            if (p / "foundry.toml").is_file():
149                return p
150
151        return None
152
153    @staticmethod
154    def is_supported(target: str, **kwargs: str) -> bool:
155        """Check if the target is a foundry project
156
157        Args:
158            target (str): path to the target
159            **kwargs: optional arguments. Used: "foundry_ignore"
160
161        Returns:
162            bool: True if the target is a foundry project
163        """
164        if kwargs.get("foundry_ignore", False):
165            return False
166
167        return Foundry.locate_project_root(target) is not None
168
169    @staticmethod
170    def config(working_dir: Union[str, Path]) -> Optional[PlatformConfig]:
171        """Return configuration data that should be passed to solc, such as remappings.
172
173        Args:
174            working_dir (str): path to the working_dir
175
176        Returns:
177            Optional[PlatformConfig]: Platform configuration data such as optimization, remappings...
178        """
179        result = PlatformConfig()
180        LOGGER.info("'forge config --json' running")
181        json_config = json.loads(
182            subprocess.run(
183                ["forge", "config", "--json"], cwd=working_dir, stdout=subprocess.PIPE, check=True
184            ).stdout
185        )
186
187        # Solc configurations
188        result.solc_version = json_config.get("solc")
189        result.via_ir = json_config.get("via_ir")
190        result.allow_paths = json_config.get("allow_paths")
191        result.offline = json_config.get("offline")
192        result.evm_version = json_config.get("evm_version")
193        result.optimizer = json_config.get("optimizer")
194        result.optimizer_runs = json_config.get("optimizer_runs")
195        result.remappings = json_config.get("remappings")
196
197        # Foundry project configurations
198        result.src_path = json_config.get("src")
199        result.tests_path = json_config.get("test")
200        result.libs_path = json_config.get("libs")
201        result.scripts_path = json_config.get("script")
202        result.out_path = json_config.get("out")
203
204        return result
205
206    # pylint: disable=no-self-use
207    def is_dependency(self, path: str) -> bool:
208        """Check if the path is a dependency
209
210        Args:
211            path (str): path to the target
212
213        Returns:
214            bool: True if the target is a dependency
215        """
216        if path in self._cached_dependencies:
217            return self._cached_dependencies[path]
218        ret = "lib" in Path(path).parts or "node_modules" in Path(path).parts
219        self._cached_dependencies[path] = ret
220        return ret
221
222    # pylint: disable=no-self-use
223    def _guessed_tests(self) -> List[str]:
224        """Guess the potential unit tests commands
225
226        Returns:
227            List[str]: The guessed unit tests commands
228        """
229        return ["forge test"]
LOGGER = <Logger CryticCompile (WARNING)>
 26class Foundry(AbstractPlatform):
 27    """
 28    Foundry platform
 29    """
 30
 31    NAME = "Foundry"
 32    PROJECT_URL = "https://github.com/foundry-rs/foundry"
 33    TYPE = Type.FOUNDRY
 34
 35    def __init__(self, target: str, **_kwargs: str):
 36        super().__init__(target, **_kwargs)
 37
 38        project_root = Foundry.locate_project_root(target)
 39        # if we are initializing this, it is indeed a foundry project and thus has a root path
 40        assert project_root is not None
 41        self._project_root: Path = project_root
 42
 43    # pylint: disable=too-many-locals,too-many-statements,too-many-branches
 44    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
 45        """Compile
 46
 47        Args:
 48            crytic_compile (CryticCompile): CryticCompile object to populate
 49            **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory"
 50
 51        """
 52
 53        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
 54            "ignore_compile", False
 55        )
 56
 57        foundry_config = None
 58
 59        if ignore_compile:
 60            LOGGER.info(
 61                "--ignore-compile used, if something goes wrong, consider removing the ignore compile flag"
 62            )
 63        else:
 64            compilation_command = [
 65                "forge",
 66                "build",
 67                "--build-info",
 68            ]
 69
 70            targeted_build = not self._project_root.samefile(self._target)
 71            if targeted_build:
 72                compilation_command += [
 73                    str(Path(self._target).resolve().relative_to(self._project_root))
 74                ]
 75
 76            compile_all = kwargs.get("foundry_compile_all", False)
 77
 78            foundry_config = self.config(self._project_root)
 79
 80            if not targeted_build and not compile_all and foundry_config:
 81                compilation_command += [
 82                    "--skip",
 83                    f"./{foundry_config.tests_path}/**",
 84                    f"./{foundry_config.scripts_path}/**",
 85                    "--force",
 86                ]
 87
 88            run(
 89                compilation_command,
 90                cwd=self._project_root,
 91            )
 92
 93        out_directory_detected = foundry_config.out_path if foundry_config else "out"
 94        out_directory_config = kwargs.get("foundry_out_directory", None)
 95        out_directory = out_directory_config if out_directory_config else out_directory_detected
 96
 97        build_directory = Path(
 98            self._project_root,
 99            out_directory,
100            "build-info",
101        )
102
103        hardhat_like_parsing(
104            crytic_compile, str(self._target), build_directory, str(self._project_root)
105        )
106
107    def clean(self, **kwargs: str) -> None:
108        """Clean compilation artifacts
109
110        Args:
111            **kwargs: optional arguments.
112        """
113
114        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
115            "ignore_compile", False
116        )
117
118        if ignore_compile:
119            return
120
121        run(["forge", "clean"], cwd=self._project_root)
122
123    @staticmethod
124    def locate_project_root(file_or_dir: str) -> Optional[Path]:
125        """Determine the project root (if the target is a Foundry project)
126
127        Foundry projects are detected through the presence of their
128        configuration file. See the following for reference:
129
130        https://github.com/foundry-rs/foundry/blob/6983a938580a1eb25d9dbd61eb8cad8cd137a86d/crates/config/README.md#foundrytoml
131
132        Args:
133            file_or_dir (str): path to the target
134
135        Returns:
136            Optional[Path]: path to the project root, if found
137        """
138
139        target = Path(file_or_dir).resolve()
140
141        # if the target is a directory, see if it has a foundry config
142        if target.is_dir() and (target / "foundry.toml").is_file():
143            return target
144
145        # if the target is a file, it might be a specific contract
146        # within a foundry project. Look in parent directories for a
147        # config file
148        for p in target.parents:
149            if (p / "foundry.toml").is_file():
150                return p
151
152        return None
153
154    @staticmethod
155    def is_supported(target: str, **kwargs: str) -> bool:
156        """Check if the target is a foundry project
157
158        Args:
159            target (str): path to the target
160            **kwargs: optional arguments. Used: "foundry_ignore"
161
162        Returns:
163            bool: True if the target is a foundry project
164        """
165        if kwargs.get("foundry_ignore", False):
166            return False
167
168        return Foundry.locate_project_root(target) is not None
169
170    @staticmethod
171    def config(working_dir: Union[str, Path]) -> Optional[PlatformConfig]:
172        """Return configuration data that should be passed to solc, such as remappings.
173
174        Args:
175            working_dir (str): path to the working_dir
176
177        Returns:
178            Optional[PlatformConfig]: Platform configuration data such as optimization, remappings...
179        """
180        result = PlatformConfig()
181        LOGGER.info("'forge config --json' running")
182        json_config = json.loads(
183            subprocess.run(
184                ["forge", "config", "--json"], cwd=working_dir, stdout=subprocess.PIPE, check=True
185            ).stdout
186        )
187
188        # Solc configurations
189        result.solc_version = json_config.get("solc")
190        result.via_ir = json_config.get("via_ir")
191        result.allow_paths = json_config.get("allow_paths")
192        result.offline = json_config.get("offline")
193        result.evm_version = json_config.get("evm_version")
194        result.optimizer = json_config.get("optimizer")
195        result.optimizer_runs = json_config.get("optimizer_runs")
196        result.remappings = json_config.get("remappings")
197
198        # Foundry project configurations
199        result.src_path = json_config.get("src")
200        result.tests_path = json_config.get("test")
201        result.libs_path = json_config.get("libs")
202        result.scripts_path = json_config.get("script")
203        result.out_path = json_config.get("out")
204
205        return result
206
207    # pylint: disable=no-self-use
208    def is_dependency(self, path: str) -> bool:
209        """Check if the path is a dependency
210
211        Args:
212            path (str): path to the target
213
214        Returns:
215            bool: True if the target is a dependency
216        """
217        if path in self._cached_dependencies:
218            return self._cached_dependencies[path]
219        ret = "lib" in Path(path).parts or "node_modules" in Path(path).parts
220        self._cached_dependencies[path] = ret
221        return ret
222
223    # pylint: disable=no-self-use
224    def _guessed_tests(self) -> List[str]:
225        """Guess the potential unit tests commands
226
227        Returns:
228            List[str]: The guessed unit tests commands
229        """
230        return ["forge test"]

Foundry platform

Foundry(target: str, **_kwargs: str)
35    def __init__(self, target: str, **_kwargs: str):
36        super().__init__(target, **_kwargs)
37
38        project_root = Foundry.locate_project_root(target)
39        # if we are initializing this, it is indeed a foundry project and thus has a root path
40        assert project_root is not None
41        self._project_root: Path = project_root

Init the object

Args: target (str): path to the target **_kwargs: optional arguments.

Raises: IncorrectPlatformInitialization: If the Platform was not correctly designed

NAME: str = 'Foundry'
PROJECT_URL: str = 'https://github.com/foundry-rs/foundry'
TYPE: crytic_compile.platform.types.Type = <Type.FOUNDRY: 12>
def compile( self, crytic_compile: crytic_compile.crytic_compile.CryticCompile, **kwargs: str) -> None:
 44    def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None:
 45        """Compile
 46
 47        Args:
 48            crytic_compile (CryticCompile): CryticCompile object to populate
 49            **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory"
 50
 51        """
 52
 53        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
 54            "ignore_compile", False
 55        )
 56
 57        foundry_config = None
 58
 59        if ignore_compile:
 60            LOGGER.info(
 61                "--ignore-compile used, if something goes wrong, consider removing the ignore compile flag"
 62            )
 63        else:
 64            compilation_command = [
 65                "forge",
 66                "build",
 67                "--build-info",
 68            ]
 69
 70            targeted_build = not self._project_root.samefile(self._target)
 71            if targeted_build:
 72                compilation_command += [
 73                    str(Path(self._target).resolve().relative_to(self._project_root))
 74                ]
 75
 76            compile_all = kwargs.get("foundry_compile_all", False)
 77
 78            foundry_config = self.config(self._project_root)
 79
 80            if not targeted_build and not compile_all and foundry_config:
 81                compilation_command += [
 82                    "--skip",
 83                    f"./{foundry_config.tests_path}/**",
 84                    f"./{foundry_config.scripts_path}/**",
 85                    "--force",
 86                ]
 87
 88            run(
 89                compilation_command,
 90                cwd=self._project_root,
 91            )
 92
 93        out_directory_detected = foundry_config.out_path if foundry_config else "out"
 94        out_directory_config = kwargs.get("foundry_out_directory", None)
 95        out_directory = out_directory_config if out_directory_config else out_directory_detected
 96
 97        build_directory = Path(
 98            self._project_root,
 99            out_directory,
100            "build-info",
101        )
102
103        hardhat_like_parsing(
104            crytic_compile, str(self._target), build_directory, str(self._project_root)
105        )

Compile

Args: crytic_compile (CryticCompile): CryticCompile object to populate **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory"

def clean(self, **kwargs: str) -> None:
107    def clean(self, **kwargs: str) -> None:
108        """Clean compilation artifacts
109
110        Args:
111            **kwargs: optional arguments.
112        """
113
114        ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get(
115            "ignore_compile", False
116        )
117
118        if ignore_compile:
119            return
120
121        run(["forge", "clean"], cwd=self._project_root)

Clean compilation artifacts

Args: **kwargs: optional arguments.

@staticmethod
def locate_project_root(file_or_dir: str) -> Union[pathlib.Path, NoneType]:
123    @staticmethod
124    def locate_project_root(file_or_dir: str) -> Optional[Path]:
125        """Determine the project root (if the target is a Foundry project)
126
127        Foundry projects are detected through the presence of their
128        configuration file. See the following for reference:
129
130        https://github.com/foundry-rs/foundry/blob/6983a938580a1eb25d9dbd61eb8cad8cd137a86d/crates/config/README.md#foundrytoml
131
132        Args:
133            file_or_dir (str): path to the target
134
135        Returns:
136            Optional[Path]: path to the project root, if found
137        """
138
139        target = Path(file_or_dir).resolve()
140
141        # if the target is a directory, see if it has a foundry config
142        if target.is_dir() and (target / "foundry.toml").is_file():
143            return target
144
145        # if the target is a file, it might be a specific contract
146        # within a foundry project. Look in parent directories for a
147        # config file
148        for p in target.parents:
149            if (p / "foundry.toml").is_file():
150                return p
151
152        return None

Determine the project root (if the target is a Foundry project)

Foundry projects are detected through the presence of their configuration file. See the following for reference:

https://github.com/foundry-rs/foundry/blob/6983a938580a1eb25d9dbd61eb8cad8cd137a86d/crates/config/README.md#foundrytoml

Args: file_or_dir (str): path to the target

Returns: Optional[Path]: path to the project root, if found

@staticmethod
def is_supported(target: str, **kwargs: str) -> bool:
154    @staticmethod
155    def is_supported(target: str, **kwargs: str) -> bool:
156        """Check if the target is a foundry project
157
158        Args:
159            target (str): path to the target
160            **kwargs: optional arguments. Used: "foundry_ignore"
161
162        Returns:
163            bool: True if the target is a foundry project
164        """
165        if kwargs.get("foundry_ignore", False):
166            return False
167
168        return Foundry.locate_project_root(target) is not None

Check if the target is a foundry project

Args: target (str): path to the target **kwargs: optional arguments. Used: "foundry_ignore"

Returns: bool: True if the target is a foundry project

@staticmethod
def config( working_dir: Union[str, pathlib.Path]) -> Union[crytic_compile.platform.abstract_platform.PlatformConfig, NoneType]:
170    @staticmethod
171    def config(working_dir: Union[str, Path]) -> Optional[PlatformConfig]:
172        """Return configuration data that should be passed to solc, such as remappings.
173
174        Args:
175            working_dir (str): path to the working_dir
176
177        Returns:
178            Optional[PlatformConfig]: Platform configuration data such as optimization, remappings...
179        """
180        result = PlatformConfig()
181        LOGGER.info("'forge config --json' running")
182        json_config = json.loads(
183            subprocess.run(
184                ["forge", "config", "--json"], cwd=working_dir, stdout=subprocess.PIPE, check=True
185            ).stdout
186        )
187
188        # Solc configurations
189        result.solc_version = json_config.get("solc")
190        result.via_ir = json_config.get("via_ir")
191        result.allow_paths = json_config.get("allow_paths")
192        result.offline = json_config.get("offline")
193        result.evm_version = json_config.get("evm_version")
194        result.optimizer = json_config.get("optimizer")
195        result.optimizer_runs = json_config.get("optimizer_runs")
196        result.remappings = json_config.get("remappings")
197
198        # Foundry project configurations
199        result.src_path = json_config.get("src")
200        result.tests_path = json_config.get("test")
201        result.libs_path = json_config.get("libs")
202        result.scripts_path = json_config.get("script")
203        result.out_path = json_config.get("out")
204
205        return result

Return configuration data that should be passed to solc, such as remappings.

Args: working_dir (str): path to the working_dir

Returns: Optional[PlatformConfig]: Platform configuration data such as optimization, remappings...

def is_dependency(self, path: str) -> bool:
208    def is_dependency(self, path: str) -> bool:
209        """Check if the path is a dependency
210
211        Args:
212            path (str): path to the target
213
214        Returns:
215            bool: True if the target is a dependency
216        """
217        if path in self._cached_dependencies:
218            return self._cached_dependencies[path]
219        ret = "lib" in Path(path).parts or "node_modules" in Path(path).parts
220        self._cached_dependencies[path] = ret
221        return ret

Check if the path is a dependency

Args: path (str): path to the target

Returns: bool: True if the target is a dependency