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 if let Some(v) = dict.remove("deny_warnings")
306 && !dict.contains_key("deny")
307 {
308 dict.insert("deny".to_string(), v);
309 }
310
311 map.insert(profile, dict);
312 }
313 Ok(map)
314 }
315
316 fn profile(&self) -> Option<Profile> {
317 self.0.profile()
318 }
319}
320
321pub(crate) struct DappHardhatDirProvider<'a>(pub(crate) &'a Path);
323
324impl Provider for DappHardhatDirProvider<'_> {
325 fn metadata(&self) -> Metadata {
326 Metadata::named("Dapp Hardhat dir compat")
327 }
328
329 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
330 let mut dict = Dict::new();
331 dict.insert(
332 "src".to_string(),
333 ProjectPathsConfig::find_source_dir(self.0)
334 .file_name()
335 .unwrap()
336 .to_string_lossy()
337 .to_string()
338 .into(),
339 );
340 dict.insert(
341 "out".to_string(),
342 ProjectPathsConfig::find_artifacts_dir(self.0)
343 .file_name()
344 .unwrap()
345 .to_string_lossy()
346 .to_string()
347 .into(),
348 );
349
350 let mut libs = vec![];
355 let node_modules = self.0.join("node_modules");
356 let lib = self.0.join("lib");
357 if node_modules.exists() {
358 if lib.exists() {
359 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
360 }
361 libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
362 } else {
363 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
364 }
365
366 dict.insert("libs".to_string(), libs.into());
367
368 Ok(Map::from([(Config::selected_profile(), dict)]))
369 }
370}
371
372pub(crate) struct DappEnvCompatProvider;
374
375impl Provider for DappEnvCompatProvider {
376 fn metadata(&self) -> Metadata {
377 Metadata::named("Dapp env compat")
378 }
379
380 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
381 use serde::de::Error as _;
382 use std::env;
383
384 let mut dict = Dict::new();
385 if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
386 dict.insert(
387 "block_number".to_string(),
388 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
389 );
390 }
391 if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
392 dict.insert("sender".to_string(), val.into());
393 }
394 if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
395 dict.insert(
396 "fork_block_number".to_string(),
397 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
398 );
399 } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
400 dict.insert(
401 "fork_block_number".to_string(),
402 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
403 );
404 }
405 if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
406 dict.insert(
407 "block_timestamp".to_string(),
408 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
409 );
410 }
411 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
412 dict.insert(
413 "optimizer_runs".to_string(),
414 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
415 );
416 }
417 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
418 let val = val.parse::<u8>().map_err(figment::Error::custom)?;
420 if val > 1 {
421 return Err(
422 format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
423 );
424 }
425 dict.insert("optimizer".to_string(), (val == 1).into());
426 }
427
428 if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
430 dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
431 }
432
433 let mut fuzz_dict = Dict::new();
434 if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
435 fuzz_dict.insert(
436 "runs".to_string(),
437 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
438 );
439 }
440 dict.insert("fuzz".to_string(), fuzz_dict.into());
441
442 let mut invariant_dict = Dict::new();
443 if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
444 invariant_dict.insert(
445 "depth".to_string(),
446 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
447 );
448 }
449 dict.insert("invariant".to_string(), invariant_dict.into());
450
451 Ok(Map::from([(Config::selected_profile(), dict)]))
452 }
453}
454
455pub(crate) struct RenameProfileProvider<P> {
471 provider: P,
472 from: Profile,
473 to: Profile,
474}
475
476impl<P> RenameProfileProvider<P> {
477 pub(crate) fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
478 Self { provider, from: from.into(), to: to.into() }
479 }
480}
481
482impl<P: Provider> Provider for RenameProfileProvider<P> {
483 fn metadata(&self) -> Metadata {
484 self.provider.metadata()
485 }
486
487 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
488 let mut data = self.provider.data()?;
489 if let Some(data) = data.remove(&self.from) {
490 return Ok(Map::from([(self.to.clone(), data)]));
491 }
492 Ok(Default::default())
493 }
494
495 fn profile(&self) -> Option<Profile> {
496 Some(self.to.clone())
497 }
498}
499
500struct UnwrapProfileProvider<P> {
516 provider: P,
517 wrapping_key: Profile,
518 profile: Profile,
519}
520
521impl<P> UnwrapProfileProvider<P> {
522 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
523 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
524 }
525}
526
527impl<P: Provider> Provider for UnwrapProfileProvider<P> {
528 fn metadata(&self) -> Metadata {
529 self.provider.metadata()
530 }
531
532 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
533 let mut data = self.provider.data()?;
534 if let Some(profiles) = data.remove(&self.wrapping_key) {
535 for (profile_str, profile_val) in profiles {
536 let profile = Profile::new(&profile_str);
537 if profile != self.profile {
538 continue;
539 }
540 match profile_val {
541 Value::Dict(_, dict) => return Ok(profile.collect(dict)),
542 bad_val => {
543 let mut err = Error::from(figment::error::Kind::InvalidType(
544 bad_val.to_actual(),
545 "dict".into(),
546 ));
547 err.metadata = Some(self.provider.metadata());
548 err.profile = Some(self.profile.clone());
549 return Err(err);
550 }
551 }
552 }
553 }
554 Ok(Default::default())
555 }
556
557 fn profile(&self) -> Option<Profile> {
558 Some(self.profile.clone())
559 }
560}
561
562pub(crate) struct WrapProfileProvider<P> {
578 provider: P,
579 wrapping_key: Profile,
580 profile: Profile,
581}
582
583impl<P> WrapProfileProvider<P> {
584 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
585 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
586 }
587}
588
589impl<P: Provider> Provider for WrapProfileProvider<P> {
590 fn metadata(&self) -> Metadata {
591 self.provider.metadata()
592 }
593
594 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
595 if let Some(inner) = self.provider.data()?.remove(&self.profile) {
596 let value = Value::from(inner);
597 let mut dict = Dict::new();
598 dict.insert(self.profile.as_str().as_str().to_snake_case(), value);
599 Ok(self.wrapping_key.collect(dict))
600 } else {
601 Ok(Default::default())
602 }
603 }
604
605 fn profile(&self) -> Option<Profile> {
606 Some(self.profile.clone())
607 }
608}
609
610pub(crate) struct OptionalStrictProfileProvider<P> {
633 provider: P,
634 profiles: Vec<Profile>,
635}
636
637impl<P> OptionalStrictProfileProvider<P> {
638 pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
639
640 pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
641 Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
642 }
643}
644
645impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
646 fn metadata(&self) -> Metadata {
647 self.provider.metadata()
648 }
649
650 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
651 let mut figment = Figment::from(&self.provider);
652 for profile in &self.profiles {
653 figment = figment.merge(UnwrapProfileProvider::new(
654 &self.provider,
655 Self::PROFILE_PROFILE,
656 profile.clone(),
657 ));
658 }
659 figment.data().map_err(|err| {
660 if let Err(root_err) = self.provider.data() {
665 return root_err;
666 }
667 err
668 })
669 }
670
671 fn profile(&self) -> Option<Profile> {
672 self.profiles.last().cloned()
673 }
674}
675
676pub struct FallbackProfileProvider<P> {
679 provider: P,
680 profile: Profile,
681 fallback: Profile,
682}
683
684impl<P> FallbackProfileProvider<P> {
685 pub fn new(provider: P, profile: impl Into<Profile>, fallback: impl Into<Profile>) -> Self {
687 Self { provider, profile: profile.into(), fallback: fallback.into() }
688 }
689}
690
691impl<P: Provider> Provider for FallbackProfileProvider<P> {
692 fn metadata(&self) -> Metadata {
693 self.provider.metadata()
694 }
695
696 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
697 let mut data = self.provider.data()?;
698 if let Some(fallback) = data.remove(&self.fallback) {
699 let mut inner = data.remove(&self.profile).unwrap_or_default();
700 for (k, v) in fallback {
701 inner.entry(k).or_insert(v);
702 }
703 Ok(self.profile.collect(inner))
704 } else {
705 Ok(data)
706 }
707 }
708
709 fn profile(&self) -> Option<Profile> {
710 Some(self.profile.clone())
711 }
712}