use std::collections::hash_map::DefaultHasher;
use std::collections::{HashMap, HashSet};
use std::fs;
use std::hash::{Hash, Hasher};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct DependencyInfo {
pub file: String,
pub dependencies: Vec<String>,
pub module_dependencies: Vec<ModuleDep>,
pub command_line: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ModuleDep {
pub module_name: String,
pub module_file: String,
pub context_hash: String,
}
pub struct ClangScanDeps {
include_paths: Vec<String>,
system_include_paths: Vec<String>,
defines: Vec<(String, Option<String>)>,
seen_files: HashSet<String>,
scan_modules: bool,
target_triple: String,
files_scanned: usize,
}
impl ClangScanDeps {
pub fn new() -> Self {
Self {
include_paths: vec![".".to_string()],
system_include_paths: vec![
"/usr/include".to_string(),
"/usr/local/include".to_string(),
],
defines: Vec::new(),
seen_files: HashSet::new(),
scan_modules: true,
target_triple: "x86_64-unknown-linux-gnu".to_string(),
files_scanned: 0,
}
}
pub fn scan_file(&mut self, filename: &str, args: &[String]) -> Result<DependencyInfo, String> {
self.seen_files.clear();
self.files_scanned = 0;
let resolved = self.resolve_file(filename)?;
let mut deps = DependencyInfo {
file: resolved.clone(),
dependencies: Vec::new(),
module_dependencies: Vec::new(),
command_line: args.to_vec(),
};
let includes = self.parse_includes(&resolved);
let mut all_deps: Vec<String> = Vec::new();
for include in &includes {
if let Ok(inc_path) = self.resolve_include(&resolved, include) {
if !self.seen_files.contains(&inc_path) {
self.seen_files.insert(inc_path.clone());
all_deps.push(inc_path.clone());
match self.scan_file_internal(&inc_path) {
Ok(sub_deps) => {
for d in &sub_deps {
if !all_deps.contains(d) {
all_deps.push(d.clone());
}
}
}
Err(_) => {} }
}
}
}
all_deps.sort();
all_deps.dedup();
deps.dependencies = all_deps;
if self.scan_modules {
deps.module_dependencies = self.scan_module_imports(&resolved);
}
self.files_scanned += 1;
Ok(deps)
}
fn scan_file_internal(&mut self, filename: &str) -> Result<Vec<String>, String> {
let source = fs::read_to_string(filename)
.map_err(|e| format!("Cannot read '{}': {}", filename, e))?;
let includes = self.extract_includes_from_source(&source);
let mut deps = Vec::new();
for include in &includes {
if let Ok(inc_path) = self.resolve_include(filename, include) {
if !self.seen_files.contains(&inc_path) {
self.seen_files.insert(inc_path.clone());
deps.push(inc_path.clone());
}
}
}
Ok(deps)
}
pub fn scan_directory(&mut self, dir: &str, args: &[String]) -> Vec<DependencyInfo> {
let mut results = Vec::new();
let dir_path = Path::new(dir);
if !dir_path.is_dir() {
return results;
}
if let Ok(entries) = fs::read_dir(dir_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(subdir) = path.to_str() {
results.extend(self.scan_directory(subdir, args));
}
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if matches!(
ext,
"c" | "C" | "cc" | "cpp" | "cxx" | "c++" | "h" | "hpp" | "hxx"
) {
if let Some(file_str) = path.to_str() {
if let Ok(info) = self.scan_file(file_str, args) {
results.push(info);
}
}
}
}
}
}
results
}
pub fn generate_makefile_deps(&self, info: &DependencyInfo) -> String {
let obj_file = self.derive_object_name(&info.file);
let mut output = format!("{}: {}", obj_file, info.file);
for dep in &info.dependencies {
output.push_str(&format!(" \\\n {}", dep));
}
output.push('\n');
output
}
pub fn generate_module_map(&self, infos: &[DependencyInfo]) -> String {
let mut output = String::from("# Module dependency map\n\n");
let mut module_deps: HashMap<String, Vec<ModuleDep>> = HashMap::new();
for info in infos {
for md in &info.module_dependencies {
module_deps
.entry(md.module_name.clone())
.or_default()
.push(md.clone());
}
}
for (module_name, deps) in &module_deps {
output.push_str(&format!("module {} {{\n", module_name));
for dep in deps {
output.push_str(&format!(
" requires {} ({})\n",
dep.module_name, dep.module_file
));
}
output.push_str("}\n\n");
}
output
}
fn parse_includes(&self, source: &str) -> Vec<String> {
self.extract_includes_from_source(source)
}
fn extract_includes_from_source(&self, source: &str) -> Vec<String> {
let mut includes = Vec::new();
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && chars[i].is_whitespace() {
i += 1;
}
if i + 7 < len && chars[i] == '#' {
let rest: String = chars[i + 1..].iter().take(7).collect();
if rest.starts_with("include") {
i += 8;
while i < len && chars[i].is_whitespace() {
i += 1;
}
if i < len && (chars[i] == '"' || chars[i] == '<') {
let delim = if chars[i] == '"' { '"' } else { '>' };
i += 1;
let start = i;
while i < len && chars[i] != delim && chars[i] != '\n' {
i += 1;
}
if i < len && chars[i] == delim {
let include_path: String = chars[start..i].iter().collect();
includes.push(include_path);
i += 1;
}
}
}
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
while i < len && chars[i] != '\n' {
i += 1;
}
continue;
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
i += 2;
while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1;
}
if i + 1 < len {
i += 2;
}
continue;
}
if i < len && chars[i] == '"' {
i += 1;
while i < len && chars[i] != '"' {
if chars[i] == '\\' {
i += 1;
}
i += 1;
}
if i < len {
i += 1;
}
continue;
}
i += 1;
}
includes
}
fn scan_module_imports(&self, filename: &str) -> Vec<ModuleDep> {
let source = match fs::read_to_string(filename) {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let mut modules = Vec::new();
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && chars[i].is_whitespace() {
i += 1;
}
if i + 6 < len {
let word: String = chars[i..].iter().take(6).collect();
if word == "import" {
i += 6;
while i < len && chars[i].is_whitespace() {
i += 1;
}
let start = i;
while i < len
&& (chars[i].is_alphanumeric()
|| chars[i] == '.'
|| chars[i] == '_'
|| chars[i] == ':')
{
i += 1;
}
if i > start {
let module_name: String = chars[start..i].iter().collect();
while i < len && chars[i] != ';' && chars[i] != '\n' {
i += 1;
}
modules.push(ModuleDep {
module_name,
module_file: String::new(),
context_hash: String::new(),
});
}
}
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
while i < len && chars[i] != '\n' {
i += 1;
}
continue;
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
i += 2;
while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1;
}
if i + 1 < len {
i += 2;
}
continue;
}
i += 1;
}
modules
}
fn is_system_header(&self, path: &str) -> bool {
self.system_include_paths
.iter()
.any(|p| path.starts_with(p))
}
fn resolve_file(&self, filename: &str) -> Result<String, String> {
let path = Path::new(filename);
if path.exists() {
return Ok(fs::canonicalize(path)
.map_err(|e| format!("Cannot resolve '{}': {}", filename, e))?
.to_string_lossy()
.to_string());
}
Err(format!("File not found: '{}'", filename))
}
fn resolve_include(&self, from_file: &str, include: &str) -> Result<String, String> {
let from_path = Path::new(from_file);
if let Some(parent) = from_path.parent() {
let candidate = parent.join(include);
if candidate.exists() {
return Ok(candidate.to_string_lossy().to_string());
}
}
for inc_path in &self.include_paths {
let candidate = Path::new(inc_path).join(include);
if candidate.exists() {
return Ok(candidate.to_string_lossy().to_string());
}
}
for sys_path in &self.system_include_paths {
let candidate = Path::new(sys_path).join(include);
if candidate.exists() {
return Ok(candidate.to_string_lossy().to_string());
}
}
Err(format!("Include file not found: '{}'", include))
}
fn derive_object_name(&self, source: &str) -> String {
let path = Path::new(source);
let stem = path
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| "a".to_string());
format!("{}.o", stem)
}
pub fn add_include_path(&mut self, path: &str) {
if !self.include_paths.contains(&path.to_string()) {
self.include_paths.push(path.to_string());
}
}
pub fn add_system_include_path(&mut self, path: &str) {
if !self.system_include_paths.contains(&path.to_string()) {
self.system_include_paths.push(path.to_string());
}
}
pub fn add_define(&mut self, name: &str, value: Option<&str>) {
self.defines
.push((name.to_string(), value.map(|v| v.to_string())));
}
pub fn set_target_triple(&mut self, triple: &str) {
self.target_triple = triple.to_string();
}
pub fn set_scan_modules(&mut self, scan: bool) {
self.scan_modules = scan;
}
pub fn files_scanned(&self) -> usize {
self.files_scanned
}
}
impl Default for ClangScanDeps {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct VirtualFileSystem {
pub files: HashMap<String, String>,
pub directories: HashSet<String>,
}
impl VirtualFileSystem {
pub fn new() -> Self {
Self {
files: HashMap::new(),
directories: HashSet::new(),
}
}
pub fn add_file(&mut self, path: &str, content: &str) {
self.files.insert(path.to_string(), content.to_string());
let mut parent = std::path::Path::new(path);
while let Some(p) = parent.parent() {
if let Some(s) = p.to_str() {
self.directories.insert(s.to_string());
}
parent = p;
}
}
pub fn exists(&self, path: &str) -> bool {
self.files.contains_key(path) || self.directories.contains(path)
}
pub fn read_file(&self, path: &str) -> Option<&str> {
self.files.get(path).map(|s| s.as_str())
}
}
impl ClangScanDeps {
pub fn discover_module_dependencies(
&mut self,
filename: &str,
) -> Result<Vec<ModuleDep>, String> {
let source = std::fs::read_to_string(filename)
.map_err(|e| format!("Cannot read '{}': {}", filename, e))?;
let mut modules = Vec::new();
let chars: Vec<char> = source.chars().collect();
let len = chars.len();
let mut i = 0;
while i < len {
while i < len && chars[i].is_whitespace() {
i += 1;
}
if i + 6 < len {
let slice: String = chars[i..i + 6].iter().collect();
if slice == "import" {
i += 6;
while i < len && chars[i].is_whitespace() {
i += 1;
}
let start = i;
while i < len
&& (chars[i].is_alphanumeric()
|| chars[i] == '.'
|| chars[i] == '_'
|| chars[i] == ':')
{
i += 1;
}
if i > start {
let module_name: String = chars[start..i].iter().collect();
modules.push(ModuleDep {
module_name,
module_file: String::new(),
context_hash: String::new(),
});
}
}
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '/' {
while i < len && chars[i] != '\n' {
i += 1;
}
continue;
}
if i + 1 < len && chars[i] == '/' && chars[i + 1] == '*' {
i += 2;
while i + 1 < len && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1;
}
if i + 1 < len {
i += 2;
}
continue;
}
i += 1;
}
Ok(modules)
}
pub fn scan_file_vfs(
&self,
vfs: &VirtualFileSystem,
filename: &str,
args: &[String],
) -> Result<DependencyInfo, String> {
let content = vfs
.read_file(filename)
.ok_or_else(|| format!("File not found in VFS: '{}'", filename))?;
let includes = self.extract_includes_from_source(content);
let mut deps = Vec::new();
for inc in &includes {
if vfs.exists(inc) {
deps.push(inc.clone());
}
}
deps.sort();
deps.dedup();
Ok(DependencyInfo {
file: filename.to_string(),
dependencies: deps,
module_dependencies: Vec::new(),
command_line: args.to_vec(),
})
}
}
#[derive(Debug, Clone)]
pub struct DepGraphNode {
pub file: String,
pub dependencies: Vec<usize>,
pub dependents: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct DependencyGraph {
pub nodes: Vec<DepGraphNode>,
file_to_idx: HashMap<String, usize>,
}
impl DependencyGraph {
pub fn new() -> Self {
Self {
nodes: Vec::new(),
file_to_idx: HashMap::new(),
}
}
pub fn build(infos: &[DependencyInfo]) -> Self {
let mut graph = Self::new();
for info in infos {
let idx = graph.get_or_add_node(&info.file);
for dep in &info.dependencies {
let dep_idx = graph.get_or_add_node(dep);
graph.nodes[idx].dependencies.push(dep_idx);
graph.nodes[dep_idx].dependents.push(idx);
}
}
graph
}
fn get_or_add_node(&mut self, file: &str) -> usize {
if let Some(&idx) = self.file_to_idx.get(file) {
return idx;
}
let idx = self.nodes.len();
self.file_to_idx.insert(file.to_string(), idx);
self.nodes.push(DepGraphNode {
file: file.to_string(),
dependencies: Vec::new(),
dependents: Vec::new(),
});
idx
}
pub fn topological_sort(&self) -> Vec<usize> {
let mut in_degree = vec![0usize; self.nodes.len()];
for node in &self.nodes {
for &dep in &node.dependencies {
if dep < in_degree.len() {
in_degree[dep] += 1;
}
}
}
let mut queue: Vec<usize> = in_degree
.iter()
.enumerate()
.filter(|&(_, &d)| d == 0)
.map(|(i, _)| i)
.collect();
let mut result = Vec::new();
while let Some(node_idx) = queue.pop() {
result.push(node_idx);
for &dep in &self.nodes[node_idx].dependencies {
if dep < in_degree.len() {
in_degree[dep] -= 1;
if in_degree[dep] == 0 {
queue.push(dep);
}
}
}
}
result
}
}
impl ClangScanDeps {
pub fn generate_depfile(&self, info: &DependencyInfo) -> String {
let obj_name = self.derive_object_name(&info.file);
let mut output = format!("{}: {}", obj_name, info.file);
for dep in &info.dependencies {
output.push_str(&format!(" \\\n {}", dep));
}
output.push('\n');
output
}
pub fn write_depfile(&self, info: &DependencyInfo, output_path: &str) -> Result<(), String> {
let content = self.generate_depfile(info);
std::fs::write(output_path, content)
.map_err(|e| format!("Failed to write depfile '{}': {}", output_path, e))
}
pub fn generate_depfile_with_target(&self, info: &DependencyInfo, target: &str) -> String {
let mut output = format!("{}: {}", target, info.file);
for dep in &info.dependencies {
output.push_str(&format!(" \\\n {}", dep));
}
output.push('\n');
output
}
pub fn generate_mq_depfile(&self, info: &DependencyInfo, target: &str) -> String {
let quoted_target = target.replace(' ', "\\ ").replace('#', "\\#");
self.generate_depfile_with_target(info, "ed_target)
}
pub fn is_system_dependency(&self, dep: &str) -> bool {
self.is_system_header(dep)
}
pub fn filter_system_deps(&self, info: &mut DependencyInfo) {
info.dependencies.retain(|dep| !self.is_system_header(dep));
}
pub fn add_pch_dependency(&self, info: &mut DependencyInfo, pch_file: &str) {
if !info.dependencies.contains(&pch_file.to_string()) {
info.dependencies.insert(0, pch_file.to_string());
}
}
pub fn compute_context_hash(args: &[String]) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for arg in args {
std::hash::Hash::hash(arg, &mut hasher);
}
format!("{:016x}", std::hash::Hasher::finish(&hasher))
}
}
#[derive(Debug, Clone)]
pub struct ModuleMapEntry {
pub module_name: String,
pub header: Option<String>,
pub export: Vec<String>,
pub requires: Vec<String>,
}
pub fn parse_module_map(content: &str) -> Vec<ModuleMapEntry> {
let mut entries = Vec::new();
let mut lines = content.lines();
while let Some(line) = lines.next() {
let trimmed = line.trim();
if trimmed.starts_with("module") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
let module_name = parts[1].to_string();
let mut entry = ModuleMapEntry {
module_name,
header: None,
export: Vec::new(),
requires: Vec::new(),
};
while let Some(inner) = lines.next() {
let inner_trimmed = inner.trim();
if inner_trimmed == "}" {
break;
}
if inner_trimmed.starts_with("header") {
let header_line = inner_trimmed.trim_start_matches("header").trim();
entry.header = Some(header_line.trim_matches('"').to_string());
} else if inner_trimmed.starts_with("export") {
let export = inner_trimmed.trim_start_matches("export").trim();
if !export.is_empty() && export != "*" {
entry.export.push(export.to_string());
}
} else if inner_trimmed.starts_with("requires") {
let req = inner_trimmed.trim_start_matches("requires").trim();
if !req.is_empty() {
entry.requires.push(req.to_string());
}
}
}
entries.push(entry);
}
}
}
entries
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_clang_scan_deps_new() {
let csd = ClangScanDeps::new();
assert_eq!(csd.files_scanned(), 0);
assert!(csd.scan_modules);
}
#[test]
fn test_extract_includes_basic() {
let csd = ClangScanDeps::new();
let source = r#"
#include <stdio.h>
#include "myheader.h"
int main() { return 0; }
"#;
let includes = csd.extract_includes_from_source(source);
assert_eq!(includes, vec!["stdio.h", "myheader.h"]);
}
#[test]
fn test_extract_includes_empty() {
let csd = ClangScanDeps::new();
assert!(csd.extract_includes_from_source("").is_empty());
}
#[test]
fn test_is_system_header() {
let mut csd = ClangScanDeps::new();
csd.add_system_include_path("/usr/include");
assert!(csd.is_system_header("/usr/include/stdio.h"));
assert!(!csd.is_system_header("/home/user/header.h"));
}
#[test]
fn test_generate_makefile_deps() {
let csd = ClangScanDeps::new();
let info = DependencyInfo {
file: "main.c".to_string(),
dependencies: vec!["header.h".to_string()],
module_dependencies: vec![],
command_line: vec![],
};
let makefile = csd.generate_makefile_deps(&info);
assert!(makefile.contains("main.o: main.c"));
}
#[test]
fn test_derive_object_name() {
let csd = ClangScanDeps::new();
assert_eq!(csd.derive_object_name("main.c"), "main.o");
}
}