use std::collections::{HashMap, BTreeSet, HashSet};
use std::sync::Arc;
use color_eyre::Result;
use color_eyre::eyre;
use crate::models::*;
use crate::models::PACKAGE_CACHE;
use crate::resolve::provider::GenericDependencyProvider;
use crate::resolve::types::{DependFieldFlags, NameType, SolverMatchSpec};
#[cfg(target_os = "linux")]
use crate::aur::is_aur_package;
use crate::world::{remove_from_no_install, get_no_install_set};
use crate::io::load_installed_packages;
use crate::plan::prepare_installation_plan;
use crate::install::execute_installation_plan;
use crate::repo::sync_channel_metadata;
use crate::parse_provides::parse_provides;
const CIRCULAR_DEPENDENCY_FILTER_PAIRS: &[(&str, &str)] = &[];
* Debian Multi-Arch and Architecture Suffix Rules
* ===============================================
*
* Debian packages can specify architecture-specific dependencies using suffixes:
*
* 1. `:any` Suffix Rules:
* - Can ONLY be used with packages that have Multi-Arch: allowed or Multi-Arch: foreign
* - Means the dependency can be satisfied by ANY architecture version of the package
* - Examples: perl:any, python3:any
*
* 2. Multi-Arch Field Values and Their Meanings:
* - Multi-Arch: allowed
* * Package can be installed for multiple architectures simultaneously
* * Different architecture versions can coexist
* * CAN satisfy :any dependencies
* * Example: interpreters like perl, python3
*
* - Multi-Arch: foreign
* * Package can satisfy dependencies of any architecture
* * Only one architecture version can be installed at a time
* * CAN satisfy :any dependencies
* * Example: architecture-independent tools
*
* - Multi-Arch: same
* * Must be same architecture as the package that depends on it
* * CANNOT satisfy :any dependencies
* * Example: shared libraries that must match requestor's architecture
*
* - No Multi-Arch field (or Multi-Arch: no)
* * Traditional behavior - architecture specific
* * CANNOT satisfy :any dependencies
* * Must match the architecture of the requesting package
*
* 3. Specific Architecture Suffixes:
* - `:amd64`, `:arm64`, etc.
* - Forces dependency to specific architecture regardless of Multi-Arch
* - Used for cross-compilation or specific architecture requirements
*
* 4. No Architecture Suffix:
* - Default behavior - architecture specific matching
* - Dependency must match the architecture of the requesting package
*
* 5. Implementation Rules in this Code:
* - For `:any`: Only allow packages with Multi-Arch: allowed/foreign
* - For specific arch (`:amd64`): Filter to that architecture only
* - For no suffix: Use default same-architecture filtering
* - Fallback: If no Multi-Arch packages found for :any, fall back to same-arch
*
* References:
* - https://wiki.debian.org/Multiarch/HOWTO
* - https://www.debian.org/doc/debian-policy/ch-relationships.html#architecture-restrictions
*/
fn update_ebin_exposure_for_user_requested(
packages: &mut InstalledPackagesMap,
user_request_world: Option<&HashMap<String, String>>,
) -> Result<()> {
let Some(user_request_world) = user_request_world else {
return Ok(());
};
for requested_name in user_request_world.keys() {
for (pkgkey, info_arc) in packages.iter_mut() {
if let Ok(pkgname) = crate::package::pkgkey2pkgname(pkgkey) {
if &pkgname == requested_name {
Arc::make_mut(info_arc).ebin_exposure = true;
log::debug!("Setting ebin_exposure=true for user-requested package: {} ({})", pkgkey, requested_name);
}
}
}
}
Ok(())
}
fn extend_ebin_by_source(packages: &mut InstalledPackagesMap) -> Result<InstalledPackagesMap> {
log::debug!("Setting ebin_exposure for {} packages based on source matching.", packages.len());
let mut user_requested_sources = std::collections::HashSet::new();
let mut packages_to_expose: InstalledPackagesMap = HashMap::new();
for (pkgkey, info) in packages.iter() {
if info.ebin_exposure == true {
match crate::package_cache::load_package_info(pkgkey) {
Ok(pkg_details) => {
if let Some(source_name) = &pkg_details.source {
if !source_name.is_empty() {
user_requested_sources.insert(source_name.clone());
}
}
}
Err(e) => {
log::warn!("Failed to load package info for {}: {} during appbin source collection. Skipping.", pkgkey, e);
}
}
}
}
log::debug!("User-requested sources for ebin_exposure logic: {:?}", user_requested_sources);
for (pkgkey, info) in packages.iter() {
if info.ebin_exposure == false {
match crate::package_cache::load_package_info(pkgkey) {
Ok(pkg_details) => {
if let Some(source_name) = &pkg_details.source {
if !source_name.is_empty() && user_requested_sources.contains(source_name) {
let mut new_info_arc = Arc::clone(info);
Arc::make_mut(&mut new_info_arc).ebin_exposure = true;
packages_to_expose.insert(pkgkey.clone(), new_info_arc);
}
}
}
Err(e) => {
log::warn!("Failed to load package info for {}: {} during ebin_exposure setting. Defaulting ebin_exposure to false.", pkgkey, e);
}
}
}
}
Ok(packages_to_expose)
}
fn pkg_is_likely_metapkg(pkgkey: &str, pkgline: Option<&str>) -> bool {
if let Ok(package) = crate::package_cache::load_package_info(pkgkey) {
if package.installed_size == 0 || package.installed_size >= 200 * 1024 {
log::debug!("pkg_is_likely_metapkg: pkgkey={}, installed_size={} -> false (size check)",
pkgkey, package.installed_size);
return false;
}
} else {
return false;
}
if let Some(pkgline) = pkgline {
if !pkgline.is_empty() {
let store_root = &crate::models::dirs().epkg_store;
if let Ok(filelist) = crate::package_cache::map_pkgline2filelist(store_root, pkgline) {
for file in &filelist {
let file_lower = file.to_lowercase();
if file_lower.starts_with("bin/") ||
file_lower.starts_with("sbin/") ||
file_lower.contains("/bin/") ||
file_lower.contains("/sbin/") {
log::debug!("pkg_is_likely_metapkg: pkgkey={} -> false (has binary: {})",
pkgkey, file);
return false;
}
}
}
}
}
log::debug!("pkg_is_likely_metapkg: pkgkey={} -> true (no binaries)", pkgkey);
true
}
pub fn get_meta_package_exposures(packages: &InstalledPackagesMap) -> Result<Vec<String>> {
let mut pkgkeys_to_expose: Vec<String> = Vec::new();
let user_requested_pkgkeys: Vec<String> = packages.iter()
.filter(|(_, info)| info.ebin_exposure)
.map(|(pkgkey, _)| pkgkey.clone())
.collect();
log::info!("get_meta_package_exposures: found {} user-requested packages", user_requested_pkgkeys.len());
if user_requested_pkgkeys.is_empty() {
return Ok(pkgkeys_to_expose);
}
for pkgkey in &user_requested_pkgkeys {
if let Some(info) = packages.get(pkgkey) {
let is_meta = pkg_is_likely_metapkg(pkgkey, Some(&info.pkgline));
log::info!("Package {} meta-package check: is_meta={}, depends_count={}",
pkgkey, is_meta, info.depends.len());
if is_meta {
log::debug!("Package {} is a meta-package, collecting all dependencies for exposure", pkgkey);
let mut to_expose: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut queue: std::collections::VecDeque<String> = std::collections::VecDeque::new();
for dep in &info.depends {
if !to_expose.contains(dep) {
to_expose.insert(dep.clone());
queue.push_back(dep.clone());
}
}
while let Some(dep_pkgkey) = queue.pop_front() {
if let Some(dep_info) = packages.get(&dep_pkgkey) {
for transitive_dep in &dep_info.depends {
if !to_expose.contains(transitive_dep) {
to_expose.insert(transitive_dep.clone());
queue.push_back(transitive_dep.clone());
}
}
}
}
for dep_pkgkey in &to_expose {
if packages.contains_key(dep_pkgkey) {
if !user_requested_pkgkeys.contains(dep_pkgkey) {
pkgkeys_to_expose.push(dep_pkgkey.clone());
log::debug!("Collecting {} for exposure (dependency of meta-package {})", dep_pkgkey, pkgkey);
}
}
}
} else {
log::debug!("Package {} has binaries, not collecting dependencies for exposure", pkgkey);
}
}
}
Ok(pkgkeys_to_expose)
}
fn setup_resolvo_provider_and_requirements(
delta_world: &HashMap<String, String>,
) -> Result<(GenericDependencyProvider, Vec<resolvo::ConditionalRequirement>)> {
let package_format = channel_config().format;
log::info!(
"Starting resolvo-based recursive dependency collection for {} packages in delta_world. Repo format: {:?}",
delta_world.len(),
package_format
);
log::debug!("delta_world contents: {:?}", delta_world);
if package_format == PackageFormat::Conda {
crate::package_cache::add_conda_virtual_packages_to_cache()?;
}
if package_format == PackageFormat::Deb {
crate::package_cache::add_deb_virtual_packages_to_cache()?;
}
let mut provider = create_resolvo_provider(package_format, delta_world);
let requirements = convert_initial_packages_to_requirements(delta_world, &mut provider)?;
log::debug!("Converted {} requirements from delta_world", requirements.len());
Ok((provider, requirements))
}
fn create_resolvo_problem_and_solver(
provider: GenericDependencyProvider,
requirements: Vec<resolvo::ConditionalRequirement>,
) -> (resolvo::Problem<std::iter::Empty<resolvo::SolvableId>>, resolvo::Solver<GenericDependencyProvider>) {
use resolvo::{Problem, Solver};
let problem = Problem::new().requirements(requirements);
let solver = Solver::new(provider);
(problem, solver)
}
fn run_solve_pass(
solver: &mut resolvo::Solver<GenericDependencyProvider>,
problem: resolvo::Problem<std::iter::Empty<resolvo::SolvableId>>,
pass_name: &str,
) -> Result<Vec<resolvo::SolvableId>> {
use resolvo::UnsolvableOrCancelled;
match solver.solve(problem) {
Ok(solvables) => {
log::debug!("Solver found solution with {} packages ({})", solvables.len(), pass_name);
Ok(solvables)
},
Err(UnsolvableOrCancelled::Unsolvable(problem)) => {
let error_msg = problem.display_user_friendly(solver).to_string();
Err(color_eyre::eyre::eyre!("Dependency resolution failed for {}:\n{}", pass_name, error_msg))
}
Err(UnsolvableOrCancelled::Cancelled(_)) => {
Err(color_eyre::eyre::eyre!("Dependency resolution was cancelled for {}", pass_name))
}
}
}
fn solve_with_resolvo(
provider: GenericDependencyProvider,
requirements: Vec<resolvo::ConditionalRequirement>,
flags: DependFieldFlags,
) -> Result<(resolvo::Solver<GenericDependencyProvider>, Vec<resolvo::SolvableId>)> {
provider.update_depend_fields(flags);
let (problem, mut solver) = create_resolvo_problem_and_solver(provider, requirements);
let package_format = channel_config().format;
let base_flags = if package_format == PackageFormat::Pacman {
DependFieldFlags::REQUIRES | DependFieldFlags::BUILD_REQUIRES
} else {
DependFieldFlags::REQUIRES
};
let pass_name = if flags != base_flags {
"1st pass (with RECOMMENDS/SUGGESTS)"
} else if package_format == PackageFormat::Pacman {
"2nd pass (REQUIRES|BUILD_REQUIRES only)"
} else {
"2nd pass (REQUIRES only)"
};
let solvables = run_solve_pass(&mut solver, problem, pass_name)?;
log::debug!("Solver resolved {} solvables", solvables.len());
Ok((solver, solvables))
}
fn resolve_dependencies_with_resolvo(
delta_world: &HashMap<String, String>,
user_request_world: Option<&HashMap<String, String>>,
) -> Result<InstalledPackagesMap> {
let (provider, requirements) = setup_resolvo_provider_and_requirements(delta_world)?;
if requirements.is_empty() {
log::info!("No valid packages to resolve");
return Ok(HashMap::new());
}
let package_format = channel_config().format;
let (base_flags, base_flag_desc) = if package_format == PackageFormat::Pacman {
(
DependFieldFlags::REQUIRES | DependFieldFlags::BUILD_REQUIRES,
"REQUIRES|BUILD_REQUIRES",
)
} else {
(DependFieldFlags::REQUIRES, "REQUIRES")
};
let mut flags = base_flags;
if !config().install.no_install_recommends {
flags = flags | DependFieldFlags::RECOMMENDS;
}
if config().install.install_suggests {
flags = flags | DependFieldFlags::SUGGESTS;
}
let (solver, solvables) = match solve_with_resolvo(provider, requirements.clone(), flags) {
Ok(result) => result,
Err(e) if e.to_string().contains("No requirements to solve") => {
return Ok(HashMap::new());
}
Err(e) if flags != base_flags => {
log::debug!(
"Dependency resolution failed with RECOMMENDS/SUGGESTS: {}. Retrying with {} only.",
e,
base_flag_desc
);
let (fresh_provider, _) = setup_resolvo_provider_and_requirements(delta_world)?;
match solve_with_resolvo(fresh_provider, requirements, base_flags) {
Ok(result) => result,
Err(e) => return Err(e),
}
}
Err(e) => return Err(e),
};
let result = build_installed_package_info_map(
&solver,
&solvables,
user_request_world,
)?;
log::info!("Collected {} packages with dependencies", result.len());
Ok(result)
}
fn resolve_dependencies_adding_makepkg_deps(
delta_world: &mut HashMap<String, String>,
user_request_world: Option<&HashMap<String, String>>,
) -> Result<InstalledPackagesMap> {
let mut all_packages_for_session =
resolve_dependencies_with_resolvo(delta_world, user_request_world)?;
let package_format = channel_config().format;
if package_format != PackageFormat::Pacman {
return Ok(all_packages_for_session);
}
#[allow(unused_mut)]
let mut has_aur_packages = false;
#[allow(unused_mut)]
let mut has_git_aur = false;
#[cfg(target_os = "linux")]
{
for pkgkey in all_packages_for_session.keys() {
if is_aur_package(pkgkey) {
has_aur_packages = true;
if let Ok(pkgname) = crate::package::pkgkey2pkgname(pkgkey) {
if pkgname.ends_with("-git") {
has_git_aur = true;
break;
}
}
}
}
}
if !has_aur_packages {
return Ok(all_packages_for_session);
}
let mut makepkg_depends: HashMap<String, String> = [
("gnupg".to_string(), String::new()),
("pacman".to_string(), String::new()),
("libarchive".to_string(), String::new()),
("coreutils".to_string(), String::new()),
("base-devel".to_string(), String::new()),
]
.into_iter()
.collect();
if has_git_aur {
makepkg_depends
.entry("git".to_string())
.or_insert_with(String::new);
}
for (pkgname, constraint) in &makepkg_depends {
delta_world.entry(pkgname.clone()).or_insert_with(|| constraint.clone());
}
remove_from_no_install(makepkg_depends.keys());
all_packages_for_session =
resolve_dependencies_with_resolvo(delta_world, user_request_world)?;
Ok(all_packages_for_session)
}
pub fn resolve_and_install_packages(
delta_world: &mut HashMap<String, String>,
user_request_world: Option<&HashMap<String, String>>,
) -> Result<crate::plan::InstallationPlan> {
use crate::plan::InstallationPlan;
delta_world.remove("no-install");
sync_channel_metadata()?;
load_installed_packages()?;
let mut all_packages_for_session =
resolve_dependencies_adding_makepkg_deps(delta_world, user_request_world)?;
update_ebin_exposure_for_user_requested(&mut all_packages_for_session, user_request_world)?;
let packages_to_expose = extend_ebin_by_source(&mut all_packages_for_session)?;
if packages_to_expose.is_empty() && all_packages_for_session.is_empty() {
let empty_msg = if user_request_world.is_some() {
"No packages to install or upgrade."
} else {
"No packages to upgrade."
};
println!("{}", empty_msg);
return Ok(InstallationPlan::default());
}
if all_packages_for_session.is_empty() && !packages_to_expose.is_empty() {
let msg = format!("Error: {} packages need exposure but no packages resolved", packages_to_expose.len());
return Err(eyre::eyre!(msg));
}
let plan = prepare_installation_plan(&all_packages_for_session, None)?;
execute_installation_plan(plan)
}
fn get_candidate_pkgkeys_from_capabilities(
provider_ref: &GenericDependencyProvider,
solvables: &[resolvo::SolvableId],
user_request_world: Option<&HashMap<String, String>>,
) -> Result<std::collections::HashSet<String>> {
use resolvo::DependencyProvider;
use resolvo::runtime::{AsyncRuntime, NowOrNeverRuntime};
let capabilities_map = match user_request_world {
Some(user_request_world) => user_request_world,
None => {
return Ok(std::collections::HashSet::new());
}
};
let solvable_pkgkeys: std::collections::HashSet<String> = solvables
.iter()
.map(|solvable_id| {
let record = &provider_ref.pool.resolve_solvable(*solvable_id).record;
record.pkgkey.clone()
})
.collect();
let mut candidate_pkgkeys = std::collections::HashSet::new();
for capability in capabilities_map.keys() {
let name_id = provider_ref.pool.intern_package_name(
NameType(capability.clone())
);
match NowOrNeverRuntime::default().block_on(provider_ref.get_candidates(name_id)) {
Some(candidates) => {
for solvable_id in &candidates.candidates {
let record = &provider_ref.pool.resolve_solvable(*solvable_id).record;
if solvable_pkgkeys.contains(&record.pkgkey) {
candidate_pkgkeys.insert(record.pkgkey.clone());
log::debug!("[RESOLVO] Found candidate pkgkey: {} (pkgkey: {})", record.pkgname, record.pkgkey);
}
}
}
None => {
log::warn!("[RESOLVO] No candidates found for capability: {}", capability);
}
}
}
log::debug!("[RESOLVO] Extracted {} candidate pkgkeys from capabilities", candidate_pkgkeys.len());
Ok(candidate_pkgkeys)
}
fn expand_no_install_with_provides(format: PackageFormat, no_install: HashSet<String>) -> HashSet<String> {
let mut expanded = no_install.clone();
for pkgname in &no_install {
match crate::package_cache::map_pkgname2packages(pkgname) {
Ok(packages) => {
for package in packages {
for provide_str in &package.provides {
let provide_map = parse_provides(provide_str, format);
for (provide_name, _version) in provide_map {
expanded.insert(provide_name);
}
}
for provide_str in &package.files {
let provide_map = parse_provides(provide_str, format);
for (provide_name, _version) in provide_map {
expanded.insert(provide_name);
}
}
}
}
Err(e) => {
log::debug!("[RESOLVO] Could not lookup packages for no_install package '{}': {}", pkgname, e);
}
}
}
let original_len = no_install.len();
let expanded_len = expanded.len();
if expanded_len > original_len {
let added: Vec<&String> = expanded.difference(&no_install).collect();
log::debug!("[RESOLVO] Expanded no_install set with {} capabilities ({} -> {}): {:?}",
expanded_len - original_len, original_len, expanded_len, added);
}
expanded
}
fn create_resolvo_provider(format: PackageFormat, delta_world: &HashMap<String, String>) -> GenericDependencyProvider {
use crate::resolve::types::DependFieldFlags;
let depend_fields = DependFieldFlags::REQUIRES;
let delta_world_keys: std::collections::HashSet<String> = delta_world.keys().cloned().collect();
let no_install = get_no_install_set();
let no_install = expand_no_install_with_provides(format, no_install);
GenericDependencyProvider::new(
format,
depend_fields,
delta_world_keys,
no_install,
)
}
fn convert_initial_packages_to_requirements(
delta_world: &HashMap<String, String>,
provider: &mut GenericDependencyProvider,
) -> Result<Vec<resolvo::ConditionalRequirement>> {
let mut requirements = Vec::new();
let ignore_missing = crate::models::config().common.ignore_missing;
for (pkgname, constraint_str) in delta_world {
if ignore_missing && !check_package_or_capability_exists(pkgname) {
log::info!(
"Package/capability '{}' not found, skipping (ignore_missing=true)",
pkgname
);
continue;
}
let final_constraints = if constraint_str.is_empty() {
PACKAGE_CACHE.world.read().unwrap().get(pkgname)
.and_then(|world_constraint_str| {
if world_constraint_str.is_empty() {
None
} else {
crate::parse_requires::parse_world_constraint(world_constraint_str)
}
})
} else {
crate::parse_requires::parse_world_constraint(constraint_str)
};
if let Some(constraints) = final_constraints {
log::debug!("Using version constraints for '{}': constraints={:?}",
pkgname, constraints);
let requirement = create_constrained_requirement(pkgname, &constraints, provider);
requirements.push(requirement);
} else {
let requirement = create_package_name_requirement(pkgname, provider);
requirements.push(requirement);
}
}
Ok(requirements)
}
fn check_package_or_capability_exists(name: &str) -> bool {
match crate::package_cache::map_pkgname2packages(name) {
Ok(packages) if !packages.is_empty() => return true,
_ => {}
}
match crate::mmio::map_provide2pkgnames(name) {
Ok(provider_pkgnames) => {
for provider_pkgname in provider_pkgnames {
match crate::package_cache::map_pkgname2packages(&provider_pkgname) {
Ok(packages) if !packages.is_empty() => return true,
_ => continue,
}
}
}
_ => {}
}
false
}
fn create_package_name_requirement(
pkgname: &str,
provider: &mut GenericDependencyProvider,
) -> resolvo::ConditionalRequirement {
use resolvo::ConditionalRequirement;
let name_id = provider.pool.intern_package_name(
NameType(pkgname.to_string())
);
let pkg_depend = crate::parse_requires::PkgDepend {
capability: pkgname.to_string(),
constraints: Vec::new(),
};
let or_deps = vec![pkg_depend];
let and_deps = vec![or_deps];
let version_set_id = provider.pool.intern_version_set(
name_id,
SolverMatchSpec::MatchSpec(and_deps),
);
ConditionalRequirement {
requirement: version_set_id.into(),
condition: None,
}
}
fn create_constrained_requirement(
pkgname: &str,
constraints: &[crate::parse_requires::VersionConstraint],
provider: &mut GenericDependencyProvider,
) -> resolvo::ConditionalRequirement {
use resolvo::ConditionalRequirement;
let name_id = provider.pool.intern_package_name(
NameType(pkgname.to_string())
);
let pkg_depend = crate::parse_requires::PkgDepend {
capability: pkgname.to_string(),
constraints: constraints.to_vec(),
};
let or_deps = vec![pkg_depend];
let and_deps = vec![or_deps];
let version_set_id = provider.pool.intern_version_set(
name_id,
SolverMatchSpec::MatchSpec(and_deps),
);
ConditionalRequirement {
requirement: version_set_id.into(),
condition: None,
}
}
fn build_installed_package_info_map(
solver: &resolvo::Solver<GenericDependencyProvider>,
solvables: &[resolvo::SolvableId],
user_request_world: Option<&HashMap<String, String>>,
) -> Result<InstalledPackagesMap> {
let provider_ref = solver.provider();
let format = provider_ref.format;
let request_world_pkgkeys = get_candidate_pkgkeys_from_capabilities(
provider_ref,
solvables,
user_request_world,
)?;
log::debug!("[RESOLVO] Found {} request_world pkgkeys out of {} resolved solvables: {:?}", request_world_pkgkeys.len(), solvables.len(), request_world_pkgkeys);
let (pkgkey_to_depends, pkgkey_to_rdepends) =
build_dependency_graph(provider_ref, solvables)?;
let (pkgkey_to_bdepends, pkgkey_to_rbdepends) = if format == PackageFormat::Pacman {
log::debug!("[RESOLVO] Building build-dependency graph for Pacman format");
provider_ref.update_depend_fields(DependFieldFlags::BUILD_REQUIRES);
let (bdepends, rbdepends) = build_dependency_graph(provider_ref, solvables)?;
(bdepends, rbdepends)
} else {
(HashMap::new(), HashMap::new())
};
let pkgkey_to_depth = calculate_pkgkey_to_depth(
&pkgkey_to_depends,
&pkgkey_to_rdepends,
&pkgkey_to_bdepends,
&pkgkey_to_rbdepends,
&request_world_pkgkeys,
)?;
let result = create_installed_package_info_map(
provider_ref,
solvables,
&pkgkey_to_depends,
&pkgkey_to_rdepends,
&pkgkey_to_bdepends,
&pkgkey_to_rbdepends,
&pkgkey_to_depth,
&request_world_pkgkeys,
)?;
Ok(result)
}
fn build_dependency_graph(
provider_ref: &GenericDependencyProvider,
solvables: &[resolvo::SolvableId],
) -> Result<(
HashMap<String, BTreeSet<String>>,
HashMap<String, BTreeSet<String>>,
)> {
use resolvo::{DependencyProvider, Interner};
use resolvo::runtime::{AsyncRuntime, NowOrNeverRuntime};
let mut pkgkey_to_depends: HashMap<String, BTreeSet<String>> = HashMap::new();
let mut pkgkey_to_rdepends: HashMap<String, BTreeSet<String>> = HashMap::new();
for solvable_id in solvables {
let record = &provider_ref.pool.resolve_solvable(*solvable_id).record;
let pkgkey = record.pkgkey.clone();
pkgkey_to_depends.entry(pkgkey.clone()).or_insert_with(BTreeSet::new);
let _package = match crate::package_cache::load_package_info(&pkgkey) {
Ok(pkg) => (*pkg).clone(),
Err(e) => {
log::warn!("Failed to load resolved package {}: {}", pkgkey, e);
continue;
}
};
let deps = match NowOrNeverRuntime::default().block_on(provider_ref.get_dependencies(*solvable_id)) {
resolvo::Dependencies::Known(known_deps) => known_deps,
resolvo::Dependencies::Unknown(reason) => {
let reason_str = provider_ref.display_string(reason).to_string();
log::warn!("Dependencies unknown for {}: {}", pkgkey, reason_str);
continue;
}
};
let dep_pkgkeys = extract_dependency_pkgkeys(provider_ref, solvables, &deps.requirements);
pkgkey_to_depends.insert(pkgkey.clone(), dep_pkgkeys.clone());
for dep_pkgkey in &dep_pkgkeys {
pkgkey_to_rdepends
.entry(dep_pkgkey.clone())
.or_insert_with(BTreeSet::new)
.insert(pkgkey.clone());
}
}
Ok((pkgkey_to_depends, pkgkey_to_rdepends))
}
fn extract_dependency_pkgkeys(
provider_ref: &GenericDependencyProvider,
solvables: &[resolvo::SolvableId],
requirements: &[resolvo::ConditionalRequirement],
) -> BTreeSet<String> {
use resolvo::{Interner, VersionSetId};
let mut dep_pkgkeys = BTreeSet::new();
for req in requirements {
let version_set_ids: Vec<VersionSetId> = match req.requirement {
resolvo::Requirement::Single(version_set_id) => vec![version_set_id],
resolvo::Requirement::Union(union_id) => {
provider_ref.version_sets_in_union(union_id).collect()
}
};
for version_set_id in version_set_ids {
let dep_name_id = provider_ref.version_set_name(version_set_id);
let dep_name = provider_ref.display_name(dep_name_id).to_string();
for other_solvable_id in solvables {
let other_record = &provider_ref.pool.resolve_solvable(*other_solvable_id).record;
if other_record.pkgname == dep_name {
dep_pkgkeys.insert(other_record.pkgkey.clone());
break;
}
if provider_ref.package_provides_capability(&other_record.pkgkey, &dep_name) {
dep_pkgkeys.insert(other_record.pkgkey.clone());
break;
}
}
}
}
dep_pkgkeys
}
fn find_leaf_nodes_by_rdepends(
remaining_rdepends: &HashMap<String, BTreeSet<String>>,
) -> Vec<String> {
remaining_rdepends
.iter()
.filter(|(_, rdepends)| rdepends.is_empty())
.map(|(pkgkey, _)| pkgkey.clone())
.collect()
}
fn find_candidate_with_least_weighted_rdepends(
remaining_rdepends: &HashMap<String, BTreeSet<String>>,
request_world_pkgkeys: &std::collections::HashSet<String>,
) -> Option<(String, usize)> {
remaining_rdepends
.iter()
.filter(|(_, rdepends)| !rdepends.is_empty())
.map(|(pkgkey, rdepends)| {
let mut weight = 0;
for rdepend in rdepends {
if rdepend.contains("lib") {
weight += 10;
} else {
weight += 1;
}
}
if pkgkey.contains("lib") {
weight = weight * 2;
}
if request_world_pkgkeys.contains(pkgkey) {
weight = weight / 2;
}
(pkgkey.clone(), weight)
})
.min_by_key(|(_, weight)| *weight)
}
fn remove_node_and_update_dependencies(
node: &str,
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
remaining_rdepends: &mut HashMap<String, BTreeSet<String>>,
pkgkey_to_depth: &mut HashMap<String, u16>,
current_depth: u16,
) {
pkgkey_to_depth.insert(node.to_string(), current_depth);
remaining_rdepends.remove(node);
if let Some(depends_list) = pkgkey_to_depends.get(node) {
for dep_pkgkey in depends_list {
if let Some(rdepends) = remaining_rdepends.get_mut(dep_pkgkey) {
rdepends.remove(node);
}
}
}
if let Some(bdepends_list) = pkgkey_to_bdepends.get(node) {
for dep_pkgkey in bdepends_list {
if let Some(rdepends) = remaining_rdepends.get_mut(dep_pkgkey) {
rdepends.remove(node);
}
}
}
}
fn process_leaf_nodes(
leaf_nodes: &[String],
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_depth: &mut HashMap<String, u16>,
remaining_rdepends: &mut HashMap<String, BTreeSet<String>>,
current_depth: u16,
) {
for leaf_pkgkey in leaf_nodes {
remove_node_and_update_dependencies(
leaf_pkgkey,
pkgkey_to_depends,
pkgkey_to_bdepends,
remaining_rdepends,
pkgkey_to_depth,
current_depth,
);
}
}
fn break_circular_dependency(
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_depth: &mut HashMap<String, u16>,
remaining_rdepends: &mut HashMap<String, BTreeSet<String>>,
request_world_pkgkeys: &std::collections::HashSet<String>,
current_depth: u16,
) -> bool {
if let Some((candidate, weight)) = find_candidate_with_least_weighted_rdepends(remaining_rdepends, request_world_pkgkeys) {
log::debug!(
"Breaking circular dependency by removing node {} with weighted rdepends ({}) at depth {}",
candidate,
weight,
current_depth
);
remove_node_and_update_dependencies(
&candidate,
pkgkey_to_depends,
pkgkey_to_bdepends,
remaining_rdepends,
pkgkey_to_depth,
current_depth,
);
return true;
}
false
}
fn update_remaining_rdepends_with_filter(
remaining_rdepends: &mut HashMap<String, BTreeSet<String>>,
pkgkey: &str,
rdeps: &BTreeSet<String>,
) {
use crate::package::pkgkey2pkgname;
let entry = remaining_rdepends.entry(pkgkey.to_string()).or_insert_with(BTreeSet::new);
for rdep in rdeps {
let mut should_filter = false;
if let Ok(pkgname) = pkgkey2pkgname(pkgkey) {
if let Ok(rdep_pkgname) = pkgkey2pkgname(rdep) {
for (pkg_a_name, pkg_b_name) in CIRCULAR_DEPENDENCY_FILTER_PAIRS {
if rdep_pkgname == *pkg_a_name && pkgname == *pkg_b_name {
should_filter = true;
break;
}
}
}
}
if !should_filter {
entry.insert(rdep.clone());
}
}
}
fn combine_and_filter_reverse_dependencies(
pkgkey_to_rdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rbdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
) -> HashMap<String, BTreeSet<String>> {
let mut remaining_rdepends: HashMap<String, BTreeSet<String>> = HashMap::new();
for (pkgkey, rdepends) in pkgkey_to_rdepends {
update_remaining_rdepends_with_filter(&mut remaining_rdepends, pkgkey, rdepends);
}
for (pkgkey, rbdepends) in pkgkey_to_rbdepends {
update_remaining_rdepends_with_filter(&mut remaining_rdepends, pkgkey, rbdepends);
}
let empty_deps = BTreeSet::new();
for pkgkey in pkgkey_to_depends.keys() {
update_remaining_rdepends_with_filter(&mut remaining_rdepends, pkgkey, &empty_deps);
}
remaining_rdepends
}
fn calculate_depths_from_graph(
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
mut remaining_rdepends: HashMap<String, BTreeSet<String>>,
request_world_pkgkeys: &std::collections::HashSet<String>,
) -> HashMap<String, u16> {
let mut pkgkey_to_depth: HashMap<String, u16> = HashMap::new();
let mut current_depth = 0;
log::debug!("[INITIAL] remaining_rdepends ({} entries):", remaining_rdepends.len());
let mut leaf_count = 0;
let mut non_leaf_count = 0;
for (pkgkey, rdepends) in remaining_rdepends.iter().take(20) {
let is_leaf = rdepends.is_empty();
if is_leaf {
leaf_count += 1;
} else {
non_leaf_count += 1;
}
let rdepends_str = if rdepends.is_empty() { "[]".to_string() } else { format!("{:?}", rdepends) };
log::debug!(" {} -> {} {} (len={})",
pkgkey,
if is_leaf { "[LEAF]" } else { "" },
rdepends_str,
rdepends.len()
);
}
if remaining_rdepends.len() > 20 {
log::debug!(" ... (and {} more)", remaining_rdepends.len() - 20);
}
log::debug!("[INITIAL] Total leaf nodes (empty rdepends): {}", leaf_count);
log::debug!("[INITIAL] Total non-leaf nodes: {}", non_leaf_count);
loop {
if remaining_rdepends.is_empty() {
break;
}
let leaf_nodes = find_leaf_nodes_by_rdepends(&remaining_rdepends);
log::debug!("[LOOP depth={}] find_leaf_nodes_by_rdepends returned {} nodes",
current_depth, leaf_nodes.len());
if leaf_nodes.is_empty() {
if break_circular_dependency(
pkgkey_to_depends,
pkgkey_to_bdepends,
&mut pkgkey_to_depth,
&mut remaining_rdepends,
request_world_pkgkeys,
current_depth,
) {
current_depth += 1;
continue;
}
log::warn!(
"Found {} packages with unresolved dependencies, assigning depth {}",
remaining_rdepends.len(),
current_depth
);
for pkgkey in remaining_rdepends.keys() {
pkgkey_to_depth.insert(pkgkey.clone(), current_depth);
}
break;
}
log::debug!(
"Found {} leaf nodes at depth {}",
leaf_nodes.len(),
current_depth
);
process_leaf_nodes(
&leaf_nodes,
pkgkey_to_depends,
pkgkey_to_bdepends,
&mut pkgkey_to_depth,
&mut remaining_rdepends,
current_depth,
);
current_depth += 1;
}
log::debug!("Calculated depths for {} packages", pkgkey_to_depth.len());
pkgkey_to_depth
}
pub fn calculate_pkgkey_to_depth(
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rbdepends: &HashMap<String, BTreeSet<String>>,
request_world_pkgkeys: &std::collections::HashSet<String>,
) -> Result<HashMap<String, u16>> {
let remaining_rdepends = combine_and_filter_reverse_dependencies(
pkgkey_to_rdepends,
pkgkey_to_rbdepends,
pkgkey_to_depends,
);
let pkgkey_to_depth = calculate_depths_from_graph(
pkgkey_to_depends,
pkgkey_to_bdepends,
remaining_rdepends,
request_world_pkgkeys,
);
Ok(pkgkey_to_depth)
}
fn create_installed_package_info_map(
provider_ref: &GenericDependencyProvider,
solvables: &[resolvo::SolvableId],
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rbdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_depth: &HashMap<String, u16>,
request_world_pkgkeys: &std::collections::HashSet<String>,
) -> Result<InstalledPackagesMap> {
let mut result = HashMap::new();
for pkgkey in pkgkey_to_depends.keys() {
if let Ok(pkgname) = crate::package::pkgkey2pkgname(pkgkey) {
if pkgname.starts_with("__") {
continue;
}
}
if let Ok(package) = crate::package_cache::load_package_info(pkgkey) {
if package.repodata_name == "virtual" {
log::debug!("Skipping virtual package: {}", pkgkey);
continue;
}
}
let depth = pkgkey_to_depth.get(pkgkey).copied().unwrap_or(0);
let ebin_exposure = request_world_pkgkeys.contains(pkgkey);
let pkg_info = create_installed_package_info(
pkgkey,
provider_ref,
solvables,
pkgkey_to_depends,
pkgkey_to_rdepends,
pkgkey_to_bdepends,
pkgkey_to_rbdepends,
depth,
ebin_exposure,
)?;
result.insert(pkgkey.clone(), Arc::new(pkg_info));
}
log::debug!("Final result size: {}", result.len());
Ok(result)
}
fn create_installed_package_info(
pkgkey: &str,
provider_ref: &GenericDependencyProvider,
solvables: &[resolvo::SolvableId],
pkgkey_to_depends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_bdepends: &HashMap<String, BTreeSet<String>>,
pkgkey_to_rbdepends: &HashMap<String, BTreeSet<String>>,
depend_depth: u16,
ebin_exposure: bool,
) -> Result<InstalledPackageInfo> {
let pkgline = String::new();
let depends_list = pkgkey_to_depends.get(pkgkey).cloned().unwrap_or_default();
let bdepends_list = pkgkey_to_bdepends.get(pkgkey).cloned().unwrap_or_default();
let arch = solvables
.iter()
.find_map(|&id| {
let rec = &provider_ref.pool.resolve_solvable(id).record;
if rec.pkgkey == pkgkey {
Some(rec.arch.clone())
} else {
None
}
})
.unwrap_or_else(|| "unknown".to_string());
let mut merged_rdepends = pkgkey_to_rdepends.get(pkgkey).cloned().unwrap_or_default();
if let Some(installed_info) = PACKAGE_CACHE.installed_packages.read().unwrap().get(pkgkey) {
merged_rdepends.extend(installed_info.rdepends.iter().cloned());
}
let mut merged_rbdepends = pkgkey_to_rbdepends.get(pkgkey).cloned().unwrap_or_default();
if let Some(installed_info) = PACKAGE_CACHE.installed_packages.read().unwrap().get(pkgkey) {
merged_rbdepends.extend(installed_info.rbdepends.iter().cloned());
}
Ok(crate::models::InstalledPackageInfo {
pkgline,
arch,
depend_depth,
install_time: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
ebin_exposure,
rdepends: merged_rdepends,
depends: depends_list,
bdepends: bdepends_list,
rbdepends: merged_rbdepends,
ebin_links: Vec::new(),
#[cfg(unix)]
xdesktop_links: Vec::new(),
pending_triggers: Vec::new(),
triggers_awaited: false,
config_failed: false,
})
}