mod aur;
mod parse;
mod query;
mod resolve;
mod reverse;
mod source;
mod srcinfo;
mod status;
mod utils;
use crate::state::modal::{DependencyInfo, DependencyStatus};
use crate::state::types::{PackageItem, Source};
use parse::parse_dep_spec;
use resolve::{batch_fetch_official_deps, fetch_package_conflicts, resolve_package_deps};
use source::{determine_dependency_source, is_system_package};
use status::determine_status;
use std::collections::{HashMap, HashSet};
use utils::dependency_priority;
pub use query::{
get_installed_packages, get_provided_packages, get_upgradable_packages,
is_package_installed_or_provided,
};
pub use reverse::{
ReverseDependencyReport, get_installed_required_by, has_installed_required_by,
resolve_reverse_dependencies,
};
pub use status::{get_installed_version, version_satisfies};
fn process_conflicts(
item: &PackageItem,
root_names: &HashSet<String>,
installed: &HashSet<String>,
provided: &HashSet<String>,
deps: &mut HashMap<String, DependencyInfo>,
) {
let conflicts = fetch_package_conflicts(&item.name, &item.source);
if conflicts.is_empty() {
return;
}
tracing::debug!("Package {} conflicts with: {:?}", item.name, conflicts);
for conflict_name in conflicts {
if conflict_name.eq_ignore_ascii_case(&item.name) {
tracing::debug!(
"Skipping self-conflict: {} conflicts with itself",
item.name
);
continue;
}
let is_installed = crate::logic::deps::query::is_package_installed_or_provided(
&conflict_name,
installed,
provided,
);
let is_in_install_list = root_names.contains(&conflict_name);
if !is_installed && !is_in_install_list {
continue;
}
let reason = if is_installed && is_in_install_list {
format!("conflicts with {conflict_name} (installed and in install list)")
} else if is_installed {
format!("conflicts with installed package {conflict_name}")
} else {
format!("conflicts with package {conflict_name} in install list")
};
let entry = deps.entry(conflict_name.clone()).or_insert_with(|| {
let (source, is_core) =
crate::logic::deps::source::determine_dependency_source(&conflict_name, installed);
let is_system =
is_core || crate::logic::deps::source::is_system_package(&conflict_name);
DependencyInfo {
name: conflict_name.clone(),
version: String::new(),
status: DependencyStatus::Conflict {
reason: reason.clone(),
},
source,
required_by: vec![item.name.clone()],
depends_on: Vec::new(),
is_core,
is_system,
}
});
if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
entry.status = DependencyStatus::Conflict { reason };
}
if !entry.required_by.contains(&item.name) {
entry.required_by.push(item.name.clone());
}
if is_in_install_list {
let reverse_reason = format!("conflicts with package {conflict_name} in install list");
let current_entry = deps.entry(item.name.clone()).or_insert_with(|| {
let (dep_source, is_core) =
crate::logic::deps::source::determine_dependency_source(&item.name, installed);
let is_system =
is_core || crate::logic::deps::source::is_system_package(&item.name);
DependencyInfo {
name: item.name.clone(),
version: String::new(),
status: DependencyStatus::Conflict {
reason: reverse_reason.clone(),
},
source: dep_source,
required_by: vec![conflict_name.clone()],
depends_on: Vec::new(),
is_core,
is_system,
}
});
if !matches!(current_entry.status, DependencyStatus::Conflict { .. }) {
current_entry.status = DependencyStatus::Conflict {
reason: reverse_reason,
};
}
if !current_entry.required_by.contains(&conflict_name) {
current_entry.required_by.push(conflict_name.clone());
}
}
}
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
fn process_batched_dependencies(
name: &str,
dep_names: Vec<String>,
installed: &HashSet<String>,
provided: &HashSet<String>,
upgradable: &HashSet<String>,
) -> Vec<DependencyInfo> {
let mut deps = Vec::new();
for dep_spec in dep_names {
let (pkg_name, version_req) = parse_dep_spec(&dep_spec);
if pkg_name == name {
continue;
}
let pkg_lower = pkg_name.to_lowercase();
if pkg_lower.ends_with(".so") || pkg_lower.contains(".so.") || pkg_lower.contains(".so=") {
continue;
}
let status = determine_status(&pkg_name, &version_req, installed, provided, upgradable);
let (dep_source, is_core) = determine_dependency_source(&pkg_name, installed);
let is_system = is_core || is_system_package(&pkg_name);
deps.push(DependencyInfo {
name: pkg_name,
version: version_req,
status,
source: dep_source,
required_by: vec![name.to_string()],
depends_on: Vec::new(),
is_core,
is_system,
});
}
deps
}
fn merge_dependency(
dep: &DependencyInfo,
parent_name: &str,
installed: &HashSet<String>,
provided: &HashSet<String>,
upgradable: &HashSet<String>,
deps: &mut HashMap<String, DependencyInfo>,
) {
let dep_name = dep.name.clone();
let existing_dep = deps.get(&dep_name).cloned();
let needs_required_by_update = existing_dep
.as_ref()
.is_none_or(|e| !e.required_by.contains(&parent_name.to_string()));
let entry = deps
.entry(dep_name.clone())
.or_insert_with(|| DependencyInfo {
name: dep_name.clone(),
version: dep.version.clone(),
status: dep.status.clone(),
source: dep.source.clone(),
required_by: vec![parent_name.to_string()],
depends_on: Vec::new(),
is_core: dep.is_core,
is_system: dep.is_system,
});
if needs_required_by_update {
entry.required_by.push(parent_name.to_string());
}
if !matches!(entry.status, DependencyStatus::Conflict { .. }) {
let existing_priority = dependency_priority(&entry.status);
let new_priority = dependency_priority(&dep.status);
if new_priority < existing_priority {
entry.status = dep.status.clone();
}
}
if !dep.version.is_empty() && dep.version != entry.version {
if matches!(entry.status, DependencyStatus::Conflict { .. }) {
if entry.version.is_empty() {
entry.version.clone_from(&dep.version);
}
return;
}
if entry.version.is_empty() {
entry.version.clone_from(&dep.version);
} else {
let existing_status =
determine_status(&entry.name, &entry.version, installed, provided, upgradable);
let new_status =
determine_status(&entry.name, &dep.version, installed, provided, upgradable);
let existing_req_priority = dependency_priority(&existing_status);
let new_req_priority = dependency_priority(&new_status);
if new_req_priority < existing_req_priority {
entry.version.clone_from(&dep.version);
entry.status = new_status;
}
}
}
}
fn resolve_single_package_deps(
item: &PackageItem,
batched_deps_cache: &HashMap<String, Vec<String>>,
installed: &HashSet<String>,
provided: &HashSet<String>,
upgradable: &HashSet<String>,
) -> Result<Vec<DependencyInfo>, String> {
let name = &item.name;
let source = &item.source;
tracing::debug!(
"Resolving direct dependencies for {} (source: {:?})",
name,
source
);
let use_batched = matches!(source, Source::Official { repo, .. } if repo != "local")
&& batched_deps_cache.contains_key(name.as_str());
if use_batched {
let dep_names = batched_deps_cache
.get(name.as_str())
.cloned()
.unwrap_or_default();
let deps = process_batched_dependencies(name, dep_names, installed, provided, upgradable);
Ok(deps)
} else {
resolve_package_deps(name, source, installed, provided, upgradable)
}
}
pub fn resolve_dependencies(items: &[PackageItem]) -> Vec<DependencyInfo> {
let _span = tracing::info_span!(
"resolve_dependencies",
stage = "dependencies",
item_count = items.len()
)
.entered();
let start_time = std::time::Instant::now();
let backtrace = std::backtrace::Backtrace::force_capture();
let backtrace_str = format!("{backtrace:?}");
let is_blocking_task = backtrace_str.contains("blocking::task")
|| backtrace_str.contains("blocking::pool")
|| backtrace_str.contains("spawn_blocking");
if !is_blocking_task {
tracing::warn!(
"[Deps] resolve_dependencies called synchronously from UI thread! This will block! Backtrace:\n{}",
backtrace_str
);
}
if items.is_empty() {
tracing::warn!("No packages provided for dependency resolution");
return Vec::new();
}
let mut deps: HashMap<String, DependencyInfo> = HashMap::new();
tracing::info!("Fetching list of installed packages...");
let installed = get_installed_packages();
tracing::info!("Found {} installed packages", installed.len());
tracing::debug!(
"Provides will be checked lazily on-demand (not building full set for performance)"
);
let provided = get_provided_packages(&installed);
let upgradable = get_upgradable_packages();
tracing::info!("Found {} upgradable packages", upgradable.len());
let root_names: HashSet<String> = items.iter().map(|i| i.name.clone()).collect();
tracing::info!("Checking conflicts for {} package(s)", items.len());
for item in items {
process_conflicts(item, &root_names, &installed, &provided, &mut deps);
}
let official_packages: Vec<&str> = items
.iter()
.filter_map(|item| {
if let Source::Official { repo, .. } = &item.source {
if *repo == "local" {
None
} else {
Some(item.name.as_str())
}
} else {
None
}
})
.collect();
let batched_deps_cache = if official_packages.is_empty() {
std::collections::HashMap::new()
} else {
batch_fetch_official_deps(&official_packages)
};
for item in items {
match resolve_single_package_deps(
item,
&batched_deps_cache,
&installed,
&provided,
&upgradable,
) {
Ok(resolved_deps) => {
tracing::debug!(
" Found {} dependencies for {}",
resolved_deps.len(),
item.name
);
for dep in resolved_deps {
merge_dependency(
&dep,
&item.name,
&installed,
&provided,
&upgradable,
&mut deps,
);
}
}
Err(e) => {
tracing::warn!(" Failed to resolve dependencies for {}: {}", item.name, e);
}
}
}
let mut result: Vec<DependencyInfo> = deps.into_values().collect();
tracing::info!("Total unique dependencies found: {}", result.len());
result.sort_by(|a, b| {
let priority_a = dependency_priority(&a.status);
let priority_b = dependency_priority(&b.status);
priority_a
.cmp(&priority_b)
.then_with(|| a.name.cmp(&b.name))
});
let elapsed = start_time.elapsed();
let duration_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
tracing::info!(
stage = "dependencies",
item_count = items.len(),
result_count = result.len(),
duration_ms = duration_ms,
"Dependency resolution complete"
);
result
}