mod cpp;
mod icpp;
mod markdown;
pub mod python;
mod tera;
use anyhow::Result;
use indicatif::ProgressBar;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::deps_cache::DepsCache;
use crate::file_index::FileIndex;
use crate::graph::{BuildGraph, Product};
use crate::processors::{format_command, run_command_capture};
pub trait DepAnalyzer: Sync + Send {
fn description(&self) -> &str;
fn enabled(&self) -> bool { true }
fn auto_detect(&self, file_index: &FileIndex) -> bool;
fn match_product(&self, product: &Product) -> Option<PathBuf>;
fn count_matches(&self, graph: &BuildGraph) -> usize {
graph.products().iter().filter(|p| self.match_product(p).is_some()).count()
}
fn matching_sources(&self, graph: &BuildGraph) -> Vec<PathBuf> {
graph.products().iter().filter_map(|p| self.match_product(p)).collect()
}
fn analyze(
&self,
ctx: &crate::build_context::BuildContext,
graph: &mut BuildGraph,
deps_cache: &mut DepsCache,
file_index: &FileIndex,
verbose: bool,
progress: &ProgressBar,
) -> Result<()>;
fn scan_hash_pieces(
&self,
_ctx: &crate::build_context::BuildContext,
_source: &Path,
) -> Result<Option<Vec<String>>> {
Ok(None)
}
}
pub fn query_pkg_config_include_paths(ctx: &crate::build_context::BuildContext, tag: &str, packages: &[String], verbose: bool) -> Vec<PathBuf> {
if packages.is_empty() {
return Vec::new();
}
let mut cmd = Command::new("pkg-config");
cmd.arg("--cflags-only-I");
cmd.args(packages);
if verbose {
eprintln!("[{}] Querying pkg-config: {}", tag, format_command(&cmd));
}
let output = match run_command_capture(ctx, &cmd) {
Ok(o) => o,
Err(e) => {
eprintln!("[{tag}] Failed to query pkg-config: {e}");
return Vec::new();
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("[{}] pkg-config failed: {}", tag, stderr.trim());
return Vec::new();
}
let paths: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
.split_whitespace()
.filter_map(|flag| flag.strip_prefix("-I").map(PathBuf::from))
.collect();
if verbose && !paths.is_empty() {
eprintln!("[{}] Found {} include paths from pkg-config", tag, paths.len());
}
paths
}
pub fn run_include_path_commands(ctx: &crate::build_context::BuildContext, tag: &str, commands: &[String], verbose: bool) -> Vec<PathBuf> {
if commands.is_empty() {
return Vec::new();
}
let mut paths = Vec::new();
for cmd_str in commands {
if cmd_str.trim().is_empty() {
continue;
}
let mut cmd = Command::new("sh");
cmd.arg("-c");
cmd.arg(cmd_str);
if verbose {
eprintln!("[{tag}] Running include path command: sh -c '{cmd_str}'");
}
let output = match run_command_capture(ctx, &cmd) {
Ok(o) => o,
Err(e) => {
eprintln!("[{tag}] Failed to run '{cmd_str}': {e}");
continue;
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("[{}] Command '{}' failed: {}", tag, cmd_str, stderr.trim());
continue;
}
let path_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
if path_str.is_empty() {
continue;
}
let path = PathBuf::from(&path_str);
if path.is_dir() {
if verbose {
eprintln!("[{}] Added include path from command: {}", tag, path.display());
}
paths.push(path);
} else if verbose {
eprintln!("[{tag}] Command output is not a directory: {path_str}");
}
}
if verbose && !paths.is_empty() {
eprintln!("[{}] Found {} include paths from commands", tag, paths.len());
}
paths
}
pub struct ScanResult {
pub deps: Vec<PathBuf>,
pub hash_pieces: Vec<String>,
}
pub fn analyze_with_scanner<F, G>(
ctx: &crate::build_context::BuildContext,
graph: &mut BuildGraph,
deps_cache: &mut DepsCache,
analyzer_name: &str,
match_product: F,
scan_deps: G,
progress: &ProgressBar,
) -> Result<()>
where
F: Fn(&crate::graph::Product) -> Option<PathBuf>,
G: Fn(&Path) -> Result<Vec<PathBuf>>,
{
let mut by_source: std::collections::BTreeMap<PathBuf, Vec<usize>> = std::collections::BTreeMap::new();
for p in graph.products() {
if let Some(source) = match_product(p) {
by_source.entry(source).or_default().push(p.id);
}
}
if by_source.is_empty() {
return Ok(());
}
for (source, product_ids) in &by_source {
progress.set_message(format!("[{}] {}", analyzer_name, source.display()));
if !source.exists() {
progress.inc(product_ids.len() as u64);
continue;
}
let deps = if let Some(cached) = deps_cache.get(ctx, analyzer_name, source) {
cached
} else {
let source_checksum = DepsCache::source_checksum(ctx, source)?;
let scanned = scan_deps(source)?;
if let Err(e) = deps_cache.set(analyzer_name, source, source_checksum, &scanned) {
crate::output::warn(&format!("failed to cache dependencies for {}: {}", source.display(), e));
}
scanned
};
if !deps.is_empty() {
for &id in product_ids {
if let Some(product) = graph.get_product_mut(id) {
let existing: HashSet<&PathBuf> = product.inputs.iter().collect();
let new_deps: Vec<PathBuf> = deps.iter()
.filter(|dep| !existing.contains(dep))
.cloned()
.collect();
product.inputs.extend(new_deps);
}
}
}
progress.inc(product_ids.len() as u64);
}
Ok(())
}
pub fn analyze_with_full_scanner<F, G>(
ctx: &crate::build_context::BuildContext,
graph: &mut BuildGraph,
deps_cache: &DepsCache,
analyzer_name: &str,
match_product: F,
scan: G,
progress: &ProgressBar,
) -> Result<()>
where
F: Fn(&crate::graph::Product) -> Option<PathBuf>,
G: Fn(&Path) -> Result<ScanResult>,
{
let mut by_source: std::collections::BTreeMap<PathBuf, Vec<usize>> = std::collections::BTreeMap::new();
for p in graph.products() {
if let Some(source) = match_product(p) {
by_source.entry(source).or_default().push(p.id);
}
}
if by_source.is_empty() {
return Ok(());
}
for (source, product_ids) in &by_source {
progress.set_message(format!("[{}] {}", analyzer_name, source.display()));
if !source.exists() {
progress.inc(product_ids.len() as u64);
continue;
}
let source_checksum = DepsCache::source_checksum(ctx, source)?;
let result = scan(source)?;
if let Err(e) = deps_cache.set(analyzer_name, source, source_checksum, &result.deps) {
crate::output::warn(&format!("failed to cache dependencies for {}: {}", source.display(), e));
}
let joined_pieces = if result.hash_pieces.is_empty() {
None
} else {
let parts: Vec<&str> = result.hash_pieces.iter().map(String::as_str).collect();
Some(crate::checksum::hash_parts(&parts))
};
for &id in product_ids {
if let Some(product) = graph.get_product_mut(id) {
if !result.deps.is_empty() {
let existing: HashSet<&PathBuf> = product.inputs.iter().collect();
let new_deps: Vec<PathBuf> = result.deps.iter()
.filter(|dep| !existing.contains(dep))
.cloned()
.collect();
product.inputs.extend(new_deps);
}
if let Some(ref piece) = joined_pieces {
product.extend_config_hash(piece);
}
}
}
progress.inc(product_ids.len() as u64);
}
Ok(())
}