1110 lines
37 KiB
Rust
1110 lines
37 KiB
Rust
use super::*;
|
|
use crate::commands::repo::groups::scan_package_specs;
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub(crate) struct UpdateCommandOptions<'a> {
|
|
pub(crate) rootfs: &'a Path,
|
|
pub(crate) no_deps: bool,
|
|
pub(crate) no_flags: bool,
|
|
pub(crate) cross_prefix: Option<&'a str>,
|
|
pub(crate) clean: bool,
|
|
pub(crate) dry_run: bool,
|
|
pub(crate) assume_yes: bool,
|
|
pub(crate) install_test_deps: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) enum UpdateOrigin {
|
|
Source {
|
|
repo_name: String,
|
|
path: PathBuf,
|
|
},
|
|
Binary {
|
|
repo_name: String,
|
|
record: Box<db::repo::BinaryRepoPackageRecord>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct UpdateCandidate {
|
|
pub(crate) installed_package: String,
|
|
pub(crate) candidate_package: String,
|
|
pub(crate) replaces_installed: bool,
|
|
pub(crate) installed_version: String,
|
|
pub(crate) installed_revision: u32,
|
|
pub(crate) installed_completed_at: Option<i64>,
|
|
pub(crate) candidate_version: String,
|
|
pub(crate) candidate_revision: u32,
|
|
pub(crate) candidate_completed_at: Option<i64>,
|
|
pub(crate) runtime_dependencies: Vec<String>,
|
|
pub(crate) provides: Vec<String>,
|
|
pub(crate) conflicts: Vec<String>,
|
|
pub(crate) repo_priority: i32,
|
|
pub(crate) origin: UpdateOrigin,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) struct SourceUpdateCandidate {
|
|
pub(crate) repo_name: String,
|
|
pub(crate) repo_priority: i32,
|
|
pub(crate) path: PathBuf,
|
|
pub(crate) completed_at: Option<i64>,
|
|
pub(crate) spec: package::PackageSpec,
|
|
}
|
|
|
|
pub(crate) fn source_update_candidate_is_better(
|
|
candidate: &SourceUpdateCandidate,
|
|
current: &SourceUpdateCandidate,
|
|
) -> bool {
|
|
match compare_package_release(
|
|
&candidate.spec.package.version,
|
|
candidate.spec.package.revision,
|
|
¤t.spec.package.version,
|
|
current.spec.package.revision,
|
|
) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => match compare_completed_at(candidate.completed_at, current.completed_at)
|
|
{
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => {
|
|
if candidate.repo_priority != current.repo_priority {
|
|
candidate.repo_priority < current.repo_priority
|
|
} else if candidate.repo_name != current.repo_name {
|
|
candidate.repo_name < current.repo_name
|
|
} else {
|
|
candidate.path < current.path
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn binary_update_candidate_is_better(
|
|
repo_name: &str,
|
|
repo_priority: i32,
|
|
record: &db::repo::BinaryRepoPackageRecord,
|
|
current_priority: i32,
|
|
current: &db::repo::BinaryRepoPackageRecord,
|
|
) -> bool {
|
|
match compare_package_release(
|
|
&record.version,
|
|
record.revision,
|
|
¤t.version,
|
|
current.revision,
|
|
) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => match compare_completed_at(record.completed_at, current.completed_at) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => {
|
|
if repo_priority != current_priority {
|
|
repo_priority < current_priority
|
|
} else if repo_name != current.repo_name {
|
|
repo_name < current.repo_name.as_str()
|
|
} else {
|
|
record.filename < current.filename
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn compare_package_release(
|
|
left_version: &str,
|
|
left_revision: u32,
|
|
right_version: &str,
|
|
right_revision: u32,
|
|
) -> Ordering {
|
|
super::compare_versions_for_updates(left_version, right_version)
|
|
.then_with(|| left_revision.cmp(&right_revision))
|
|
}
|
|
|
|
pub(crate) fn compare_completed_at(left: Option<i64>, right: Option<i64>) -> Ordering {
|
|
match (left, right) {
|
|
(Some(left), Some(right)) => left.cmp(&right),
|
|
(Some(_), None) => Ordering::Greater,
|
|
(None, Some(_)) => Ordering::Less,
|
|
(None, None) => Ordering::Equal,
|
|
}
|
|
}
|
|
|
|
fn update_candidate_is_newer_than_installed(
|
|
candidate_version: &str,
|
|
candidate_revision: u32,
|
|
candidate_completed_at: Option<i64>,
|
|
installed_version: &str,
|
|
installed_revision: u32,
|
|
installed_completed_at: Option<i64>,
|
|
) -> bool {
|
|
match compare_package_release(
|
|
candidate_version,
|
|
candidate_revision,
|
|
installed_version,
|
|
installed_revision,
|
|
) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => {
|
|
compare_completed_at(candidate_completed_at, installed_completed_at)
|
|
== Ordering::Greater
|
|
}
|
|
}
|
|
}
|
|
|
|
fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
|
|
let metadata = fs::metadata(path)
|
|
.with_context(|| format!("Failed to read metadata for {}", path.display()))?;
|
|
let modified = metadata
|
|
.modified()
|
|
.with_context(|| format!("Failed to read modification time for {}", path.display()))?;
|
|
Ok(Some(crate::metadata_time::system_time_to_unix(modified)?))
|
|
}
|
|
|
|
fn installed_package_completed_at(
|
|
installed: &db::InstalledPackageRecord,
|
|
db_path: &Path,
|
|
rootfs: &Path,
|
|
) -> Result<Option<i64>> {
|
|
if installed.completed_at.is_some() {
|
|
return Ok(installed.completed_at);
|
|
}
|
|
|
|
let mut latest = None;
|
|
for relative_path in db::get_package_files(db_path, &installed.name)? {
|
|
let path = rootfs.join(&relative_path);
|
|
if !path.exists() {
|
|
continue;
|
|
}
|
|
if let Some(modified) = path_modified_unix_timestamp(&path)? {
|
|
latest = Some(latest.map_or(modified, |current: i64| current.max(modified)));
|
|
}
|
|
}
|
|
|
|
Ok(latest)
|
|
}
|
|
|
|
fn update_origin_is_binary(origin: &UpdateOrigin) -> bool {
|
|
matches!(origin, UpdateOrigin::Binary { .. })
|
|
}
|
|
|
|
fn update_origin_label(origin: &UpdateOrigin) -> String {
|
|
match origin {
|
|
UpdateOrigin::Source { repo_name, path } => {
|
|
format!("source:{repo_name}:{}", path.display())
|
|
}
|
|
UpdateOrigin::Binary { repo_name, record } => {
|
|
format!("binary:{repo_name}:{}", record.filename)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn update_candidate_is_preferred(
|
|
candidate: &UpdateCandidate,
|
|
current: &UpdateCandidate,
|
|
prefer_binary: bool,
|
|
) -> bool {
|
|
match compare_package_release(
|
|
&candidate.candidate_version,
|
|
candidate.candidate_revision,
|
|
¤t.candidate_version,
|
|
current.candidate_revision,
|
|
) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => match compare_completed_at(
|
|
candidate.candidate_completed_at,
|
|
current.candidate_completed_at,
|
|
) {
|
|
Ordering::Greater => true,
|
|
Ordering::Less => false,
|
|
Ordering::Equal => match (
|
|
update_origin_is_binary(&candidate.origin),
|
|
update_origin_is_binary(¤t.origin),
|
|
) {
|
|
(true, false) => prefer_binary,
|
|
(false, true) => !prefer_binary,
|
|
_ => {
|
|
if candidate.repo_priority != current.repo_priority {
|
|
candidate.repo_priority < current.repo_priority
|
|
} else {
|
|
update_origin_label(&candidate.origin)
|
|
< update_origin_label(¤t.origin)
|
|
}
|
|
}
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
fn candidate_request_path(
|
|
candidate: &UpdateCandidate,
|
|
config: &config::Config,
|
|
rootfs: &Path,
|
|
) -> Result<PathBuf> {
|
|
match &candidate.origin {
|
|
UpdateOrigin::Source { path, .. } => Ok(path.clone()),
|
|
UpdateOrigin::Binary { repo_name, record } => {
|
|
let repo_cfg = config
|
|
.binary_repos
|
|
.get(repo_name)
|
|
.with_context(|| format!("Binary repo '{}' not found in config", repo_name))?;
|
|
db::repo::fetch_binary_package_archive(
|
|
repo_name,
|
|
repo_cfg,
|
|
rootfs,
|
|
record,
|
|
&config.package_cache_dir,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn sync_source_repositories_for_update(config: &config::Config) -> Result<()> {
|
|
if config.repo_settings.prefer_binary {
|
|
return Ok(());
|
|
}
|
|
|
|
let mirrors = config.enabled_source_mirror_map();
|
|
if mirrors.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut repo_lock = locking::open_lock(config)?;
|
|
let repo_lock_path = locking::lock_path(config);
|
|
let _repo_lock_guard = locking::try_write(&mut repo_lock, &repo_lock_path, "update sync")?;
|
|
db::repo::sync_mirrors(&config.repo_clone_dir, &mirrors)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn configured_source_scan_roots(config: &config::Config) -> Vec<(String, i32, PathBuf)> {
|
|
let mut roots = Vec::new();
|
|
let mut repos: Vec<_> = config
|
|
.source_repos
|
|
.iter()
|
|
.filter(|(_, repo)| repo.enabled)
|
|
.collect();
|
|
repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
|
|
|
|
for (repo_name, repo) in repos {
|
|
let repo_root = config.repo_clone_dir.join(repo_name);
|
|
if repo.subdirs.is_empty() {
|
|
roots.push((repo_name.clone(), repo.priority, repo_root));
|
|
} else {
|
|
for subdir in &repo.subdirs {
|
|
roots.push((repo_name.clone(), repo.priority, repo_root.join(subdir)));
|
|
}
|
|
}
|
|
}
|
|
|
|
roots
|
|
}
|
|
|
|
pub(crate) fn collect_best_source_update_candidates(
|
|
config: &config::Config,
|
|
target_real_names: &HashSet<String>,
|
|
) -> Result<HashMap<String, SourceUpdateCandidate>> {
|
|
let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new();
|
|
|
|
for (repo_name, repo_priority, root) in configured_source_scan_roots(config) {
|
|
if !root.exists() {
|
|
continue;
|
|
}
|
|
|
|
for spec_path in scan_package_specs(&root)? {
|
|
let mut spec = package::PackageSpec::from_file(&spec_path)?;
|
|
spec.apply_config(config);
|
|
let stream_name = spec.package.effective_real_name().to_string();
|
|
if !target_real_names.contains(&stream_name) {
|
|
continue;
|
|
}
|
|
|
|
let candidate = SourceUpdateCandidate {
|
|
repo_name: repo_name.clone(),
|
|
repo_priority,
|
|
path: spec_path.clone(),
|
|
completed_at: path_modified_unix_timestamp(&spec_path)?,
|
|
spec,
|
|
};
|
|
|
|
let replace = match best.get(&stream_name) {
|
|
Some(current) => source_update_candidate_is_better(&candidate, current),
|
|
None => true,
|
|
};
|
|
|
|
if replace {
|
|
best.insert(stream_name, candidate);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(best)
|
|
}
|
|
|
|
fn collect_best_source_replacement_candidates(
|
|
config: &config::Config,
|
|
target_packages: &HashSet<String>,
|
|
) -> Result<HashMap<String, SourceUpdateCandidate>> {
|
|
let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new();
|
|
|
|
for (repo_name, repo_priority, root) in configured_source_scan_roots(config) {
|
|
if !root.exists() {
|
|
continue;
|
|
}
|
|
|
|
for spec_path in scan_package_specs(&root)? {
|
|
let mut spec = package::PackageSpec::from_file(&spec_path)?;
|
|
spec.apply_config(config);
|
|
let candidate = SourceUpdateCandidate {
|
|
repo_name: repo_name.clone(),
|
|
repo_priority,
|
|
path: spec_path.clone(),
|
|
completed_at: path_modified_unix_timestamp(&spec_path)?,
|
|
spec,
|
|
};
|
|
|
|
for replaced in &candidate.spec.alternatives.replaces {
|
|
if !target_packages.contains(replaced) {
|
|
continue;
|
|
}
|
|
let replace = match best.get(replaced) {
|
|
Some(current) => source_update_candidate_is_better(&candidate, current),
|
|
None => true,
|
|
};
|
|
if replace {
|
|
best.insert(replaced.clone(), candidate.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(best)
|
|
}
|
|
|
|
fn collect_best_binary_update_candidates(
|
|
config: &config::Config,
|
|
rootfs: &Path,
|
|
target_real_names: &HashSet<String>,
|
|
) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> {
|
|
let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new();
|
|
let host_arch = std::env::consts::ARCH;
|
|
let mut repos: Vec<_> = config
|
|
.binary_repos
|
|
.iter()
|
|
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
|
|
.collect();
|
|
repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
|
|
|
|
for (repo_name, repo_cfg) in repos {
|
|
let records = db::repo::list_binary_repo_packages(
|
|
repo_name,
|
|
repo_cfg,
|
|
rootfs,
|
|
&config.package_cache_dir,
|
|
)
|
|
.with_context(|| format!("Failed to list binary repo '{}'", repo_name))?;
|
|
|
|
for record in records {
|
|
let stream_name = record.effective_real_name().to_string();
|
|
if !target_real_names.contains(&stream_name) {
|
|
continue;
|
|
}
|
|
|
|
let replace = match best.get(&stream_name) {
|
|
Some((current_priority, current)) => binary_update_candidate_is_better(
|
|
repo_name,
|
|
repo_cfg.priority,
|
|
&record,
|
|
*current_priority,
|
|
current,
|
|
),
|
|
None => true,
|
|
};
|
|
|
|
if replace {
|
|
best.insert(stream_name, (repo_cfg.priority, record));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(best)
|
|
}
|
|
|
|
fn collect_best_binary_replacement_candidates(
|
|
config: &config::Config,
|
|
rootfs: &Path,
|
|
target_packages: &HashSet<String>,
|
|
) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> {
|
|
let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new();
|
|
let host_arch = std::env::consts::ARCH;
|
|
let mut repos: Vec<_> = config
|
|
.binary_repos
|
|
.iter()
|
|
.filter(|(_, repo)| repo.enabled && repo.supports_arch(host_arch))
|
|
.collect();
|
|
repos.sort_by(|a, b| a.1.priority.cmp(&b.1.priority).then_with(|| a.0.cmp(b.0)));
|
|
|
|
for (repo_name, repo_cfg) in repos {
|
|
let records = db::repo::list_binary_repo_packages(
|
|
repo_name,
|
|
repo_cfg,
|
|
rootfs,
|
|
&config.package_cache_dir,
|
|
)
|
|
.with_context(|| format!("Failed to list binary repo '{}'", repo_name))?;
|
|
|
|
for record in records {
|
|
for replaced in &record.replaces {
|
|
if !target_packages.contains(replaced) {
|
|
continue;
|
|
}
|
|
let replace = match best.get(replaced) {
|
|
Some((current_priority, current)) => binary_update_candidate_is_better(
|
|
repo_name,
|
|
repo_cfg.priority,
|
|
&record,
|
|
*current_priority,
|
|
current,
|
|
),
|
|
None => true,
|
|
};
|
|
if replace {
|
|
best.insert(replaced.clone(), (repo_cfg.priority, record.clone()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(best)
|
|
}
|
|
|
|
pub(crate) fn select_update_candidate(
|
|
installed: &db::InstalledPackageRecord,
|
|
installed_completed_at: Option<i64>,
|
|
source_replacement_candidates: &HashMap<String, SourceUpdateCandidate>,
|
|
binary_replacement_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
|
|
source_candidates: &HashMap<String, SourceUpdateCandidate>,
|
|
binary_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
|
|
prefer_binary: bool,
|
|
) -> Option<UpdateCandidate> {
|
|
let mut best: Option<UpdateCandidate> = None;
|
|
let stream_name = installed.effective_real_name();
|
|
|
|
if let Some(candidate) = source_replacement_candidates.get(&installed.name) {
|
|
best = Some(UpdateCandidate {
|
|
installed_package: installed.name.clone(),
|
|
candidate_package: candidate.spec.package.name.clone(),
|
|
replaces_installed: true,
|
|
installed_version: installed.version.clone(),
|
|
installed_revision: installed.revision,
|
|
installed_completed_at,
|
|
candidate_version: candidate.spec.package.version.clone(),
|
|
candidate_revision: candidate.spec.package.revision,
|
|
candidate_completed_at: candidate.completed_at,
|
|
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
|
|
provides: candidate.spec.alternatives.provides.clone(),
|
|
conflicts: candidate.spec.alternatives.conflicts.clone(),
|
|
repo_priority: candidate.repo_priority,
|
|
origin: UpdateOrigin::Source {
|
|
repo_name: candidate.repo_name.clone(),
|
|
path: candidate.path.clone(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if let Some((repo_priority, record)) = binary_replacement_candidates.get(&installed.name) {
|
|
let binary_candidate = UpdateCandidate {
|
|
installed_package: installed.name.clone(),
|
|
candidate_package: record.name.clone(),
|
|
replaces_installed: true,
|
|
installed_version: installed.version.clone(),
|
|
installed_revision: installed.revision,
|
|
installed_completed_at,
|
|
candidate_version: record.version.clone(),
|
|
candidate_revision: record.revision,
|
|
candidate_completed_at: record.completed_at,
|
|
runtime_dependencies: record.runtime_dependencies.clone(),
|
|
provides: record.provides.clone(),
|
|
conflicts: record.conflicts.clone(),
|
|
repo_priority: *repo_priority,
|
|
origin: UpdateOrigin::Binary {
|
|
repo_name: record.repo_name.clone(),
|
|
record: Box::new(record.clone()),
|
|
},
|
|
};
|
|
|
|
if best.as_ref().is_none_or(|current| {
|
|
update_candidate_is_preferred(&binary_candidate, current, prefer_binary)
|
|
}) {
|
|
best = Some(binary_candidate);
|
|
}
|
|
}
|
|
|
|
if best.is_some() {
|
|
return best;
|
|
}
|
|
|
|
if let Some(candidate) = source_candidates.get(stream_name)
|
|
&& update_candidate_is_newer_than_installed(
|
|
&candidate.spec.package.version,
|
|
candidate.spec.package.revision,
|
|
candidate.completed_at,
|
|
&installed.version,
|
|
installed.revision,
|
|
installed_completed_at,
|
|
)
|
|
{
|
|
best = Some(UpdateCandidate {
|
|
installed_package: installed.name.clone(),
|
|
candidate_package: candidate.spec.package.name.clone(),
|
|
replaces_installed: false,
|
|
installed_version: installed.version.clone(),
|
|
installed_revision: installed.revision,
|
|
installed_completed_at,
|
|
candidate_version: candidate.spec.package.version.clone(),
|
|
candidate_revision: candidate.spec.package.revision,
|
|
candidate_completed_at: candidate.completed_at,
|
|
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
|
|
provides: candidate.spec.alternatives.provides.clone(),
|
|
conflicts: candidate.spec.alternatives.conflicts.clone(),
|
|
repo_priority: candidate.repo_priority,
|
|
origin: UpdateOrigin::Source {
|
|
repo_name: candidate.repo_name.clone(),
|
|
path: candidate.path.clone(),
|
|
},
|
|
});
|
|
}
|
|
|
|
if let Some((repo_priority, record)) = binary_candidates.get(stream_name)
|
|
&& update_candidate_is_newer_than_installed(
|
|
&record.version,
|
|
record.revision,
|
|
record.completed_at,
|
|
&installed.version,
|
|
installed.revision,
|
|
installed_completed_at,
|
|
)
|
|
{
|
|
let binary_candidate = UpdateCandidate {
|
|
installed_package: installed.name.clone(),
|
|
candidate_package: record.name.clone(),
|
|
replaces_installed: false,
|
|
installed_version: installed.version.clone(),
|
|
installed_revision: installed.revision,
|
|
installed_completed_at,
|
|
candidate_version: record.version.clone(),
|
|
candidate_revision: record.revision,
|
|
candidate_completed_at: record.completed_at,
|
|
runtime_dependencies: record.runtime_dependencies.clone(),
|
|
provides: record.provides.clone(),
|
|
conflicts: record.conflicts.clone(),
|
|
repo_priority: *repo_priority,
|
|
origin: UpdateOrigin::Binary {
|
|
repo_name: record.repo_name.clone(),
|
|
record: Box::new(record.clone()),
|
|
},
|
|
};
|
|
|
|
if best.as_ref().is_none_or(|current| {
|
|
update_candidate_is_preferred(&binary_candidate, current, prefer_binary)
|
|
}) {
|
|
best = Some(binary_candidate);
|
|
}
|
|
}
|
|
|
|
best
|
|
}
|
|
|
|
pub(crate) fn collect_update_candidates(
|
|
config: &config::Config,
|
|
rootfs: &Path,
|
|
requested_packages: &[String],
|
|
) -> Result<Vec<UpdateCandidate>> {
|
|
let db_path = config.installed_db_path(rootfs);
|
|
let installed = db::list_installed_package_records(&db_path)?;
|
|
if installed.is_empty() {
|
|
return Ok(Vec::new());
|
|
}
|
|
|
|
let mut installed_by_real_name: HashMap<String, Vec<&db::InstalledPackageRecord>> =
|
|
HashMap::new();
|
|
for record in &installed {
|
|
installed_by_real_name
|
|
.entry(record.effective_real_name().to_string())
|
|
.or_default()
|
|
.push(record);
|
|
}
|
|
|
|
let active_by_real_name: HashMap<String, db::InstalledPackageRecord> = installed_by_real_name
|
|
.iter()
|
|
.filter_map(|(real_name, records)| {
|
|
super::super::select_primary_installed_record(records.iter().copied())
|
|
.cloned()
|
|
.map(|record| (real_name.clone(), record))
|
|
})
|
|
.collect();
|
|
|
|
let target_real_names: HashSet<String> = if requested_packages.is_empty() {
|
|
active_by_real_name.keys().cloned().collect()
|
|
} else {
|
|
let mut targets = HashSet::new();
|
|
for package in requested_packages {
|
|
if let Some(record) = installed.iter().find(|record| record.name == *package) {
|
|
targets.insert(record.effective_real_name().to_string());
|
|
} else if active_by_real_name.contains_key(package) {
|
|
targets.insert(package.clone());
|
|
} else {
|
|
ui::warn(format!("Package '{}' is not installed; skipping", package));
|
|
}
|
|
}
|
|
targets
|
|
};
|
|
let target_package_names: HashSet<String> = if requested_packages.is_empty() {
|
|
active_by_real_name
|
|
.values()
|
|
.map(|record| record.name.clone())
|
|
.collect()
|
|
} else {
|
|
let mut targets = HashSet::new();
|
|
for package in requested_packages {
|
|
if let Some(record) = installed.iter().find(|record| record.name == *package) {
|
|
targets.insert(record.name.clone());
|
|
} else if let Some(record) = active_by_real_name.get(package) {
|
|
targets.insert(record.name.clone());
|
|
}
|
|
}
|
|
targets
|
|
};
|
|
|
|
let source_candidates = if config.repo_settings.prefer_binary {
|
|
HashMap::new()
|
|
} else {
|
|
collect_best_source_update_candidates(config, &target_real_names)?
|
|
};
|
|
let source_replacement_candidates = if config.repo_settings.prefer_binary {
|
|
HashMap::new()
|
|
} else {
|
|
collect_best_source_replacement_candidates(config, &target_package_names)?
|
|
};
|
|
let binary_candidates =
|
|
collect_best_binary_update_candidates(config, rootfs, &target_real_names)?;
|
|
let binary_replacement_candidates =
|
|
collect_best_binary_replacement_candidates(config, rootfs, &target_package_names)?;
|
|
|
|
let mut updates = Vec::new();
|
|
let mut targets: Vec<_> = target_real_names.into_iter().collect();
|
|
targets.sort();
|
|
for target in targets {
|
|
let Some(installed) = active_by_real_name.get(&target) else {
|
|
continue;
|
|
};
|
|
let installed_completed_at = installed_package_completed_at(installed, &db_path, rootfs)?;
|
|
if let Some(candidate) = select_update_candidate(
|
|
installed,
|
|
installed_completed_at,
|
|
&source_replacement_candidates,
|
|
&binary_replacement_candidates,
|
|
&source_candidates,
|
|
&binary_candidates,
|
|
config.repo_settings.prefer_binary,
|
|
) {
|
|
updates.push(candidate);
|
|
}
|
|
}
|
|
|
|
Ok(updates)
|
|
}
|
|
|
|
pub(crate) fn collect_missing_update_dependencies(
|
|
candidates: &[UpdateCandidate],
|
|
db_path: &Path,
|
|
) -> Result<Vec<String>> {
|
|
let mut planned_provides = HashSet::new();
|
|
for candidate in candidates {
|
|
planned_provides.insert(candidate.candidate_package.clone());
|
|
for provide in &candidate.provides {
|
|
planned_provides.insert(provide.clone());
|
|
}
|
|
}
|
|
|
|
let mut missing = Vec::new();
|
|
for candidate in candidates {
|
|
for dep in &candidate.runtime_dependencies {
|
|
if planned_provides.contains(deps::dep_name(dep)) {
|
|
continue;
|
|
}
|
|
if deps::is_dep_satisfied_in_db(dep, db_path)? {
|
|
continue;
|
|
}
|
|
if !missing.contains(dep) {
|
|
missing.push(dep.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(missing)
|
|
}
|
|
|
|
fn update_release(version: &str, revision: u32) -> String {
|
|
format!("{version}-{revision}")
|
|
}
|
|
|
|
fn package_word(count: usize) -> &'static str {
|
|
if count == 1 { "package" } else { "packages" }
|
|
}
|
|
|
|
fn update_package_label(candidate: &UpdateCandidate) -> String {
|
|
if candidate.replaces_installed {
|
|
format!(
|
|
"{} → {}",
|
|
candidate.installed_package, candidate.candidate_package
|
|
)
|
|
} else {
|
|
candidate.candidate_package.clone()
|
|
}
|
|
}
|
|
|
|
fn format_update_summary(
|
|
updates: &[UpdateCandidate],
|
|
missing_dependencies: &[String],
|
|
download_size: Option<u64>,
|
|
) -> String {
|
|
let labels: Vec<_> = updates.iter().map(update_package_label).collect();
|
|
let label_width = labels
|
|
.iter()
|
|
.map(|label| label.chars().count())
|
|
.max()
|
|
.unwrap_or(0);
|
|
let mut output = format!("Updates ({})\n", updates.len());
|
|
|
|
for (candidate, label) in updates.iter().zip(labels) {
|
|
let installed = update_release(&candidate.installed_version, candidate.installed_revision);
|
|
let available = update_release(&candidate.candidate_version, candidate.candidate_revision);
|
|
let newer_build = candidate.installed_version == candidate.candidate_version
|
|
&& candidate.installed_revision == candidate.candidate_revision
|
|
&& compare_completed_at(
|
|
candidate.candidate_completed_at,
|
|
candidate.installed_completed_at,
|
|
) == Ordering::Greater;
|
|
let build_note = if newer_build { " (newer build)" } else { "" };
|
|
output.push_str(&format!(
|
|
" {label:<label_width$} {installed} → {available}{build_note}\n"
|
|
));
|
|
}
|
|
|
|
if let Some(bytes) = download_size {
|
|
output.push_str(&format!("\nDownload: {}\n", human_bytes(bytes)));
|
|
}
|
|
if !missing_dependencies.is_empty() {
|
|
output.push_str(&format!(
|
|
"Dependencies: {}\n",
|
|
missing_dependencies.join(", ")
|
|
));
|
|
}
|
|
output.push('\n');
|
|
output
|
|
}
|
|
|
|
fn planned_update_download_size(
|
|
updates: &[UpdateCandidate],
|
|
dependency_plan: Option<&planner::ExecutionPlan>,
|
|
) -> Option<u64> {
|
|
let mut total = 0u64;
|
|
for candidate in updates {
|
|
let UpdateOrigin::Binary { record, .. } = &candidate.origin else {
|
|
return None;
|
|
};
|
|
total = total.checked_add(record.size)?;
|
|
}
|
|
|
|
if let Some(plan) = dependency_plan {
|
|
for step in plan.actionable_steps() {
|
|
match &step.origin {
|
|
planner::PlanOrigin::Binary { record, .. } => {
|
|
total = total.checked_add(record.size)?;
|
|
}
|
|
planner::PlanOrigin::Source { .. } => return None,
|
|
planner::PlanOrigin::Installed => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(total)
|
|
}
|
|
|
|
fn start_update_status(package_count: usize) -> (ProgressBar, bool) {
|
|
let interactive = std::io::stdout().is_terminal() && std::io::stderr().is_terminal();
|
|
if !interactive {
|
|
println!(
|
|
"Updating {package_count} {}...",
|
|
package_word(package_count)
|
|
);
|
|
return (ProgressBar::hidden(), false);
|
|
}
|
|
|
|
let status = ProgressBar::new_spinner();
|
|
status.set_style(
|
|
ProgressStyle::default_spinner()
|
|
.template("{spinner:.cyan} {msg}")
|
|
.unwrap_or_else(|_| ProgressStyle::default_spinner()),
|
|
);
|
|
status.enable_steady_tick(std::time::Duration::from_millis(80));
|
|
status.set_message(format!(
|
|
"Updating {package_count} {}",
|
|
package_word(package_count)
|
|
));
|
|
(status, true)
|
|
}
|
|
|
|
fn print_update_complete(updates: &[UpdateCandidate], interactive: bool) {
|
|
if interactive {
|
|
println!("Updated {} {}", updates.len(), package_word(updates.len()));
|
|
return;
|
|
}
|
|
|
|
for candidate in updates {
|
|
println!(
|
|
"Updated {} {} → {}",
|
|
update_package_label(candidate),
|
|
update_release(&candidate.installed_version, candidate.installed_revision),
|
|
update_release(&candidate.candidate_version, candidate.candidate_revision)
|
|
);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn run_update_command(
|
|
packages: &[String],
|
|
config: &config::Config,
|
|
options: UpdateCommandOptions<'_>,
|
|
) -> Result<()> {
|
|
let updates = collect_update_candidates(config, options.rootfs, packages)?;
|
|
if updates.is_empty() {
|
|
println!("All packages are up to date.");
|
|
return Ok(());
|
|
}
|
|
|
|
let conflict_subjects: Vec<_> = updates
|
|
.iter()
|
|
.map(|candidate| super::super::InstallConflictSubject {
|
|
package: candidate.candidate_package.clone(),
|
|
provides: candidate.provides.clone(),
|
|
conflicts: candidate.conflicts.clone(),
|
|
})
|
|
.collect();
|
|
super::super::validate_no_transaction_conflicts(&conflict_subjects)?;
|
|
|
|
let db_path = config.installed_db_path(options.rootfs);
|
|
let missing_deps = if options.no_deps {
|
|
Vec::new()
|
|
} else {
|
|
collect_missing_update_dependencies(&updates, &db_path)?
|
|
};
|
|
|
|
let dependency_plan = if missing_deps.is_empty() {
|
|
None
|
|
} else {
|
|
Some(planner::build_dependency_install_plan(
|
|
config,
|
|
options.rootfs,
|
|
&missing_deps,
|
|
planner::PlannerOptions {
|
|
assume_yes: options.assume_yes,
|
|
prefer_binary: config.repo_settings.prefer_binary,
|
|
local_sibling_root: None,
|
|
include_test_deps: options.install_test_deps,
|
|
lib32_only_requested_specs: false,
|
|
},
|
|
)?)
|
|
};
|
|
let download_size = planned_update_download_size(&updates, dependency_plan.as_ref());
|
|
print!(
|
|
"{}",
|
|
format_update_summary(&updates, &missing_deps, download_size)
|
|
);
|
|
|
|
if !options.dry_run && !options.assume_yes && !ui::prompt_yes_no("Proceed?", true)? {
|
|
anyhow::bail!("Aborted");
|
|
}
|
|
|
|
if options.dry_run {
|
|
super::super::resolve_installed_conflicts_for_subjects(
|
|
&conflict_subjects,
|
|
options.rootfs,
|
|
config,
|
|
true,
|
|
)?;
|
|
}
|
|
|
|
if options.dry_run {
|
|
if let Some(plan) = dependency_plan.as_ref() {
|
|
crate::commands::print_plan_summary(plan);
|
|
}
|
|
println!("Dry run: no changes made.");
|
|
return Ok(());
|
|
}
|
|
|
|
let (status, interactive) = start_update_status(updates.len());
|
|
let update_result = (|| -> Result<()> {
|
|
let mut transaction_requests = Vec::new();
|
|
if let Some(plan) = dependency_plan.as_ref() {
|
|
crate::commands::print_plan_summary(plan);
|
|
transaction_requests.extend(super::super::install_requests_for_plan(
|
|
plan,
|
|
config,
|
|
options.rootfs,
|
|
)?);
|
|
}
|
|
|
|
for candidate in &updates {
|
|
status.set_message(format!(
|
|
"Updating {} {} · preparing {}",
|
|
updates.len(),
|
|
package_word(updates.len()),
|
|
candidate.candidate_package
|
|
));
|
|
let request = candidate_request_path(candidate, config, options.rootfs)?;
|
|
transaction_requests.push(request);
|
|
}
|
|
|
|
status.set_message(format!(
|
|
"Updating {} {} · installing",
|
|
updates.len(),
|
|
package_word(updates.len())
|
|
));
|
|
super::super::run_update_transaction_install_requests(
|
|
super::super::DirectInstallOptions {
|
|
rootfs: options.rootfs,
|
|
no_deps: true,
|
|
no_flags: options.no_flags,
|
|
cross_prefix: options.cross_prefix,
|
|
clean: options.clean,
|
|
dry_run: false,
|
|
lib32_only: false,
|
|
install_test_deps: options.install_test_deps,
|
|
},
|
|
config,
|
|
&transaction_requests,
|
|
)?;
|
|
|
|
if options.clean {
|
|
crate::commands::clean_build_workspace(config)?;
|
|
}
|
|
Ok(())
|
|
})();
|
|
status.finish_and_clear();
|
|
update_result?;
|
|
print_update_complete(&updates, interactive);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod ux_tests {
|
|
use super::*;
|
|
|
|
fn binary_record(name: &str, version: &str, size: u64) -> db::repo::BinaryRepoPackageRecord {
|
|
db::repo::BinaryRepoPackageRecord {
|
|
repo_name: "core".into(),
|
|
name: name.into(),
|
|
real_name: None,
|
|
version: version.into(),
|
|
revision: 1,
|
|
abi_breaking: false,
|
|
built_against: Vec::new(),
|
|
completed_at: None,
|
|
filename: format!("{name}-{version}-1-x86_64.depot.pkg.tar.zst"),
|
|
size,
|
|
sha512: String::new(),
|
|
description: None,
|
|
homepage: None,
|
|
license: None,
|
|
provides: Vec::new(),
|
|
conflicts: Vec::new(),
|
|
replaces: Vec::new(),
|
|
runtime_dependencies: Vec::new(),
|
|
optional_dependencies: Vec::new(),
|
|
groups: Vec::new(),
|
|
}
|
|
}
|
|
|
|
fn binary_update(
|
|
name: &str,
|
|
installed_version: &str,
|
|
candidate_version: &str,
|
|
size: u64,
|
|
) -> UpdateCandidate {
|
|
UpdateCandidate {
|
|
installed_package: name.into(),
|
|
candidate_package: name.into(),
|
|
replaces_installed: false,
|
|
installed_version: installed_version.into(),
|
|
installed_revision: 1,
|
|
installed_completed_at: None,
|
|
candidate_version: candidate_version.into(),
|
|
candidate_revision: 1,
|
|
candidate_completed_at: None,
|
|
runtime_dependencies: Vec::new(),
|
|
provides: Vec::new(),
|
|
conflicts: Vec::new(),
|
|
repo_priority: 0,
|
|
origin: UpdateOrigin::Binary {
|
|
repo_name: "core".into(),
|
|
record: Box::new(binary_record(name, candidate_version, size)),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn update_summary_is_compact_and_aligned() {
|
|
let updates = [
|
|
binary_update("curl", "8.11.0", "8.12.0", 3 * 1024),
|
|
binary_update("openssl", "3.4.0", "3.4.1", 5 * 1024),
|
|
];
|
|
|
|
assert_eq!(
|
|
format_update_summary(&updates, &["ca-certificates".into()], Some(8 * 1024)),
|
|
concat!(
|
|
"Updates (2)\n",
|
|
" curl 8.11.0-1 → 8.12.0-1\n",
|
|
" openssl 3.4.0-1 → 3.4.1-1\n",
|
|
"\n",
|
|
"Download: 8.0 KiB\n",
|
|
"Dependencies: ca-certificates\n",
|
|
"\n",
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn update_summary_marks_replacements_and_newer_builds() {
|
|
let mut update = binary_update("icu78", "78.1", "79.1", 1024);
|
|
update.candidate_package = "icu79".into();
|
|
update.replaces_installed = true;
|
|
update.installed_version = "79.1".into();
|
|
update.installed_completed_at = Some(1);
|
|
update.candidate_completed_at = Some(2);
|
|
|
|
let summary = format_update_summary(&[update], &[], Some(1024));
|
|
assert!(summary.contains("icu78 → icu79"));
|
|
assert!(summary.contains("79.1-1 → 79.1-1 (newer build)"));
|
|
}
|
|
|
|
#[test]
|
|
fn planned_download_size_requires_all_binary_inputs() {
|
|
let binary = binary_update("curl", "8.11.0", "8.12.0", 3 * 1024);
|
|
assert_eq!(
|
|
planned_update_download_size(std::slice::from_ref(&binary), None),
|
|
Some(3 * 1024)
|
|
);
|
|
|
|
let mut source = binary;
|
|
source.origin = UpdateOrigin::Source {
|
|
repo_name: "core".into(),
|
|
path: PathBuf::from("/repo/curl.toml"),
|
|
};
|
|
assert_eq!(planned_update_download_size(&[source], None), None);
|
|
}
|
|
}
|