Skip to content

Commit c712f08

Browse files
committed
Do not add home bin path to PATH if it's already there
This is to allow users to control the order via PATH if they so desire. Fix #11020
1 parent 404889d commit c712f08

File tree

2 files changed

+102
-5
lines changed

2 files changed

+102
-5
lines changed

src/bin/cargo/main.rs

+22-5
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,28 @@ fn is_executable<P: AsRef<Path>>(path: P) -> bool {
212212
}
213213

214214
fn search_directories(config: &Config) -> Vec<PathBuf> {
215-
let mut dirs = vec![config.home().clone().into_path_unlocked().join("bin")];
216-
if let Some(val) = env::var_os("PATH") {
217-
dirs.extend(env::split_paths(&val));
218-
}
219-
dirs
215+
let mut path_dirs = if let Some(val) = env::var_os("PATH") {
216+
env::split_paths(&val).collect()
217+
} else {
218+
vec![]
219+
};
220+
221+
let home_bin = config.home().clone().into_path_unlocked().join("bin");
222+
223+
// If any of that PATH elements contains `home_bin`, do not
224+
// add it again. This is so that the users can control priority
225+
// of it using PATH, while preserving the historical
226+
// behavior of preferring it over system global directories even
227+
// when not in PATH at all.
228+
// See https://github.com/rust-lang/cargo/issues/11020 for details.
229+
//
230+
// Note: `p == home_bin` will ignore trailing slash, but we don't
231+
// `canonicalize` the paths.
232+
if !path_dirs.iter().any(|p| p == &home_bin) {
233+
path_dirs.insert(0, home_bin);
234+
};
235+
236+
path_dirs
220237
}
221238

222239
fn init_git_transports(config: &Config) {

tests/testsuite/cargo_command.rs

+80
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ use std::path::{Path, PathBuf};
77
use std::process::Stdio;
88
use std::str;
99

10+
use cargo_test_support::basic_manifest;
11+
use cargo_test_support::paths::CargoPathExt;
1012
use cargo_test_support::registry::Package;
1113
use cargo_test_support::tools::echo_subcommand;
1214
use cargo_test_support::{
1315
basic_bin_manifest, cargo_exe, cargo_process, paths, project, project_in_home,
1416
};
17+
use cargo_util::paths::join_paths;
1518

1619
fn path() -> Vec<PathBuf> {
1720
env::split_paths(&env::var_os("PATH").unwrap_or_default()).collect()
@@ -341,6 +344,83 @@ fn cargo_subcommand_env() {
341344
.run();
342345
}
343346

347+
#[cargo_test]
348+
fn cargo_cmd_bins_vs_explicit_path() {
349+
// Set up `cargo-foo` binary in two places: inside `$HOME/.cargo/bin` and outside of it
350+
//
351+
// Return paths to both places
352+
fn set_up_cargo_foo() -> (PathBuf, PathBuf) {
353+
let p = project()
354+
.at("cargo-foo")
355+
.file("Cargo.toml", &basic_manifest("cargo-foo", "1.0.0"))
356+
.file(
357+
"src/bin/cargo-foo.rs",
358+
r#"fn main() { println!("INSIDE"); }"#,
359+
)
360+
.file(
361+
"src/bin/cargo-foo2.rs",
362+
r#"fn main() { println!("OUTSIDE"); }"#,
363+
)
364+
.build();
365+
p.cargo("build").run();
366+
let cargo_bin_dir = paths::home().join(".cargo/bin");
367+
cargo_bin_dir.mkdir_p();
368+
let root_bin_dir = paths::root().join("bin");
369+
root_bin_dir.mkdir_p();
370+
let exe_name = format!("cargo-foo{}", env::consts::EXE_SUFFIX);
371+
fs::rename(p.bin("cargo-foo"), cargo_bin_dir.join(&exe_name)).unwrap();
372+
fs::rename(p.bin("cargo-foo2"), root_bin_dir.join(&exe_name)).unwrap();
373+
374+
(root_bin_dir, cargo_bin_dir)
375+
}
376+
377+
let (outside_dir, inside_dir) = set_up_cargo_foo();
378+
379+
// If `$CARGO_HOME/bin` is not in a path, prefer it over anything in `$PATH`.
380+
//
381+
// This is the historical behavior we don't want to break.
382+
cargo_process("foo").with_stdout_contains("INSIDE").run();
383+
384+
// When `$CARGO_HOME/bin` is in the `$PATH`
385+
// use only `$PATH` so the user-defined ordering is respected.
386+
{
387+
cargo_process("foo")
388+
.env(
389+
"PATH",
390+
join_paths(&[&inside_dir, &outside_dir], "PATH").unwrap(),
391+
)
392+
.with_stdout_contains("INSIDE")
393+
.run();
394+
395+
cargo_process("foo")
396+
// Note: trailing slash
397+
.env(
398+
"PATH",
399+
join_paths(&[inside_dir.join(""), outside_dir.join("")], "PATH").unwrap(),
400+
)
401+
.with_stdout_contains("INSIDE")
402+
.run();
403+
404+
cargo_process("foo")
405+
.env(
406+
"PATH",
407+
join_paths(&[&outside_dir, &inside_dir], "PATH").unwrap(),
408+
)
409+
.with_stdout_contains("OUTSIDE")
410+
.run();
411+
412+
cargo_process("foo")
413+
// Note: trailing slash
414+
.env(
415+
"PATH",
416+
join_paths(&[outside_dir.join(""), inside_dir.join("")], "PATH").unwrap(),
417+
)
418+
.with_stdout_contains("OUTSIDE")
419+
.run();
420+
}
421+
}
422+
423+
#[test]
344424
#[cargo_test]
345425
fn cargo_subcommand_args() {
346426
let p = echo_subcommand();

0 commit comments

Comments
 (0)