Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve foundry compilation #488

Merged
merged 15 commits into from
Oct 5, 2023
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we dont make it abstract, to avoid having a code duplication for all platforms, to just return None?

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