foundry_config/providers/
ext.rs1use crate::{Config, extend, utils};
2use figment::{
3 Error, Figment, Metadata, Profile, Provider,
4 providers::{Env, Format, Toml},
5 value::{Dict, Map, Value},
6};
7use foundry_compilers::ProjectPathsConfig;
8use heck::ToSnakeCase;
9use std::{
10 cell::OnceCell,
11 path::{Path, PathBuf},
12};
13
14pub(crate) trait ProviderExt: Provider + Sized {
15 fn rename(
16 self,
17 from: impl Into<Profile>,
18 to: impl Into<Profile>,
19 ) -> RenameProfileProvider<Self> {
20 RenameProfileProvider::new(self, from, to)
21 }
22
23 fn wrap(
24 self,
25 wrapping_key: impl Into<Profile>,
26 profile: impl Into<Profile>,
27 ) -> WrapProfileProvider<Self> {
28 WrapProfileProvider::new(self, wrapping_key, profile)
29 }
30
31 fn strict_select(
32 self,
33 profiles: impl IntoIterator<Item = impl Into<Profile>>,
34 ) -> OptionalStrictProfileProvider<Self> {
35 OptionalStrictProfileProvider::new(self, profiles)
36 }
37
38 fn fallback(
39 self,
40 profile: impl Into<Profile>,
41 fallback: impl Into<Profile>,
42 ) -> FallbackProfileProvider<Self> {
43 FallbackProfileProvider::new(self, profile, fallback)
44 }
45}
46
47impl<P: Provider> ProviderExt for P {}
48
49pub(crate) struct TomlFileProvider {
52 env_var: Option<&'static str>,
53 env_val: OnceCell<Option<String>>,
54 default: PathBuf,
55 cache: OnceCell<Result<Map<Profile, Dict>, Error>>,
56}
57
58impl TomlFileProvider {
59 pub(crate) fn new(env_var: Option<&'static str>, default: PathBuf) -> Self {
60 Self { env_var, env_val: OnceCell::new(), default, cache: OnceCell::new() }
61 }
62
63 fn env_val(&self) -> Option<&str> {
64 self.env_val.get_or_init(|| self.env_var.and_then(Env::var)).as_deref()
65 }
66
67 fn file(&self) -> PathBuf {
68 self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
69 }
70
71 fn is_missing(&self) -> bool {
72 if let Some(file) = self.env_val() {
73 let path = Path::new(&file);
74 if !path.exists() {
75 return true;
76 }
77 }
78 false
79 }
80
81 fn read(&self) -> Result<Map<Profile, Dict>, Error> {
83 use serde::de::Error as _;
84
85 let local_path = self.file();
87 if !local_path.exists() {
88 if let Some(file) = self.env_val() {
89 return Err(Error::custom(format!(
90 "Config file `{}` set in env var `{}` does not exist",
91 file,
92 self.env_var.unwrap()
93 )));
94 }
95 return Ok(Map::new());
96 }
97
98 let local_provider = Toml::file(local_path.clone()).nested();
100
101 let local_path_str = local_path.to_string_lossy();
103 let local_content = std::fs::read_to_string(&local_path)
104 .map_err(|e| Error::custom(e.to_string()).with_path(&local_path_str))?;
105 let partial_config: extend::ExtendsPartialConfig = toml::from_str(&local_content)
106 .map_err(|e| Error::custom(e.to_string()).with_path(&local_path_str))?;
107
108 let selected_profile = Config::selected_profile();
110 let extends_config = partial_config.profile.as_ref().and_then(|profiles| {
111 let profile_str = selected_profile.to_string();
112 profiles.get(&profile_str).and_then(|cfg| cfg.extends.as_ref())
113 });
114
115 if let Some(extends_config) = extends_config {
117 let extends_path = extends_config.path();
118 let extends_strategy = extends_config.strategy();
119 let relative_base_path = PathBuf::from(extends_path);
120 let local_dir = local_path.parent().ok_or_else(|| {
121 Error::custom(format!(
122 "Could not determine parent directory of config file: {}",
123 local_path.display()
124 ))
125 })?;
126
127 let base_path =
128 foundry_compilers::utils::canonicalize(local_dir.join(&relative_base_path))
129 .map_err(|e| {
130 Error::custom(format!(
131 "Failed to resolve inherited config path: {}: {e}",
132 relative_base_path.display()
133 ))
134 })?;
135
136 if !base_path.is_file() {
138 return Err(Error::custom(format!(
139 "Inherited config file does not exist or is not a file: {}",
140 base_path.display()
141 )));
142 }
143
144 if foundry_compilers::utils::canonicalize(&local_path).ok().as_ref() == Some(&base_path)
146 {
147 return Err(Error::custom(format!(
148 "Config file {} cannot inherit from itself.",
149 local_path.display()
150 )));
151 }
152
153 let base_path_str = base_path.to_string_lossy();
155 let base_content = std::fs::read_to_string(&base_path)
156 .map_err(|e| Error::custom(e.to_string()).with_path(&base_path_str))?;
157 let base_partial: extend::ExtendsPartialConfig = toml::from_str(&base_content)
158 .map_err(|e| Error::custom(e.to_string()).with_path(&base_path_str))?;
159
160 let base_extends = base_partial
162 .profile
163 .as_ref()
164 .and_then(|profiles| {
165 let profile_str = selected_profile.to_string();
166 profiles.get(&profile_str)
167 })
168 .and_then(|profile| profile.extends.as_ref());
169
170 if base_extends.is_some() {
172 return Err(Error::custom(format!(
173 "Nested inheritance is not allowed. Base file '{}' cannot have an 'extends' field in profile '{selected_profile}'.",
174 base_path.display()
175 )));
176 }
177
178 let base_provider = Toml::file(base_path).nested();
180
181 match extends_strategy {
183 extend::ExtendStrategy::ExtendArrays => {
184 Figment::new().merge(base_provider).admerge(local_provider).data()
189 }
190 extend::ExtendStrategy::ReplaceArrays => {
191 Figment::new().merge(base_provider).merge(local_provider).data()
195 }
196 extend::ExtendStrategy::NoCollision => {
197 let base_data = base_provider.data()?;
199 let local_data = local_provider.data()?;
200
201 let profile_key = Profile::new("profile");
202 if let (Some(local_profiles), Some(base_profiles)) =
203 (local_data.get(&profile_key), base_data.get(&profile_key))
204 {
205 let profile_str = selected_profile.to_string();
207 let base_dict = base_profiles.get(&profile_str).and_then(|v| v.as_dict());
208 let local_dict = local_profiles.get(&profile_str).and_then(|v| v.as_dict());
209
210 if let (Some(local_dict), Some(base_dict)) = (local_dict, base_dict) {
212 let collisions: Vec<&String> = local_dict
213 .keys()
214 .filter(|key| {
215 *key != "extends" && base_dict.contains_key(*key)
217 })
218 .collect();
219
220 if !collisions.is_empty() {
221 return Err(Error::custom(format!(
222 "Key collision detected in profile '{profile_str}' when extending '{extends_path}'. \
223 Conflicting keys: {collisions:?}. Use 'extends.strategy' or 'extends_strategy' to specify how to handle conflicts."
224 )));
225 }
226 }
227 }
228
229 Figment::new().merge(base_provider).merge(local_provider).data()
231 }
232 }
233 } else {
234 local_provider.data()
236 }
237 }
238}
239
240impl Provider for TomlFileProvider {
241 fn metadata(&self) -> Metadata {
242 if self.is_missing() {
243 Metadata::named("TOML file provider")
244 } else {
245 Toml::file(self.file()).nested().metadata()
246 }
247 }
248
249 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
250 self.cache.get_or_init(|| self.read()).clone()
251 }
252}
253
254pub(crate) struct ForcedSnakeCaseData<P>(pub(crate) P);
257
258impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
259 fn metadata(&self) -> Metadata {
260 self.0.metadata()
261 }
262
263 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
264 let mut map = self.0.data()?;
265 for (profile, dict) in &mut map {
266 if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
267 continue;
269 }
270 let dict2 = std::mem::take(dict);
271 *dict = dict2.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect();
272 }
273 Ok(map)
274 }
275
276 fn profile(&self) -> Option<Profile> {
277 self.0.profile()
278 }
279}
280
281pub(crate) struct BackwardsCompatTomlProvider<P>(pub(crate) P);
283
284impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
285 fn metadata(&self) -> Metadata {
286 self.0.metadata()
287 }
288
289 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
290 let mut map = Map::new();
291 let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
292 .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
293 .map(Value::from)
294 .ok();
295 for (profile, mut dict) in self.0.data()? {
296 if let Some(v) = solc_env.clone() {
297 dict.insert("solc".to_string(), v);
299 } else if let Some(v) = dict.remove("solc_version") {
300 if !dict.contains_key("solc") {
302 dict.insert("solc".to_string(), v);
303 }
304 }
305 map.insert(profile, dict);
306 }
307 Ok(map)
308 }
309
310 fn profile(&self) -> Option<Profile> {
311 self.0.profile()
312 }
313}
314
315pub(crate) struct DappHardhatDirProvider<'a>(pub(crate) &'a Path);
317
318impl Provider for DappHardhatDirProvider<'_> {
319 fn metadata(&self) -> Metadata {
320 Metadata::named("Dapp Hardhat dir compat")
321 }
322
323 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
324 let mut dict = Dict::new();
325 dict.insert(
326 "src".to_string(),
327 ProjectPathsConfig::find_source_dir(self.0)
328 .file_name()
329 .unwrap()
330 .to_string_lossy()
331 .to_string()
332 .into(),
333 );
334 dict.insert(
335 "out".to_string(),
336 ProjectPathsConfig::find_artifacts_dir(self.0)
337 .file_name()
338 .unwrap()
339 .to_string_lossy()
340 .to_string()
341 .into(),
342 );
343
344 let mut libs = vec![];
349 let node_modules = self.0.join("node_modules");
350 let lib = self.0.join("lib");
351 if node_modules.exists() {
352 if lib.exists() {
353 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
354 }
355 libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
356 } else {
357 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
358 }
359
360 dict.insert("libs".to_string(), libs.into());
361
362 Ok(Map::from([(Config::selected_profile(), dict)]))
363 }
364}
365
366pub(crate) struct DappEnvCompatProvider;
368
369impl Provider for DappEnvCompatProvider {
370 fn metadata(&self) -> Metadata {
371 Metadata::named("Dapp env compat")
372 }
373
374 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
375 use serde::de::Error as _;
376 use std::env;
377
378 let mut dict = Dict::new();
379 if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
380 dict.insert(
381 "block_number".to_string(),
382 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
383 );
384 }
385 if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
386 dict.insert("sender".to_string(), val.into());
387 }
388 if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
389 dict.insert(
390 "fork_block_number".to_string(),
391 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
392 );
393 } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
394 dict.insert(
395 "fork_block_number".to_string(),
396 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
397 );
398 }
399 if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
400 dict.insert(
401 "block_timestamp".to_string(),
402 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
403 );
404 }
405 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
406 dict.insert(
407 "optimizer_runs".to_string(),
408 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
409 );
410 }
411 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
412 let val = val.parse::<u8>().map_err(figment::Error::custom)?;
414 if val > 1 {
415 return Err(
416 format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
417 );
418 }
419 dict.insert("optimizer".to_string(), (val == 1).into());
420 }
421
422 if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
424 dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
425 }
426
427 let mut fuzz_dict = Dict::new();
428 if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
429 fuzz_dict.insert(
430 "runs".to_string(),
431 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
432 );
433 }
434 dict.insert("fuzz".to_string(), fuzz_dict.into());
435
436 let mut invariant_dict = Dict::new();
437 if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
438 invariant_dict.insert(
439 "depth".to_string(),
440 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
441 );
442 }
443 dict.insert("invariant".to_string(), invariant_dict.into());
444
445 Ok(Map::from([(Config::selected_profile(), dict)]))
446 }
447}
448
449pub(crate) struct RenameProfileProvider<P> {
465 provider: P,
466 from: Profile,
467 to: Profile,
468}
469
470impl<P> RenameProfileProvider<P> {
471 pub(crate) fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
472 Self { provider, from: from.into(), to: to.into() }
473 }
474}
475
476impl<P: Provider> Provider for RenameProfileProvider<P> {
477 fn metadata(&self) -> Metadata {
478 self.provider.metadata()
479 }
480
481 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
482 let mut data = self.provider.data()?;
483 if let Some(data) = data.remove(&self.from) {
484 return Ok(Map::from([(self.to.clone(), data)]));
485 }
486 Ok(Default::default())
487 }
488
489 fn profile(&self) -> Option<Profile> {
490 Some(self.to.clone())
491 }
492}
493
494struct UnwrapProfileProvider<P> {
510 provider: P,
511 wrapping_key: Profile,
512 profile: Profile,
513}
514
515impl<P> UnwrapProfileProvider<P> {
516 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
517 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
518 }
519}
520
521impl<P: Provider> Provider for UnwrapProfileProvider<P> {
522 fn metadata(&self) -> Metadata {
523 self.provider.metadata()
524 }
525
526 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
527 let mut data = self.provider.data()?;
528 if let Some(profiles) = data.remove(&self.wrapping_key) {
529 for (profile_str, profile_val) in profiles {
530 let profile = Profile::new(&profile_str);
531 if profile != self.profile {
532 continue;
533 }
534 match profile_val {
535 Value::Dict(_, dict) => return Ok(profile.collect(dict)),
536 bad_val => {
537 let mut err = Error::from(figment::error::Kind::InvalidType(
538 bad_val.to_actual(),
539 "dict".into(),
540 ));
541 err.metadata = Some(self.provider.metadata());
542 err.profile = Some(self.profile.clone());
543 return Err(err);
544 }
545 }
546 }
547 }
548 Ok(Default::default())
549 }
550
551 fn profile(&self) -> Option<Profile> {
552 Some(self.profile.clone())
553 }
554}
555
556pub(crate) struct WrapProfileProvider<P> {
572 provider: P,
573 wrapping_key: Profile,
574 profile: Profile,
575}
576
577impl<P> WrapProfileProvider<P> {
578 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
579 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
580 }
581}
582
583impl<P: Provider> Provider for WrapProfileProvider<P> {
584 fn metadata(&self) -> Metadata {
585 self.provider.metadata()
586 }
587
588 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
589 if let Some(inner) = self.provider.data()?.remove(&self.profile) {
590 let value = Value::from(inner);
591 let mut dict = Dict::new();
592 dict.insert(self.profile.as_str().as_str().to_snake_case(), value);
593 Ok(self.wrapping_key.collect(dict))
594 } else {
595 Ok(Default::default())
596 }
597 }
598
599 fn profile(&self) -> Option<Profile> {
600 Some(self.profile.clone())
601 }
602}
603
604pub(crate) struct OptionalStrictProfileProvider<P> {
627 provider: P,
628 profiles: Vec<Profile>,
629}
630
631impl<P> OptionalStrictProfileProvider<P> {
632 pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
633
634 pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
635 Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
636 }
637}
638
639impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
640 fn metadata(&self) -> Metadata {
641 self.provider.metadata()
642 }
643
644 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
645 let mut figment = Figment::from(&self.provider);
646 for profile in &self.profiles {
647 figment = figment.merge(UnwrapProfileProvider::new(
648 &self.provider,
649 Self::PROFILE_PROFILE,
650 profile.clone(),
651 ));
652 }
653 figment.data().map_err(|err| {
654 if let Err(root_err) = self.provider.data() {
659 return root_err;
660 }
661 err
662 })
663 }
664
665 fn profile(&self) -> Option<Profile> {
666 self.profiles.last().cloned()
667 }
668}
669
670pub struct FallbackProfileProvider<P> {
673 provider: P,
674 profile: Profile,
675 fallback: Profile,
676}
677
678impl<P> FallbackProfileProvider<P> {
679 pub fn new(provider: P, profile: impl Into<Profile>, fallback: impl Into<Profile>) -> Self {
681 Self { provider, profile: profile.into(), fallback: fallback.into() }
682 }
683}
684
685impl<P: Provider> Provider for FallbackProfileProvider<P> {
686 fn metadata(&self) -> Metadata {
687 self.provider.metadata()
688 }
689
690 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
691 let mut data = self.provider.data()?;
692 if let Some(fallback) = data.remove(&self.fallback) {
693 let mut inner = data.remove(&self.profile).unwrap_or_default();
694 for (k, v) in fallback {
695 inner.entry(k).or_insert(v);
696 }
697 Ok(self.profile.collect(inner))
698 } else {
699 Ok(data)
700 }
701 }
702
703 fn profile(&self) -> Option<Profile> {
704 Some(self.profile.clone())
705 }
706}