#![allow(dead_code)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use crate::config::{CcgoConfig, DependencyConfig};
use crate::dependency::graph::{DependencyGraph, DependencyNode};
use crate::dependency::version_resolver::{ConflictStrategy, VersionRequirement, VersionResolver};
const MAX_DEPTH: usize = 50;
pub struct DependencyResolver {
graph: DependencyGraph,
visited: HashMap<String, String>,
project_root: PathBuf,
ccgo_home: PathBuf,
version_resolver: VersionResolver,
}
impl DependencyResolver {
pub fn new(project_root: PathBuf, ccgo_home: PathBuf) -> Self {
Self {
graph: DependencyGraph::new(),
visited: HashMap::new(),
project_root,
ccgo_home,
version_resolver: VersionResolver::new(),
}
}
pub fn with_strategy(
project_root: PathBuf,
ccgo_home: PathBuf,
strategy: ConflictStrategy,
) -> Self {
Self {
graph: DependencyGraph::new(),
visited: HashMap::new(),
project_root,
ccgo_home,
version_resolver: VersionResolver::with_strategy(strategy),
}
}
pub fn set_strategy(&mut self, strategy: ConflictStrategy) {
self.version_resolver.set_strategy(strategy);
}
pub fn resolve(&mut self, dependencies: &[DependencyConfig]) -> Result<DependencyGraph> {
println!("\n📊 Resolving dependency graph...");
println!(" Strategy: {}", self.version_resolver.strategy_name());
for dep in dependencies {
self.graph.add_root(dep.name.clone());
}
for dep in dependencies {
self.resolve_dependency(dep, 0)?;
}
if let Some(cycle) = self.graph.detect_cycles() {
anyhow::bail!(
"Circular dependency detected: {} -> {}",
cycle.join(" -> "),
cycle[0]
);
}
if self.version_resolver.has_conflicts() {
self.version_resolver.print_conflicts();
if self.version_resolver.is_strict() {
anyhow::bail!(
"Version conflicts detected (strict mode enabled). \
Use --conflict-strategy=first|highest|lowest to resolve automatically."
);
}
match self.version_resolver.resolve_all() {
Ok(resolved) => {
println!(
"\n ✓ Resolved {} version conflicts using '{}' strategy",
resolved.len(),
self.version_resolver.strategy_name()
);
for (pkg, version) in &resolved {
println!(" {} → {}", pkg, version);
}
}
Err(e) => {
eprintln!("\n ⚠️ Failed to resolve some conflicts: {}", e);
}
}
}
println!(" ✓ Dependency graph resolved");
Ok(self.graph.clone())
}
fn resolve_dependency(&mut self, dep: &DependencyConfig, depth: usize) -> Result<()> {
if depth >= MAX_DEPTH {
anyhow::bail!(
"Maximum dependency depth ({}) exceeded for '{}'",
MAX_DEPTH,
dep.name
);
}
if self.handle_visited_dependency(dep) {
return Ok(());
}
if !dep.version.is_empty() {
let _ = self.version_resolver.add_requirement(
dep.name.clone(),
"root".to_string(),
dep.version.clone(),
);
}
self.visited.insert(dep.name.clone(), dep.version.clone());
self.process_dependency_config(dep, depth)
}
fn handle_visited_dependency(&mut self, dep: &DependencyConfig) -> bool {
if let Some(existing_version) = self.visited.get(&dep.name) {
self.check_version_compatibility(dep, existing_version);
return true;
}
false
}
fn check_version_compatibility(&self, dep: &DependencyConfig, existing_version: &str) {
if existing_version.is_empty() || dep.version.is_empty() || existing_version == dep.version
{
return;
}
let existing_req = VersionRequirement::parse(existing_version);
let new_req = VersionRequirement::parse(&dep.version);
if let (Ok(existing), Ok(new)) = (existing_req, new_req) {
if !existing.is_compatible_with(&new) {
eprintln!(
" ⚠️ Version conflict for '{}': have {}, need {}",
dep.name, existing_version, dep.version
);
}
}
}
fn process_dependency_config(&mut self, dep: &DependencyConfig, depth: usize) -> Result<()> {
let dep_path = self.locate_dependency(dep)?;
let dep_config_path = dep_path.join("CCGO.toml");
if !dep_config_path.exists() {
self.add_node_to_graph(dep, vec![], depth);
return Ok(());
}
let dep_config = CcgoConfig::load_from(&dep_config_path)
.with_context(|| format!("Failed to load CCGO.toml for '{}'", dep.name))?;
self.process_transitive_deps(dep, &dep_path, &dep_config, depth)
}
fn process_transitive_deps(
&mut self,
dep: &DependencyConfig,
dep_path: &Path,
dep_config: &CcgoConfig,
depth: usize,
) -> Result<()> {
let mut transitive_deps = dep_config.dependencies.clone();
for trans_dep in &mut transitive_deps {
if let Some(ref path) = trans_dep.path {
if !Path::new(path).is_absolute() {
let resolved_path = dep_path.join(path);
trans_dep.path = Some(resolved_path.to_string_lossy().to_string());
}
}
}
let dep_names: Vec<String> = transitive_deps.iter().map(|d| d.name.clone()).collect();
self.add_node_to_graph(dep, dep_names.clone(), depth);
for trans_dep in &transitive_deps {
self.graph.add_edge(&trans_dep.name, &dep.name);
self.resolve_dependency(trans_dep, depth + 1)?;
}
Ok(())
}
fn locate_dependency(&self, dep: &DependencyConfig) -> Result<PathBuf> {
if let Some(ref path) = dep.path {
let dep_path = if Path::new(path).is_absolute() {
PathBuf::from(path)
} else {
self.project_root.join(path)
};
if !dep_path.exists() {
anyhow::bail!(
"Path dependency '{}' not found at: {}",
dep.name,
dep_path.display()
);
}
Ok(dep_path)
} else if let Some(ref git_url) = dep.git {
let hash_input = format!("{}:{}", dep.name, git_url);
let full_hash = format!("{:x}", md5::compute(hash_input.as_bytes()));
let short_hash = &full_hash[..16];
let new_path = self
.ccgo_home
.join("git")
.join("checkouts")
.join(&full_hash);
if new_path.exists() {
return Ok(new_path);
}
let legacy_path = self
.ccgo_home
.join("registry")
.join(format!("{}-{}", dep.name, short_hash));
if legacy_path.exists() {
return Ok(legacy_path);
}
anyhow::bail!(
"Git dependency '{}' not found in cache. Run 'ccgo fetch' first.",
dep.name
);
} else if dep.zip.is_some() {
Ok(self.project_root.join(".ccgo").join("deps").join(&dep.name))
} else if !dep.version.is_empty() {
let local_cache = self
.ccgo_home
.join("packages")
.join(dep.name.to_lowercase())
.join(&dep.version);
if local_cache.exists() {
Ok(local_cache)
} else {
anyhow::bail!(
"Dependency '{}' version {} not found in local cache ({}).\n\
Hint: in the source project run `ccgo install`\n\
to populate the cache, or add a git/path/zip source.",
dep.name,
dep.version,
local_cache.display()
);
}
} else {
anyhow::bail!(
"Dependency '{}' has no valid source (git, path, zip, or version)",
dep.name
);
}
}
fn add_node_to_graph(
&mut self,
dep: &DependencyConfig,
dependencies: Vec<String>,
depth: usize,
) {
let source = self.build_source_string(dep);
let node = DependencyNode {
name: dep.name.clone(),
version: dep.version.clone(),
source,
dependencies,
depth,
config: dep.clone(),
};
self.graph.add_node(node);
}
fn build_source_string(&self, dep: &DependencyConfig) -> String {
if let Some(ref git) = dep.git {
format!("git+{}", git)
} else if let Some(ref path) = dep.path {
format!("path+{}", path)
} else if let Some(ref zip) = dep.zip {
format!("zip+{}", zip)
} else {
format!("registry+{}@{}", dep.name, dep.version)
}
}
pub fn graph(&self) -> &DependencyGraph {
&self.graph
}
pub fn version_resolver(&self) -> &VersionResolver {
&self.version_resolver
}
pub fn has_version_conflicts(&self) -> bool {
self.version_resolver.has_conflicts()
}
}
pub fn resolve_dependencies(
dependencies: &[DependencyConfig],
project_root: &Path,
ccgo_home: &Path,
) -> Result<DependencyGraph> {
resolve_dependencies_with_strategy(
dependencies,
project_root,
ccgo_home,
ConflictStrategy::default(),
)
}
pub fn resolve_dependencies_with_strategy(
dependencies: &[DependencyConfig],
project_root: &Path,
ccgo_home: &Path,
strategy: ConflictStrategy,
) -> Result<DependencyGraph> {
let mut resolver = DependencyResolver::with_strategy(
project_root.to_path_buf(),
ccgo_home.to_path_buf(),
strategy,
);
resolver.resolve(dependencies)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn create_test_dependency(name: &str, deps: Vec<&str>) -> DependencyConfig {
DependencyConfig {
name: name.to_string(),
version: "1.0.0".to_string(),
path: Some(format!("./{}", name)),
default_features: Some(true),
..Default::default()
}
}
#[test]
fn test_simple_resolution() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_a_dir = project_root.join("dep_a");
fs::create_dir_all(&dep_a_dir).unwrap();
fs::write(
dep_a_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_a"
version = "1.0.0"
"#,
)
.unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![create_test_dependency("dep_a", vec![])];
let result = resolver.resolve(&deps);
assert!(result.is_ok());
let graph = result.unwrap();
assert_eq!(graph.nodes().len(), 1);
assert!(graph.get_node("dep_a").is_some());
}
#[test]
fn test_max_depth_exceeded() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let dep = create_test_dependency("test", vec![]);
let result = resolver.resolve_dependency(&dep, MAX_DEPTH);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Maximum dependency depth"));
}
#[test]
fn test_transitive_dependencies() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_c_dir = project_root.join("dep_c");
fs::create_dir_all(&dep_c_dir).unwrap();
fs::write(
dep_c_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_c"
version = "1.0.0"
"#,
)
.unwrap();
let dep_b_dir = project_root.join("dep_b");
fs::create_dir_all(&dep_b_dir).unwrap();
fs::write(
dep_b_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_b"
version = "1.0.0"
[[dependencies]]
name = "dep_c"
version = "1.0.0"
path = "../dep_c"
"#,
)
.unwrap();
let dep_a_dir = project_root.join("dep_a");
fs::create_dir_all(&dep_a_dir).unwrap();
fs::write(
dep_a_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_a"
version = "1.0.0"
[[dependencies]]
name = "dep_b"
version = "1.0.0"
path = "../dep_b"
"#,
)
.unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![create_test_dependency("dep_a", vec![])];
let result = resolver.resolve(&deps);
assert!(result.is_ok());
let graph = result.unwrap();
assert_eq!(graph.nodes().len(), 3);
assert!(graph.get_node("dep_a").is_some());
assert!(graph.get_node("dep_b").is_some());
assert!(graph.get_node("dep_c").is_some());
let sorted = graph.topological_sort().unwrap();
let pos_c = sorted.iter().position(|n| n == "dep_c").unwrap();
let pos_b = sorted.iter().position(|n| n == "dep_b").unwrap();
let pos_a = sorted.iter().position(|n| n == "dep_a").unwrap();
assert!(pos_c < pos_b, "dep_c should come before dep_b");
assert!(pos_b < pos_a, "dep_b should come before dep_a");
}
#[test]
fn test_circular_dependency_detection() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_a_dir = project_root.join("dep_a");
fs::create_dir_all(&dep_a_dir).unwrap();
fs::write(
dep_a_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_a"
version = "1.0.0"
[[dependencies]]
name = "dep_b"
version = "1.0.0"
path = "../dep_b"
"#,
)
.unwrap();
let dep_b_dir = project_root.join("dep_b");
fs::create_dir_all(&dep_b_dir).unwrap();
fs::write(
dep_b_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_b"
version = "1.0.0"
[[dependencies]]
name = "dep_c"
version = "1.0.0"
path = "../dep_c"
"#,
)
.unwrap();
let dep_c_dir = project_root.join("dep_c");
fs::create_dir_all(&dep_c_dir).unwrap();
fs::write(
dep_c_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_c"
version = "1.0.0"
[[dependencies]]
name = "dep_a"
version = "1.0.0"
path = "../dep_a"
"#,
)
.unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![create_test_dependency("dep_a", vec![])];
let result = resolver.resolve(&deps);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Circular dependency detected"));
}
#[test]
fn test_shared_dependency() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_d_dir = project_root.join("dep_d");
fs::create_dir_all(&dep_d_dir).unwrap();
fs::write(
dep_d_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_d"
version = "1.0.0"
"#,
)
.unwrap();
let dep_b_dir = project_root.join("dep_b");
fs::create_dir_all(&dep_b_dir).unwrap();
fs::write(
dep_b_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_b"
version = "1.0.0"
[[dependencies]]
name = "dep_d"
version = "1.0.0"
path = "../dep_d"
"#,
)
.unwrap();
let dep_c_dir = project_root.join("dep_c");
fs::create_dir_all(&dep_c_dir).unwrap();
fs::write(
dep_c_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_c"
version = "1.0.0"
[[dependencies]]
name = "dep_d"
version = "1.0.0"
path = "../dep_d"
"#,
)
.unwrap();
let dep_a_dir = project_root.join("dep_a");
fs::create_dir_all(&dep_a_dir).unwrap();
fs::write(
dep_a_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_a"
version = "1.0.0"
[[dependencies]]
name = "dep_b"
version = "1.0.0"
path = "../dep_b"
[[dependencies]]
name = "dep_c"
version = "1.0.0"
path = "../dep_c"
"#,
)
.unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![create_test_dependency("dep_a", vec![])];
let result = resolver.resolve(&deps);
assert!(result.is_ok());
let graph = result.unwrap();
assert_eq!(graph.nodes().len(), 4);
let stats = graph.stats();
assert!(stats.shared_count > 0);
}
#[test]
fn test_missing_ccgo_toml() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_a_dir = project_root.join("dep_a");
fs::create_dir_all(&dep_a_dir).unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![create_test_dependency("dep_a", vec![])];
let result = resolver.resolve(&deps);
assert!(result.is_ok());
let graph = result.unwrap();
assert_eq!(graph.nodes().len(), 1);
assert!(graph.get_node("dep_a").is_some());
let node = graph.get_node("dep_a").unwrap();
assert_eq!(node.dependencies.len(), 0);
}
#[test]
fn test_version_conflict_warning() {
let temp_dir = TempDir::new().unwrap();
let project_root = temp_dir.path();
let dep_d_dir = project_root.join("dep_d");
fs::create_dir_all(&dep_d_dir).unwrap();
fs::write(
dep_d_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_d"
version = "2.0.0"
"#,
)
.unwrap();
let dep_b_dir = project_root.join("dep_b");
fs::create_dir_all(&dep_b_dir).unwrap();
fs::write(
dep_b_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_b"
version = "1.0.0"
[[dependencies]]
name = "dep_d"
version = "1.0.0"
path = "../dep_d"
"#,
)
.unwrap();
let dep_c_dir = project_root.join("dep_c");
fs::create_dir_all(&dep_c_dir).unwrap();
fs::write(
dep_c_dir.join("CCGO.toml"),
r#"
[package]
name = "dep_c"
version = "1.0.0"
[[dependencies]]
name = "dep_d"
version = "2.0.0"
path = "../dep_d"
"#,
)
.unwrap();
let ccgo_home = temp_dir.path().join(".ccgo");
let mut resolver = DependencyResolver::new(project_root.to_path_buf(), ccgo_home);
let deps = vec![
create_test_dependency("dep_b", vec![]),
create_test_dependency("dep_c", vec![]),
];
let result = resolver.resolve(&deps);
assert!(result.is_ok());
let graph = result.unwrap();
assert_eq!(graph.nodes().len(), 3); }
}