1use std::{
2 collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
4 process::Command,
5};
6
7use anyhow::{Context, Result};
8use serde::Deserialize;
9
10#[derive(Debug, Default, Deserialize)]
12#[allow(dead_code)]
13struct MesonData {
14 #[serde(default)]
15 pub benchmarks: Vec<Benchmark>,
16 #[serde(default)]
17 pub buildoptions: Vec<BuildOption>,
18 #[serde(default)]
19 pub buildsystem_files: Vec<String>,
20 #[serde(default)]
21 pub compilers: HashMap<String, HashMap<String, Compiler>>,
22 #[serde(default)]
23 pub dependencies: Vec<Dependency>,
24 #[serde(default)]
25 pub install_plan: HashMap<String, HashMap<String, InstallPlan>>,
26 #[serde(default)]
27 pub installed: HashMap<String, String>,
28 #[serde(default)]
29 pub machines: HashMap<String, MachineInfo>,
30 #[serde(default)]
31 pub projectinfo: Option<ProjectInfo>,
32 #[serde(default)]
33 pub targets: Vec<Target>,
34 #[serde(default)]
35 pub tests: Vec<Test>,
36}
37
38#[derive(Debug)]
41pub struct MesonIntrospection {
42 build_dir: PathBuf,
44
45 #[allow(dead_code)]
47 data: MesonData,
48
49 introspected_headers: HashSet<PathBuf>,
51
52 installed_headers: HashSet<PathBuf>,
54}
55
56#[derive(Debug, Deserialize)]
57pub struct Benchmark {
58 pub name: String,
59 pub suite: Vec<String>,
60 pub cmd: Vec<String>,
61 #[serde(default)]
62 pub env: HashMap<String, String>,
63 #[serde(default)]
64 pub depends: Vec<String>,
65 pub workdir: Option<String>,
66 pub timeout: u32,
67 pub protocol: String,
68 pub priority: i32,
69}
70
71#[derive(Debug, Deserialize)]
72pub struct BuildOption {
73 pub name: String,
74 #[serde(rename = "type")]
75 pub option_type: String,
76 pub value: serde_json::Value,
77 pub section: String,
78 pub machine: String,
79 pub description: String,
80 #[serde(default)]
81 pub choices: Vec<String>,
82}
83
84#[derive(Debug, Deserialize)]
85pub struct Compiler {
86 pub id: String,
87 #[serde(default)]
88 pub exelist: Vec<String>,
89 #[serde(default)]
90 pub linker_exelist: Vec<String>,
91 #[serde(default)]
92 pub file_suffixes: Vec<String>,
93 #[serde(default)]
94 pub default_suffix: Option<String>,
95 pub version: String,
96 pub full_version: String,
97 #[serde(default)]
98 pub linker_id: Option<String>,
99}
100
101#[derive(Debug, Deserialize)]
102pub struct Dependency {
103 pub name: String,
104 #[serde(rename = "type")]
105 pub dep_type: String,
106 pub version: String,
107 #[serde(default)]
108 pub compile_args: Vec<String>,
109 #[serde(default)]
110 pub link_args: Vec<String>,
111 #[serde(default)]
112 pub include_directories: Vec<String>,
113 #[serde(default)]
114 pub sources: Vec<String>,
115}
116
117#[derive(Debug, Deserialize)]
118pub struct InstallPlan {
119 pub destination: String,
120 #[serde(default)]
121 pub tag: Option<String>,
122 #[serde(default)]
123 pub subproject: Option<String>,
124 #[serde(default)]
125 pub install_rpath: Option<String>,
126 #[serde(default)]
127 pub exclude_dirs: Vec<String>,
128 #[serde(default)]
129 pub exclude_files: Vec<String>,
130}
131
132#[derive(Debug, Deserialize)]
133pub struct MachineInfo {
134 pub system: String,
135 pub cpu_family: String,
136 pub cpu: String,
137 pub endian: String,
138}
139
140#[derive(Debug, Deserialize)]
141pub struct ProjectInfo {
142 pub version: String,
143 pub descriptive_name: String,
144 #[serde(default)]
145 pub license: Vec<String>,
146 #[serde(default)]
147 pub subprojects: Vec<SubprojectInfo>,
148}
149
150#[derive(Debug, Deserialize)]
151pub struct SubprojectInfo {
152 pub name: String,
153 pub version: String,
154 pub descriptive_name: String,
155}
156
157#[derive(Debug, Deserialize)]
158pub struct Target {
159 pub name: String,
160 pub id: String,
161 #[serde(rename = "type")]
162 pub target_type: String,
163 pub defined_in: String,
164 pub filename: Vec<String>,
165 pub build_by_default: bool,
166 #[serde(default)]
167 pub target_sources: Vec<TargetSource>,
168 #[serde(default)]
169 pub dependencies: Vec<String>,
170 #[serde(default)]
171 pub subproject: Option<String>,
172 pub installed: bool,
173}
174
175#[derive(Debug, Deserialize)]
176pub struct TargetSource {
177 #[serde(default)]
178 pub language: Option<String>,
179 #[serde(default)]
180 pub compiler: Vec<String>,
181 #[serde(default)]
182 pub parameters: Vec<String>,
183 #[serde(default)]
184 pub sources: Vec<String>,
185 #[serde(default)]
186 pub generated_sources: Vec<String>,
187}
188
189#[derive(Debug, Deserialize)]
190pub struct Test {
191 pub name: String,
192 pub suite: Vec<String>,
193 pub cmd: Vec<String>,
194 #[serde(default)]
195 pub env: HashMap<String, String>,
196 #[serde(default)]
197 pub depends: Vec<String>,
198 pub workdir: Option<String>,
199 pub timeout: u32,
200 pub protocol: String,
201 pub priority: i32,
202}
203
204fn find_build_dir(project_root: &Path, config_build_dir: Option<&str>) -> Option<PathBuf> {
210 if let Some(dir) = config_build_dir {
212 let path = project_root.join(dir);
213 if is_build_dir(&path) {
214 return Some(path);
215 }
216 }
217
218 for name in &["build", "builddir", "_build"] {
220 let path = project_root.join(name);
221 if is_build_dir(&path) {
222 return Some(path);
223 }
224 }
225
226 if let Ok(entries) = std::fs::read_dir(project_root) {
228 for entry in entries.filter_map(std::result::Result::ok) {
229 let path = entry.path();
230 if path.is_dir() && is_build_dir(&path) {
231 return Some(path);
232 }
233 }
234 }
235
236 None
237}
238
239fn is_build_dir(path: &Path) -> bool {
241 path.join("meson-info").join("meson-info.json").exists()
242}
243
244impl MesonIntrospection {
245 #[doc(hidden)]
247 pub fn mock(
248 introspected_headers: HashSet<PathBuf>,
249 installed_headers: HashSet<PathBuf>,
250 ) -> Self {
251 Self {
252 build_dir: PathBuf::from("/mock/build"),
253 data: MesonData::default(),
254 introspected_headers,
255 installed_headers,
256 }
257 }
258
259 pub fn new(project_root: &Path, config_build_dir: Option<&str>) -> Result<Option<Self>> {
262 let Some(build_dir) = find_build_dir(project_root, config_build_dir) else {
264 return Ok(None);
265 };
266
267 let output = Command::new("meson")
269 .arg("introspect")
270 .arg(&build_dir)
271 .arg("--all")
272 .output()
273 .context("Failed to run meson introspect (is meson installed?)")?;
274
275 if !output.status.success() {
276 let stderr = String::from_utf8_lossy(&output.stderr);
277 anyhow::bail!("meson introspect --all failed: {}", stderr);
278 }
279
280 let stdout = String::from_utf8(output.stdout)
282 .context("meson introspect output is not valid UTF-8")?;
283
284 let data: MesonData = serde_json::from_str(&stdout)
285 .context("Failed to parse meson introspect JSON output")?;
286
287 let introspected_headers = Self::compute_introspected_headers(&data, &build_dir)?;
289
290 let installed_headers = Self::compute_installed_headers(&data);
292
293 Ok(Some(Self {
294 build_dir,
295 data,
296 introspected_headers,
297 installed_headers,
298 }))
299 }
300
301 fn compute_introspected_headers(
303 data: &MesonData,
304 _build_dir: &Path,
305 ) -> Result<HashSet<PathBuf>> {
306 let mut headers = HashSet::new();
307
308 for target in &data.targets {
310 let produces_gir = target.filename.iter().any(|f| f.ends_with(".gir"));
312 if !produces_gir {
313 continue;
314 }
315
316 for source in &target.target_sources {
318 for arg in &source.compiler {
319 if let Some(filelist_path) = arg.strip_prefix("--filelist=") {
320 let content = std::fs::read_to_string(filelist_path)
322 .context(format!("Failed to read GIR filelist: {}", filelist_path))?;
323
324 for line in content.lines() {
325 let line = line.trim();
326 if line.ends_with(".h") {
327 headers.insert(PathBuf::from(line));
328 }
329 }
330 }
331 }
332 }
333 }
334
335 Ok(headers)
336 }
337
338 fn compute_installed_headers(data: &MesonData) -> HashSet<PathBuf> {
340 let mut headers = HashSet::new();
341 for source_path in data.installed.keys() {
342 if source_path.ends_with(".h") {
343 headers.insert(PathBuf::from(source_path));
344 }
345 }
346 headers
347 }
348
349 pub fn get_introspected_headers(&self) -> &HashSet<PathBuf> {
351 &self.introspected_headers
352 }
353
354 pub fn get_installed_headers(&self) -> &HashSet<PathBuf> {
356 &self.installed_headers
357 }
358}
359
360#[derive(Debug, Clone)]
362pub struct CompilerInfo {
363 pub compiler: String,
365 pub flags: Vec<String>,
367}
368
369#[derive(Debug, Deserialize)]
371struct CompileCommand {
372 directory: String,
374 command: String,
376 file: String,
378}
379
380impl MesonIntrospection {
381 pub fn load_compiler_map(&self) -> Result<HashMap<PathBuf, CompilerInfo>> {
384 let compile_commands_path = self.build_dir.join("compile_commands.json");
385 if !compile_commands_path.exists() {
386 anyhow::bail!(
387 "compile_commands.json not found in {}",
388 self.build_dir.display()
389 );
390 }
391
392 let content = std::fs::read_to_string(&compile_commands_path)
393 .context("Failed to read compile_commands.json")?;
394
395 let commands: Vec<CompileCommand> =
396 serde_json::from_str(&content).context("Failed to parse compile_commands.json")?;
397
398 let mut map = HashMap::new();
399
400 for cmd in commands {
401 let (compiler, flags) = Self::parse_compile_command(&cmd.command);
402
403 let file_path = if Path::new(&cmd.file).is_absolute() {
405 PathBuf::from(&cmd.file)
406 } else {
407 PathBuf::from(&cmd.directory).join(&cmd.file)
408 };
409
410 let file_path = file_path
412 .canonicalize()
413 .unwrap_or_else(|_| file_path.clone());
414
415 map.insert(file_path, CompilerInfo { compiler, flags });
416 }
417
418 tracing::debug!("Loaded {} entries from compile_commands.json", map.len());
419
420 Ok(map)
421 }
422
423 fn parse_compile_command(command: &str) -> (String, Vec<String>) {
425 let parts: Vec<&str> = command.split_whitespace().collect();
426 if parts.is_empty() {
427 return ("cc".to_string(), Vec::new());
428 }
429
430 let (compiler_idx, compiler) = if parts[0] == "ccache" && parts.len() > 1 {
432 (1, parts[1].to_string())
433 } else {
434 (0, parts[0].to_string())
435 };
436
437 let mut flags = Vec::new();
440 let mut i = compiler_idx + 1;
441
442 while i < parts.len() {
443 let flag = parts[i];
444
445 if flag == "-o" || flag == "-MF" || flag == "-MQ" {
447 i += 2; continue;
449 }
450
451 if flag == "-c" || flag == "-MD" {
453 i += 1;
454 continue;
455 }
456
457 if flag.ends_with(".c") && i == parts.len() - 1 {
459 break;
460 }
461
462 flags.push(flag.to_string());
463 i += 1;
464 }
465
466 (compiler, flags)
467 }
468}