Skip to content

Commit d94cb5d

Browse files
zackeesclaude
andcommitted
fbuild-core(subprocess): dedupe wait/kill, fix Windows PATH edge cases, add passthrough test
Three CodeRabbit findings in the new running-process-backed subprocess module: * The wait/timeout/kill/error-mapping block was duplicated verbatim between `run_command_passthrough` and `run_captured`. Extract a private `wait_or_kill()` helper so the contract (timeout kills the child, errors flatten into `FbuildError::Timeout` / `::Other`) lives in one place. * `run_command_passthrough` had zero test coverage. Add a smoke test that spawns `cmd /C exit 7` / `sh -c 'exit 7'` and asserts the returned `i32` equals the child's real exit code (plus a 0 case). * `compute_env` on Windows had two bugs: 1. `format!("{};{}", exe_dir_str, current_path)` produced a trailing-semicolon `PATH` of `"exe_dir;"` whenever the inherited `PATH` was empty. Guard the empty case explicitly. 2. The exe-dir prepend happened *before* `strip_msys_env`, and `strip_msys_env` drops PATH entries containing `\msys`, `/msys`, or `/usr/`. An exe_dir whose path contained one of those substrings would therefore be silently stripped back out. Reorder so `strip_msys_env` runs first and the exe_dir prepend is applied to the already-cleaned PATH. Addresses CodeRabbit review on #200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent aec6df4 commit d94cb5d

1 file changed

Lines changed: 60 additions & 29 deletions

File tree

crates/fbuild-core/src/subprocess.rs

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,23 @@ pub fn run_command_passthrough(
6363
timeout: Option<Duration>,
6464
) -> Result<i32> {
6565
let config = build_config(args, cwd, env, /*capture=*/ false, StdinMode::Inherit)?;
66-
let process = NativeProcess::new(config);
66+
let mut process = NativeProcess::new(config);
6767
process.start().map_err(|e| spawn_err(args, e))?;
68+
wait_or_kill(&mut process, args, timeout)
69+
}
70+
71+
/// Wait on a running `NativeProcess` and translate its outcome into a
72+
/// `FbuildError`-flavoured `Result<i32>`.
73+
///
74+
/// On timeout the child is killed (best-effort) before the error is
75+
/// returned. Shared by [`run_command_passthrough`] and [`run_captured`]
76+
/// so that the timeout/kill/error-mapping contract lives in exactly one
77+
/// place.
78+
fn wait_or_kill(
79+
process: &mut NativeProcess,
80+
args: &[&str],
81+
timeout: Option<Duration>,
82+
) -> Result<i32> {
6883
match process.wait(timeout) {
6984
Ok(code) => Ok(code),
7085
Err(ProcessError::Timeout) => {
@@ -86,24 +101,9 @@ fn run_captured(
86101
args: &[&str],
87102
timeout: Option<Duration>,
88103
) -> Result<ToolOutput> {
89-
let process = NativeProcess::new(config);
104+
let mut process = NativeProcess::new(config);
90105
process.start().map_err(|e| spawn_err(args, e))?;
91-
let exit_code = match process.wait(timeout) {
92-
Ok(code) => code,
93-
Err(ProcessError::Timeout) => {
94-
let _ = process.kill();
95-
return Err(FbuildError::Timeout(format!(
96-
"command timed out after {}s",
97-
timeout.map(|d| d.as_secs()).unwrap_or(0)
98-
)));
99-
}
100-
Err(e) => {
101-
return Err(FbuildError::Other(format!(
102-
"command {:?} failed: {}",
103-
args, e
104-
)))
105-
}
106-
};
106+
let exit_code = wait_or_kill(&mut process, args, timeout)?;
107107

108108
let stdout = join_lines(process.captured_stdout());
109109
let stderr = join_lines(process.captured_stderr());
@@ -200,6 +200,14 @@ fn compute_env(program: &str, overlay: Option<&[(&str, &str)]>) -> Option<Vec<(S
200200
{
201201
let mut env_map: std::collections::BTreeMap<String, String> = std::env::vars().collect();
202202

203+
// Strip MSYS/MSYS2 environment variables first, *before* we
204+
// prepend the exe directory, so that an exe_dir whose path
205+
// happens to contain `\msys` or `/usr/` is not filtered back
206+
// out by strip_msys_env's PATH-entry scrubbing.
207+
if is_msys_environment(&env_map) {
208+
strip_msys_env(&mut env_map);
209+
}
210+
203211
// Prepend the executable's directory to PATH so that child
204212
// processes (e.g., cc1plus launched by g++) can find DLLs in
205213
// the same bin/ dir.
@@ -211,20 +219,15 @@ fn compute_env(program: &str, overlay: Option<&[(&str, &str)]>) -> Option<Vec<(S
211219
.or_else(|| env_map.get("Path"))
212220
.cloned()
213221
.unwrap_or_default();
214-
env_map.insert(
215-
"PATH".to_string(),
216-
format!("{};{}", exe_dir_str, current_path),
217-
);
222+
let new_path = if current_path.is_empty() {
223+
exe_dir_str
224+
} else {
225+
format!("{};{}", exe_dir_str, current_path)
226+
};
227+
env_map.insert("PATH".to_string(), new_path);
218228
}
219229
}
220230

221-
// Strip MSYS/MSYS2 environment variables that interfere with
222-
// native Windows toolchain binaries finding their internal
223-
// tools.
224-
if is_msys_environment(&env_map) {
225-
strip_msys_env(&mut env_map);
226-
}
227-
228231
if let Some(vars) = overlay {
229232
for (k, v) in vars {
230233
env_map.insert((*k).to_string(), (*v).to_string());
@@ -370,4 +373,32 @@ mod tests {
370373
assert!(result.success());
371374
assert!(result.stderr.contains("err"), "got: {:?}", result);
372375
}
376+
377+
#[test]
378+
fn run_passthrough_propagates_exit_code() {
379+
// Smoke test for run_command_passthrough: the returned i32 must
380+
// match the child's real exit status. We use the shell `exit`
381+
// builtin (cross-platform via cmd /C on Windows, sh -c elsewhere)
382+
// because it is always available and the passthrough entry point
383+
// does not capture output for us to assert on.
384+
let args = if cfg!(windows) {
385+
vec!["cmd", "/C", "exit 7"]
386+
} else {
387+
vec!["sh", "-c", "exit 7"]
388+
};
389+
let code = run_command_passthrough(&args, None, None, None).unwrap();
390+
assert_eq!(
391+
code, 7,
392+
"run_command_passthrough should return child exit code"
393+
);
394+
395+
// And zero is zero.
396+
let args_ok = if cfg!(windows) {
397+
vec!["cmd", "/C", "exit 0"]
398+
} else {
399+
vec!["sh", "-c", "exit 0"]
400+
};
401+
let code_ok = run_command_passthrough(&args_ok, None, None, None).unwrap();
402+
assert_eq!(code_ok, 0);
403+
}
373404
}

0 commit comments

Comments
 (0)