#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) REPO_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd) declare -A ROOT_SCRIPT_SCOPE_ALIASES=( ["DeployAddressMapper.s.sol"]="utils" ["DeployAddressMapperOtherChain.s.sol"]="utils" ["DeployCCIPRouter.s.sol"]="ccip" ["DeployCCIPSender.s.sol"]="ccip" ["DeployCCIPSenderMainnet.s.sol"]="ccip" ["DeployCCIPReceiver.s.sol"]="ccip" ["DeployCCIPReceiverMainnet.s.sol"]="ccip" ["DeployCCIPRelay.s.sol"]="relay" ["DeployCCIPWETH9Bridge.s.sol"]="ccip" ["DeployCCIPWETH10Bridge.s.sol"]="ccip" ["DeployComplianceRegistry.s.sol"]="compliance" ["DeployOMNLStack.s.sol"]="hybx-omnl" ["DeployMirrorCoordinator.s.sol"]="hybx-omnl" ["DeployCompliantUSDC.s.sol"]="tokens" ["DeployCompliantUSDT.s.sol"]="tokens" ["DeployCWAssetReserveVerifier.s.sol"]="bridge/integration" ["DeployCWReserveVerifier.s.sol"]="bridge/integration" ["DeployFeeCollector.s.sol"]="utils" ["DeployGenericStateChannelManager.s.sol"]="channels" ["DeployMainnetTether.s.sol"]="tether" ["DeployMirrorManager.s.sol"]="mirror" ["DeployMulticall.s.sol"]="utils" ["DeployMultiSig.s.sol"]="governance" ["DeployOfficialUSDC138.s.sol"]="tokens" ["DeployOfficialUSDT138.s.sol"]="tokens" ["DeployOracle.s.sol"]="oracle" ["DeployPaymentChannelManager.s.sol"]="channels" ["DeployTokenRegistry.s.sol"]="utils" ["DeployTransactionMirror.s.sol"]="mirror" ["DeployWETH.s.sol"]="tokens" ["DeployWETH10.s.sol"]="tokens" ["DeployWETHWithCCIP.s.sol"]="full" ) usage() { cat <<'EOF' Usage: bash scripts/forge/scope.sh list bash scripts/forge/scope.sh build [scope] [forge build args...] bash scripts/forge/scope.sh test [scope] [forge test args...] bash scripts/forge/scope.sh script [scope] [forge script args...] bash scripts/forge/scope.sh create [scope] [forge create args...] bash scripts/forge/scope.sh orphans [--json] Examples: bash scripts/forge/scope.sh build treasury bash scripts/forge/scope.sh test flash --match-path 'test/flash/*.t.sol' bash scripts/forge/scope.sh script bridge/trustless script/bridge/trustless/DeployTrustlessBridge.s.sol:DeployTrustlessBridge --rpc-url "$RPC_URL_138" bash scripts/forge/scope.sh create tokens contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken --rpc-url "$RPC_URL_138" --private-key "$PRIVATE_KEY" --legacy FORGE_SCOPE=vault bash scripts/forge/scope.sh test --match-path 'test/vault/*.t.sol' Notes: - Root `forge test` / `forge build` (no scope) honor `foundry.toml` `[profile.default] skip` for legacy Uniswap V2 vendor trees (old solc); scoped builds unchanged. - Omit [scope] to use FORGE_SCOPE, otherwise default to 'full'. - 'full' preserves the historical repo-wide Forge behavior. - Any other scope is resolved relative to contracts/, for example: treasury -> contracts/treasury bridge/trustless -> contracts/bridge/trustless - If no explicit scope is given, the runner tries to infer one from script/test/build paths and common root-level deployment script names. EOF } die() { echo "error: $*" >&2 exit 1 } info() { echo "$*" >&2 } resolve_scope() { local raw="${1:-${FORGE_SCOPE:-full}}" raw="${raw#contracts/}" raw="${raw#/}" if [[ -z "$raw" ]]; then raw="full" fi printf '%s\n' "$raw" } scope_label() { printf '%s\n' "${1//\//-}" } scope_exists() { local scope="$1" [[ "$scope" == "full" || -d "$REPO_ROOT/contracts/$scope" ]] } infer_scope_from_script_alias() { local raw="${1%%:*}" local base base=$(basename "$raw") if [[ -n "${ROOT_SCRIPT_SCOPE_ALIASES[$base]:-}" ]]; then printf '%s\n' "${ROOT_SCRIPT_SCOPE_ALIASES[$base]}" return 0 fi return 1 } infer_scope_from_script_imports() { local raw="${1%%:*}" local script_path="$REPO_ROOT/${raw#./}" [[ -f "$script_path" ]] || return 1 python3 - "$REPO_ROOT" "$script_path" <<'PY' from pathlib import Path import re import sys repo_root = Path(sys.argv[1]).resolve() script_path = Path(sys.argv[2]).resolve() text = script_path.read_text(errors="ignore") imports = re.findall(r'^\s*import\s+(?:\{[^}]+\}\s+from\s+)?["\']([^"\']+)["\'];', text, re.M) for imp in imports: if not imp.startswith(("./", "../")): continue candidate = (script_path.parent / imp).resolve() try: rel = candidate.relative_to(repo_root).as_posix() except ValueError: continue if not rel.startswith("contracts/"): continue scope = rel[len("contracts/"):] if candidate.is_file(): scope = str(Path(scope).parent).replace("\\", "/") scope = scope.strip(".") while scope: if (repo_root / "contracts" / scope).is_dir(): print(scope) raise SystemExit(0) scope = scope.rsplit("/", 1)[0] if "/" in scope else "" raise SystemExit(1) PY } extract_scope_from_path() { local raw="${1%%:*}" raw="${raw#./}" case "$raw" in contracts/*) raw="${raw#contracts/}" ;; test/*) raw="${raw#test/}" ;; script/*) raw="${raw#script/}" ;; *) return 1 ;; esac local candidate for candidate in "$raw" "${raw#deploy/}"; do [[ -n "$candidate" ]] || continue if [[ -d "$REPO_ROOT/contracts/$candidate" ]]; then printf '%s\n' "$candidate" return 0 fi local probe="${candidate%/*}" if [[ "$probe" != "$candidate" ]]; then while [[ -n "$probe" ]]; do if [[ -d "$REPO_ROOT/contracts/$probe" ]]; then printf '%s\n' "$probe" return 0 fi if [[ "$probe" != *"/"* ]]; then break fi probe="${probe%/*}" done fi done if [[ "$1" == script/* ]]; then infer_scope_from_script_alias "$1" && return 0 infer_scope_from_script_imports "$1" && return 0 fi return 1 } infer_scope_from_args() { local arg inferred for arg in "$@"; do inferred=$(extract_scope_from_path "$arg" || true) if [[ -n "$inferred" ]]; then printf '%s\n' "$inferred" return 0 fi done return 1 } list_scopes() { ( cd "$REPO_ROOT" echo "full" find contracts -mindepth 1 -maxdepth 2 -type d | sed 's#^contracts/##' | sort ) } prepare_scope_env() { local scope="$1" local command="$2" if [[ "$scope" == "full" ]]; then return 0 fi local src_dir="contracts/$scope" [[ -d "$REPO_ROOT/$src_dir" ]] || die "unknown scope '$scope' (expected directory '$src_dir')" local label label=$(scope_label "$scope") export FOUNDRY_SRC="$src_dir" export FOUNDRY_OUT="out/scopes/$label" export FOUNDRY_CACHE_PATH="cache/scopes/$label" export FOUNDRY_SPARSE_MODE="${FOUNDRY_SPARSE_MODE:-true}" if [[ "$command" == "test" ]]; then local test_dir="test/$scope" if [[ -d "$REPO_ROOT/$test_dir" ]]; then export FOUNDRY_TEST="$test_dir" fi fi if [[ "$command" == "script" ]]; then local script_dir="script/$scope" if [[ -d "$REPO_ROOT/$script_dir" ]]; then export FOUNDRY_SCRIPT="$script_dir" fi fi } main() { cd "$REPO_ROOT" local command="${1:-}" [[ -n "$command" ]] || { usage exit 1 } shift || true case "$command" in help|-h|--help) usage ;; list) list_scopes ;; orphans) exec python3 scripts/forge/report-contract-reachability.py "$@" ;; build|test|script|create) local scope="" if [[ $# -gt 0 && "$1" != --* ]]; then local maybe_scope maybe_scope=$(resolve_scope "$1") if scope_exists "$maybe_scope"; then scope="$maybe_scope" shift fi fi if [[ -z "$scope" ]]; then if [[ -n "${FORGE_SCOPE:-}" ]]; then scope=$(resolve_scope) else scope=$(infer_scope_from_args "$@" || printf 'full\n') fi fi prepare_scope_env "$scope" "$command" if [[ "$scope" == "full" ]]; then info "Forge scope: full repo" else info "Forge scope: $scope" info " src=$FOUNDRY_SRC out=$FOUNDRY_OUT cache=$FOUNDRY_CACHE_PATH" [[ -n "${FOUNDRY_TEST:-}" ]] && info " test=$FOUNDRY_TEST" [[ -n "${FOUNDRY_SCRIPT:-}" ]] && info " script=$FOUNDRY_SCRIPT" fi case "$command" in build) if [[ "$scope" == "full" ]]; then exec forge build "$@" fi exec forge build --skip test --skip script "$@" ;; test) if [[ "$scope" != "full" && -z "${FOUNDRY_TEST:-}" ]]; then die "scope '$scope' has no matching test/ directory" fi exec forge test "$@" ;; script) [[ $# -gt 0 ]] || die "script command requires a script target, e.g. script/treasury/DeployTreasuryExecutor138.s.sol:DeployTreasuryExecutor138" exec forge script "$@" ;; create) [[ $# -gt 0 ]] || die "create command requires a contract target, e.g. contracts/tokens/CompliantFiatToken.sol:CompliantFiatToken" exec forge create "$@" ;; esac ;; *) die "unknown command '$command'" ;; esac } main "$@"