Skip to content

Commit 5da11a5

Browse files
committed
Auto merge of #7295 - zachlute:config-toml-extension, r=alexcrichton
Allow using 'config.toml' instead of just 'config' files. Fixes #7273 Note that this change only makes 'config.toml' optional to use instead of 'config'. If both exist, we will print a warning and prefer 'config', since that would be the existing behavior if both existed today. We should also consider a separate change to make config.toml the default and update docs, etc.
2 parents 531c4bf + 7119683 commit 5da11a5

File tree

3 files changed

+268
-31
lines changed

3 files changed

+268
-31
lines changed

src/cargo/util/config.rs

+88-31
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ impl Config {
662662
let mut cfg = CV::Table(HashMap::new(), PathBuf::from("."));
663663
let home = self.home_path.clone().into_path_unlocked();
664664

665-
walk_tree(path, &home, |path| {
665+
self.walk_tree(path, &home, |path| {
666666
let mut contents = String::new();
667667
let mut file = File::open(&path)?;
668668
file.read_to_string(&mut contents)
@@ -689,6 +689,76 @@ impl Config {
689689
}
690690
}
691691

692+
/// The purpose of this function is to aid in the transition to using
693+
/// .toml extensions on Cargo's config files, which were historically not used.
694+
/// Both 'config.toml' and 'credentials.toml' should be valid with or without extension.
695+
/// When both exist, we want to prefer the one without an extension for
696+
/// backwards compatibility, but warn the user appropriately.
697+
fn get_file_path(
698+
&self,
699+
dir: &Path,
700+
filename_without_extension: &str,
701+
warn: bool,
702+
) -> CargoResult<Option<PathBuf>> {
703+
let possible = dir.join(filename_without_extension);
704+
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));
705+
706+
if fs::metadata(&possible).is_ok() {
707+
if warn && fs::metadata(&possible_with_extension).is_ok() {
708+
// We don't want to print a warning if the version
709+
// without the extension is just a symlink to the version
710+
// WITH an extension, which people may want to do to
711+
// support multiple Cargo versions at once and not
712+
// get a warning.
713+
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
714+
target_path == possible_with_extension
715+
} else {
716+
false
717+
};
718+
719+
if !skip_warning {
720+
self.shell().warn(format!(
721+
"Both `{}` and `{}` exist. Using `{}`",
722+
possible.display(),
723+
possible_with_extension.display(),
724+
possible.display()
725+
))?;
726+
}
727+
}
728+
729+
Ok(Some(possible))
730+
} else if fs::metadata(&possible_with_extension).is_ok() {
731+
Ok(Some(possible_with_extension))
732+
} else {
733+
Ok(None)
734+
}
735+
}
736+
737+
fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
738+
where
739+
F: FnMut(&Path) -> CargoResult<()>,
740+
{
741+
let mut stash: HashSet<PathBuf> = HashSet::new();
742+
743+
for current in paths::ancestors(pwd) {
744+
if let Some(path) = self.get_file_path(&current.join(".cargo"), "config", true)? {
745+
walk(&path)?;
746+
stash.insert(path);
747+
}
748+
}
749+
750+
// Once we're done, also be sure to walk the home directory even if it's not
751+
// in our history to be sure we pick up that standard location for
752+
// information.
753+
if let Some(path) = self.get_file_path(home, "config", true)? {
754+
if !stash.contains(&path) {
755+
walk(&path)?;
756+
}
757+
}
758+
759+
Ok(())
760+
}
761+
692762
/// Gets the index for a registry.
693763
pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> {
694764
validate_package_name(registry, "registry name", "")?;
@@ -726,10 +796,10 @@ impl Config {
726796
/// present.
727797
fn load_credentials(&self, cfg: &mut ConfigValue) -> CargoResult<()> {
728798
let home_path = self.home_path.clone().into_path_unlocked();
729-
let credentials = home_path.join("credentials");
730-
if fs::metadata(&credentials).is_err() {
731-
return Ok(());
732-
}
799+
let credentials = match self.get_file_path(&home_path, "credentials", true)? {
800+
Some(credentials) => credentials,
801+
None => return Ok(()),
802+
};
733803

734804
let mut contents = String::new();
735805
let mut file = File::open(&credentials)?;
@@ -1673,36 +1743,23 @@ pub fn homedir(cwd: &Path) -> Option<PathBuf> {
16731743
::home::cargo_home_with_cwd(cwd).ok()
16741744
}
16751745

1676-
fn walk_tree<F>(pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
1677-
where
1678-
F: FnMut(&Path) -> CargoResult<()>,
1679-
{
1680-
let mut stash: HashSet<PathBuf> = HashSet::new();
1681-
1682-
for current in paths::ancestors(pwd) {
1683-
let possible = current.join(".cargo").join("config");
1684-
if fs::metadata(&possible).is_ok() {
1685-
walk(&possible)?;
1686-
stash.insert(possible);
1687-
}
1688-
}
1689-
1690-
// Once we're done, also be sure to walk the home directory even if it's not
1691-
// in our history to be sure we pick up that standard location for
1692-
// information.
1693-
let config = home.join("config");
1694-
if !stash.contains(&config) && fs::metadata(&config).is_ok() {
1695-
walk(&config)?;
1696-
}
1697-
1698-
Ok(())
1699-
}
1700-
17011746
pub fn save_credentials(cfg: &Config, token: String, registry: Option<String>) -> CargoResult<()> {
1747+
// If 'credentials.toml' exists, we should write to that, otherwise
1748+
// use the legacy 'credentials'. There's no need to print the warning
1749+
// here, because it would already be printed at load time.
1750+
let home_path = cfg.home_path.clone().into_path_unlocked();
1751+
let filename = match cfg.get_file_path(&home_path, "credentials", false)? {
1752+
Some(path) => match path.file_name() {
1753+
Some(filename) => Path::new(filename).to_owned(),
1754+
None => Path::new("credentials").to_owned(),
1755+
},
1756+
None => Path::new("credentials").to_owned(),
1757+
};
1758+
17021759
let mut file = {
17031760
cfg.home_path.create_dir()?;
17041761
cfg.home_path
1705-
.open_rw(Path::new("credentials"), cfg, "credentials' config file")?
1762+
.open_rw(filename, cfg, "credentials' config file")?
17061763
};
17071764

17081765
let (key, value) = {

tests/testsuite/config.rs

+134
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::borrow::Borrow;
22
use std::collections;
33
use std::fs;
4+
use std::io;
5+
use std::os;
6+
use std::path::Path;
47

58
use crate::support::{paths, project};
69
use cargo::core::{enable_nightly_features, Shell};
@@ -48,6 +51,50 @@ fn write_config(config: &str) {
4851
fs::write(path, config).unwrap();
4952
}
5053

54+
fn write_config_toml(config: &str) {
55+
let path = paths::root().join(".cargo/config.toml");
56+
fs::create_dir_all(path.parent().unwrap()).unwrap();
57+
fs::write(path, config).unwrap();
58+
}
59+
60+
// Several test fail on windows if the user does not have permission to
61+
// create symlinks (the `SeCreateSymbolicLinkPrivilege`). Instead of
62+
// disabling these test on Windows, use this function to test whether we
63+
// have permission, and return otherwise. This way, we still don't run these
64+
// tests most of the time, but at least we do if the user has the right
65+
// permissions.
66+
// This function is derived from libstd fs tests.
67+
pub fn got_symlink_permission() -> bool {
68+
if cfg!(unix) {
69+
return true;
70+
}
71+
let link = paths::root().join("some_hopefully_unique_link_name");
72+
let target = paths::root().join("nonexisting_target");
73+
74+
match symlink_file(&target, &link) {
75+
Ok(_) => true,
76+
// ERROR_PRIVILEGE_NOT_HELD = 1314
77+
Err(ref err) if err.raw_os_error() == Some(1314) => false,
78+
Err(_) => true,
79+
}
80+
}
81+
82+
#[cfg(unix)]
83+
fn symlink_file(target: &Path, link: &Path) -> io::Result<()> {
84+
os::unix::fs::symlink(target, link)
85+
}
86+
87+
#[cfg(windows)]
88+
fn symlink_file(target: &Path, link: &Path) -> io::Result<()> {
89+
os::windows::fs::symlink_file(target, link)
90+
}
91+
92+
fn symlink_config_to_config_toml() {
93+
let toml_path = paths::root().join(".cargo/config.toml");
94+
let symlink_path = paths::root().join(".cargo/config");
95+
t!(symlink_file(&toml_path, &symlink_path));
96+
}
97+
5198
fn new_config(env: &[(&str, &str)]) -> Config {
5299
enable_nightly_features(); // -Z advanced-env
53100
let output = Box::new(fs::File::create(paths::root().join("shell.out")).unwrap());
@@ -112,6 +159,93 @@ f1 = 123
112159
assert_eq!(s, S { f1: Some(456) });
113160
}
114161

162+
#[cargo_test]
163+
fn config_works_with_extension() {
164+
write_config_toml(
165+
"\
166+
[foo]
167+
f1 = 1
168+
",
169+
);
170+
171+
let config = new_config(&[]);
172+
173+
assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
174+
}
175+
176+
#[cargo_test]
177+
fn config_ambiguous_filename_symlink_doesnt_warn() {
178+
// Windows requires special permissions to create symlinks.
179+
// If we don't have permission, just skip this test.
180+
if !got_symlink_permission() {
181+
return;
182+
};
183+
184+
write_config_toml(
185+
"\
186+
[foo]
187+
f1 = 1
188+
",
189+
);
190+
191+
symlink_config_to_config_toml();
192+
193+
let config = new_config(&[]);
194+
195+
assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
196+
197+
// It should NOT have warned for the symlink.
198+
drop(config); // Paranoid about flushing the file.
199+
let path = paths::root().join("shell.out");
200+
let output = fs::read_to_string(path).unwrap();
201+
let unexpected = "\
202+
warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config`
203+
";
204+
if lines_match(unexpected, &output) {
205+
panic!(
206+
"Found unexpected:\n{}\nActual error:\n{}\n",
207+
unexpected, output
208+
);
209+
}
210+
}
211+
212+
#[cargo_test]
213+
fn config_ambiguous_filename() {
214+
write_config(
215+
"\
216+
[foo]
217+
f1 = 1
218+
",
219+
);
220+
221+
write_config_toml(
222+
"\
223+
[foo]
224+
f1 = 2
225+
",
226+
);
227+
228+
let config = new_config(&[]);
229+
230+
// It should use the value from the one without the extension for
231+
// backwards compatibility.
232+
assert_eq!(config.get::<Option<i32>>("foo.f1").unwrap(), Some(1));
233+
234+
// But it also should have warned.
235+
drop(config); // Paranoid about flushing the file.
236+
let path = paths::root().join("shell.out");
237+
let output = fs::read_to_string(path).unwrap();
238+
let expected = "\
239+
warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config`
240+
";
241+
if !lines_match(expected, &output) {
242+
panic!(
243+
"Did not find expected:\n{}\nActual error:\n{}\n",
244+
expected, output
245+
);
246+
}
247+
}
248+
115249
#[cargo_test]
116250
fn config_unused_fields() {
117251
write_config(

tests/testsuite/login.rs

+46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fs::{self, File};
22
use std::io::prelude::*;
3+
use std::path::PathBuf;
34

45
use crate::support::cargo_process;
56
use crate::support::install::cargo_home;
@@ -13,6 +14,15 @@ const ORIGINAL_TOKEN: &str = "api-token";
1314

1415
fn setup_new_credentials() {
1516
let config = cargo_home().join("credentials");
17+
setup_new_credentials_at(config);
18+
}
19+
20+
fn setup_new_credentials_toml() {
21+
let config = cargo_home().join("credentials.toml");
22+
setup_new_credentials_at(config);
23+
}
24+
25+
fn setup_new_credentials_at(config: PathBuf) {
1626
t!(fs::create_dir_all(config.parent().unwrap()));
1727
t!(t!(File::create(&config))
1828
.write_all(format!(r#"token = "{token}""#, token = ORIGINAL_TOKEN).as_bytes()));
@@ -84,6 +94,42 @@ fn login_with_new_credentials() {
8494
assert!(check_token(TOKEN, None));
8595
}
8696

97+
#[cargo_test]
98+
fn credentials_work_with_extension() {
99+
registry::init();
100+
setup_new_credentials_toml();
101+
102+
cargo_process("login --host")
103+
.arg(registry_url().to_string())
104+
.arg(TOKEN)
105+
.run();
106+
107+
// Ensure that we get the new token for the registry
108+
assert!(check_token(TOKEN, None));
109+
}
110+
111+
#[cargo_test]
112+
fn credentials_ambiguous_filename() {
113+
registry::init();
114+
setup_new_credentials();
115+
setup_new_credentials_toml();
116+
117+
cargo_process("login --host")
118+
.arg(registry_url().to_string())
119+
.arg(TOKEN)
120+
.with_stderr_contains(
121+
"\
122+
[WARNING] Both `[..]/credentials` and `[..]/credentials.toml` exist. Using `[..]/credentials`
123+
",
124+
)
125+
.run();
126+
127+
// It should use the value from the one without the extension
128+
// for backwards compatibility. check_token explicitly checks
129+
// 'credentials' without the extension, which is what we want.
130+
assert!(check_token(TOKEN, None));
131+
}
132+
87133
#[cargo_test]
88134
fn login_with_old_and_new_credentials() {
89135
setup_new_credentials();

0 commit comments

Comments
 (0)