Skip to main content

gobject_linter/
meson.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::{Path, PathBuf},
4    process::Command,
5};
6
7use anyhow::{Context, Result};
8use serde::Deserialize;
9
10/// Raw meson introspection data
11#[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/// Represents the full output of `meson introspect --all` with pre-computed
39/// caches
40#[derive(Debug)]
41pub struct MesonIntrospection {
42    /// Path to the meson build directory
43    build_dir: PathBuf,
44
45    /// Raw meson data
46    #[allow(dead_code)]
47    data: MesonData,
48
49    /// Pre-computed set of GIR-introspected headers
50    introspected_headers: HashSet<PathBuf>,
51
52    /// Pre-computed set of installed headers
53    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
204/// Find the meson build directory by looking for meson-info/meson-info.json
205/// Searches in:
206/// 1. User-specified build_dir from config
207/// 2. Common build directory names (build/, builddir/, _build/)
208/// 3. Any directory containing meson-info/
209fn find_build_dir(project_root: &Path, config_build_dir: Option<&str>) -> Option<PathBuf> {
210    // 1. Check user-specified build_dir
211    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    // 2. Check common build directory names
219    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    // 3. Search for any directory containing meson-info/
227    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
239/// Check if a directory is a valid meson build directory
240fn is_build_dir(path: &Path) -> bool {
241    path.join("meson-info").join("meson-info.json").exists()
242}
243
244impl MesonIntrospection {
245    /// Create a mock MesonIntrospection for testing with specified headers
246    #[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    /// Create MesonIntrospection by finding the build directory and running
260    /// `meson introspect --all` Returns None if no build directory is found
261    pub fn new(project_root: &Path, config_build_dir: Option<&str>) -> Result<Option<Self>> {
262        // Find build directory
263        let Some(build_dir) = find_build_dir(project_root, config_build_dir) else {
264            return Ok(None);
265        };
266
267        // Run meson introspect --all
268        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        // Parse JSON output
281        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        // Pre-compute introspected headers
288        let introspected_headers = Self::compute_introspected_headers(&data, &build_dir)?;
289
290        // Pre-compute installed headers
291        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    /// Compute headers scanned by g-ir-scanner (GObject introspection headers)
302    fn compute_introspected_headers(
303        data: &MesonData,
304        _build_dir: &Path,
305    ) -> Result<HashSet<PathBuf>> {
306        let mut headers = HashSet::new();
307
308        // Find all targets that produce .gir files
309        for target in &data.targets {
310            // Check if this target produces a .gir file
311            let produces_gir = target.filename.iter().any(|f| f.ends_with(".gir"));
312            if !produces_gir {
313                continue;
314            }
315
316            // Look for --filelist argument in g-ir-scanner command
317            for source in &target.target_sources {
318                for arg in &source.compiler {
319                    if let Some(filelist_path) = arg.strip_prefix("--filelist=") {
320                        // Read the filelist and extract .h files
321                        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    /// Compute installed header files (all headers that will be installed)
339    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    /// Get headers scanned by g-ir-scanner (GObject introspection headers)
350    pub fn get_introspected_headers(&self) -> &HashSet<PathBuf> {
351        &self.introspected_headers
352    }
353
354    /// Get installed header files (all headers that will be installed)
355    pub fn get_installed_headers(&self) -> &HashSet<PathBuf> {
356        &self.installed_headers
357    }
358}
359
360/// Compiler information extracted from compile_commands.json
361#[derive(Debug, Clone)]
362pub struct CompilerInfo {
363    /// Compiler executable (e.g., "cc", "gcc", "/usr/bin/clang")
364    pub compiler: String,
365    /// Compilation flags (-I, -D, etc.)
366    pub flags: Vec<String>,
367}
368
369/// Entry in compile_commands.json (standard format used by CMake, Meson, etc.)
370#[derive(Debug, Deserialize)]
371struct CompileCommand {
372    /// Build directory
373    directory: String,
374    /// Full compilation command
375    command: String,
376    /// Source file path (relative to directory)
377    file: String,
378}
379
380impl MesonIntrospection {
381    /// Load compile_commands.json and build a map from source file →
382    /// CompilerInfo
383    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            // Resolve file path (may be relative to build directory)
404            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            // Canonicalize to get absolute path
411            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    /// Parse a compilation command string to extract compiler and flags
424    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        // First part is compiler (skip ccache if present)
431        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        // Extract flags (skip compiler, -o, -c, -MF, -MQ, -MD and their arguments, and
438        // the input file)
439        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            // Skip output-related flags and their arguments
446            if flag == "-o" || flag == "-MF" || flag == "-MQ" {
447                i += 2; // Skip flag and its argument
448                continue;
449            }
450
451            // Skip standalone flags
452            if flag == "-c" || flag == "-MD" {
453                i += 1;
454                continue;
455            }
456
457            // Skip the input file (last .c file)
458            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}