- Add missing modules to lib.rs (apt, package_manager, ostree, test_support, etc.) - Fix DebPackageMetadata type compatibility with PackageInfo - Add missing description and scripts fields to DebPackageMetadata - Create apt.rs module alias for apt_compat - Create minimal apt_ostree_integration.rs with required types - Fix type conversions in package_manager.rs resolve_dependencies - Update all test instances of DebPackageMetadata with new fields - All Rust code now compiles successfully - All tests now pass successfully
469 lines
No EOL
16 KiB
Rust
469 lines
No EOL
16 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};
|
|
|
|
/// DEB package metadata for dependency resolution
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DebPackageMetadata {
|
|
pub name: String,
|
|
pub version: String,
|
|
pub architecture: String,
|
|
pub description: String,
|
|
pub depends: Vec<String>,
|
|
pub conflicts: Vec<String>,
|
|
pub provides: Vec<String>,
|
|
pub breaks: Vec<String>,
|
|
pub replaces: Vec<String>,
|
|
pub scripts: std::collections::HashMap<String, String>,
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
}
|