apt-ostree/src/dependency_resolver.rs

455 lines
No EOL
15 KiB
Rust

//! Package Dependency Resolver for APT-OSTree
//!
//! This module implements dependency resolution for DEB packages in the context
//! of OSTree commits, ensuring proper layering order and conflict resolution.
use std::collections::{HashMap, HashSet, VecDeque};
use tracing::{info, warn, debug};
use serde::{Serialize, Deserialize};
use crate::error::{AptOstreeError, AptOstreeResult};
use crate::apt_ostree_integration::DebPackageMetadata;
/// Dependency relationship types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum DependencyRelation {
Depends,
Recommends,
Suggests,
Conflicts,
Breaks,
Provides,
Replaces,
}
/// Dependency constraint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyConstraint {
pub package_name: String,
pub version_constraint: Option<VersionConstraint>,
pub relation: DependencyRelation,
}
/// Version constraint
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionConstraint {
pub operator: VersionOperator,
pub version: String,
}
/// Version comparison operators
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum VersionOperator {
LessThan,
LessThanOrEqual,
Equal,
GreaterThanOrEqual,
GreaterThan,
NotEqual,
}
/// Resolved dependency graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyGraph {
pub nodes: HashMap<String, PackageNode>,
pub edges: Vec<DependencyEdge>,
}
/// Package node in dependency graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageNode {
pub name: String,
pub metadata: DebPackageMetadata,
pub dependencies: Vec<DependencyConstraint>,
pub level: usize,
pub visited: bool,
}
/// Dependency edge in graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependencyEdge {
pub from: String,
pub to: String,
pub relation: DependencyRelation,
}
/// Dependency resolver for OSTree packages
pub struct DependencyResolver {
available_packages: HashMap<String, DebPackageMetadata>,
}
impl DependencyResolver {
/// Create a new dependency resolver
pub fn new() -> Self {
Self {
available_packages: HashMap::new(),
}
}
/// Add available packages to the resolver
pub fn add_available_packages(&mut self, packages: Vec<DebPackageMetadata>) {
for package in packages {
self.available_packages.insert(package.name.clone(), package);
}
info!("Added {} available packages to resolver", self.available_packages.len());
}
/// Resolve dependencies for a list of packages
pub fn resolve_dependencies(&self, package_names: &[String]) -> AptOstreeResult<ResolvedDependencies> {
info!("Resolving dependencies for {} packages", package_names.len());
// Build dependency graph
let graph = self.build_dependency_graph(package_names)?;
// Check for conflicts
let conflicts = self.check_conflicts(&graph)?;
if !conflicts.is_empty() {
return Err(AptOstreeError::DependencyConflict(
format!("Dependency conflicts found: {:?}", conflicts)
));
}
// Topological sort for layering order
let layering_order = self.topological_sort(&graph)?;
// Calculate dependency levels
let leveled_packages = self.calculate_dependency_levels(&graph, &layering_order)?;
Ok(ResolvedDependencies {
packages: layering_order,
levels: leveled_packages,
graph,
})
}
/// Build dependency graph from package names
fn build_dependency_graph(&self, package_names: &[String]) -> AptOstreeResult<DependencyGraph> {
let mut graph = DependencyGraph {
nodes: HashMap::new(),
edges: Vec::new(),
};
// Add requested packages
for package_name in package_names {
if let Some(metadata) = self.available_packages.get(package_name) {
let node = PackageNode {
name: package_name.clone(),
metadata: metadata.clone(),
dependencies: self.parse_dependencies(&metadata.depends),
level: 0,
visited: false,
};
graph.nodes.insert(package_name.clone(), node);
} else {
return Err(AptOstreeError::PackageNotFound(package_name.clone()));
}
}
// Add dependencies recursively
let mut to_process: VecDeque<String> = package_names.iter().cloned().collect();
let mut processed = HashSet::new();
while let Some(package_name) = to_process.pop_front() {
if processed.contains(&package_name) {
continue;
}
processed.insert(package_name.clone());
if let Some(node) = graph.nodes.get(&package_name) {
// Collect dependencies to avoid borrow checker issues
let dependencies = node.dependencies.clone();
for dep_constraint in &dependencies {
let dep_name = &dep_constraint.package_name;
// Add dependency node if not already present
if !graph.nodes.contains_key(dep_name) {
if let Some(dep_metadata) = self.available_packages.get(dep_name) {
let dep_node = PackageNode {
name: dep_name.clone(),
metadata: dep_metadata.clone(),
dependencies: self.parse_dependencies(&dep_metadata.depends),
level: 0,
visited: false,
};
graph.nodes.insert(dep_name.clone(), dep_node);
to_process.push_back(dep_name.clone());
} else {
warn!("Dependency not found: {}", dep_name);
}
}
// Add edge
graph.edges.push(DependencyEdge {
from: package_name.clone(),
to: dep_name.clone(),
relation: dep_constraint.relation.clone(),
});
}
}
}
info!("Built dependency graph with {} nodes and {} edges", graph.nodes.len(), graph.edges.len());
Ok(graph)
}
/// Parse dependency strings into structured constraints
fn parse_dependencies(&self, deps_str: &[String]) -> Vec<DependencyConstraint> {
let mut constraints = Vec::new();
for dep_str in deps_str {
// Simple parsing - in real implementation, this would be more sophisticated
let parts: Vec<&str> = dep_str.split_whitespace().collect();
if !parts.is_empty() {
let package_name = parts[0].to_string();
let version_constraint = if parts.len() > 1 {
self.parse_version_constraint(&parts[1..])
} else {
None
};
constraints.push(DependencyConstraint {
package_name,
version_constraint,
relation: DependencyRelation::Depends,
});
}
}
constraints
}
/// Parse version constraint from string parts
fn parse_version_constraint(&self, parts: &[&str]) -> Option<VersionConstraint> {
if parts.is_empty() {
return None;
}
let constraint_str = parts.join(" ");
// Simple version constraint parsing
// In real implementation, this would handle complex Debian version constraints
if constraint_str.starts_with(">=") {
Some(VersionConstraint {
operator: VersionOperator::GreaterThanOrEqual,
version: constraint_str[2..].trim().to_string(),
})
} else if constraint_str.starts_with("<=") {
Some(VersionConstraint {
operator: VersionOperator::LessThanOrEqual,
version: constraint_str[2..].trim().to_string(),
})
} else if constraint_str.starts_with(">") {
Some(VersionConstraint {
operator: VersionOperator::GreaterThan,
version: constraint_str[1..].trim().to_string(),
})
} else if constraint_str.starts_with("<") {
Some(VersionConstraint {
operator: VersionOperator::LessThan,
version: constraint_str[1..].trim().to_string(),
})
} else if constraint_str.starts_with("=") {
Some(VersionConstraint {
operator: VersionOperator::Equal,
version: constraint_str[1..].trim().to_string(),
})
} else {
// Assume exact version match
Some(VersionConstraint {
operator: VersionOperator::Equal,
version: constraint_str.to_string(),
})
}
}
/// Check for dependency conflicts
fn check_conflicts(&self, graph: &DependencyGraph) -> AptOstreeResult<Vec<String>> {
let mut conflicts = Vec::new();
// Check for direct conflicts
for node in graph.nodes.values() {
for conflict in &node.metadata.conflicts {
if graph.nodes.contains_key(conflict) {
conflicts.push(format!("{} conflicts with {}", node.name, conflict));
}
}
}
// Check for circular dependencies
if self.has_circular_dependencies(graph)? {
conflicts.push("Circular dependency detected".to_string());
}
if !conflicts.is_empty() {
warn!("Found {} conflicts", conflicts.len());
}
Ok(conflicts)
}
/// Check for circular dependencies using DFS
fn has_circular_dependencies(&self, graph: &DependencyGraph) -> AptOstreeResult<bool> {
let mut visited = HashSet::new();
let mut rec_stack = HashSet::new();
for node_name in graph.nodes.keys() {
if !visited.contains(node_name) {
if self.is_cyclic_util(graph, node_name, &mut visited, &mut rec_stack)? {
return Ok(true);
}
}
}
Ok(false)
}
/// Utility function for cycle detection
fn is_cyclic_util(
&self,
graph: &DependencyGraph,
node_name: &str,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> AptOstreeResult<bool> {
visited.insert(node_name.to_string());
rec_stack.insert(node_name.to_string());
for edge in &graph.edges {
if edge.from == *node_name {
let neighbor = &edge.to;
if !visited.contains(neighbor) {
if self.is_cyclic_util(graph, neighbor, visited, rec_stack)? {
return Ok(true);
}
} else if rec_stack.contains(neighbor) {
return Ok(true);
}
}
}
rec_stack.remove(node_name);
Ok(false)
}
/// Perform topological sort for layering order
fn topological_sort(&self, graph: &DependencyGraph) -> AptOstreeResult<Vec<String>> {
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
let mut result = Vec::new();
// Initialize in-degrees
for node_name in graph.nodes.keys() {
in_degree.insert(node_name.clone(), 0);
}
// Calculate in-degrees
for edge in &graph.edges {
*in_degree.get_mut(&edge.to).unwrap() += 1;
}
// Add nodes with no dependencies to queue
for (node_name, degree) in &in_degree {
if *degree == 0 {
queue.push_back(node_name.clone());
}
}
// Process queue
while let Some(node_name) = queue.pop_front() {
result.push(node_name.clone());
// Reduce in-degree of neighbors
for edge in &graph.edges {
if edge.from == *node_name {
let neighbor = &edge.to;
if let Some(degree) = in_degree.get_mut(neighbor) {
*degree -= 1;
if *degree == 0 {
queue.push_back(neighbor.clone());
}
}
}
}
}
// Check if all nodes were processed
if result.len() != graph.nodes.len() {
return Err(AptOstreeError::DependencyConflict(
"Circular dependency detected during topological sort".to_string()
));
}
info!("Topological sort completed: {:?}", result);
Ok(result)
}
/// Calculate dependency levels for layering
fn calculate_dependency_levels(
&self,
graph: &DependencyGraph,
layering_order: &[String],
) -> AptOstreeResult<Vec<Vec<String>>> {
let mut levels: Vec<Vec<String>> = Vec::new();
let mut node_levels: HashMap<String, usize> = HashMap::new();
for node_name in layering_order {
let mut max_dep_level = 0;
// Find maximum level of dependencies
for edge in &graph.edges {
if edge.from == *node_name {
if let Some(dep_level) = node_levels.get(&edge.to) {
max_dep_level = max_dep_level.max(*dep_level + 1);
}
}
}
node_levels.insert(node_name.clone(), max_dep_level);
// Add to appropriate level
while levels.len() <= max_dep_level {
levels.push(Vec::new());
}
levels[max_dep_level].push(node_name.clone());
}
info!("Calculated {} dependency levels", levels.len());
for (i, level) in levels.iter().enumerate() {
debug!("Level {}: {:?}", i, level);
}
Ok(levels)
}
}
/// Resolved dependencies result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedDependencies {
pub packages: Vec<String>,
pub levels: Vec<Vec<String>>,
pub graph: DependencyGraph,
}
impl ResolvedDependencies {
/// Get packages in layering order
pub fn layering_order(&self) -> &[String] {
&self.packages
}
/// Get packages grouped by dependency level
pub fn by_level(&self) -> &[Vec<String>] {
&self.levels
}
/// Get total number of packages
pub fn package_count(&self) -> usize {
self.packages.len()
}
/// Get number of dependency levels
pub fn level_count(&self) -> usize {
self.levels.len()
}
}