diff --git a/crates/anvil/src/anvil.rs b/crates/anvil/src/anvil.rs index 48e17ed44d29..c73f32935615 100644 --- a/crates/anvil/src/anvil.rs +++ b/crates/anvil/src/anvil.rs @@ -48,6 +48,11 @@ fn main() { fn run() -> Result<()> { utils::load_dotenv(); + if let Some(to) = utils::should_redirect_to() { + utils::redirect_execution(to)?; + return Ok(()); + } + let mut args = Anvil::parse(); args.global.init()?; args.node.evm_opts.resolve_rpc_alias(); @@ -69,7 +74,7 @@ fn run() -> Result<()> { &mut std::io::stdout(), ), } - return Ok(()) + return Ok(()); } let _ = fdlimit::raise_fd_limit(); diff --git a/crates/cast/bin/main.rs b/crates/cast/bin/main.rs index 01bab16ac16c..93b5ac773f5a 100644 --- a/crates/cast/bin/main.rs +++ b/crates/cast/bin/main.rs @@ -52,6 +52,11 @@ fn run() -> Result<()> { utils::subscriber(); utils::enable_paint(); + if let Some(to) = utils::should_redirect_to() { + utils::redirect_execution(to)?; + return Ok(()); + } + let args = CastArgs::parse(); args.global.init()?; main_args(args) diff --git a/crates/chisel/bin/main.rs b/crates/chisel/bin/main.rs index ca3fc1ff593e..0e331bf51131 100644 --- a/crates/chisel/bin/main.rs +++ b/crates/chisel/bin/main.rs @@ -119,6 +119,11 @@ fn run() -> eyre::Result<()> { utils::subscriber(); utils::load_dotenv(); + if let Some(to) = utils::should_redirect_to() { + utils::redirect_execution(to)?; + return Ok(()); + } + let args = Chisel::parse(); args.global.init()?; main_args(args) @@ -158,7 +163,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { DispatchResult::CommandFailed(e) => sh_err!("{e}")?, _ => panic!("Unexpected result: Please report this bug."), } - return Ok(()) + return Ok(()); } Some(ChiselSubcommand::Load { id }) | Some(ChiselSubcommand::View { id }) => { // For both of these subcommands, we need to attempt to load the session from cache @@ -166,7 +171,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { DispatchResult::CommandSuccess(_) => { /* Continue */ } DispatchResult::CommandFailed(e) => { sh_err!("{e}")?; - return Ok(()) + return Ok(()); } _ => panic!("Unexpected result! Please report this bug."), } @@ -179,7 +184,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { } _ => panic!("Unexpected result! Please report this bug."), } - return Ok(()) + return Ok(()); } } Some(ChiselSubcommand::ClearCache) => { @@ -188,11 +193,11 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { DispatchResult::CommandFailed(e) => sh_err!("{e}")?, _ => panic!("Unexpected result! Please report this bug."), } - return Ok(()) + return Ok(()); } Some(ChiselSubcommand::Eval { command }) => { dispatch_repl_line(&mut dispatcher, command).await?; - return Ok(()) + return Ok(()); } None => { /* No chisel subcommand present; Continue */ } } @@ -234,7 +239,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { } Err(ReadlineError::Interrupted) => { if interrupt { - break + break; } else { sh_println!("(To exit, press Ctrl+C again)")?; interrupt = true; @@ -243,7 +248,7 @@ async fn main_args(args: Chisel) -> eyre::Result<()> { Err(ReadlineError::Eof) => break, Err(err) => { sh_err!("{err:?}")?; - break + break; } } } diff --git a/crates/cli/src/utils/bin_redirect.rs b/crates/cli/src/utils/bin_redirect.rs new file mode 100644 index 000000000000..d3b7ed1d46d3 --- /dev/null +++ b/crates/cli/src/utils/bin_redirect.rs @@ -0,0 +1,27 @@ +use std::path::PathBuf; + +/// Loads config and checks if there is a binary remapping for the current binary. +/// If there is a remapping, returns the path to the binary that should be executed. +/// Returns `None` if the binary is not remapped _or_ if the current binary is not found in the +/// config. +pub fn should_redirect_to() -> Option { + let current_exe = std::env::current_exe().ok()?; + let binary_name = current_exe.file_stem()?.to_str()?; + let config = foundry_config::Config::load(); + config.binary_mappings().redirect_for(binary_name).cloned() +} + +/// Launches the `to` binary with the same arguments as the current binary. +/// E.g. if user runs `forge build --arg1 --arg2`, and `to` is `/path/to/custom/forge`, then +/// this function will run `/path/to/custom/forge build --arg1 --arg2`. +pub fn redirect_execution(to: PathBuf) -> eyre::Result<()> { + let args = std::env::args().skip(1).collect::>(); + let status = std::process::Command::new(to) + .args(args) + .status() + .map_err(|e| eyre::eyre!("Failed to run command: {}", e))?; + if !status.success() { + eyre::bail!("Command failed with status: {}", status); + } + Ok(()) +} diff --git a/crates/cli/src/utils/mod.rs b/crates/cli/src/utils/mod.rs index 6bf8c5b5d162..d62a91bb09b4 100644 --- a/crates/cli/src/utils/mod.rs +++ b/crates/cli/src/utils/mod.rs @@ -27,6 +27,9 @@ pub use suggestions::*; mod abi; pub use abi::*; +mod bin_redirect; +pub use bin_redirect::*; + // reexport all `foundry_config::utils` #[doc(hidden)] pub use foundry_config::utils::*; diff --git a/crates/config/src/binary_mappings.rs b/crates/config/src/binary_mappings.rs new file mode 100644 index 000000000000..796c3d19bd48 --- /dev/null +++ b/crates/config/src/binary_mappings.rs @@ -0,0 +1,109 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; + +/// Binaries that can be remapped. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum BinaryName { + Forge, + Anvil, + Cast, + Chisel, +} + +impl TryFrom<&str> for BinaryName { + type Error = eyre::Error; + + fn try_from(value: &str) -> eyre::Result { + match value { + "forge" => Ok(Self::Forge), + "anvil" => Ok(Self::Anvil), + "cast" => Ok(Self::Cast), + "chisel" => Ok(Self::Chisel), + _ => eyre::bail!("Invalid binary name: {value}"), + } + } +} + +/// Contains the config for binary remappings, +/// e.g. ability to redirect any of the foundry binaries to some other binary. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct BinaryMappings { + /// The mappings from binary name to the path of the binary. + #[serde(flatten)] + pub mappings: HashMap, +} + +impl BinaryMappings { + /// Tells if the binary name is remapped to some other binary. + /// This function will return `None` if the binary name cannot be parsed or if + /// the binary name is not remapped. + pub fn redirect_for(&self, binary_name: &str) -> Option<&PathBuf> { + // Sanitize the path so that it + let binary_name = Path::new(binary_name).file_stem()?.to_str()?; + let binary_name = BinaryName::try_from(binary_name).ok()?; + self.mappings.get(&binary_name) + } +} + +impl From for BinaryMappings +where + T: Into>, +{ + fn from(mappings: T) -> Self { + Self { mappings: mappings.into() } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn binary_names() { + assert_eq!(BinaryName::try_from("forge").unwrap(), BinaryName::Forge); + assert_eq!(BinaryName::try_from("anvil").unwrap(), BinaryName::Anvil); + assert_eq!(BinaryName::try_from("cast").unwrap(), BinaryName::Cast); + assert_eq!(BinaryName::try_from("chisel").unwrap(), BinaryName::Chisel); + } + + #[test] + fn binary_names_serde() { + let test_vector = [ + (BinaryName::Forge, r#""forge""#), + (BinaryName::Anvil, r#""anvil""#), + (BinaryName::Cast, r#""cast""#), + (BinaryName::Chisel, r#""chisel""#), + ]; + + for (binary_name, expected) in test_vector.iter() { + let serialized = serde_json::to_string(binary_name).unwrap(); + assert_eq!(serialized, *expected); + + let deserialized: BinaryName = serde_json::from_str(expected).unwrap(); + assert_eq!(deserialized, *binary_name); + } + } + + #[test] + fn redirect_to() { + let mappings = BinaryMappings::from([ + (BinaryName::Forge, PathBuf::from("forge-zksync")), + (BinaryName::Anvil, PathBuf::from("anvil-zksync")), + (BinaryName::Cast, PathBuf::from("cast-zksync")), + (BinaryName::Chisel, PathBuf::from("chisel-zksync")), + ]); + + assert_eq!(mappings.redirect_for("forge"), Some(&PathBuf::from("forge-zksync"))); + assert_eq!(mappings.redirect_for("anvil"), Some(&PathBuf::from("anvil-zksync"))); + assert_eq!(mappings.redirect_for("cast"), Some(&PathBuf::from("cast-zksync"))); + assert_eq!(mappings.redirect_for("chisel"), Some(&PathBuf::from("chisel-zksync"))); + assert_eq!(mappings.redirect_for("invalid"), None); + assert_eq!(mappings.redirect_for("/usr/bin/forge"), Some(&PathBuf::from("forge-zksync"))); + assert_eq!(mappings.redirect_for("anvil.exe"), Some(&PathBuf::from("anvil-zksync"))); + } +} diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 805aaf7cd163..5c3970a32ea4 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -38,6 +38,7 @@ use foundry_compilers::{ ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig, RestrictionsWithVersion, VyperLanguage, }; +use network_family::NetworkFamily; use regex::Regex; use revm_primitives::{map::AddressHashMap, FixedBytes, SpecId}; use semver::Version; @@ -122,6 +123,11 @@ use bind_json::BindJsonConfig; mod compilation; use compilation::{CompilationRestrictions, SettingsOverrides}; +mod binary_mappings; +use binary_mappings::BinaryMappings; + +mod network_family; + /// Foundry configuration /// /// # Defaults @@ -512,6 +518,17 @@ pub struct Config { #[serde(default)] pub compilation_restrictions: Vec, + /// Configuration for alternative versions of foundry tools to be used. + #[serde(default)] + pub binary_mappings: Option, + + /// Network family configuration. + /// If specified, network family can be used to change certain defaults (such as + /// binary mappings). Note, however, that network family only changes _defaults_, + /// so if the configuration is explicitly provided, it takes precedence. + #[serde(default)] + pub network_family: NetworkFamily, + /// PRIVATE: This structure may grow, As such, constructing this structure should /// _always_ be done using a public constructor or update syntax: /// @@ -1990,6 +2007,11 @@ impl Config { } } + /// Returns the binary mappings. + pub fn binary_mappings(&self) -> BinaryMappings { + self.binary_mappings.clone().unwrap_or_else(|| self.network_family.binary_mappings()) + } + /// The path provided to this function should point to a cached chain folder. fn get_cached_blocks(chain_path: &Path) -> eyre::Result> { let mut blocks = vec![]; @@ -2379,6 +2401,8 @@ impl Default for Config { additional_compiler_profiles: Default::default(), compilation_restrictions: Default::default(), eof: false, + binary_mappings: Default::default(), + network_family: NetworkFamily::Ethereum, _non_exhaustive: (), } } @@ -4834,4 +4858,68 @@ mod tests { Ok(()) }); } + + #[test] + fn test_binary_mappings() { + figment::Jail::expect_with(|jail| { + // No mappings by default. + let config = Config::load(); + assert_eq!(config.binary_mappings(), BinaryMappings::default()); + + // Load specified mappings. + jail.create_file( + "foundry.toml", + r#" + [profile.default] + binary_mappings = { "forge" = "forge-zksync", "anvil" = "anvil-zksync" } + "#, + )?; + let config = Config::load(); + assert_eq!( + config.binary_mappings(), + BinaryMappings::from([ + (binary_mappings::BinaryName::Forge, PathBuf::from("forge-zksync")), + (binary_mappings::BinaryName::Anvil, PathBuf::from("anvil-zksync")) + ]) + ); + + // Override via network family. + jail.create_file( + "foundry.toml", + r#" + [profile.default] + network_family = "zksync" + "#, + )?; + let config = Config::load(); + assert_eq!( + config.binary_mappings(), + BinaryMappings::from([ + (binary_mappings::BinaryName::Forge, PathBuf::from("forge-zksync")), + (binary_mappings::BinaryName::Cast, PathBuf::from("cast-zksync")), + (binary_mappings::BinaryName::Anvil, PathBuf::from("anvil-zksync")) + ]) + ); + + // Config precedence. + jail.create_file( + "foundry.toml", + r#" + [profile.default] + binary_mappings = { "forge" = "something-custom", "anvil" = "something-else" } + network_family = "zksync" + "#, + )?; + let config = Config::load(); + assert_eq!( + config.binary_mappings(), + BinaryMappings::from([ + (binary_mappings::BinaryName::Forge, PathBuf::from("something-custom")), + (binary_mappings::BinaryName::Anvil, PathBuf::from("something-else")) + ]) + ); + + Ok(()) + }); + } } diff --git a/crates/config/src/network_family.rs b/crates/config/src/network_family.rs new file mode 100644 index 000000000000..8b9053753956 --- /dev/null +++ b/crates/config/src/network_family.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +use crate::binary_mappings::{BinaryMappings, BinaryName}; + +#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum NetworkFamily { + #[default] + Ethereum, + Zksync, +} + +impl NetworkFamily { + pub fn binary_mappings(self) -> BinaryMappings { + match self { + Self::Ethereum => BinaryMappings::default(), + Self::Zksync => Self::zksync_mappings(), + } + } + + fn zksync_mappings() -> BinaryMappings { + BinaryMappings::from([ + (BinaryName::Forge, "forge-zksync".into()), + (BinaryName::Anvil, "anvil-zksync".into()), + (BinaryName::Cast, "cast-zksync".into()), + // Chisel is not currently supported on ZKsync. + ]) + } +} diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index d60c1639a05a..a847591799d5 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -34,6 +34,11 @@ fn run() -> Result<()> { utils::subscriber(); utils::enable_paint(); + if let Some(to) = utils::should_redirect_to() { + utils::redirect_execution(to)?; + return Ok(()); + } + let args = Forge::parse(); args.global.init()?; init_execution_context(&args.cmd); diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 545cebac87a8..ea5a07f220a7 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -161,6 +161,8 @@ forgetest!(can_extract_config_values, |prj, cmd| { additional_compiler_profiles: Default::default(), compilation_restrictions: Default::default(), eof: false, + binary_mappings: Default::default(), + network_family: Default::default(), _non_exhaustive: (), }; prj.write_config(input.clone());