use std::collections::HashSet;
use std::path::Path;
use std::process::{Command, Stdio};
pub const DEFAULT_COMPILER: &str = "cc";
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SystemIncludes {
pub paths: Vec<String>,
pub queried: Vec<String>,
pub failed: Vec<(String, String)>,
}
pub fn query(compilers: &[String]) -> SystemIncludes {
let mut out = SystemIncludes::default();
let mut seen_paths: HashSet<String> = HashSet::new();
let mut asked: HashSet<&str> = HashSet::new();
for compiler in compilers {
if !asked.insert(compiler.as_str()) {
continue;
}
match run_query(compiler) {
Ok(stderr) => {
out.queried.push(compiler.clone());
for dir in parse_search_list(&stderr) {
if Path::new(&dir).is_dir() && seen_paths.insert(dir.clone()) {
out.paths.push(dir);
}
}
}
Err(e) => out.failed.push((compiler.clone(), e)),
}
}
out
}
fn run_query(compiler: &str) -> Result<String, String> {
let output = Command::new(compiler)
.args(["-E", "-Wp,-v", "-x", "c", "-"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::piped())
.output()
.map_err(|e| e.to_string())?;
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
if !output.status.success() && !stderr.contains(SEARCH_LIST_START) {
return Err(format!(
"exited with {} and printed no search list",
output
.status
.code()
.map_or_else(|| "a signal".to_string(), |c| format!("status {c}"))
));
}
Ok(stderr)
}
const SEARCH_LIST_START: &str = "#include <...> search starts here:";
const QUOTE_LIST_START: &str = "#include \"...\" search starts here:";
const SEARCH_LIST_END: &str = "End of search list.";
fn parse_search_list(stderr: &str) -> Vec<String> {
let mut dirs = Vec::new();
let mut in_list = false;
for line in stderr.lines() {
let trimmed = line.trim();
if trimmed == QUOTE_LIST_START || trimmed == SEARCH_LIST_START {
in_list = true;
continue;
}
if trimmed == SEARCH_LIST_END {
in_list = false;
continue;
}
if !in_list || trimmed.is_empty() {
continue;
}
if line == trimmed {
continue;
}
let dir = match trimmed.strip_suffix("(framework directory)") {
Some(_) => continue,
None => trimmed,
};
dirs.push(dir.to_string());
}
dirs
}
#[cfg(test)]
mod tests {
use super::*;
const GCC_STDERR: &str = r#"Using built-in specs.
ignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"
ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/12/include-fixed"
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/12/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
"#;
#[test]
fn parses_gcc_search_list() {
assert_eq!(
parse_search_list(GCC_STDERR),
vec![
"/usr/lib/gcc/x86_64-linux-gnu/12/include",
"/usr/local/include",
"/usr/include/x86_64-linux-gnu",
"/usr/include",
]
);
}
#[test]
fn ignores_nonexistent_directory_preamble() {
let dirs = parse_search_list(GCC_STDERR);
assert!(!dirs.iter().any(|d| d.contains("include-fixed")));
assert!(!dirs.iter().any(|d| d.contains("local/include/x86_64")));
}
#[test]
fn collects_both_quoted_and_angle_sections() {
let stderr = "#include \"...\" search starts here:\n \
/project/quoted\n\
#include <...> search starts here:\n \
/usr/include\n\
End of search list.\n";
assert_eq!(
parse_search_list(stderr),
vec!["/project/quoted", "/usr/include"]
);
}
#[test]
fn ignores_everything_outside_the_markers() {
let stderr = "clang version 14.0.6\nTarget: x86_64-pc-linux-gnu\n\
/some/path/that/is/not/in/a/list\n";
assert!(parse_search_list(stderr).is_empty());
}
#[test]
fn skips_framework_directories() {
let stderr = "#include <...> search starts here:\n \
/usr/include\n \
/System/Library/Frameworks (framework directory)\n\
End of search list.\n";
assert_eq!(parse_search_list(stderr), vec!["/usr/include"]);
}
#[test]
fn unindented_line_inside_the_markers_is_not_a_path() {
let stderr = "#include <...> search starts here:\n\
some diagnostic flush left\n \
/usr/include\n\
End of search list.\n";
assert_eq!(parse_search_list(stderr), vec!["/usr/include"]);
}
#[test]
fn a_missing_compiler_is_reported_not_fatal() {
let res = query(&["definitely-not-a-real-compiler-xyz".to_string()]);
assert!(res.paths.is_empty());
assert!(res.queried.is_empty());
assert_eq!(res.failed.len(), 1);
assert_eq!(res.failed[0].0, "definitely-not-a-real-compiler-xyz");
}
#[test]
fn each_distinct_compiler_is_asked_once() {
let res = query(&[
"definitely-not-a-real-compiler-xyz".to_string(),
"definitely-not-a-real-compiler-xyz".to_string(),
]);
assert_eq!(res.failed.len(), 1);
}
#[test]
fn returned_paths_exist_and_are_deduplicated() {
let res = query(&[DEFAULT_COMPILER.to_string()]);
if res.queried.is_empty() {
return;
}
assert!(
!res.paths.is_empty(),
"a working cc should report at least one existing system include dir"
);
for p in &res.paths {
assert!(Path::new(p).is_dir(), "{p} should exist");
}
let unique: HashSet<&String> = res.paths.iter().collect();
assert_eq!(
unique.len(),
res.paths.len(),
"paths should be deduplicated"
);
}
}