Skip to content

Commit ca3cb2d

Browse files
authored
Rollup merge of rust-lang#137673 - ChrisDenton:search-path-bug, r=dtolnay
Fix Windows `Command` search path bug Currently `Command::new` on Windows works differently depending on whether any environment variable is set. For example, ```rust // Searches for "myapp" in the application and system paths first (aka Windows native behaviour). Command::new("myapp").spawn(); // Search for "myapp" in `PATH` first Command::new("myapp").env("a", "b").spawn(); ``` This is a bug because the search path should only change if `PATH` is changed for the child (i.e. `.env("PATH", "...")`). This was discussed in a libs-api meeting where the exact semantics of `Command::new` was not decided but there seemed to be broad agreement that this particular thing is just a bug that can be fixed. r? libs-api
2 parents 220fa0f + 4fcebee commit ca3cb2d

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

library/std/src/sys/pal/windows/process.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -260,9 +260,10 @@ impl Command {
260260
needs_stdin: bool,
261261
proc_thread_attribute_list: Option<&ProcThreadAttributeList<'_>>,
262262
) -> io::Result<(Process, StdioPipes)> {
263+
let env_saw_path = self.env.have_changed_path();
263264
let maybe_env = self.env.capture_if_changed();
264265

265-
let child_paths = if let Some(env) = maybe_env.as_ref() {
266+
let child_paths = if env_saw_path && let Some(env) = maybe_env.as_ref() {
266267
env.get(&EnvKey::new("PATH")).map(|s| s.as_os_str())
267268
} else {
268269
None
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//@ run-pass
2+
//@ only-windows
3+
//@ needs-subprocess
4+
//@ no-prefer-dynamic
5+
6+
// Test Windows std::process::Command search path semantics when setting PATH on the child process.
7+
// NOTE: The exact semantics here are (possibly) subject to change.
8+
9+
use std::process::Command;
10+
use std::{env, fs, path};
11+
12+
fn main() {
13+
if env::args().skip(1).any(|s| s == "--child") {
14+
child();
15+
} else if env::args().skip(1).any(|s| s == "--parent") {
16+
parent();
17+
} else {
18+
setup();
19+
}
20+
}
21+
22+
// Set up the directories so that there are three app dirs:
23+
// app: Where the parent app is run from
24+
// parent: In the parent's PATH env var
25+
// child: In the child's PATH env var
26+
fn setup() {
27+
let exe = env::current_exe().unwrap();
28+
29+
fs::create_dir_all("app").unwrap();
30+
fs::copy(&exe, "app/myapp.exe").unwrap();
31+
fs::create_dir_all("parent").unwrap();
32+
fs::copy(&exe, "parent/myapp.exe").unwrap();
33+
fs::create_dir_all("child").unwrap();
34+
fs::copy(&exe, "child/myapp.exe").unwrap();
35+
36+
let parent_path = path::absolute("parent").unwrap();
37+
let status =
38+
Command::new("./app/myapp.exe").env("PATH", parent_path).arg("--parent").status().unwrap();
39+
// print the status in case of abnormal exit
40+
dbg!(status);
41+
assert!(status.success());
42+
}
43+
44+
// The child simply prints the name of its parent directory.
45+
fn child() {
46+
let exe = env::current_exe().unwrap();
47+
let parent = exe.parent().unwrap().file_name().unwrap();
48+
println!("{}", parent.display());
49+
}
50+
51+
fn parent() {
52+
let exe = env::current_exe().unwrap();
53+
let name = exe.file_name().unwrap();
54+
55+
// By default, the application dir will be search first for the exe
56+
let output = Command::new(&name).arg("--child").output().unwrap();
57+
assert_eq!(output.stdout, b"app\n");
58+
59+
// Setting an environment variable should not change the above.
60+
let output = Command::new(&name).arg("--child").env("a", "b").output().unwrap();
61+
assert_eq!(output.stdout, b"app\n");
62+
63+
// Setting a child path means that path will be searched first.
64+
let child_path = path::absolute("child").unwrap();
65+
let output = Command::new(&name).arg("--child").env("PATH", child_path).output().unwrap();
66+
assert_eq!(output.stdout, b"child\n");
67+
68+
// Setting a child path that does not contain the exe (currently) means
69+
// we fallback to searching the app dir.
70+
let output = Command::new(&name).arg("--child").env("PATH", "").output().unwrap();
71+
assert_eq!(output.stdout, b"app\n");
72+
}

0 commit comments

Comments
 (0)