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 = 3;
#[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 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)
.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),
})
}
}
fn resolve_invocation(compiler: &str, args: &[String], windows_aware: bool) -> Option<Vec<String>> {
let output = Command::new(compiler)
.arg("-###")
.args(args)
.output()
.ok()?;
let stderr = String::from_utf8_lossy(&output.stderr);
resolve::resolved_semantic_tokens(&stderr, windows_aware)
}
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::*;
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: &[],
windows_aware: true,
}
}
#[test]
fn probe_runs_prober_once_then_serves_from_cache() {
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 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 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,
windows_aware: true,
};
let Ok(config) = CcProber.probe(&request) else {
return;
};
if let Some(tokens) = config.resolved_tokens {
assert!(
tokens.iter().any(|t| t == "-O2"),
"resolved `-cc1` tokens should carry -O2: {tokens:?}"
);
}
}
}