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