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 kwargs.get("auto_compile", False) and 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
16 changes: 16 additions & 0 deletions crytic_compile/cryticparser/cryticparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ def init(parser: ArgumentParser) -> None:
default=DEFAULTS_FLAG_IN_CONFIG["skip_clean"],
)

group_compile.add_argument(
"--auto-compile",
help="Try to get the solc options automatically when compiling a single file",
action="store_true",
dest="auto_compile",
default=DEFAULTS_FLAG_IN_CONFIG["auto_compile"],
)

_init_solc(parser)
_init_truffle(parser)
_init_embark(parser)
Expand Down Expand Up @@ -520,3 +528,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"],
)
2 changes: 2 additions & 0 deletions crytic_compile/cryticparser/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"hardhat_artifacts_directory": None,
"foundry_ignore_compile": False,
"foundry_out_directory": "out",
"foundry_compile_all": False,
"export_dir": "crytic-export",
"compile_libraries": None,
"auto_compile": False,
}
37 changes: 36 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,19 @@ 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?

@abc.abstractmethod
def config(working_dir: str) -> Optional[PlatformConfig]:
"""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
16 changes: 14 additions & 2 deletions crytic_compile/platform/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
import json
import os
from pathlib import Path
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Any
from typing import TYPE_CHECKING, Dict, List, Tuple, Type, Any, Optional

from crytic_compile.platform import Type as TypePlatform
from crytic_compile.platform import standard

# Cycle dependency
from crytic_compile.platform.abstract_platform import AbstractPlatform
from crytic_compile.platform.abstract_platform import AbstractPlatform, PlatformConfig

if TYPE_CHECKING:
from crytic_compile import CryticCompile
Expand Down Expand Up @@ -118,6 +118,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
return False
return Path(target).parts[-1].endswith("_export_archive.json")

@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. Always false

Expand Down
14 changes: 13 additions & 1 deletion crytic_compile/platform/brownie.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,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 Filename, convert_filename
Expand Down Expand Up @@ -114,6 +114,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
or os.path.isfile(os.path.join(target, "brownie-config.yml"))
)

@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 (not supported for brownie)

Expand Down
16 changes: 14 additions & 2 deletions crytic_compile/platform/buidler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
import shutil
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, List, Tuple
from typing import TYPE_CHECKING, List, Tuple, Optional

from crytic_compile.compiler.compiler import CompilerVersion
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
from crytic_compile.platform.abstract_platform import AbstractPlatform, PlatformConfig

# Handle cycle
from .solc import relative_to_short
Expand Down Expand Up @@ -193,6 +193,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
is_typescript = os.path.isfile(os.path.join(target, "buidler.config.ts"))
return is_javascript or is_typescript

@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
14 changes: 13 additions & 1 deletion crytic_compile/platform/dapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,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.types import Type
from crytic_compile.utils.naming import convert_filename, extract_name
from crytic_compile.utils.subprocess import run
Expand Down Expand Up @@ -157,6 +157,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
return "dapp " in txt
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 (not supported for brownie)

Expand Down
16 changes: 14 additions & 2 deletions crytic_compile/platform/embark.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import shutil
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING, List, Optional

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, extract_filename, extract_name
Expand Down Expand Up @@ -200,6 +200,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
return False
return os.path.isfile(os.path.join(target, "embark.json"))

@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
14 changes: 13 additions & 1 deletion crytic_compile/platform/etherlime.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 @@ -192,6 +192,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
14 changes: 13 additions & 1 deletion crytic_compile/platform/etherscan.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 import solc_standard_json
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 Filename
Expand Down Expand Up @@ -430,6 +430,18 @@ def is_supported(target: str, **kwargs: str) -> bool:
target = target[target.find(":") + 1 :]
return bool(re.match(r"^\s*0x[a-zA-Z0-9]{40}\s*$", target))

@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
Loading