Skip to main content

foundry_cli/opts/build/
paths.rs

1use clap::{Parser, ValueHint};
2use eyre::{Result, ensure};
3use foundry_compilers::{artifacts::remappings::Remapping, utils::canonicalized};
4use foundry_config::{
5    Config, figment,
6    figment::{
7        Metadata, Profile, Provider,
8        error::Kind::InvalidType,
9        value::{Dict, Map, Value},
10    },
11    find_project_root, remappings_from_env_var,
12};
13use serde::Serialize;
14use std::path::PathBuf;
15
16/// Common arguments for a project's paths.
17#[derive(Clone, Debug, Default, Serialize, Parser)]
18#[command(next_help_heading = "Project options")]
19pub struct ProjectPathOpts {
20    /// The project's root path.
21    ///
22    /// By default root of the Git repository, if in one,
23    /// or the current working directory.
24    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
25    #[serde(skip)]
26    pub root: Option<PathBuf>,
27
28    /// The contracts source directory.
29    #[arg(long, short = 'C', value_hint = ValueHint::DirPath, value_name = "PATH")]
30    #[serde(rename = "src", skip_serializing_if = "Option::is_none")]
31    pub contracts: Option<PathBuf>,
32
33    /// The project's remappings.
34    #[arg(long, short = 'R')]
35    #[serde(skip)]
36    pub remappings: Vec<Remapping>,
37
38    /// The project's remappings from the environment.
39    #[arg(long, value_name = "ENV")]
40    #[serde(skip)]
41    pub remappings_env: Option<String>,
42
43    /// The path to the compiler cache.
44    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub cache_path: Option<PathBuf>,
47
48    /// The path to the library folder.
49    #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
50    #[serde(rename = "libs", skip_serializing_if = "Vec::is_empty")]
51    pub lib_paths: Vec<PathBuf>,
52
53    /// Use the Hardhat-style project layout.
54    ///
55    /// This is the same as using: `--contracts contracts --lib-paths node_modules`.
56    #[arg(long, conflicts_with = "contracts", visible_alias = "hh")]
57    #[serde(skip)]
58    pub hardhat: bool,
59
60    /// Path to the config file.
61    ///
62    /// The project root is the config file's parent directory.
63    #[arg(
64        long,
65        conflicts_with = "root",
66        value_hint = ValueHint::FilePath,
67        value_name = "FILE"
68    )]
69    #[serde(skip)]
70    pub config_path: Option<PathBuf>,
71}
72
73impl ProjectPathOpts {
74    /// Returns the root directory to use for configuring the project.
75    ///
76    /// Uses the parent of `--config-path`, then `--root`, otherwise see [`find_project_root`].
77    ///
78    /// # Panics
79    ///
80    /// Panics if `--config-path` is invalid or the project root cannot be found.
81    #[track_caller]
82    pub fn project_root(&self) -> PathBuf {
83        if self.config_path.is_some() {
84            // Preserve the existing config-path validation diagnostics.
85            self.try_project_root().unwrap_or_else(|err| panic!("error: {err}"))
86        } else {
87            self.try_project_root().expect("could not determine project root")
88        }
89    }
90
91    /// Returns the remappings to add to the config
92    pub fn get_remappings(&self) -> Vec<Remapping> {
93        let mut remappings = self.remappings.clone();
94        if let Some(remappings_env) = self.remappings_env.as_deref()
95            && let Some(env_remappings) = remappings_from_env_var(remappings_env)
96        {
97            match env_remappings {
98                Ok(env_remappings) => remappings.extend(env_remappings),
99                Err(err) => {
100                    let _ = sh_warn!(
101                        "failed to parse env var remappings from `{remappings_env}`: {err}"
102                    );
103                }
104            }
105        }
106        remappings
107    }
108
109    /// Resolves the project root without loading configuration, validating `--config-path` first.
110    pub fn try_project_root(&self) -> Result<PathBuf> {
111        if let Some(config_path) = &self.config_path {
112            ensure!(config_path.exists(), "config-path `{}` does not exist", config_path.display());
113            ensure!(
114                config_path.ends_with(Config::FILE_NAME),
115                "the config-path must be a path to a foundry.toml file"
116            );
117            ensure!(config_path.is_file(), "config-path `{}` is not a file", config_path.display());
118            let config_path = canonicalized(config_path);
119            Ok(config_path.parent().unwrap().to_path_buf())
120        } else if let Some(root) = &self.root {
121            Ok(root.clone())
122        } else {
123            Ok(find_project_root(None)?)
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::ProjectPathOpts;
131
132    #[test]
133    fn get_remappings_ignores_invalid_remappings_env_var() {
134        let env_name = "FOUNDRY_CLI_TEST_INVALID_REMAPPINGS";
135        unsafe {
136            std::env::set_var(env_name, "this-is-not-a-remapping");
137        }
138
139        let opts =
140            ProjectPathOpts { remappings_env: Some(env_name.to_string()), ..Default::default() };
141        let remappings = opts.get_remappings();
142        assert!(remappings.is_empty());
143
144        unsafe {
145            std::env::remove_var(env_name);
146        }
147    }
148
149    #[test]
150    fn get_remappings_parses_valid_remappings_env_var() {
151        let env_name = "FOUNDRY_CLI_TEST_VALID_REMAPPINGS";
152        unsafe {
153            std::env::set_var(env_name, "forge-std/=lib/forge-std/src/");
154        }
155
156        let opts =
157            ProjectPathOpts { remappings_env: Some(env_name.to_string()), ..Default::default() };
158        let remappings = opts.get_remappings();
159        assert_eq!(remappings.len(), 1);
160        assert_eq!(remappings[0].name, "forge-std/");
161        assert_eq!(remappings[0].path, "lib/forge-std/src/");
162
163        unsafe {
164            std::env::remove_var(env_name);
165        }
166    }
167}
168
169foundry_config::impl_figment_convert!(ProjectPathOpts);
170
171// Make this args a `figment::Provider` so that it can be merged into the `Config`
172impl Provider for ProjectPathOpts {
173    fn metadata(&self) -> Metadata {
174        Metadata::named("Project Paths Args Provider")
175    }
176
177    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
178        let value = Value::serialize(self)?;
179        let error = InvalidType(value.to_actual(), "map".into());
180        let mut dict = value.into_dict().ok_or(error)?;
181
182        let mut libs =
183            self.lib_paths.iter().map(|p| format!("{}", p.display())).collect::<Vec<_>>();
184
185        if self.hardhat {
186            dict.insert("src".to_string(), "contracts".to_string().into());
187            libs.push("node_modules".to_string());
188        }
189
190        if !libs.is_empty() {
191            dict.insert("libs".to_string(), libs.into());
192        }
193
194        Ok(Map::from([(Config::selected_profile(), dict)]))
195    }
196}