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"]
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
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
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"
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.
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:
Args: file_or_dir (str): path to the target
Returns: Optional[Path]: path to the project root, if found
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
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...
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