Skip to content

Commit a858aa0

Browse files
committed
Implement CARGO_CONFIG_PATH config file search override
This implements the CARGO_CONFIG_PATH environment variable, which defines in which order `cargo` searches for config files. This overrides the default "search parent directories" order. Fixes #7887
1 parent cf00ee1 commit a858aa0

File tree

2 files changed

+193
-85
lines changed

2 files changed

+193
-85
lines changed

src/cargo/util/config/mod.rs

+143-85
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,131 @@ macro_rules! get_value_typed {
117117
};
118118
}
119119

120+
/// The purpose of this function is to aid in the transition to using
121+
/// .toml extensions on Cargo's config files, which were historically not used.
122+
/// Both 'config.toml' and 'credentials.toml' should be valid with or without extension.
123+
/// When both exist, we want to prefer the one without an extension for
124+
/// backwards compatibility, but warn the user appropriately.
125+
fn get_file_path(
126+
dir: &Path,
127+
filename_without_extension: &str,
128+
warn: bool,
129+
shell: &RefCell<Shell>,
130+
) -> CargoResult<Option<PathBuf>> {
131+
let possible = dir.join(filename_without_extension);
132+
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));
133+
134+
if possible.exists() {
135+
if warn && possible_with_extension.exists() {
136+
// We don't want to print a warning if the version
137+
// without the extension is just a symlink to the version
138+
// WITH an extension, which people may want to do to
139+
// support multiple Cargo versions at once and not
140+
// get a warning.
141+
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
142+
target_path == possible_with_extension
143+
} else {
144+
false
145+
};
146+
147+
if !skip_warning {
148+
shell.borrow_mut().warn(format!(
149+
"Both `{}` and `{}` exist. Using `{}`",
150+
possible.display(),
151+
possible_with_extension.display(),
152+
possible.display()
153+
))?;
154+
}
155+
}
156+
157+
Ok(Some(possible))
158+
} else if possible_with_extension.exists() {
159+
Ok(Some(possible_with_extension))
160+
} else {
161+
Ok(None)
162+
}
163+
}
164+
165+
/// Select how config files are found
166+
#[derive(Debug)]
167+
pub enum ConfigResolver {
168+
/// Default heirarchical algorithm: walk up from current dir towards root, and
169+
/// also fall back to home directory.
170+
Heirarchical { cwd: PathBuf, home: Option<PathBuf> },
171+
/// Search a list of paths in the specified order
172+
ConfigPath { path: Vec<PathBuf> },
173+
}
174+
175+
impl ConfigResolver {
176+
fn from_env(cwd: &Path, home: Option<&Path>, env: &HashMap<String, String>) -> Self {
177+
if let Some(path) = env.get("CARGO_CONFIG_PATH") {
178+
Self::with_path(&path)
179+
} else {
180+
ConfigResolver::Heirarchical {
181+
cwd: cwd.to_path_buf(),
182+
home: home.map(Path::to_path_buf),
183+
}
184+
}
185+
}
186+
187+
fn with_path(path: &str) -> Self {
188+
ConfigResolver::ConfigPath {
189+
path: path.split(':').map(PathBuf::from).collect(),
190+
}
191+
}
192+
193+
fn resolve<F>(&self, shell: &RefCell<Shell>, mut walk: F) -> CargoResult<()>
194+
where
195+
F: FnMut(&Path) -> CargoResult<()>,
196+
{
197+
let mut stash: HashSet<PathBuf> = HashSet::new();
198+
199+
match self {
200+
ConfigResolver::Heirarchical { cwd, home } => {
201+
for current in paths::ancestors(cwd) {
202+
if let Some(path) =
203+
get_file_path(&current.join(".cargo"), "config", true, shell)?
204+
{
205+
walk(&path)?;
206+
stash.insert(path);
207+
}
208+
}
209+
210+
// Once we're done, also be sure to walk the home directory even if it's not
211+
// in our history to be sure we pick up that standard location for
212+
// information.
213+
if let Some(home) = home {
214+
if let Some(path) = get_file_path(home, "config", true, shell)? {
215+
if !stash.contains(&path) {
216+
walk(&path)?;
217+
}
218+
}
219+
}
220+
}
221+
222+
ConfigResolver::ConfigPath { path } => {
223+
for dir in path {
224+
if let Some(path) = get_file_path(&dir.join(".cargo"), "config", true, shell)? {
225+
if !stash.contains(dir) {
226+
walk(&path)?;
227+
stash.insert(path);
228+
}
229+
}
230+
}
231+
}
232+
}
233+
Ok(())
234+
}
235+
}
236+
120237
/// Configuration information for cargo. This is not specific to a build, it is information
121238
/// relating to cargo itself.
122239
#[derive(Debug)]
123240
pub struct Config {
124241
/// The location of the user's 'home' directory. OS-dependent.
125242
home_path: Filesystem,
243+
/// How to resolve paths of configuration files
244+
resolver: ConfigResolver,
126245
/// Information about how to write messages to the shell
127246
shell: RefCell<Shell>,
128247
/// A collection of configuration options
@@ -210,6 +329,7 @@ impl Config {
210329
};
211330

212331
Config {
332+
resolver: ConfigResolver::from_env(&cwd, Some(&homedir), &env),
213333
home_path: Filesystem::new(homedir),
214334
shell: RefCell::new(shell),
215335
cwd,
@@ -418,10 +538,14 @@ impl Config {
418538
}
419539
}
420540

421-
/// Reloads on-disk configuration values, starting at the given path and
422-
/// walking up its ancestors.
423541
pub fn reload_rooted_at<P: AsRef<Path>>(&mut self, path: P) -> CargoResult<()> {
424-
let values = self.load_values_from(path.as_ref())?;
542+
let path = path.as_ref();
543+
// We treat the path as advisory if the user has overridden the config search path
544+
let values = self.load_values_from(&ConfigResolver::from_env(
545+
path,
546+
homedir(path).as_ref().map(PathBuf::as_path),
547+
&self.env,
548+
))?;
425549
self.values.replace(values);
426550
self.merge_cli_args()?;
427551
Ok(())
@@ -774,22 +898,26 @@ impl Config {
774898

775899
/// Loads configuration from the filesystem.
776900
pub fn load_values(&self) -> CargoResult<HashMap<String, ConfigValue>> {
777-
self.load_values_from(&self.cwd)
901+
self.load_values_from(&self.resolver)
778902
}
779903

780-
fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> {
904+
fn load_values_from(
905+
&self,
906+
resolver: &ConfigResolver,
907+
) -> CargoResult<HashMap<String, ConfigValue>> {
781908
// This definition path is ignored, this is just a temporary container
782909
// representing the entire file.
783910
let mut cfg = CV::Table(HashMap::new(), Definition::Path(PathBuf::from(".")));
784-
let home = self.home_path.clone().into_path_unlocked();
785911

786-
self.walk_tree(path, &home, |path| {
787-
let value = self.load_file(path)?;
788-
cfg.merge(value, false)
789-
.chain_err(|| format!("failed to merge configuration at `{}`", path.display()))?;
790-
Ok(())
791-
})
792-
.chain_err(|| "could not load Cargo configuration")?;
912+
resolver
913+
.resolve(&self.shell, |path| {
914+
let value = self.load_file(path)?;
915+
cfg.merge(value, false).chain_err(|| {
916+
format!("failed to merge configuration at `{}`", path.display())
917+
})?;
918+
Ok(())
919+
})
920+
.chain_err(|| "could not load Cargo configuration")?;
793921

794922
match cfg {
795923
CV::Table(map, _) => Ok(map),
@@ -935,76 +1063,6 @@ impl Config {
9351063
Ok(())
9361064
}
9371065

938-
/// The purpose of this function is to aid in the transition to using
939-
/// .toml extensions on Cargo's config files, which were historically not used.
940-
/// Both 'config.toml' and 'credentials.toml' should be valid with or without extension.
941-
/// When both exist, we want to prefer the one without an extension for
942-
/// backwards compatibility, but warn the user appropriately.
943-
fn get_file_path(
944-
&self,
945-
dir: &Path,
946-
filename_without_extension: &str,
947-
warn: bool,
948-
) -> CargoResult<Option<PathBuf>> {
949-
let possible = dir.join(filename_without_extension);
950-
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));
951-
952-
if possible.exists() {
953-
if warn && possible_with_extension.exists() {
954-
// We don't want to print a warning if the version
955-
// without the extension is just a symlink to the version
956-
// WITH an extension, which people may want to do to
957-
// support multiple Cargo versions at once and not
958-
// get a warning.
959-
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
960-
target_path == possible_with_extension
961-
} else {
962-
false
963-
};
964-
965-
if !skip_warning {
966-
self.shell().warn(format!(
967-
"Both `{}` and `{}` exist. Using `{}`",
968-
possible.display(),
969-
possible_with_extension.display(),
970-
possible.display()
971-
))?;
972-
}
973-
}
974-
975-
Ok(Some(possible))
976-
} else if possible_with_extension.exists() {
977-
Ok(Some(possible_with_extension))
978-
} else {
979-
Ok(None)
980-
}
981-
}
982-
983-
fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
984-
where
985-
F: FnMut(&Path) -> CargoResult<()>,
986-
{
987-
let mut stash: HashSet<PathBuf> = HashSet::new();
988-
989-
for current in paths::ancestors(pwd) {
990-
if let Some(path) = self.get_file_path(&current.join(".cargo"), "config", true)? {
991-
walk(&path)?;
992-
stash.insert(path);
993-
}
994-
}
995-
996-
// Once we're done, also be sure to walk the home directory even if it's not
997-
// in our history to be sure we pick up that standard location for
998-
// information.
999-
if let Some(path) = self.get_file_path(home, "config", true)? {
1000-
if !stash.contains(&path) {
1001-
walk(&path)?;
1002-
}
1003-
}
1004-
1005-
Ok(())
1006-
}
1007-
10081066
/// Gets the index for a registry.
10091067
pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
10101068
validate_package_name(registry, "registry name", "")?;
@@ -1044,7 +1102,7 @@ impl Config {
10441102
/// Loads credentials config from the credentials file, if present.
10451103
pub fn load_credentials(&mut self) -> CargoResult<()> {
10461104
let home_path = self.home_path.clone().into_path_unlocked();
1047-
let credentials = match self.get_file_path(&home_path, "credentials", true)? {
1105+
let credentials = match get_file_path(&home_path, "credentials", true, &self.shell)? {
10481106
Some(credentials) => credentials,
10491107
None => return Ok(()),
10501108
};
@@ -1560,7 +1618,7 @@ pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -
15601618
// use the legacy 'credentials'. There's no need to print the warning
15611619
// here, because it would already be printed at load time.
15621620
let home_path = cfg.home_path.clone().into_path_unlocked();
1563-
let filename = match cfg.get_file_path(&home_path, "credentials", false)? {
1621+
let filename = match get_file_path(&home_path, "credentials", false, &cfg.shell)? {
15641622
Some(path) => match path.file_name() {
15651623
Some(filename) => Path::new(filename).to_owned(),
15661624
None => Path::new("credentials").to_owned(),

tests/testsuite/config.rs

+50
Original file line numberDiff line numberDiff line change
@@ -1258,3 +1258,53 @@ fn string_list_advanced_env() {
12581258
"error in environment variable `CARGO_KEY3`: expected string, found integer",
12591259
);
12601260
}
1261+
1262+
#[cargo_test]
1263+
fn config_path() {
1264+
let somedir = paths::root().join("somedir");
1265+
fs::create_dir_all(somedir.join(".cargo")).unwrap();
1266+
fs::write(
1267+
somedir.join(".cargo").join("config"),
1268+
"
1269+
b = 'replacedfile'
1270+
c = 'newfile'
1271+
",
1272+
)
1273+
.unwrap();
1274+
1275+
let otherdir = paths::root().join("otherdir");
1276+
fs::create_dir_all(otherdir.join(".cargo")).unwrap();
1277+
fs::write(
1278+
otherdir.join(".cargo").join("config"),
1279+
"
1280+
a = 'file1'
1281+
b = 'file2'
1282+
",
1283+
)
1284+
.unwrap();
1285+
1286+
let cwd = paths::root().join("cwd");
1287+
fs::create_dir_all(cwd.join(".cargo")).unwrap();
1288+
fs::write(
1289+
cwd.join(".cargo").join("config"),
1290+
"
1291+
a = 'BAD1'
1292+
b = 'BAD2'
1293+
c = 'BAD3'
1294+
",
1295+
)
1296+
.unwrap();
1297+
1298+
let mut config = ConfigBuilder::new()
1299+
.cwd(&cwd)
1300+
.env(
1301+
"CARGO_CONFIG_PATH",
1302+
format!("{}:{}", somedir.display(), otherdir.display()),
1303+
)
1304+
.build();
1305+
config.reload_rooted_at(paths::root()).unwrap();
1306+
1307+
assert_eq!(config.get::<String>("a").unwrap(), "file1");
1308+
assert_eq!(config.get::<String>("b").unwrap(), "replacedfile");
1309+
assert_eq!(config.get::<String>("c").unwrap(), "newfile");
1310+
}

0 commit comments

Comments
 (0)