use std::io::Write;
use std::path::Path;
use std::process::ExitCode;
use super::super::util::exit_code_from_i32;
use super::tool_resolution::resolve_compiler_path;
#[cfg(test)]
pub(super) static CWD_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
pub(super) fn release_cwd_for_command(cmd: &mut std::process::Command, child_cwd: &Path) {
cmd.current_dir(child_cwd);
let _ = std::env::set_current_dir(std::env::temp_dir());
}
fn run_with_released_cwd(
cmd: &mut std::process::Command,
) -> std::io::Result<std::process::ExitStatus> {
if let Ok(cwd) = std::env::current_dir() {
release_cwd_for_command(cmd, &cwd);
}
cmd.status()
}
pub(super) fn run_passthrough(args: &[String]) -> ExitCode {
let tool = &args[0];
let tool_args = args.get(1..).unwrap_or(&[]);
let resolved = resolve_compiler_path(tool);
let mut cmd = std::process::Command::new(&resolved);
cmd.args(tool_args);
match run_with_released_cwd(&mut cmd) {
Ok(status) => exit_code_from_i32(status.code().unwrap_or(1)),
Err(e) => {
eprintln!("zccache: failed to run {}: {e}", resolved.display());
ExitCode::FAILURE
}
}
}
pub(super) fn run_locally(
tool: &Path,
args: &[String],
cwd: &Path,
env: &[(String, String)],
stdin_bytes: &[u8],
reason: &str,
) -> ExitCode {
eprintln!(
"zccache[warn][F]: {reason}; running {} directly, uncached",
tool.display()
);
crate::cli_core::core::lifecycle::write_event(
crate::cli_core::core::lifecycle::EVENT_WRAPPER_LOCAL_FALLBACK,
serde_json::json!({
"tool": tool.to_string_lossy(),
"cwd": cwd.to_string_lossy(),
"reason": reason,
"phase": "pre-dispatch",
"route": "wrapper",
}),
);
let mut command = std::process::Command::new(tool);
command
.args(args)
.envs(env.iter().map(|(key, value)| (key, value)));
command.stdin(if stdin_bytes.is_empty() {
std::process::Stdio::inherit()
} else {
std::process::Stdio::piped()
});
release_cwd_for_command(&mut command, cwd);
let mut child = match command.spawn() {
Ok(child) => child,
Err(error) => {
eprintln!(
"zccache[err][F]: failed to run {} locally: {error}",
tool.display()
);
return ExitCode::FAILURE;
}
};
if !stdin_bytes.is_empty() {
if let Some(mut stdin) = child.stdin.take() {
if let Err(error) = stdin.write_all(stdin_bytes) {
eprintln!("zccache[err][F]: failed to replay compiler stdin: {error}");
return ExitCode::FAILURE;
}
}
}
match child.wait() {
Ok(status) => exit_code_from_i32(status.code().unwrap_or(1)),
Err(error) => {
eprintln!(
"zccache[err][F]: failed waiting for {}: {error}",
tool.display()
);
ExitCode::FAILURE
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn noop_tool() -> std::path::PathBuf {
if cfg!(windows) {
std::path::PathBuf::from("cmd.exe")
} else {
std::path::PathBuf::from("true")
}
}
fn noop_args() -> Vec<String> {
if cfg!(windows) {
vec!["/c".to_string(), "exit".to_string(), "0".to_string()]
} else {
Vec::new()
}
}
#[test]
fn run_passthrough_releases_wrapper_cwd() {
let _guard = CWD_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().ok();
let build_dir = tempfile::tempdir().unwrap();
let canonical_build_dir = std::fs::canonicalize(build_dir.path()).unwrap();
std::env::set_current_dir(&canonical_build_dir).unwrap();
let mut args = vec![noop_tool().to_string_lossy().into_owned()];
args.extend(noop_args());
let _ = run_passthrough(&args);
let after = std::env::current_dir().unwrap();
let after_canonical = std::fs::canonicalize(&after).unwrap_or(after);
assert_ne!(
after_canonical, canonical_build_dir,
"issue #555: run_passthrough must release the wrapper's CWD \
before returning so the build dir is not pinned by the wrapper's \
kernel handle on Windows",
);
if let Some(cwd) = original_cwd {
let _ = std::env::set_current_dir(cwd);
}
}
#[test]
fn direct_rustfmt_policy_releases_wrapper_cwd() {
let _guard = CWD_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().ok();
let build_dir = tempfile::tempdir().unwrap();
let canonical_build_dir = std::fs::canonicalize(build_dir.path()).unwrap();
std::env::set_current_dir(&canonical_build_dir).unwrap();
let tool = noop_tool();
let args: Vec<String> = noop_args();
let mut command = std::process::Command::new(&tool);
command.args(&args);
release_cwd_for_command(&mut command, &canonical_build_dir);
let _ = command.status();
let after = std::env::current_dir().unwrap();
let after_canonical = std::fs::canonicalize(&after).unwrap_or(after);
assert_ne!(
after_canonical, canonical_build_dir,
"issue #555: direct rustfmt execution must release the wrapper's CWD",
);
if let Some(cwd) = original_cwd {
let _ = std::env::set_current_dir(cwd);
}
}
#[test]
fn local_fallback_preserves_tool_exit_code() {
let _guard = CWD_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let original_cwd = std::env::current_dir().ok();
let tool = noop_tool();
let args = noop_args();
let exit = run_locally(
&tool,
&args,
&std::env::current_dir().unwrap(),
&[("ZCCACHE_TEST_FALLBACK".to_string(), "1".to_string())],
&[],
"test pre-dispatch failure",
);
assert_eq!(exit, ExitCode::SUCCESS);
if let Some(cwd) = original_cwd {
let _ = std::env::set_current_dir(cwd);
}
}
}