diff --git a/crytic_compile/crytic_compile.py b/crytic_compile/crytic_compile.py index e437afef..5c238598 100644 --- a/crytic_compile/crytic_compile.py +++ b/crytic_compile/crytic_compile.py @@ -14,6 +14,12 @@ from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, Type, Union +from solc_select.solc_select import ( + install_artifacts, + installed_versions, + current_version, + artifact_path, +) from crytic_compile.compilation_unit import CompilationUnit from crytic_compile.platform import all_platforms, solc_standard_json from crytic_compile.platform.abstract_platform import AbstractPlatform @@ -84,6 +90,7 @@ class CryticCompile: Main class. """ + # pylint: disable=too-many-branches def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str) -> None: """See https://github.com/crytic/crytic-compile/wiki/Configuration Target is usually a file or a project directory. It can be an AbstractPlatform @@ -114,8 +121,55 @@ def __init__(self, target: Union[str, AbstractPlatform], **kwargs: str) -> None: self._working_dir = Path.cwd() + # pylint: disable=too-many-nested-blocks if isinstance(target, str): platform = self._init_platform(target, **kwargs) + # If the platform is Solc it means we are trying to compile a single + # we try to see if we are in a known compilation framework to retrieve + # information like remappings and solc version + if isinstance(platform, Solc): + # Try to get the platform of the current working directory + platform_wd = next( + ( + p(target) + for p in get_platforms() + if p.is_supported(str(self._working_dir), **kwargs) + ), + None, + ) + # If no platform has been found or if it's a Solc we can't do anything + if platform_wd and not isinstance(platform_wd, Solc): + platform_config = platform_wd.config(str(self._working_dir)) + if platform_config: + kwargs["solc_args"] = "" + kwargs["solc_remaps"] = "" + + if platform_config.remappings: + kwargs["solc_remaps"] = platform_config.remappings + if ( + platform_config.solc_version + and platform_config.solc_version != current_version()[0] + ): + solc_version = platform_config.solc_version + if solc_version in installed_versions(): + kwargs["solc"] = str(artifact_path(solc_version).absolute()) + else: + # Respect foundry offline option and don't install a missing solc version + if not platform_config.offline: + install_artifacts([solc_version]) + kwargs["solc"] = str(artifact_path(solc_version).absolute()) + if platform_config.optimizer: + kwargs["solc_args"] += "--optimize" + if platform_config.optimizer_runs: + kwargs[ + "solc_args" + ] += f"--optimize-runs {platform_config.optimizer_runs}" + if platform_config.via_ir: + kwargs["solc_args"] += "--via-ir" + if platform_config.allow_paths: + kwargs["solc_args"] += f"--allow-paths {platform_config.allow_paths}" + if platform_config.evm_version: + kwargs["solc_args"] += f"--evm-version {platform_config.evm_version}" else: platform = target diff --git a/crytic_compile/cryticparser/cryticparser.py b/crytic_compile/cryticparser/cryticparser.py index 4b412a76..d8b3da6a 100755 --- a/crytic_compile/cryticparser/cryticparser.py +++ b/crytic_compile/cryticparser/cryticparser.py @@ -520,3 +520,11 @@ def _init_foundry(parser: ArgumentParser) -> None: dest="foundry_out_directory", default=DEFAULTS_FLAG_IN_CONFIG["foundry_out_directory"], ) + + group_foundry.add_argument( + "--foundry-compile-all", + help="Don't skip compiling test and script", + action="store_true", + dest="foundry_compile_all", + default=DEFAULTS_FLAG_IN_CONFIG["foundry_compile_all"], + ) diff --git a/crytic_compile/cryticparser/defaults.py b/crytic_compile/cryticparser/defaults.py index fda8dc0a..9635f365 100755 --- a/crytic_compile/cryticparser/defaults.py +++ b/crytic_compile/cryticparser/defaults.py @@ -45,6 +45,7 @@ "hardhat_artifacts_directory": None, "foundry_ignore_compile": False, "foundry_out_directory": "out", + "foundry_compile_all": False, "export_dir": "crytic-export", "compile_libraries": None, } diff --git a/crytic_compile/platform/abstract_platform.py b/crytic_compile/platform/abstract_platform.py index c266520a..b369e155 100644 --- a/crytic_compile/platform/abstract_platform.py +++ b/crytic_compile/platform/abstract_platform.py @@ -4,7 +4,8 @@ This gives the skeleton for any platform supported by crytic-compile """ import abc -from typing import TYPE_CHECKING, List, Dict +from typing import TYPE_CHECKING, List, Dict, Optional +from dataclasses import dataclass, field from crytic_compile.platform import Type from crytic_compile.utils.unit_tests import guess_tests @@ -22,6 +23,27 @@ class IncorrectPlatformInitialization(Exception): pass +# pylint: disable=too-many-instance-attributes +@dataclass +class PlatformConfig: + """ + This class represents a generic platform configuration + """ + + offline: bool = False + remappings: Optional[str] = None + solc_version: Optional[str] = None + optimizer: bool = False + optimizer_runs: Optional[int] = None + via_ir: bool = False + allow_paths: Optional[str] = None + evm_version: Optional[str] = None + src_path: str = "src" + tests_path: str = "test" + libs_path: List[str] = field(default_factory=lambda: ["lib"]) + scripts_path: str = "script" + + class AbstractPlatform(metaclass=abc.ABCMeta): """ This is the abstract class for the platform @@ -154,6 +176,18 @@ def is_dependency(self, path: str) -> bool: """ return False + @staticmethod + def config(working_dir: str) -> Optional[PlatformConfig]: # pylint: disable=unused-argument + """Return configuration data that should be passed to solc, such as version, remappings ecc. + + Args: + working_dir (str): path to the target + + Returns: + Optional[PlatformConfig]: Platform configuration data such as optimization, remappings... + """ + return None + # Only _guessed_tests is an abstract method # guessed_tests will call the generic guess_tests and appends to the list # platforms-dependent tests diff --git a/crytic_compile/platform/buidler.py b/crytic_compile/platform/buidler.py index 3f577d9f..9c596cbd 100755 --- a/crytic_compile/platform/buidler.py +++ b/crytic_compile/platform/buidler.py @@ -9,13 +9,13 @@ from pathlib import Path from typing import TYPE_CHECKING, List, Tuple +from crytic_compile.compilation_unit import CompilationUnit from crytic_compile.compiler.compiler import CompilerVersion +from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name from crytic_compile.utils.natspec import Natspec -from crytic_compile.compilation_unit import CompilationUnit -from .abstract_platform import AbstractPlatform # Handle cycle from .solc import relative_to_short diff --git a/crytic_compile/platform/dapp.py b/crytic_compile/platform/dapp.py index 5749569b..3cc38a7a 100755 --- a/crytic_compile/platform/dapp.py +++ b/crytic_compile/platform/dapp.py @@ -19,10 +19,10 @@ from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name -from crytic_compile.utils.subprocess import run # Handle cycle from crytic_compile.utils.natspec import Natspec +from crytic_compile.utils.subprocess import run if TYPE_CHECKING: from crytic_compile import CryticCompile diff --git a/crytic_compile/platform/foundry.py b/crytic_compile/platform/foundry.py index f0d6c7f8..d46494d5 100755 --- a/crytic_compile/platform/foundry.py +++ b/crytic_compile/platform/foundry.py @@ -3,10 +3,12 @@ """ import logging import os +import subprocess from pathlib import Path -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, List, Optional +import toml -from crytic_compile.platform.abstract_platform import AbstractPlatform +from crytic_compile.platform.abstract_platform import AbstractPlatform, PlatformConfig from crytic_compile.platform.types import Type from crytic_compile.platform.hardhat import hardhat_like_parsing from crytic_compile.utils.subprocess import run @@ -24,7 +26,7 @@ class Foundry(AbstractPlatform): """ NAME = "Foundry" - PROJECT_URL = "https://github.com/gakonst/foundry" + PROJECT_URL = "https://github.com/foundry-rs/foundry" TYPE = Type.FOUNDRY # pylint: disable=too-many-locals,too-many-statements,too-many-branches @@ -49,12 +51,26 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ) if not ignore_compile: + compilation_command = [ + "forge", + "build", + "--build-info", + ] + + compile_all = kwargs.get("foundry_compile_all", False) + + if not compile_all: + foundry_config = self.config(str(crytic_compile.working_dir.absolute())) + if foundry_config: + compilation_command += [ + "--skip", + f"*/{foundry_config.tests_path}/**", + f"*/{foundry_config.scripts_path}/**", + "--force", + ] + run( - [ - "forge", - "build", - "--build-info", - ], + compilation_command, cwd=self._target, ) @@ -98,6 +114,61 @@ def is_supported(target: str, **kwargs: str) -> bool: return os.path.isfile(os.path.join(target, "foundry.toml")) + @staticmethod + def config(working_dir: str) -> Optional[PlatformConfig]: + """Return configuration data that should be passed to solc, such as remappings. + + Args: + working_dir (str): path to the working directory + + Returns: + Optional[PlatformConfig]: Platform configuration data such as optimization, remappings... + """ + result = PlatformConfig() + result.remappings = ( + subprocess.run(["forge", "remappings"], stdout=subprocess.PIPE, check=True) + .stdout.decode("utf-8") + .replace("\n", " ") + .strip() + ) + with open("foundry.toml", "r", encoding="utf-8") as f: + foundry_toml = toml.loads(f.read()) + default_profile = foundry_toml["profile"]["default"] + + if "solc_version" in default_profile: + result.solc_version = default_profile["solc_version"] + if "offline" in default_profile: + result.offline = default_profile["offline"] + if "optimizer" in default_profile: + result.optimizer = default_profile["optimizer"] + else: + # Default to true + result.optimizer = True + if "optimizer_runs" in default_profile: + result.optimizer_runs = default_profile["optimizer_runs"] + else: + # Default to 200 + result.optimizer_runs = 200 + if "via_ir" in default_profile: + result.via_ir = default_profile["via_ir"] + if "allow_paths" in default_profile: + result.allow_paths = default_profile["allow_paths"] + if "evm_version" in default_profile: + result.evm_version = default_profile["evm_version"] + else: + # Default to london + result.evm_version = "london" + if "src" in default_profile: + result.src_path = default_profile["src"] + if "test" in default_profile: + result.tests_path = default_profile["test"] + if "libs" in default_profile: + result.libs_path = default_profile["libs"] + if "script" in default_profile: + result.scripts_path = default_profile["script"] + + return result + # pylint: disable=no-self-use def is_dependency(self, path: str) -> bool: """Check if the path is a dependency diff --git a/crytic_compile/platform/hardhat.py b/crytic_compile/platform/hardhat.py index ca3f5f84..35a02122 100755 --- a/crytic_compile/platform/hardhat.py +++ b/crytic_compile/platform/hardhat.py @@ -9,17 +9,17 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union +from crytic_compile.compilation_unit import CompilationUnit from crytic_compile.compiler.compiler import CompilerVersion +from crytic_compile.platform.abstract_platform import AbstractPlatform from crytic_compile.platform.exceptions import InvalidCompilation + +# Handle cycle +from crytic_compile.platform.solc import relative_to_short from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename, extract_name from crytic_compile.utils.natspec import Natspec from crytic_compile.utils.subprocess import run -from crytic_compile.platform.abstract_platform import AbstractPlatform - -# Handle cycle -from crytic_compile.platform.solc import relative_to_short -from crytic_compile.compilation_unit import CompilationUnit if TYPE_CHECKING: from crytic_compile import CryticCompile diff --git a/crytic_compile/platform/waffle.py b/crytic_compile/platform/waffle.py index 6735649a..053bc7c3 100755 --- a/crytic_compile/platform/waffle.py +++ b/crytic_compile/platform/waffle.py @@ -14,7 +14,7 @@ from crytic_compile.compilation_unit import CompilationUnit from crytic_compile.compiler.compiler import CompilerVersion -from crytic_compile.platform.abstract_platform import AbstractPlatform +from crytic_compile.platform.abstract_platform import AbstractPlatform, PlatformConfig from crytic_compile.platform.exceptions import InvalidCompilation from crytic_compile.platform.types import Type from crytic_compile.utils.naming import convert_filename @@ -260,6 +260,18 @@ def is_supported(target: str, **kwargs: str) -> bool: return False + @staticmethod + def config(working_dir: str) -> Optional[PlatformConfig]: + """Return configuration data that should be passed to solc, such as remappings. + + Args: + working_dir (str): path to the working directory + + Returns: + Optional[PlatformConfig]: Platform configuration data such as optimization, remappings... + """ + return None + def is_dependency(self, path: str) -> bool: """Check if the path is a dependency diff --git a/setup.py b/setup.py index e96d7533..fe021686 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ version="0.3.4", packages=find_packages(), python_requires=">=3.8", - install_requires=["pycryptodome>=3.4.6", "cbor2", "solc-select>=v1.0.4"], + install_requires=["pycryptodome>=3.4.6", "cbor2", "solc-select>=v1.0.4", "toml>=0.10.2"], extras_require={ "test": [ "pytest",