foundry_cli/opts/build/
paths.rs1use 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#[derive(Clone, Debug, Default, Serialize, Parser)]
18#[command(next_help_heading = "Project options")]
19pub struct ProjectPathOpts {
20 #[arg(long, value_hint = ValueHint::DirPath, value_name = "PATH")]
25 #[serde(skip)]
26 pub root: Option<PathBuf>,
27
28 #[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 #[arg(long, short = 'R')]
35 #[serde(skip)]
36 pub remappings: Vec<Remapping>,
37
38 #[arg(long, value_name = "ENV")]
40 #[serde(skip)]
41 pub remappings_env: Option<String>,
42
43 #[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 #[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 #[arg(long, conflicts_with = "contracts", visible_alias = "hh")]
57 #[serde(skip)]
58 pub hardhat: bool,
59
60 #[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 #[track_caller]
82 pub fn project_root(&self) -> PathBuf {
83 if self.config_path.is_some() {
84 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 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 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
171impl 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}