use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result};
use serde::Deserialize;
#[derive(Debug, Default, Deserialize)]
#[allow(dead_code)]
struct MesonData {
#[serde(default)]
pub benchmarks: Vec<Benchmark>,
#[serde(default)]
pub buildoptions: Vec<BuildOption>,
#[serde(default)]
pub buildsystem_files: Vec<String>,
#[serde(default)]
pub compilers: HashMap<String, HashMap<String, Compiler>>,
#[serde(default)]
pub dependencies: Vec<Dependency>,
#[serde(default)]
pub install_plan: HashMap<String, HashMap<String, InstallPlan>>,
#[serde(default)]
pub installed: HashMap<String, String>,
#[serde(default)]
pub machines: HashMap<String, MachineInfo>,
#[serde(default)]
pub projectinfo: Option<ProjectInfo>,
#[serde(default)]
pub targets: Vec<Target>,
#[serde(default)]
pub tests: Vec<Test>,
}
#[derive(Debug)]
pub struct MesonIntrospection {
build_dir: PathBuf,
#[allow(dead_code)]
data: MesonData,
introspected_headers: HashSet<PathBuf>,
installed_headers: HashSet<PathBuf>,
}
#[derive(Debug, Deserialize)]
pub struct Benchmark {
pub name: String,
pub suite: Vec<String>,
pub cmd: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub depends: Vec<String>,
pub workdir: Option<String>,
pub timeout: u32,
pub protocol: String,
pub priority: i32,
}
#[derive(Debug, Deserialize)]
pub struct BuildOption {
pub name: String,
#[serde(rename = "type")]
pub option_type: String,
pub value: serde_json::Value,
pub section: String,
pub machine: String,
pub description: String,
#[serde(default)]
pub choices: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Compiler {
pub id: String,
#[serde(default)]
pub exelist: Vec<String>,
#[serde(default)]
pub linker_exelist: Vec<String>,
#[serde(default)]
pub file_suffixes: Vec<String>,
#[serde(default)]
pub default_suffix: Option<String>,
pub version: String,
pub full_version: String,
#[serde(default)]
pub linker_id: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct Dependency {
pub name: String,
#[serde(rename = "type")]
pub dep_type: String,
pub version: String,
#[serde(default)]
pub compile_args: Vec<String>,
#[serde(default)]
pub link_args: Vec<String>,
#[serde(default)]
pub include_directories: Vec<String>,
#[serde(default)]
pub sources: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct InstallPlan {
pub destination: String,
#[serde(default)]
pub tag: Option<String>,
#[serde(default)]
pub subproject: Option<String>,
#[serde(default)]
pub install_rpath: Option<String>,
#[serde(default)]
pub exclude_dirs: Vec<String>,
#[serde(default)]
pub exclude_files: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct MachineInfo {
pub system: String,
pub cpu_family: String,
pub cpu: String,
pub endian: String,
}
#[derive(Debug, Deserialize)]
pub struct ProjectInfo {
pub version: String,
pub descriptive_name: String,
#[serde(default)]
pub license: Vec<String>,
#[serde(default)]
pub subprojects: Vec<SubprojectInfo>,
}
#[derive(Debug, Deserialize)]
pub struct SubprojectInfo {
pub name: String,
pub version: String,
pub descriptive_name: String,
}
#[derive(Debug, Deserialize)]
pub struct Target {
pub name: String,
pub id: String,
#[serde(rename = "type")]
pub target_type: String,
pub defined_in: String,
pub filename: Vec<String>,
pub build_by_default: bool,
#[serde(default)]
pub target_sources: Vec<TargetSource>,
#[serde(default)]
pub dependencies: Vec<String>,
#[serde(default)]
pub subproject: Option<String>,
pub installed: bool,
}
#[derive(Debug, Deserialize)]
pub struct TargetSource {
#[serde(default)]
pub language: Option<String>,
#[serde(default)]
pub compiler: Vec<String>,
#[serde(default)]
pub parameters: Vec<String>,
#[serde(default)]
pub sources: Vec<String>,
#[serde(default)]
pub generated_sources: Vec<String>,
}
#[derive(Debug, Deserialize)]
pub struct Test {
pub name: String,
pub suite: Vec<String>,
pub cmd: Vec<String>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(default)]
pub depends: Vec<String>,
pub workdir: Option<String>,
pub timeout: u32,
pub protocol: String,
pub priority: i32,
}
fn find_build_dir(project_root: &Path, config_build_dir: Option<&str>) -> Option<PathBuf> {
if let Some(dir) = config_build_dir {
let path = project_root.join(dir);
if is_build_dir(&path) {
return Some(path);
}
}
for name in &["build", "builddir", "_build"] {
let path = project_root.join(name);
if is_build_dir(&path) {
return Some(path);
}
}
if let Ok(entries) = std::fs::read_dir(project_root) {
for entry in entries.filter_map(std::result::Result::ok) {
let path = entry.path();
if path.is_dir() && is_build_dir(&path) {
return Some(path);
}
}
}
None
}
fn is_build_dir(path: &Path) -> bool {
path.join("meson-info").join("meson-info.json").exists()
}
impl MesonIntrospection {
#[doc(hidden)]
pub fn mock(
introspected_headers: HashSet<PathBuf>,
installed_headers: HashSet<PathBuf>,
) -> Self {
Self {
build_dir: PathBuf::from("/mock/build"),
data: MesonData::default(),
introspected_headers,
installed_headers,
}
}
pub fn new(project_root: &Path, config_build_dir: Option<&str>) -> Result<Option<Self>> {
let Some(build_dir) = find_build_dir(project_root, config_build_dir) else {
return Ok(None);
};
let output = Command::new("meson")
.arg("introspect")
.arg(&build_dir)
.arg("--all")
.output()
.context("Failed to run meson introspect (is meson installed?)")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("meson introspect --all failed: {}", stderr);
}
let stdout = String::from_utf8(output.stdout)
.context("meson introspect output is not valid UTF-8")?;
let data: MesonData = serde_json::from_str(&stdout)
.context("Failed to parse meson introspect JSON output")?;
let introspected_headers = Self::compute_introspected_headers(&data, &build_dir)?;
let installed_headers = Self::compute_installed_headers(&data);
Ok(Some(Self {
build_dir,
data,
introspected_headers,
installed_headers,
}))
}
fn compute_introspected_headers(
data: &MesonData,
_build_dir: &Path,
) -> Result<HashSet<PathBuf>> {
let mut headers = HashSet::new();
for target in &data.targets {
let produces_gir = target.filename.iter().any(|f| f.ends_with(".gir"));
if !produces_gir {
continue;
}
for source in &target.target_sources {
for arg in &source.compiler {
if let Some(filelist_path) = arg.strip_prefix("--filelist=") {
let content = std::fs::read_to_string(filelist_path)
.context(format!("Failed to read GIR filelist: {}", filelist_path))?;
for line in content.lines() {
let line = line.trim();
if line.ends_with(".h") {
headers.insert(PathBuf::from(line));
}
}
}
}
}
}
Ok(headers)
}
fn compute_installed_headers(data: &MesonData) -> HashSet<PathBuf> {
let mut headers = HashSet::new();
for source_path in data.installed.keys() {
if source_path.ends_with(".h") {
headers.insert(PathBuf::from(source_path));
}
}
headers
}
pub fn get_introspected_headers(&self) -> &HashSet<PathBuf> {
&self.introspected_headers
}
pub fn get_installed_headers(&self) -> &HashSet<PathBuf> {
&self.installed_headers
}
}
#[derive(Debug, Clone)]
pub struct CompilerInfo {
pub compiler: String,
pub flags: Vec<String>,
}
#[derive(Debug, Deserialize)]
struct CompileCommand {
directory: String,
command: String,
file: String,
}
impl MesonIntrospection {
pub fn load_compiler_map(&self) -> Result<HashMap<PathBuf, CompilerInfo>> {
let compile_commands_path = self.build_dir.join("compile_commands.json");
if !compile_commands_path.exists() {
anyhow::bail!(
"compile_commands.json not found in {}",
self.build_dir.display()
);
}
let content = std::fs::read_to_string(&compile_commands_path)
.context("Failed to read compile_commands.json")?;
let commands: Vec<CompileCommand> =
serde_json::from_str(&content).context("Failed to parse compile_commands.json")?;
let mut map = HashMap::new();
for cmd in commands {
let (compiler, flags) = Self::parse_compile_command(&cmd.command);
let file_path = if Path::new(&cmd.file).is_absolute() {
PathBuf::from(&cmd.file)
} else {
PathBuf::from(&cmd.directory).join(&cmd.file)
};
let file_path = file_path
.canonicalize()
.unwrap_or_else(|_| file_path.clone());
map.insert(file_path, CompilerInfo { compiler, flags });
}
tracing::debug!("Loaded {} entries from compile_commands.json", map.len());
Ok(map)
}
fn parse_compile_command(command: &str) -> (String, Vec<String>) {
let parts: Vec<&str> = command.split_whitespace().collect();
if parts.is_empty() {
return ("cc".to_string(), Vec::new());
}
let (compiler_idx, compiler) = if parts[0] == "ccache" && parts.len() > 1 {
(1, parts[1].to_string())
} else {
(0, parts[0].to_string())
};
let mut flags = Vec::new();
let mut i = compiler_idx + 1;
while i < parts.len() {
let flag = parts[i];
if flag == "-o" || flag == "-MF" || flag == "-MQ" {
i += 2; continue;
}
if flag == "-c" || flag == "-MD" {
i += 1;
continue;
}
if flag.ends_with(".c") && i == parts.len() - 1 {
break;
}
flags.push(flag.to_string());
i += 1;
}
(compiler, flags)
}
}