Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,15 @@ pub struct Config {
pub remappings: Vec<RelativeRemapping>,
/// Whether to autodetect remappings.
pub auto_detect_remappings: bool,
/// Scope of remappings resolution.
///
/// When set to `Global`, only project-level remappings are used and dependency
/// remappings (from nested foundry.toml or remappings.txt) are ignored.
/// This is useful for Soldeer and similar package managers where dependencies
/// ship their own remappings that conflict with the project's structure.
///
/// Defaults to `Default` which preserves current behavior.
pub remappings_scope: RemappingsScope,
/// Library addresses to link.
pub libraries: Vec<String>,
/// Whether to enable the build cache.
Expand Down Expand Up @@ -586,6 +595,32 @@ pub enum DenyLevel {
Notes,
}

/// Determines how remappings are resolved across the project and its dependencies.
///
/// By default, Foundry uses the "closest" remappings.txt to each file, so dependencies'
/// remappings can override or conflict with project remappings. With `Global`, only
/// project-level remappings are used, making them truly global and ignoring any
/// remappings.txt files inside dependencies.
///
/// This is particularly useful for Soldeer and similar package managers where dependencies
/// may ship their own remappings.txt files that reference paths like `node_modules/` which
/// don't exist in the project's structure.
///
/// See <https://github.com/foundry-rs/foundry/issues/12420>
#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RemappingsScope {
/// Use the default behavior: remappings are collected from the project root,
/// foundry.toml, remappings.txt, environment variables, and auto-detected from
/// library directories (including nested foundry.toml files in dependencies).
#[default]
Default,
/// Only use project-level remappings. Remappings from dependency directories
/// (e.g., remappings.txt or foundry.toml inside lib folders) are ignored.
/// Project remappings become truly global and override any dependency remappings.
Global,
}

// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
// booleans.
impl<'de> Deserialize<'de> for DenyLevel {
Expand Down Expand Up @@ -910,6 +945,9 @@ impl Config {
auto_detect_remappings: figment
.extract_inner::<bool>("auto_detect_remappings")
.unwrap_or(true),
remappings_scope: figment
.extract_inner::<RemappingsScope>("remappings_scope")
.unwrap_or_default(),
lib_paths: figment
.extract_inner::<Vec<PathBuf>>("libs")
.map(Cow::Owned)
Expand Down Expand Up @@ -2600,6 +2638,7 @@ impl Default for Config {
verbosity: 0,
remappings: vec![],
auto_detect_remappings: true,
remappings_scope: RemappingsScope::Default,
libraries: vec![],
ignored_error_codes: vec![
SolidityErrorCode::SpdxLicenseNotProvided,
Expand Down Expand Up @@ -3148,6 +3187,51 @@ mod tests {
});
}

#[test]
fn test_remappings_scope_global_ignores_dependency_remappings() {
// When remappings_scope = "global", only project-level remappings are used.
// Dependency foundry.toml remappings (e.g. node_modules paths) must be ignored.
// See https://github.com/foundry-rs/foundry/issues/12420
figment::Jail::expect_with(|jail| {
let lib_path = jail.directory().join("lib/nested-dep");
std::fs::create_dir_all(&lib_path).unwrap();
jail.create_file(
"lib/nested-dep/foundry.toml",
r#"
[profile.default]
remappings = ["nested-pkg/=node_modules/nested-pkg/"]
"#,
)?;

jail.create_file(
"foundry.toml",
r#"
[profile.default]
remappings_scope = "global"
remappings = ["nested-pkg/=lib/nested-dep/"]
auto_detect_remappings = true
"#,
)?;

let config = Config::load().unwrap();
assert_eq!(config.remappings_scope, RemappingsScope::Global);

let remappings: Vec<_> = config.get_all_remappings().collect();
// Project remapping must be present
assert!(
remappings.iter().any(|r| r.path.contains("lib/nested-dep")),
"project remapping should be present"
);
// Dependency remapping (node_modules) must NOT be present
assert!(
!remappings.iter().any(|r| r.path.contains("node_modules")),
"dependency remapping (node_modules) should be ignored when remappings_scope = global"
);

Ok(())
});
}

#[test]
fn test_can_update_libs() {
figment::Jail::expect_with(|jail| {
Expand Down
25 changes: 22 additions & 3 deletions crates/config/src/providers/remappings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::{Config, foundry_toml_dirs, remappings_from_env_var, remappings_from_newline};
use crate::{
Config, RemappingsScope, foundry_toml_dirs, remappings_from_env_var, remappings_from_newline,
};
use figment::{
Error, Figment, Metadata, Profile, Provider,
value::{Dict, Map},
Expand Down Expand Up @@ -129,6 +131,11 @@ impl Remappings {
pub struct RemappingsProvider<'a> {
/// Whether to auto detect remappings from the `lib_paths`
pub auto_detect_remappings: bool,
/// The scope of remappings resolution.
///
/// When set to `Global`, only project-level remappings are used and dependency
/// remappings (from nested foundry.toml or remappings.txt) are ignored.
pub remappings_scope: RemappingsScope,
/// The lib/dependency directories to scan for remappings
pub lib_paths: Cow<'a, Vec<PathBuf>>,
/// the root path used to turn an absolute `Remapping`, as we're getting it from
Expand Down Expand Up @@ -208,9 +215,21 @@ impl RemappingsProvider<'_> {
// TODO: if a lib specifies contexts for remappings manually, we need to figure out how to
// resolve that
if self.auto_detect_remappings {
// When remappings_scope is Global, skip nested foundry.toml remappings.
// This prevents dependency-level remappings from overriding project remappings.
// See https://github.com/foundry-rs/foundry/issues/12420
let is_global_scope = matches!(self.remappings_scope, RemappingsScope::Global);

let (nested_foundry_remappings, auto_detected_remappings) = rayon::join(
|| self.find_nested_foundry_remappings(),
|| self.auto_detect_remappings(),
|| {
if is_global_scope {
trace!(target: "forge", "skipping nested foundry.toml remappings (remappings_scope = Global)");
Vec::new()
} else {
self.find_nested_foundry_remappings().collect()
}
},
|| self.auto_detect_remappings().collect::<Vec<_>>(),
);

let mut lib_remappings = BTreeMap::new();
Expand Down