mod cpp;
mod icpp;
mod markdown;
pub mod python;
mod tera;
use crate::deps_cache::DepsCache;
use crate::file_index::FileIndex;
use crate::graph::{BuildGraph, Product};
use crate::processors::{format_command, run_command_capture};
use anyhow::Result;
use indicatif::ProgressBar;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::Command;
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(())
}