use pep508_rs::pep440_rs::{Version, VersionSpecifiers};
use pep508_rs::{Requirement, VerbatimUrl, VersionOrUrl};
use std::collections::{HashMap, VecDeque};
use std::error::Error;
use std::str::FromStr;
use crate::core::marker;
use crate::dependencies::package::{self, PackageUrl};
#[derive(Debug, Clone)]
pub struct ResolutionNode {
pub name: String,
pub version: String,
pub dependencies: Vec<String>,
pub urls: Vec<PackageUrl>,
}
pub struct DependencyGraph {
pub packages: HashMap<String, ResolutionNode>,
}
#[allow(clippy::implicit_hasher)]
pub async fn resolve(
root_deps: &HashMap<String, String>,
python_version: &str,
) -> Result<DependencyGraph, Box<dyn Error>> {
let mut resolved = HashMap::<String, ResolutionNode>::new();
let mut queue = VecDeque::<(String, Option<String>)>::new();
let marker_env = marker::build_marker_environment(python_version)?;
for (name, version_req) in root_deps {
queue.push_back((name.clone(), Some(version_req.clone())));
}
while let Some((name, version_constraint)) = queue.pop_front() {
let name_lower = name.to_lowercase().replace('-', "_");
if let Some(existing) = resolved.get(&name_lower) {
if let Some(constraint_str) = version_constraint {
if let Ok(specifiers) = VersionSpecifiers::from_str(&constraint_str) {
if let Ok(version) = Version::from_str(&existing.version) {
if !specifiers.contains(&version) {
return Err(format!(
"Conflict detected for package {}: existing version {} does not satisfy new constraint {}",
name, existing.version, constraint_str
).into());
}
}
}
}
continue;
}
let fetch_version = version_constraint.as_deref().and_then(|v| {
let trimmed = v.trim_start_matches("==");
if trimmed.chars().next().is_some_and(|c| c.is_ascii_digit()) {
Some(trimmed)
} else {
None }
});
let info = package::fetch_package_info(&name, fetch_version).await?;
let resolved_version = info.info.version.clone();
let mut sub_deps = Vec::new();
if let Some(requires_dist) = info.info.requires_dist {
for req_str in requires_dist {
if marker::should_include_requirement(&req_str, &marker_env) {
let req = Requirement::<VerbatimUrl>::from_str(&req_str)?;
let sub_name = req.name.to_string();
let sub_constraint = match req.version_or_url {
Some(VersionOrUrl::VersionSpecifier(spec)) => Some(spec.to_string()),
_ => None,
};
sub_deps.push(sub_name.clone());
queue.push_back((sub_name, sub_constraint));
}
}
}
resolved.insert(
name_lower,
ResolutionNode {
name: info.info.name,
version: resolved_version,
dependencies: sub_deps,
urls: info.urls,
},
);
}
Ok(DependencyGraph { packages: resolved })
}