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);
260
261impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
262 fn metadata(&self) -> Metadata {
263 self.0.metadata()
264 }
265
266 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
267 let mut map = self.0.data()?;
268 for (profile, dict) in &mut map {
269 if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
270 continue;
272 }
273
274 if profile.as_str().as_str() == Config::PROFILE_SECTION {
275 let dict2 = std::mem::take(dict);
278 *dict = dict2
279 .into_iter()
280 .map(|(profile_name, v)| {
281 let v = snake_case_value_keys(v);
283 (profile_name, v)
284 })
285 .collect();
286 continue;
287 }
288
289 let dict2 = std::mem::take(dict);
290 *dict = dict2.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect();
291 }
292 Ok(map)
293 }
294
295 fn profile(&self) -> Option<Profile> {
296 self.0.profile()
297 }
298}
299
300fn snake_case_value_keys(value: Value) -> Value {
302 match value {
303 Value::Dict(tag, dict) => {
304 let new_dict = dict
305 .into_iter()
306 .map(|(k, v)| (k.to_snake_case(), snake_case_value_keys(v)))
307 .collect();
308 Value::Dict(tag, new_dict)
309 }
310 Value::Array(tag, arr) => {
311 let new_arr = arr.into_iter().map(snake_case_value_keys).collect();
312 Value::Array(tag, new_arr)
313 }
314 other => other,
315 }
316}
317
318pub(crate) struct BackwardsCompatTomlProvider<P>(pub(crate) P);
320
321impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
322 fn metadata(&self) -> Metadata {
323 self.0.metadata()
324 }
325
326 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
327 let mut map = Map::new();
328 let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
329 .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
330 .map(Value::from)
331 .ok();
332 for (profile, mut dict) in self.0.data()? {
333 if let Some(v) = solc_env.clone() {
334 dict.insert("solc".to_string(), v);
336 } else if let Some(v) = dict.remove("solc_version") {
337 if !dict.contains_key("solc") {
339 dict.insert("solc".to_string(), v);
340 }
341 }
342 if let Some(v) = dict.remove("deny_warnings")
343 && !dict.contains_key("deny")
344 {
345 dict.insert("deny".to_string(), v);
346 }
347
348 map.insert(profile, dict);
349 }
350 Ok(map)
351 }
352
353 fn profile(&self) -> Option<Profile> {
354 self.0.profile()
355 }
356}
357
358pub(crate) struct DappHardhatDirProvider<'a>(pub(crate) &'a Path);
360
361impl Provider for DappHardhatDirProvider<'_> {
362 fn metadata(&self) -> Metadata {
363 Metadata::named("Dapp Hardhat dir compat")
364 }
365
366 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
367 let mut dict = Dict::new();
368 dict.insert(
369 "src".to_string(),
370 ProjectPathsConfig::find_source_dir(self.0)
371 .file_name()
372 .unwrap()
373 .to_string_lossy()
374 .to_string()
375 .into(),
376 );
377 dict.insert(
378 "out".to_string(),
379 ProjectPathsConfig::find_artifacts_dir(self.0)
380 .file_name()
381 .unwrap()
382 .to_string_lossy()
383 .to_string()
384 .into(),
385 );
386
387 let mut libs = vec![];
392 let node_modules = self.0.join("node_modules");
393 let lib = self.0.join("lib");
394 if node_modules.exists() {
395 if lib.exists() {
396 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
397 }
398 libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
399 } else {
400 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
401 }
402
403 dict.insert("libs".to_string(), libs.into());
404
405 Ok(Map::from([(Config::selected_profile(), dict)]))
406 }
407}
408
409pub(crate) struct DappEnvCompatProvider;
411
412impl Provider for DappEnvCompatProvider {
413 fn metadata(&self) -> Metadata {
414 Metadata::named("Dapp env compat")
415 }
416
417 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
418 use serde::de::Error as _;
419 use std::env;
420
421 let mut dict = Dict::new();
422 if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
423 dict.insert(
424 "block_number".to_string(),
425 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
426 );
427 }
428 if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
429 dict.insert("sender".to_string(), val.into());
430 }
431 if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
432 dict.insert(
433 "fork_block_number".to_string(),
434 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
435 );
436 } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
437 dict.insert(
438 "fork_block_number".to_string(),
439 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
440 );
441 }
442 if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
443 dict.insert(
444 "block_timestamp".to_string(),
445 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
446 );
447 }
448 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
449 dict.insert(
450 "optimizer_runs".to_string(),
451 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
452 );
453 }
454 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
455 let val = val.parse::<u8>().map_err(figment::Error::custom)?;
457 if val > 1 {
458 return Err(
459 format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
460 );
461 }
462 dict.insert("optimizer".to_string(), (val == 1).into());
463 }
464
465 if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
467 dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
468 }
469
470 let mut fuzz_dict = Dict::new();
471 if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
472 fuzz_dict.insert(
473 "runs".to_string(),
474 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
475 );
476 }
477 dict.insert("fuzz".to_string(), fuzz_dict.into());
478
479 let mut invariant_dict = Dict::new();
480 if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
481 invariant_dict.insert(
482 "depth".to_string(),
483 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
484 );
485 }
486 dict.insert("invariant".to_string(), invariant_dict.into());
487
488 Ok(Map::from([(Config::selected_profile(), dict)]))
489 }
490}
491
492pub(crate) struct RenameProfileProvider<P> {
508 provider: P,
509 from: Profile,
510 to: Profile,
511}
512
513impl<P> RenameProfileProvider<P> {
514 pub(crate) fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
515 Self { provider, from: from.into(), to: to.into() }
516 }
517}
518
519impl<P: Provider> Provider for RenameProfileProvider<P> {
520 fn metadata(&self) -> Metadata {
521 self.provider.metadata()
522 }
523
524 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
525 let mut data = self.provider.data()?;
526 if let Some(data) = data.remove(&self.from) {
527 return Ok(Map::from([(self.to.clone(), data)]));
528 }
529 Ok(Default::default())
530 }
531
532 fn profile(&self) -> Option<Profile> {
533 Some(self.to.clone())
534 }
535}
536
537struct UnwrapProfileProvider<P> {
553 provider: P,
554 wrapping_key: Profile,
555 profile: Profile,
556}
557
558impl<P> UnwrapProfileProvider<P> {
559 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
560 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
561 }
562}
563
564impl<P: Provider> Provider for UnwrapProfileProvider<P> {
565 fn metadata(&self) -> Metadata {
566 self.provider.metadata()
567 }
568
569 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
570 let mut data = self.provider.data()?;
571 if let Some(profiles) = data.remove(&self.wrapping_key) {
572 for (profile_str, profile_val) in profiles {
573 let profile = Profile::new(&profile_str);
574 if profile != self.profile {
575 continue;
576 }
577 match profile_val {
578 Value::Dict(_, dict) => return Ok(profile.collect(dict)),
579 bad_val => {
580 let mut err = Error::from(figment::error::Kind::InvalidType(
581 bad_val.to_actual(),
582 "dict".into(),
583 ));
584 err.metadata = Some(self.provider.metadata());
585 err.profile = Some(self.profile.clone());
586 return Err(err);
587 }
588 }
589 }
590 }
591 Ok(Default::default())
592 }
593
594 fn profile(&self) -> Option<Profile> {
595 Some(self.profile.clone())
596 }
597}
598
599pub(crate) struct WrapProfileProvider<P> {
615 provider: P,
616 wrapping_key: Profile,
617 profile: Profile,
618}
619
620impl<P> WrapProfileProvider<P> {
621 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
622 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
623 }
624}
625
626impl<P: Provider> Provider for WrapProfileProvider<P> {
627 fn metadata(&self) -> Metadata {
628 self.provider.metadata()
629 }
630
631 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
632 if let Some(inner) = self.provider.data()?.remove(&self.profile) {
633 let value = Value::from(inner);
634 let mut dict = Dict::new();
635 dict.insert(self.profile.as_str().as_str().to_snake_case(), value);
636 Ok(self.wrapping_key.collect(dict))
637 } else {
638 Ok(Default::default())
639 }
640 }
641
642 fn profile(&self) -> Option<Profile> {
643 Some(self.profile.clone())
644 }
645}
646
647pub(crate) struct OptionalStrictProfileProvider<P> {
670 provider: P,
671 profiles: Vec<Profile>,
672}
673
674impl<P> OptionalStrictProfileProvider<P> {
675 pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
676
677 pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
678 Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
679 }
680}
681
682impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
683 fn metadata(&self) -> Metadata {
684 self.provider.metadata()
685 }
686
687 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
688 let mut figment = Figment::from(&self.provider);
689 for profile in &self.profiles {
690 figment = figment.merge(UnwrapProfileProvider::new(
691 &self.provider,
692 Self::PROFILE_PROFILE,
693 profile.clone(),
694 ));
695 }
696 figment.data().map_err(|err| {
697 if let Err(root_err) = self.provider.data() {
702 return root_err;
703 }
704 err
705 })
706 }
707
708 fn profile(&self) -> Option<Profile> {
709 self.profiles.last().cloned()
710 }
711}
712
713pub struct FallbackProfileProvider<P> {
716 provider: P,
717 profile: Profile,
718 fallback: Profile,
719}
720
721impl<P> FallbackProfileProvider<P> {
722 pub fn new(provider: P, profile: impl Into<Profile>, fallback: impl Into<Profile>) -> Self {
724 Self { provider, profile: profile.into(), fallback: fallback.into() }
725 }
726}
727
728impl<P: Provider> Provider for FallbackProfileProvider<P> {
729 fn metadata(&self) -> Metadata {
730 self.provider.metadata()
731 }
732
733 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
734 let mut data = self.provider.data()?;
735 if let Some(fallback) = data.remove(&self.fallback) {
736 let mut inner = data.remove(&self.profile).unwrap_or_default();
737 for (k, v) in fallback {
738 inner.entry(k).or_insert(v);
739 }
740 Ok(self.profile.collect(inner))
741 } else {
742 Ok(data)
743 }
744 }
745
746 fn profile(&self) -> Option<Profile> {
747 Some(self.profile.clone())
748 }
749}