mod cpp;
mod icpp;
mod markdown;
pub mod python;
mod sass;
mod tera;
use crate::deps_cache::{AnalyzerId, ClassifyResult, DepsCache, DepsCacheStats};
use crate::file_index::FileIndex;
use crate::graph::{BuildGraph, Product};
use crate::processor::{format_command, run_command_capture};
use anyhow::{Context, Result};
use indicatif::ProgressBar;
use std::collections::{BTreeMap, BTreeSet, 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 scan(
&self,
ctx: &crate::build_context::BuildContext,
source: &Path,
file_index: &FileIndex,
) -> Result<ScanResult>;
fn always_rescan(&self) -> bool {
false
}
fn fingerprint_parts(&self, ctx: &crate::build_context::BuildContext) -> Result<Vec<String>>;
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
}
#[derive(Debug, Default)]
pub struct ScanResult {
pub deps: Vec<PathBuf>,
pub hash_pieces: Vec<String>,
pub absent: Vec<PathBuf>,
}
impl ScanResult {
pub const fn deps(deps: Vec<PathBuf>, absent: Vec<PathBuf>) -> Self {
Self {
deps,
hash_pieces: Vec::new(),
absent,
}
}
}
fn joined_hash_pieces(result: &ScanResult) -> Option<String> {
if result.hash_pieces.is_empty() {
return None;
}
let parts: Vec<&str> = result.hash_pieces.iter().map(String::as_str).collect();
Some(crate::checksum::hash_parts(&parts))
}
struct ActiveAnalyzer {
iname: String,
analyzer: Box<dyn DepAnalyzer>,
fingerprint: String,
}
pub struct Analysis {
analyzers: Vec<ActiveAnalyzer>,
deps_cache: DepsCache,
file_index: FileIndex,
}
impl Analysis {
pub fn new(
ctx: &crate::build_context::BuildContext,
analyzers: Vec<(String, Box<dyn DepAnalyzer>)>,
deps_cache: DepsCache,
file_index: FileIndex,
) -> Result<Self> {
let mut active: Vec<ActiveAnalyzer> = analyzers
.into_iter()
.map(|(iname, analyzer)| {
let parts = analyzer.fingerprint_parts(ctx).with_context(|| {
format!("Failed to fingerprint the configuration of analyzer '{iname}'")
})?;
let parts: Vec<&str> = parts.iter().map(String::as_str).collect();
Ok(ActiveAnalyzer {
fingerprint: crate::checksum::hash_parts(&parts),
iname,
analyzer,
})
})
.collect::<Result<_>>()?;
active.sort_by(|a, b| a.iname.cmp(&b.iname));
Ok(Self {
analyzers: active,
deps_cache,
file_index,
})
}
pub const fn is_empty(&self) -> bool {
self.analyzers.is_empty()
}
pub fn count_matches(&self, graph: &BuildGraph) -> usize {
self.analyzers
.iter()
.map(|a| a.analyzer.count_matches(graph))
.sum()
}
pub fn unique_sources(&self, graph: &BuildGraph) -> HashSet<(usize, PathBuf)> {
self.analyzers
.iter()
.enumerate()
.flat_map(|(index, a)| {
a.analyzer
.matching_sources(graph)
.into_iter()
.map(move |source| (index, source))
})
.collect()
}
pub fn classify(
&self,
ctx: &crate::build_context::BuildContext,
index: usize,
source: &Path,
) -> ClassifyResult {
let a = &self.analyzers[index];
if a.analyzer.always_rescan() {
return ClassifyResult::Miss;
}
self.deps_cache.classify(ctx, a.id(), source)
}
pub const fn stats(&self) -> &DepsCacheStats {
self.deps_cache.stats()
}
pub fn analyze_graph(
&mut self,
ctx: &crate::build_context::BuildContext,
graph: &mut BuildGraph,
progress: &ProgressBar,
) -> Result<()> {
for index in 0..self.analyzers.len() {
let mut by_source: BTreeMap<PathBuf, Vec<usize>> = BTreeMap::new();
for p in graph.products() {
if let Some(source) = self.analyzers[index].analyzer.match_product(p) {
by_source.entry(source).or_default().push(p.id);
}
}
for (source, product_ids) in &by_source {
progress.set_message(format!(
"[{}] {}",
self.analyzers[index].iname,
source.display()
));
if source.exists() {
let result = self.scan_source(ctx, index, source)?;
let piece = joined_hash_pieces(&result);
for &id in product_ids {
graph.add_inputs(id, &result.deps);
if let Some(piece) = &piece {
graph.extend_cache_key(id, piece);
}
}
}
progress.inc(product_ids.len() as u64);
}
}
Ok(())
}
pub fn reanalyze(
&mut self,
ctx: &crate::build_context::BuildContext,
graph: &mut BuildGraph,
changed: &[usize],
) -> Result<bool> {
if self.analyzers.is_empty() {
return Ok(false);
}
let rewritten: HashSet<PathBuf> = changed
.iter()
.filter_map(|&id| graph.get_product(id))
.flat_map(|p| p.outputs.iter().cloned())
.collect();
if rewritten.is_empty() {
return Ok(false);
}
let targets: BTreeSet<usize> = graph
.products()
.iter()
.filter(|p| {
p.analyzer_inputs().iter().any(|i| rewritten.contains(i))
|| self.analyzers.iter().any(|a| {
a.analyzer
.match_product(p)
.is_some_and(|source| rewritten.contains(&source))
})
})
.map(|p| p.id)
.collect();
let mut moved = false;
for id in targets {
let before = graph
.get_product(id)
.map(|p| (p.inputs.clone(), p.cache_key.clone()));
graph.reset_analysis(id);
let mut pieces: Vec<String> = Vec::new();
for index in 0..self.analyzers.len() {
let product = graph
.get_product(id)
.expect(crate::errors::INVALID_PRODUCT_ID);
let Some(source) = self.analyzers[index].analyzer.match_product(product) else {
continue;
};
if !source.exists() {
continue;
}
let result = self.scan_source(ctx, index, &source)?;
graph.add_inputs(id, &result.deps);
pieces.extend(joined_hash_pieces(&result));
}
graph.set_analyzer_pieces(id, pieces);
let after = graph
.get_product(id)
.map(|p| (p.inputs.clone(), p.cache_key.clone()));
moved |= before != after;
}
Ok(moved)
}
fn scan_source(
&mut self,
ctx: &crate::build_context::BuildContext,
index: usize,
source: &Path,
) -> Result<ScanResult> {
let Self {
analyzers,
deps_cache,
file_index,
} = self;
let a = &analyzers[index];
let cached = !a.analyzer.always_rescan();
if cached {
if let Some(deps) = deps_cache.get(ctx, a.id(), source) {
return Ok(ScanResult::deps(deps, Vec::new()));
}
} else {
deps_cache.count_uncached_scan();
}
let source_checksum = DepsCache::source_checksum(ctx, source)?;
let result = a.analyzer.scan(ctx, source, file_index)?;
if cached && !result.hash_pieces.is_empty() {
anyhow::bail!(
"analyzer '{}' returned hash pieces for {} but caches its results; \
hash pieces are not cached, so it must set always_rescan",
a.iname,
source.display()
);
}
let absent: Vec<PathBuf> = result
.absent
.iter()
.filter(|path| std::fs::symlink_metadata(path).is_err())
.cloned()
.collect();
if result.deps.iter().any(|dep| !dep.exists()) {
return Ok(result);
}
if let Err(e) = deps_cache.set(ctx, a.id(), source, source_checksum, &result.deps, &absent)
{
crate::output::warn(&format!(
"failed to cache dependencies for {}: {}",
source.display(),
e
));
}
Ok(result)
}
}
impl ActiveAnalyzer {
fn id(&self) -> AnalyzerId<'_> {
AnalyzerId {
iname: &self.iname,
fingerprint: &self.fingerprint,
}
}
}
pub fn probed_before(candidates: &[PathBuf], found: Option<&PathBuf>) -> Vec<PathBuf> {
candidates
.iter()
.take_while(|c| Some(*c) != found)
.cloned()
.collect()
}
pub fn config_fingerprint<T: serde::Serialize>(config: &T) -> Result<Vec<String>> {
Ok(vec![
toml::to_string(config).context("Failed to serialize analyzer config")?,
])
}