mod cache;
mod resolve;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;
pub const PROBE_SCHEMA_VERSION: u32 = 4;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedConfig {
pub schema_version: u32,
pub prober: String,
pub compiler_name: String,
pub version_line: String,
pub resolved_tokens: Option<Vec<String>>,
}
pub struct ProbeRequest<'a> {
pub compiler: &'a str,
pub args: &'a [String],
pub key_args: &'a [String],
pub per_tu_paths: &'a [String],
pub windows_aware: bool,
}
pub trait Prober {
fn id(&self) -> &'static str;
fn probe(&self, req: &ProbeRequest<'_>) -> Result<ResolvedConfig>;
}
pub struct CcProber;
impl Prober for CcProber {
fn id(&self) -> &'static str {
"cc"
}
fn probe(&self, req: &ProbeRequest<'_>) -> Result<ResolvedConfig> {
let output = Command::new(req.compiler)
.env("LC_ALL", "C")
.arg("--version")
.output()
.with_context(|| format!("running `{} --version`", req.compiler))?;
if !output.status.success() {
anyhow::bail!("`{} --version` exited {}", req.compiler, output.status);
}
let version_line = String::from_utf8_lossy(&output.stdout)
.lines()
.next()
.unwrap_or("unknown")
.to_string();
let compiler_name = Path::new(req.compiler)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(req.compiler)
.to_string();
Ok(ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: self.id().to_string(),
compiler_name,
version_line,
resolved_tokens: resolve_invocation(
req.compiler,
req.args,
req.windows_aware,
req.per_tu_paths,
),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProbedFamily {
Gnu,
Clang,
}
fn family_probe_cache_dir() -> std::path::PathBuf {
std::env::var_os("KACHE_CACHE_DIR")
.map(|s| crate::config::shellexpand(&s.to_string_lossy()))
.unwrap_or_else(crate::config::default_cache_dir)
}
pub fn probe_compiler_family(program: &str) -> Option<ProbedFamily> {
let cache_dir = family_probe_cache_dir();
let key = cache::probe_key_isolated("cc-family", program);
if let Some(ref k) = key
&& let Some(hit) = cache::load(&cache_dir, k)
{
match hit.version_line.as_str() {
"clang" => return Some(ProbedFamily::Clang),
"gnu" => return Some(ProbedFamily::Gnu),
"none" => return None, _ => {} }
}
let family = run_family_probe(program);
let family_str = match family {
Ok(Some(ProbedFamily::Clang)) => "clang",
Ok(Some(ProbedFamily::Gnu)) => "gnu",
Ok(None) => "none",
Err(_) => return None, };
if let Some(ref k) = key {
cache::store(
&cache_dir,
k,
&ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: "cc-family".to_string(),
compiler_name: std::path::Path::new(program)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(program)
.to_string(),
version_line: family_str.to_string(),
resolved_tokens: None,
},
);
}
match family {
Ok(Some(f)) => Some(f),
_ => None,
}
}
const FAMILY_PROBE_SOURCE: &[u8] = b"\
#if defined(__clang__)\n\
KACHE_PROBE_CLANG\n\
#elif defined(__GNUC__)\n\
KACHE_PROBE_GNU\n\
#endif\n";
fn run_family_probe(program: &str) -> Result<Option<ProbedFamily>, ()> {
use std::io::Read;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
let source_file = tempfile::NamedTempFile::new().map_err(|_| ())?;
std::fs::write(source_file.path(), FAMILY_PROBE_SOURCE).map_err(|_| ())?;
let mut child_cmd = Command::new(program);
child_cmd
.args(["-E", "-P", "-x", "c"])
.arg(source_file.path())
.env("LC_ALL", "C")
.env("KACHE_FAMILY_PROBE_ACTIVE", "1")
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null());
crate::platform::configure_process_group(&mut child_cmd);
let mut child = match child_cmd.spawn() {
Ok(c) => c,
Err(_) => return Err(()),
};
let pid = child.id();
let mut stdout_handle = match child.stdout.take() {
Some(s) => s,
None => return Err(()),
};
let (tx, rx) = std::sync::mpsc::channel();
let tx_read = tx.clone();
std::thread::spawn(move || {
const MAX_PROBE_OUTPUT: usize = 1 << 20; let mut buf = Vec::with_capacity(8192);
let mut chunk = [0u8; 8192];
loop {
match stdout_handle.read(&mut chunk) {
Ok(0) => break,
Ok(n) => {
let retain = n.min(MAX_PROBE_OUTPUT.saturating_sub(buf.len()));
buf.extend_from_slice(&chunk[..retain]);
}
Err(_) => break,
}
}
let _ = tx_read.send(Ok(buf));
});
let tx_wait = tx.clone();
std::thread::spawn(move || {
let status = child.wait().ok();
let _ = tx_wait.send(Err(status));
});
let mut output = None;
let mut exit_status = None;
let start = Instant::now();
let timeout = Duration::from_secs(5);
loop {
if matches!((output.is_some(), exit_status.is_some()), (true, true)) {
break;
}
if start.elapsed() >= timeout {
break;
}
let remaining = timeout.saturating_sub(start.elapsed());
match rx.recv_timeout(remaining) {
Ok(Ok(buf)) => output = Some(buf),
Ok(Err(status)) => exit_status = Some(status),
Err(_) => break,
}
}
let Some(output_buf) = output.as_ref() else {
crate::platform::kill_process_group(pid);
return Err(());
};
let Some(Some(status)) = exit_status.as_ref() else {
crate::platform::kill_process_group(pid);
return Err(());
};
if !status.success() {
return Ok(None);
}
let stdout_str = String::from_utf8_lossy(output_buf);
let clang = stdout_str.contains("KACHE_PROBE_CLANG");
let gnu = stdout_str.contains("KACHE_PROBE_GNU");
match (clang, gnu) {
(true, false) => Ok(Some(ProbedFamily::Clang)),
(false, true) => Ok(Some(ProbedFamily::Gnu)),
_ => Ok(None),
}
}
fn resolve_invocation(
compiler: &str,
args: &[String],
windows_aware: bool,
per_tu_paths: &[String],
) -> Option<Vec<String>> {
let output = Command::new(compiler)
.env("LC_ALL", "C")
.arg("-###")
.args(args)
.output()
.ok()?;
let stderr = String::from_utf8_lossy(&output.stderr);
let resolved = resolve::resolved_semantic_tokens(&stderr, windows_aware, per_tu_paths);
if resolved.is_none() {
tracing::debug!(
compiler,
exit_code = ?output.status.code(),
stderr_lines = stderr.lines().count(),
stdout_lines = output.stdout.iter().filter(|b| **b == b'\n').count(),
"cc -### resolved no cc1 line; probe-captured flags will refuse. head:\n{}",
probe_stderr_head(&stderr)
);
}
resolved
}
fn probe_stderr_head(stderr: &str) -> String {
const MAX_LINES: usize = 12;
const MAX_CHARS: usize = 300;
stderr
.lines()
.take(MAX_LINES)
.map(|line| {
let line = line.trim();
match line.char_indices().nth(MAX_CHARS) {
Some((cut, _)) => format!("{}…", &line[..cut]),
None => line.to_string(),
}
})
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Debug)]
pub enum LiveProbeDiagnostic {
NoCompiler,
ProbeError { detail: String },
Resolved { version_line: String },
Unresolved {
version_line: String,
stderr_head: String,
},
}
pub fn live_probe_diagnostic() -> LiveProbeDiagnostic {
live_probe_diagnostic_for("cc")
}
fn live_probe_diagnostic_for(compiler: &str) -> LiveProbeDiagnostic {
match Command::new(compiler)
.env("LC_ALL", "C")
.arg("--version")
.output()
{
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return LiveProbeDiagnostic::NoCompiler;
}
Err(error) => {
return LiveProbeDiagnostic::ProbeError {
detail: format!("could not run `cc --version`: {error}"),
};
}
Ok(output) if !output.status.success() => {
return LiveProbeDiagnostic::ProbeError {
detail: format!("`{compiler} --version` exited {}", output.status),
};
}
Ok(_) => {}
}
let dir = match tempfile::tempdir() {
Ok(dir) => dir,
Err(error) => {
return LiveProbeDiagnostic::ProbeError {
detail: format!("could not create compiler-probe directory: {error}"),
};
}
};
let source = dir.path().join("kache-doctor-probe.c");
if let Err(error) = std::fs::write(&source, "int kache_doctor_probe(void) { return 0; }\n") {
return LiveProbeDiagnostic::ProbeError {
detail: format!("could not write compiler-probe source: {error}"),
};
}
let args: Vec<String> = ["-O2", "-x", "c", "-c"]
.iter()
.map(|s| s.to_string())
.chain([source.to_string_lossy().into_owned()])
.collect();
let req = ProbeRequest {
compiler,
args: &args,
key_args: &args,
per_tu_paths: &[],
windows_aware: true,
};
let config = match CcProber.probe(&req) {
Ok(config) => config,
Err(error) => {
return LiveProbeDiagnostic::ProbeError {
detail: format!("compiler probe failed: {error:#}"),
};
}
};
if config.resolved_tokens.is_some() {
return LiveProbeDiagnostic::Resolved {
version_line: config.version_line,
};
}
let stderr_head = Command::new(compiler)
.env("LC_ALL", "C")
.arg("-###")
.args(&args)
.output()
.map(|o| probe_stderr_head(&String::from_utf8_lossy(&o.stderr)))
.unwrap_or_default();
LiveProbeDiagnostic::Unresolved {
version_line: config.version_line,
stderr_head,
}
}
pub fn probe(
cache_dir: &Path,
prober: &dyn Prober,
req: &ProbeRequest<'_>,
) -> Result<ResolvedConfig> {
let key = cache::probe_key(prober.id(), req);
if let Some(key) = &key
&& let Some(hit) = cache::load(cache_dir, key)
{
return Ok(hit);
}
crate::opcounts::record_probe_run();
let config = prober.probe(req)?;
if let Some(key) = &key {
cache::store(cache_dir, key, &config);
}
Ok(config)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_family(s: &str) -> Option<ProbedFamily> {
match s {
"clang" => Some(ProbedFamily::Clang),
"gnu" => Some(ProbedFamily::Gnu),
_ => None,
}
}
use std::sync::atomic::{AtomicUsize, Ordering};
use tempfile::{NamedTempFile, TempDir};
#[derive(Default)]
struct CountingProber {
runs: AtomicUsize,
}
impl Prober for CountingProber {
fn id(&self) -> &'static str {
"test"
}
fn probe(&self, _req: &ProbeRequest<'_>) -> Result<ResolvedConfig> {
self.runs.fetch_add(1, Ordering::SeqCst);
Ok(ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: "test".to_string(),
compiler_name: "fake".to_string(),
version_line: "fake 1.0".to_string(),
resolved_tokens: None,
})
}
}
fn req(compiler: &str) -> ProbeRequest<'_> {
ProbeRequest {
compiler,
args: &[],
key_args: &[],
per_tu_paths: &[],
windows_aware: true,
}
}
#[test]
fn probe_runs_prober_once_then_serves_from_cache() {
let _lock = crate::config::config_path_lock();
let cache = TempDir::new().unwrap();
let compiler = NamedTempFile::new().unwrap();
let prober = CountingProber::default();
let req = req(compiler.path().to_str().unwrap());
let first = probe(cache.path(), &prober, &req).unwrap();
let second = probe(cache.path(), &prober, &req).unwrap();
assert_eq!(first, second, "memoized result must match the original");
assert_eq!(
prober.runs.load(Ordering::SeqCst),
1,
"second probe must be served from the on-disk cache"
);
}
#[test]
fn probe_falls_back_to_running_when_compiler_is_unresolvable() {
let _lock = crate::config::config_path_lock();
let cache = TempDir::new().unwrap();
let prober = CountingProber::default();
let req = req("/nonexistent/kache-probe-test-cc");
let _ = probe(cache.path(), &prober, &req).unwrap();
let _ = probe(cache.path(), &prober, &req).unwrap();
assert_eq!(
prober.runs.load(Ordering::SeqCst),
2,
"an unkeyable probe is not memoized — both calls run"
);
}
#[test]
fn cc_prober_has_stable_id() {
assert_eq!(CcProber.id(), "cc");
}
#[test]
fn cc_prober_reads_a_real_compiler_version() {
let Ok(config) = CcProber.probe(&req("cc")) else {
return;
};
assert!(
!config.version_line.is_empty(),
"version line should be populated"
);
assert_eq!(config.prober, "cc");
assert_eq!(config.schema_version, PROBE_SCHEMA_VERSION);
}
#[test]
fn live_probe_diagnostic_classifies_an_absent_compiler() {
match live_probe_diagnostic_for("kache-test-definitely-not-a-compiler") {
LiveProbeDiagnostic::NoCompiler => {}
other => panic!("absent compiler must classify NoCompiler, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn live_probe_diagnostic_flags_a_broken_compiler_as_probe_error() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let unexecutable = dir.path().join("cc-unexecutable");
std::fs::write(&unexecutable, "#!/bin/sh\nexit 0\n").unwrap();
std::fs::set_permissions(&unexecutable, std::fs::Permissions::from_mode(0o644)).unwrap();
match live_probe_diagnostic_for(&unexecutable.to_string_lossy()) {
LiveProbeDiagnostic::ProbeError { detail } => {
assert!(
detail.contains("could not run"),
"unexpected detail: {detail}"
);
}
other => panic!("unexecutable compiler must be ProbeError, got {other:?}"),
}
let failing = dir.path().join("cc-version-fails");
std::fs::write(&failing, "#!/bin/sh\nexit 1\n").unwrap();
std::fs::set_permissions(&failing, std::fs::Permissions::from_mode(0o755)).unwrap();
match live_probe_diagnostic_for(&failing.to_string_lossy()) {
LiveProbeDiagnostic::ProbeError { detail } => {
assert!(
detail.contains("--version` exited")
&& !detail.contains("compiler probe failed"),
"expected the --version preflight to report the failure, got: {detail}"
);
}
other => panic!("--version failure must be ProbeError, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn live_probe_diagnostic_classifies_unresolvable_and_real_compilers() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let fake = dir.path().join("cc-resolves-nothing");
std::fs::write(&fake, "#!/bin/sh\necho fake-cc 1.0\nexit 0\n").unwrap();
std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
match live_probe_diagnostic_for(&fake.to_string_lossy()) {
LiveProbeDiagnostic::Unresolved { version_line, .. } => {
assert_eq!(version_line, "fake-cc 1.0");
}
other => panic!("resolving nothing must be Unresolved, got {other:?}"),
}
match live_probe_diagnostic_for("cc") {
LiveProbeDiagnostic::Resolved { .. } | LiveProbeDiagnostic::Unresolved { .. } => {}
LiveProbeDiagnostic::NoCompiler => eprintln!("skipping: no `cc` on PATH"),
LiveProbeDiagnostic::ProbeError { detail } => {
panic!("a working host `cc` must not classify as ProbeError: {detail}")
}
}
}
#[test]
fn cc_prober_resolves_the_invocation_with_flags() {
let src = NamedTempFile::new().unwrap();
let args: Vec<String> = ["-O2", "-x", "c", "-c", src.path().to_str().unwrap()]
.iter()
.map(|s| s.to_string())
.collect();
let request = ProbeRequest {
compiler: "cc",
args: &args,
key_args: &args,
per_tu_paths: &[],
windows_aware: true,
};
let Ok(config) = CcProber.probe(&request) else {
eprintln!("skipping: no `cc` on PATH");
return;
};
let family = match run_family_probe("cc") {
Ok(Some(f)) => f,
_ => {
eprintln!("skipping: `cc` is not a gcc/clang-family driver");
return;
}
};
let Some(tokens) = config.resolved_tokens else {
let head = Command::new("cc")
.env("LC_ALL", "C")
.arg("-###")
.args(&args)
.output()
.map(|o| probe_stderr_head(&String::from_utf8_lossy(&o.stderr)))
.unwrap_or_else(|e| format!("(could not re-run cc -###: {e})"));
panic!(
"`cc -###` resolved no compile line on a {family:?}-family driver \
({}); every probe-keyed flag would silently refuse to cache (#626).\n\
-### head:\n{head}",
config.version_line
);
};
assert!(
tokens.iter().any(|t| t == "-O2"),
"resolved tokens should carry -O2: {tokens:?}"
);
}
#[test]
fn probe_stderr_head_is_bounded_on_lines_and_chars() {
let long_line = "x".repeat(1000);
let many = (0..50)
.map(|i| format!("line{i} {long_line}"))
.collect::<Vec<_>>()
.join("\n");
let head = super::probe_stderr_head(&many);
assert_eq!(head.lines().count(), 12, "line budget must be enforced");
for line in head.lines() {
assert!(
line.chars().count() <= 301,
"char budget must be enforced (300 + ellipsis): {}",
line.chars().count()
);
assert!(line.ends_with('\u{2026}'), "a clipped line must say so");
}
}
#[test]
fn probe_stderr_head_clips_on_char_boundaries() {
let wide = "é".repeat(400);
let head = super::probe_stderr_head(&wide);
assert!(head.chars().count() <= 301);
assert!(head.starts_with('é'));
}
#[test]
fn probe_stderr_head_leaves_short_output_alone() {
let head = super::probe_stderr_head("clang version 19\nTarget: x86_64\n");
assert_eq!(head, "clang version 19\nTarget: x86_64");
}
#[test]
fn family_probe_cache_dir_expands_tilde() {
let Some(home) = dirs::home_dir() else {
eprintln!("skipping: no home dir");
return;
};
let lock = crate::config::config_path_lock();
let previous = std::env::var_os("KACHE_CACHE_DIR");
unsafe { std::env::set_var("KACHE_CACHE_DIR", "~") };
let bare = family_probe_cache_dir();
unsafe { std::env::set_var("KACHE_CACHE_DIR", "~/kache-cache") };
let nested = family_probe_cache_dir();
unsafe { std::env::set_var("KACHE_CACHE_DIR", "/abs/kache-cache") };
let absolute = family_probe_cache_dir();
unsafe {
match previous.as_ref() {
Some(prev) => std::env::set_var("KACHE_CACHE_DIR", prev),
None => std::env::remove_var("KACHE_CACHE_DIR"),
}
}
drop(lock);
assert_eq!(bare, home, "bare tilde must expand to the home dir");
assert_eq!(nested, home.join("kache-cache"));
assert_eq!(absolute, std::path::PathBuf::from("/abs/kache-cache"));
}
struct TestCacheDirGuard {
_lock: crate::test_support::ProcessStateTestGuard,
previous: Option<std::ffi::OsString>,
}
impl Drop for TestCacheDirGuard {
fn drop(&mut self) {
unsafe {
match self.previous.as_ref() {
Some(prev) => std::env::set_var("KACHE_CACHE_DIR", prev),
None => std::env::remove_var("KACHE_CACHE_DIR"),
}
}
}
}
fn set_test_cache_dir(path: &std::path::Path) -> TestCacheDirGuard {
let lock = crate::config::config_path_lock();
let previous = std::env::var_os("KACHE_CACHE_DIR");
unsafe {
std::env::set_var("KACHE_CACHE_DIR", path);
}
TestCacheDirGuard {
_lock: lock,
previous,
}
}
#[test]
fn parse_family_handles_valid_and_invalid_inputs() {
assert_eq!(parse_family("clang"), Some(ProbedFamily::Clang));
assert_eq!(parse_family("gnu"), Some(ProbedFamily::Gnu));
assert_eq!(parse_family("invalid"), None);
assert_eq!(parse_family(""), None);
}
#[test]
fn family_probe_detects_system_cc() {
let temp = TempDir::new().unwrap();
let _guard = set_test_cache_dir(temp.path());
let res = probe_compiler_family("cc");
if res.is_none() {
return;
}
assert!(matches!(
res,
Some(ProbedFamily::Clang) | Some(ProbedFamily::Gnu)
));
}
#[test]
fn family_probe_returns_none_for_non_compiler() {
let temp = TempDir::new().unwrap();
let _guard = set_test_cache_dir(temp.path());
let res = probe_compiler_family("cargo");
assert_eq!(res, None);
}
#[test]
fn family_probe_cached_result_roundtrips() {
let temp = TempDir::new().unwrap();
let _guard = set_test_cache_dir(temp.path());
let compiler =
create_mock_probe_script(temp.path(), "mock_family_roundtrip", "echo KACHE_PROBE_GNU");
let program = compiler.to_str().unwrap();
let res1 = probe_family_retrying(program).unwrap();
let key = cache::probe_key_isolated("cc-family", program).unwrap();
let mut hit = cache::load(temp.path(), &key).expect("probe result must be persisted");
let original_family = hit.version_line.clone();
let inverted_family = if original_family == "clang" {
"gnu"
} else {
"clang"
};
hit.version_line = inverted_family.to_string();
cache::store(temp.path(), &key, &hit);
let res2 = probe_compiler_family(program).unwrap();
assert_ne!(res1, res2);
assert_eq!(res2, parse_family(inverted_family).unwrap());
}
#[test]
fn family_probe_reads_cached_gnu_clang_none_and_corrupt() {
let temp = TempDir::new().unwrap();
let _guard = set_test_cache_dir(temp.path());
let compiler =
create_mock_probe_script(temp.path(), "mock_cached_none", "echo KACHE_PROBE_GNU");
let prog = compiler.to_str().unwrap();
let key = cache::probe_key_isolated("cc-family", prog).unwrap();
cache::store(
temp.path(),
&key,
&ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: "cc-family".to_string(),
compiler_name: "dummy".to_string(),
version_line: "gnu".to_string(),
resolved_tokens: None,
},
);
assert_eq!(probe_compiler_family(prog), Some(ProbedFamily::Gnu));
cache::store(
temp.path(),
&key,
&ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: "cc-family".to_string(),
compiler_name: "dummy".to_string(),
version_line: "clang".to_string(),
resolved_tokens: None,
},
);
assert_eq!(probe_compiler_family(prog), Some(ProbedFamily::Clang));
cache::store(
temp.path(),
&key,
&ResolvedConfig {
schema_version: PROBE_SCHEMA_VERSION,
prober: "cc-family".to_string(),
compiler_name: "dummy".to_string(),
version_line: "none".to_string(),
resolved_tokens: None,
},
);
assert_eq!(probe_compiler_family(prog), None);
}
fn create_mock_probe_script(
dir: &std::path::Path,
name: &str,
body: &str,
) -> std::path::PathBuf {
#[cfg(unix)]
{
let path = dir.join(name);
std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap();
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
#[cfg(windows)]
{
let path = dir.join(format!("{name}.bat"));
std::fs::write(&path, format!("@echo off\r\n{body}\r\n")).unwrap();
path
}
}
fn probe_family_retrying(program: &str) -> Option<ProbedFamily> {
for _ in 0..10 {
if let Some(family) = probe_compiler_family(program) {
return Some(family);
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
probe_compiler_family(program)
}
#[test]
fn family_probe_executes_scripts_and_parses_outputs() {
let temp = TempDir::new().unwrap();
let _guard = set_test_cache_dir(temp.path());
let gnu_script = create_mock_probe_script(temp.path(), "mock_gnu", "echo KACHE_PROBE_GNU");
let gnu_str = gnu_script.to_str().unwrap();
assert_eq!(probe_family_retrying(gnu_str), Some(ProbedFamily::Gnu));
assert_eq!(probe_compiler_family(gnu_str), Some(ProbedFamily::Gnu));
let clang_script =
create_mock_probe_script(temp.path(), "mock_clang", "echo KACHE_PROBE_CLANG");
let clang_str = clang_script.to_str().unwrap();
assert_eq!(probe_family_retrying(clang_str), Some(ProbedFamily::Clang));
assert_eq!(probe_compiler_family(clang_str), Some(ProbedFamily::Clang));
let both_script = create_mock_probe_script(
temp.path(),
"mock_both",
if cfg!(windows) {
"echo KACHE_PROBE_CLANG\r\necho KACHE_PROBE_GNU"
} else {
"echo KACHE_PROBE_CLANG\necho KACHE_PROBE_GNU"
},
);
let both_str = both_script.to_str().unwrap();
assert_eq!(probe_compiler_family(both_str), None);
let unk_script = create_mock_probe_script(temp.path(), "mock_unk", "echo UNKNOWN_COMPILER");
let unk_str = unk_script.to_str().unwrap();
assert_eq!(probe_compiler_family(unk_str), None);
let fail_script = create_mock_probe_script(
temp.path(),
"mock_fail",
if cfg!(windows) { "exit /b 1" } else { "exit 1" },
);
let fail_str = fail_script.to_str().unwrap();
assert_eq!(probe_compiler_family(fail_str), None);
}
#[test]
fn run_family_probe_handles_large_output() {
let temp = TempDir::new().unwrap();
let large_body = if cfg!(windows) {
"for /L %%i in (1,1,200) do echo 01234567890123456789012345678901234567890123456789\r\necho KACHE_PROBE_GNU"
} else {
"yes '0123456789012345678901234567890123456789' | head -n 300\necho KACHE_PROBE_GNU"
};
let script = create_mock_probe_script(temp.path(), "mock_large", large_body);
let mut res = run_family_probe(script.to_str().unwrap());
for _ in 0..10 {
if res.is_ok() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
res = run_family_probe(script.to_str().unwrap());
}
assert_eq!(res, Ok(Some(ProbedFamily::Gnu)));
}
#[cfg(unix)]
#[test]
fn probe_spawns_pin_lc_all_c() {
let temp = TempDir::new().unwrap();
let _cache_guard = set_test_cache_dir(temp.path());
let script = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/mock_cc_lc_all.sh");
let compiler = script.to_str().unwrap();
let req = ProbeRequest {
compiler,
args: &["-O2".to_string()],
key_args: &["-O2".to_string()],
per_tu_paths: &[],
windows_aware: false,
};
let config = CcProber.probe(&req).expect("probe succeeds");
assert_eq!(config.version_line, "mock-cc 1.0");
assert!(config.resolved_tokens.is_some());
let family = run_family_probe(compiler).expect("family probe succeeds");
assert_eq!(family, Some(ProbedFamily::Gnu));
let diag = live_probe_diagnostic_for(compiler);
match diag {
LiveProbeDiagnostic::Resolved { version_line } => {
assert_eq!(version_line, "mock-cc 1.0");
}
other => panic!("expected Resolved diagnostic, got: {other:?}"),
}
}
}