//! Utility functions use crate::Config; use ethers_core::types::{serde_helpers::Numeric, U256}; use ethers_solc::{ remappings::{Remapping, RemappingError}, EvmVersion, }; use figment::value::Value; use revm_primitives::SpecId; use serde::{de::Error, Deserialize, Deserializer}; use std::{ path::{Path, PathBuf}, str::FromStr, }; use toml_edit::{Document, Item}; /// Loads the config for the current project workspace pub fn load_config() -> Config { load_config_with_root(None) } /// Loads the config for the current project workspace or the provided root path pub fn load_config_with_root(root: Option) -> Config { if let Some(root) = root { Config::load_with_root(root) } else { Config::load_with_root(find_project_root_path(None).unwrap()) } .sanitized() } /// Returns the path of the top-level directory of the working git tree. If there is no working /// tree, an error is returned. pub fn find_git_root_path(relative_to: impl AsRef) -> eyre::Result { let path = std::process::Command::new("git") .args(["rev-parse", "--show-toplevel"]) .current_dir(relative_to.as_ref()) .output()? .stdout; let path = std::str::from_utf8(&path)?.trim_end_matches('\n'); Ok(PathBuf::from(path)) } /// Returns the root path to set for the project root /// /// traverse the dir tree up and look for a `foundry.toml` file starting at the given path or cwd, /// but only until the root dir of the current repo so that /// /// ```text /// -- foundry.toml /// /// -- repo /// |__ .git /// |__sub /// |__ [given_path | cwd] /// ``` /// will still detect `repo` as root pub fn find_project_root_path(path: Option<&PathBuf>) -> std::io::Result { let cwd = &std::env::current_dir()?; let cwd = path.unwrap_or(cwd); let boundary = find_git_root_path(cwd) .ok() .filter(|p| !p.as_os_str().is_empty()) .unwrap_or_else(|| cwd.clone()); let mut cwd = cwd.as_path(); // traverse as long as we're in the current git repo cwd while cwd.starts_with(&boundary) { let file_path = cwd.join(Config::FILE_NAME); if file_path.is_file() { return Ok(cwd.to_path_buf()) } if let Some(parent) = cwd.parent() { cwd = parent; } else { break } } // no foundry.toml found Ok(boundary) } /// Returns all [`Remapping`]s contained in the `remappings` str separated by newlines /// /// # Example /// /// ``` /// use foundry_config::remappings_from_newline; /// let remappings: Result, _> = remappings_from_newline( /// r#" /// file-ds-test/=lib/ds-test/ /// file-other/=lib/other/ /// "#, /// ) /// .collect(); /// ``` pub fn remappings_from_newline( remappings: &str, ) -> impl Iterator> + '_ { remappings.lines().map(|x| x.trim()).filter(|x| !x.is_empty()).map(Remapping::from_str) } /// Returns the remappings from the given var /// /// Returns `None` if the env var is not set, otherwise all Remappings, See /// `remappings_from_newline` pub fn remappings_from_env_var(env_var: &str) -> Option, RemappingError>> { let val = std::env::var(env_var).ok()?; Some(remappings_from_newline(&val).collect()) } /// Converts the `val` into a `figment::Value::Array` /// /// The values should be separated by commas, surrounding brackets are also supported `[a,b,c]` pub fn to_array_value(val: &str) -> Result { let value: Value = match Value::from(val) { Value::String(_, val) => val .trim_start_matches('[') .trim_end_matches(']') .split(',') .map(|s| s.to_string()) .collect::>() .into(), Value::Empty(_, _) => Vec::::new().into(), val @ Value::Array(_, _) => val, _ => return Err(format!("Invalid value `{val}`, expected an array").into()), }; Ok(value) } /// Returns a list of _unique_ paths to all folders under `root` that contain a `foundry.toml` file /// /// This will also resolve symlinks /// /// # Example /// /// ```no_run /// use foundry_config::utils; /// let dirs = utils::foundry_toml_dirs("./lib"); /// ``` /// /// for following layout this will return /// `["lib/dep1", "lib/dep2"]` /// /// ```text /// lib /// └── dep1 /// │ ├── foundry.toml /// └── dep2 /// ├── foundry.toml /// ``` pub fn foundry_toml_dirs(root: impl AsRef) -> Vec { walkdir::WalkDir::new(root) .max_depth(1) .into_iter() .filter_map(Result::ok) .filter(|e| e.file_type().is_dir()) .filter_map(|e| ethers_solc::utils::canonicalize(e.path()).ok()) .filter(|p| p.join(Config::FILE_NAME).exists()) .collect() } /// Returns a remapping for the given dir pub(crate) fn get_dir_remapping(dir: impl AsRef) -> Option { let dir = dir.as_ref(); if let Some(dir_name) = dir.file_name().and_then(|s| s.to_str()).filter(|s| !s.is_empty()) { let mut r = Remapping { context: None, name: format!("{dir_name}/"), path: format!("{}", dir.display()), }; if !r.path.ends_with('/') { r.path.push('/') } Some(r) } else { None } } /// Returns all available `profile` keys in a given `.toml` file /// /// i.e. The toml below would return would return `["default", "ci", "local"]` /// ```toml /// [profile.default] /// ... /// [profile.ci] /// ... /// [profile.local] /// ``` pub fn get_available_profiles(toml_path: impl AsRef) -> eyre::Result> { let mut result = vec![Config::DEFAULT_PROFILE.to_string()]; if !toml_path.as_ref().exists() { return Ok(result) } let doc = read_toml(toml_path)?; if let Some(Item::Table(profiles)) = doc.as_table().get(Config::PROFILE_SECTION) { for (_, (profile, _)) in profiles.iter().enumerate() { let p = profile.to_string(); if !result.contains(&p) { result.push(p); } } } Ok(result) } /// Returns a [`toml_edit::Document`] loaded from the provided `path`. /// Can raise an error in case of I/O or parsing errors. fn read_toml(path: impl AsRef) -> eyre::Result { let path = path.as_ref().to_owned(); let doc: Document = std::fs::read_to_string(path)?.parse()?; Ok(doc) } /// Deserialize stringified percent. The value must be between 0 and 100 inclusive. pub(crate) fn deserialize_stringified_percent<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let num: U256 = Numeric::deserialize(deserializer)?.try_into().map_err(serde::de::Error::custom)?; let num: u64 = num.try_into().map_err(serde::de::Error::custom)?; if num <= 100 { num.try_into().map_err(serde::de::Error::custom) } else { Err(serde::de::Error::custom("percent must be lte 100")) } } /// Deserialize an usize or pub(crate) fn deserialize_usize_or_max<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { #[derive(Deserialize)] #[serde(untagged)] enum Val { Number(usize), Text(String), } let num = match Val::deserialize(deserializer)? { Val::Number(num) => num, Val::Text(s) => { match s.as_str() { "max" | "MAX" | "Max" => { // toml limitation i64::MAX as usize } s => s.parse::().map_err(D::Error::custom).unwrap(), } } }; Ok(num) } /// Returns the [SpecId] derived from [EvmVersion] #[inline] pub fn evm_spec_id(evm_version: &EvmVersion) -> SpecId { match evm_version { EvmVersion::Homestead => SpecId::HOMESTEAD, EvmVersion::TangerineWhistle => SpecId::TANGERINE, EvmVersion::SpuriousDragon => SpecId::SPURIOUS_DRAGON, EvmVersion::Byzantium => SpecId::BYZANTIUM, EvmVersion::Constantinople => SpecId::CONSTANTINOPLE, EvmVersion::Petersburg => SpecId::PETERSBURG, EvmVersion::Istanbul => SpecId::ISTANBUL, EvmVersion::Berlin => SpecId::BERLIN, EvmVersion::London => SpecId::LONDON, EvmVersion::Paris => SpecId::MERGE, EvmVersion::Shanghai => SpecId::SHANGHAI, } } #[cfg(test)] mod tests { use crate::get_available_profiles; use std::path::Path; #[test] fn get_profiles_from_toml() { figment::Jail::expect_with(|jail| { jail.create_file( "foundry.toml", r#" [foo.baz] libs = ['node_modules', 'lib'] [profile.default] libs = ['node_modules', 'lib'] [profile.ci] libs = ['node_modules', 'lib'] [profile.local] libs = ['node_modules', 'lib'] "#, )?; let path = Path::new("./foundry.toml"); let profiles = get_available_profiles(path).unwrap(); assert_eq!( profiles, vec!["default".to_string(), "ci".to_string(), "local".to_string()] ); Ok(()) }); } }