Skip to main content

foundry_cli/opts/build/
paths.rs

1use clap::{Parser, ValueHint};
2use eyre::Result;
3use foundry_compilers::artifacts::remappings::Remapping;
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    /// This will be the `--root` argument if provided, otherwise see [`find_project_root`].
77    ///
78    /// # Panics
79    ///
80    /// Panics if the project root directory cannot be found. See [`find_project_root`].
81    #[track_caller]
82    pub fn project_root(&self) -> PathBuf {
83        self.root
84            .clone()
85            .unwrap_or_else(|| find_project_root(None).expect("could not determine project root"))
86    }
87
88    /// Returns the remappings to add to the config
89    pub fn get_remappings(&self) -> Vec<Remapping> {
90        let mut remappings = self.remappings.clone();
91        if let Some(remappings_env) = self.remappings_env.as_deref()
92            && let Some(env_remappings) = remappings_from_env_var(remappings_env)
93        {
94            match env_remappings {
95                Ok(env_remappings) => remappings.extend(env_remappings),
96                Err(err) => {
97                    let _ = sh_warn!(
98                        "failed to parse env var remappings from `{remappings_env}`: {err}"
99                    );
100                }
101            }
102        }
103        remappings
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::ProjectPathOpts;
110
111    #[test]
112    fn get_remappings_ignores_invalid_remappings_env_var() {
113        let env_name = "FOUNDRY_CLI_TEST_INVALID_REMAPPINGS";
114        unsafe {
115            std::env::set_var(env_name, "this-is-not-a-remapping");
116        }
117
118        let opts =
119            ProjectPathOpts { remappings_env: Some(env_name.to_string()), ..Default::default() };
120        let remappings = opts.get_remappings();
121        assert!(remappings.is_empty());
122
123        unsafe {
124            std::env::remove_var(env_name);
125        }
126    }
127
128    #[test]
129    fn get_remappings_parses_valid_remappings_env_var() {
130        let env_name = "FOUNDRY_CLI_TEST_VALID_REMAPPINGS";
131        unsafe {
132            std::env::set_var(env_name, "forge-std/=lib/forge-std/src/");
133        }
134
135        let opts =
136            ProjectPathOpts { remappings_env: Some(env_name.to_string()), ..Default::default() };
137        let remappings = opts.get_remappings();
138        assert_eq!(remappings.len(), 1);
139        assert_eq!(remappings[0].name, "forge-std/");
140        assert_eq!(remappings[0].path, "lib/forge-std/src/");
141
142        unsafe {
143            std::env::remove_var(env_name);
144        }
145    }
146}
147
148foundry_config::impl_figment_convert!(ProjectPathOpts);
149
150// Make this args a `figment::Provider` so that it can be merged into the `Config`
151impl Provider for ProjectPathOpts {
152    fn metadata(&self) -> Metadata {
153        Metadata::named("Project Paths Args Provider")
154    }
155
156    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
157        let value = Value::serialize(self)?;
158        let error = InvalidType(value.to_actual(), "map".into());
159        let mut dict = value.into_dict().ok_or(error)?;
160
161        let mut libs =
162            self.lib_paths.iter().map(|p| format!("{}", p.display())).collect::<Vec<_>>();
163
164        if self.hardhat {
165            dict.insert("src".to_string(), "contracts".to_string().into());
166            libs.push("node_modules".to_string());
167        }
168
169        if !libs.is_empty() {
170            dict.insert("libs".to_string(), libs.into());
171        }
172
173        Ok(Map::from([(Config::selected_profile(), dict)]))
174    }
175}