diff --git a/crytic_compile/cryticparser/cryticparser.py b/crytic_compile/cryticparser/cryticparser.py index 8c14884d..3854754b 100755 --- a/crytic_compile/cryticparser/cryticparser.py +++ b/crytic_compile/cryticparser/cryticparser.py @@ -473,3 +473,11 @@ def _init_foundry(parser: ArgumentParser) -> None: dest="foundry_deny", default=DEFAULTS_FLAG_IN_CONFIG["foundry_deny"], ) + + group_foundry.add_argument( + "--foundry-no-force", + help="Enable incremental compilation (skips forge clean and --force flag)", + action="store_true", + dest="foundry_no_force", + default=DEFAULTS_FLAG_IN_CONFIG["foundry_no_force"], + ) diff --git a/crytic_compile/cryticparser/defaults.py b/crytic_compile/cryticparser/defaults.py index f130c493..d79cf80b 100755 --- a/crytic_compile/cryticparser/defaults.py +++ b/crytic_compile/cryticparser/defaults.py @@ -47,6 +47,7 @@ "foundry_build_info_directory": None, "foundry_compile_all": False, "foundry_deny": None, + "foundry_no_force": False, "export_dir": "crytic-export", "compile_libraries": None, } diff --git a/crytic_compile/platform/foundry.py b/crytic_compile/platform/foundry.py index 392264ee..b14209a2 100755 --- a/crytic_compile/platform/foundry.py +++ b/crytic_compile/platform/foundry.py @@ -5,6 +5,7 @@ import json import logging import re +import shutil import subprocess from pathlib import Path from typing import TYPE_CHECKING, TypeVar @@ -68,7 +69,7 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: Args: crytic_compile (CryticCompile): CryticCompile object to populate **kwargs: optional arguments. Used: "foundry_ignore_compile", "foundry_out_directory", - "foundry_build_info_directory", "foundry_deny" + "foundry_build_info_directory", "foundry_deny", "foundry_no_force" """ @@ -105,9 +106,22 @@ def compile(self, crytic_compile: "CryticCompile", **kwargs: str) -> None: ] compile_all = kwargs.get("foundry_compile_all", False) + no_force = kwargs.get("foundry_no_force", False) foundry_config = self.config(self._project_root) + # When no_force is enabled, we must compile all files (including tests) + # to ensure test changes are detected. Otherwise tests would be skipped + # and test modifications wouldn't trigger recompilation. + # We also clean build-info to prevent multiple compilation units from accumulating. + if no_force: + compile_all = True + out_dir = foundry_config.out_path if foundry_config else "out" + build_info_dir = Path(self._project_root, out_dir, "build-info") + if build_info_dir.exists(): + shutil.rmtree(build_info_dir) + LOGGER.info("Cleaned %s for fresh build-info generation", build_info_dir) + if not targeted_build and not compile_all and foundry_config: compilation_command += [ "--skip", @@ -142,14 +156,16 @@ def clean(self, **kwargs: str) -> None: """Clean compilation artifacts Args: - **kwargs: optional arguments. + **kwargs: optional arguments. Used: "foundry_ignore_compile", "ignore_compile", + "foundry_no_force" """ - ignore_compile = kwargs.get("foundry_ignore_compile", False) or kwargs.get( "ignore_compile", False ) + no_force = kwargs.get("foundry_no_force", False) - if ignore_compile: + # Skip cleaning when using incremental compilation mode + if ignore_compile or no_force: return run(["forge", "clean"], cwd=self._project_root)