Skip to content

Commit

Permalink
Merge pull request #488 from crytic/dev-auto-compile
Browse files Browse the repository at this point in the history
Improve foundry compilation
  • Loading branch information
montyly authored Oct 5, 2023
2 parents feac1a1 + 7f85442 commit fa60e8d
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 19 deletions.
54 changes: 54 additions & 0 deletions crytic_compile/crytic_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions crytic_compile/cryticparser/cryticparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
)
1 change: 1 addition & 0 deletions crytic_compile/cryticparser/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
36 changes: 35 additions & 1 deletion crytic_compile/platform/abstract_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crytic_compile/platform/buidler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crytic_compile/platform/dapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 79 additions & 8 deletions crytic_compile/platform/foundry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
)

Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions crytic_compile/platform/hardhat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion crytic_compile/platform/waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit fa60e8d

Please sign in to comment.