use std::process::ExitCode;
use std::sync::Arc;
use crate::core::NormalizedPath;
use crate::protocol::{ExecCachePolicy, ExecOutputStreams, Request, Response};
use super::daemon::ensure_daemon;
use super::util::{absolute_path, connect, exit_code_from_i32, resolve_endpoint};
pub(crate) struct ExecParams {
pub(crate) input_files: Vec<String>,
pub(crate) input_env: Vec<String>,
pub(crate) input_extra: Option<String>,
pub(crate) output_stdout: bool,
pub(crate) output_stderr: bool,
pub(crate) output_files: Vec<String>,
pub(crate) tool_hash: Option<String>,
pub(crate) no_cache: bool,
pub(crate) no_cwd_in_key: bool,
pub(crate) endpoint: Option<String>,
pub(crate) tool_command: Vec<String>,
pub(crate) include_scan: Vec<String>,
pub(crate) include_dir: Vec<String>,
pub(crate) system_include: Vec<String>,
pub(crate) iquote_dir: Vec<String>,
pub(crate) depfile: Option<String>,
pub(crate) non_deterministic: bool,
pub(crate) key_args_filter: Vec<String>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn cmd_exec(params: ExecParams) -> ExitCode {
let ExecParams {
input_files,
input_env,
input_extra,
output_stdout,
output_stderr,
output_files,
tool_hash,
no_cache,
no_cwd_in_key,
endpoint,
tool_command,
include_scan,
include_dir,
system_include,
iquote_dir,
depfile,
non_deterministic,
key_args_filter,
} = params;
if tool_command.is_empty() {
eprintln!(
"zccache exec: expected `--` followed by the tool command\n\
example: zccache exec --input-file src/foo.cpp -- fastled-lint src/foo.cpp"
);
return ExitCode::from(2);
}
let tool_str = &tool_command[0];
let tool_args: Vec<String> = tool_command[1..].to_vec();
let tool_resolved: NormalizedPath = match resolve_tool_path(tool_str) {
Some(p) => p,
None => {
eprintln!(
"zccache exec: tool not found: {tool_str} (PATH lookup failed and the value is not an absolute path)"
);
return ExitCode::from(127);
}
};
let mut env_pairs: Vec<(String, String)> = Vec::with_capacity(input_env.len());
for name in &input_env {
let value = std::env::var(name).unwrap_or_default();
env_pairs.push((name.clone(), value));
}
let cwd_norm: NormalizedPath = std::env::current_dir().unwrap_or_default().into();
let input_file_paths: Vec<NormalizedPath> =
input_files.iter().map(|p| absolute_path(p)).collect();
let output_file_paths: Vec<NormalizedPath> =
output_files.iter().map(NormalizedPath::from).collect();
let parsed_tool_hash: Option<[u8; 32]> = match tool_hash.as_deref() {
Some(hex) => match parse_hex_32(hex) {
Some(bytes) => Some(bytes),
None => {
eprintln!(
"zccache exec: --tool-hash must be 64 hex characters (32 bytes); got {} chars",
hex.len()
);
return ExitCode::from(2);
}
},
None => None,
};
let extra_bytes = Arc::new(input_extra.map(String::into_bytes).unwrap_or_default());
let include_scan_files: Vec<NormalizedPath> =
include_scan.iter().map(|p| absolute_path(p)).collect();
let include_dirs: Vec<NormalizedPath> = include_dir.iter().map(|p| absolute_path(p)).collect();
let system_include_dirs: Vec<NormalizedPath> =
system_include.iter().map(|p| absolute_path(p)).collect();
let iquote_dirs: Vec<NormalizedPath> = iquote_dir.iter().map(|p| absolute_path(p)).collect();
let depfile_path: Option<NormalizedPath> = depfile.as_deref().map(absolute_path);
let request = Request::GenericToolExec {
tool: tool_resolved,
args: tool_args,
cwd: cwd_norm,
env: env_pairs,
input_files: input_file_paths,
input_extra: extra_bytes,
output_streams: ExecOutputStreams {
stdout: output_stdout,
stderr: output_stderr,
},
output_files: output_file_paths,
tool_hash: parsed_tool_hash,
cache_policy: if no_cache {
ExecCachePolicy::Bypass
} else {
ExecCachePolicy::Normal
},
cwd_in_key: !no_cwd_in_key,
include_scan_files,
include_dirs,
system_include_dirs,
iquote_dirs,
depfile: depfile_path,
non_deterministic,
key_args_filter,
};
let endpoint = resolve_endpoint(endpoint.as_deref());
super::util::run_async(async move {
if let Err(e) = ensure_daemon(&endpoint).await {
eprintln!("zccache exec: failed to start daemon: {e}");
return ExitCode::from(2);
}
let mut conn = match connect(&endpoint).await {
Ok(c) => c,
Err(e) => {
eprintln!("zccache exec: cannot connect to daemon: {e}");
return ExitCode::from(2);
}
};
if let Err(e) = conn.send(&request).await {
eprintln!("zccache exec: send error: {e}");
return ExitCode::from(2);
}
match conn.recv::<Response>().await {
Ok(Some(Response::GenericToolExecResult {
exit_code,
stdout,
stderr,
cached,
cache_key_hex,
..
})) => {
use std::io::Write;
let _ = std::io::stdout().write_all(&stdout);
let _ = std::io::stderr().write_all(&stderr);
tracing::debug!(%cache_key_hex, %cached, "zccache exec result");
exit_code_from_i32(exit_code)
}
Ok(Some(Response::Error { message })) => {
eprintln!("zccache exec: daemon error: {message}");
ExitCode::from(2)
}
Ok(other) => {
eprintln!("zccache exec: unexpected response: {other:?}");
ExitCode::from(2)
}
Err(e) => {
eprintln!("zccache exec: recv error: {e}");
ExitCode::from(2)
}
}
})
}
fn resolve_tool_path(tool: &str) -> Option<NormalizedPath> {
let p = std::path::Path::new(tool);
if p.is_absolute() || p.components().count() > 1 {
if p.is_file() {
return Some(p.into());
}
return Some(absolute_path(tool));
}
let path_env = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path_env) {
let candidate = dir.join(tool);
if candidate.is_file() {
return Some(candidate.into());
}
#[cfg(windows)]
{
for ext in [".exe", ".bat", ".cmd"] {
let with_ext = dir.join(format!("{tool}{ext}"));
if with_ext.is_file() {
return Some(with_ext.into());
}
}
}
}
None
}
fn parse_hex_32(s: &str) -> Option<[u8; 32]> {
if s.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, byte) in out.iter_mut().enumerate() {
let start = i * 2;
*byte = u8::from_str_radix(&s[start..start + 2], 16).ok()?;
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_hex_32_round_trip() {
let bytes: [u8; 32] = std::array::from_fn(|i| i as u8);
let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(parse_hex_32(&hex), Some(bytes));
}
#[test]
fn parse_hex_32_rejects_wrong_length() {
assert!(parse_hex_32("deadbeef").is_none());
assert!(parse_hex_32(&"a".repeat(65)).is_none());
}
#[test]
fn parse_hex_32_rejects_non_hex() {
let mut bad = "0".repeat(64);
bad.replace_range(0..1, "z");
assert!(parse_hex_32(&bad).is_none());
}
}