Implement package replacement feature across the codebase
- Introduced a new `replaces` field in various structs to support package replacements. - Updated database schema to include a `replaces` table and corresponding queries. - Modified the package index to handle replacements and ensure they take precedence over exact package names. - Enhanced the `create_source_repo_index` function to include replacement entries in the index. - Updated tests to validate the new replacement functionality, including parsing and metadata generation. - Refactored relevant functions and structures to accommodate the new `replaces` feature in package specifications and alternatives.
This commit is contained in:
Generated
+1
-1
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.26.0"
|
version = "0.26.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.26.0"
|
version = "0.26.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
project(
|
project(
|
||||||
'depot',
|
'depot',
|
||||||
version: '0.26.0',
|
version: '0.26.1',
|
||||||
meson_version: '>=0.60.0',
|
meson_version: '>=0.60.0',
|
||||||
default_options: ['buildtype=release'],
|
default_options: ['buildtype=release'],
|
||||||
)
|
)
|
||||||
|
|||||||
+11
-7
@@ -890,19 +890,23 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_effective_rustflags_applies_replace_rules() {
|
fn test_effective_rustflags_applies_replace_rules() {
|
||||||
let mut flags = BuildFlags::default();
|
let flags = BuildFlags {
|
||||||
flags.rustflags = vec!["-C".into(), "debuginfo=2".into()];
|
rustflags: vec!["-C".into(), "debuginfo=2".into()],
|
||||||
flags.replace_rustflags = vec!["debuginfo=2=>opt-level=2".into()];
|
replace_rustflags: vec!["debuginfo=2=>opt-level=2".into()],
|
||||||
|
..BuildFlags::default()
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(effective_rustflags(&flags), vec!["-C", "opt-level=2"]);
|
assert_eq!(effective_rustflags(&flags), vec!["-C", "opt-level=2"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_effective_rustflags_appends_rustltoflags_when_enabled() {
|
fn test_effective_rustflags_appends_rustltoflags_when_enabled() {
|
||||||
let mut flags = BuildFlags::default();
|
let flags = BuildFlags {
|
||||||
flags.rustflags = vec!["-C".into(), "opt-level=3".into()];
|
rustflags: vec!["-C".into(), "opt-level=3".into()],
|
||||||
flags.rustltoflags = vec!["-Clinker-plugin-lto".into(), "-Cembed-bitcode=yes".into()];
|
rustltoflags: vec!["-Clinker-plugin-lto".into(), "-Cembed-bitcode=yes".into()],
|
||||||
flags.use_lto = true;
|
use_lto: true,
|
||||||
|
..BuildFlags::default()
|
||||||
|
};
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
effective_rustflags(&flags),
|
effective_rustflags(&flags),
|
||||||
|
|||||||
+568
-54
@@ -756,7 +756,8 @@ fn package_spec_from_repo_record(
|
|||||||
alternatives: package::Alternatives {
|
alternatives: package::Alternatives {
|
||||||
provides: record.provides.clone(),
|
provides: record.provides.clone(),
|
||||||
conflicts: record.conflicts.clone(),
|
conflicts: record.conflicts.clone(),
|
||||||
replaces: Vec::new(),
|
replaces: record.replaces.clone(),
|
||||||
|
lib32: None,
|
||||||
},
|
},
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
source: Vec::new(),
|
source: Vec::new(),
|
||||||
@@ -1224,6 +1225,7 @@ fn build_lib32_companion_package(
|
|||||||
struct PlannedStagedInstall {
|
struct PlannedStagedInstall {
|
||||||
is_update: bool,
|
is_update: bool,
|
||||||
remove_paths: Vec<String>,
|
remove_paths: Vec<String>,
|
||||||
|
replacement_removals: Vec<String>,
|
||||||
renamed_transition: Option<RenamedPackageTransition>,
|
renamed_transition: Option<RenamedPackageTransition>,
|
||||||
hook_context: install::hooks::HookExecutionContextOwned,
|
hook_context: install::hooks::HookExecutionContextOwned,
|
||||||
}
|
}
|
||||||
@@ -1427,6 +1429,24 @@ fn collect_conflicting_installed_packages(
|
|||||||
Ok(removals)
|
Ok(removals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_installed_replacement_packages(
|
||||||
|
db_path: &Path,
|
||||||
|
pkg_spec: &package::PackageSpec,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let installed = db::get_installed_packages(db_path)?;
|
||||||
|
let mut replacements: Vec<String> = pkg_spec
|
||||||
|
.alternatives
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.filter(|name| *name != &pkg_spec.package.name)
|
||||||
|
.filter(|name| installed.contains(*name))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
replacements.sort();
|
||||||
|
replacements.dedup();
|
||||||
|
Ok(replacements)
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_installed_package_with_hooks(
|
fn remove_installed_package_with_hooks(
|
||||||
package: &str,
|
package: &str,
|
||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
@@ -1651,9 +1671,11 @@ fn plan_staged_install(
|
|||||||
let db_path = config.installed_db_path(rootfs);
|
let db_path = config.installed_db_path(rootfs);
|
||||||
|
|
||||||
let new_manifest = staging::generate_manifest_with_dirs(destdir)?;
|
let new_manifest = staging::generate_manifest_with_dirs(destdir)?;
|
||||||
|
let replacement_removals = collect_installed_replacement_packages(&db_path, pkg_spec)?;
|
||||||
let renamed_transition = build_renamed_package_transition(&db_path, pkg_spec, &new_manifest)?;
|
let renamed_transition = build_renamed_package_transition(&db_path, pkg_spec, &new_manifest)?;
|
||||||
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some()
|
let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some()
|
||||||
|| renamed_transition.is_some();
|
|| renamed_transition.is_some()
|
||||||
|
|| !replacement_removals.is_empty();
|
||||||
let mut remove_paths =
|
let mut remove_paths =
|
||||||
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_manifest)?;
|
db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_manifest)?;
|
||||||
if let Some(transition) = &renamed_transition {
|
if let Some(transition) = &renamed_transition {
|
||||||
@@ -1695,6 +1717,7 @@ fn plan_staged_install(
|
|||||||
Ok(PlannedStagedInstall {
|
Ok(PlannedStagedInstall {
|
||||||
is_update,
|
is_update,
|
||||||
remove_paths,
|
remove_paths,
|
||||||
|
replacement_removals,
|
||||||
renamed_transition,
|
renamed_transition,
|
||||||
hook_context: install::hooks::HookExecutionContextOwned {
|
hook_context: install::hooks::HookExecutionContextOwned {
|
||||||
operation,
|
operation,
|
||||||
@@ -1831,7 +1854,22 @@ fn install_planned_packages_to_rootfs(
|
|||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
) -> Result<Vec<InstalledPackageOutcome>> {
|
) -> Result<Vec<InstalledPackageOutcome>> {
|
||||||
let mut installed = Vec::with_capacity(plans.len());
|
let mut installed = Vec::with_capacity(plans.len());
|
||||||
for plan in plans {
|
let mut removed_replacements = HashSet::new();
|
||||||
|
for (idx, plan) in plans.iter().enumerate() {
|
||||||
|
ui::info(format!(
|
||||||
|
"{}/{} Installing package {}-{}-{}",
|
||||||
|
idx + 1,
|
||||||
|
plans.len(),
|
||||||
|
plan.spec.package.name,
|
||||||
|
plan.spec.package.version,
|
||||||
|
plan.spec.package.revision
|
||||||
|
));
|
||||||
|
for package in &plan.staged.replacement_removals {
|
||||||
|
if removed_replacements.insert(package.clone()) {
|
||||||
|
ui::info(format!("Removing package {}...", package));
|
||||||
|
remove_installed_package_with_hooks(package, rootfs, config)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?;
|
install_staged_to_rootfs(&plan.spec, &plan.destdir, rootfs, config, &plan.staged)?;
|
||||||
installed.push(InstalledPackageOutcome {
|
installed.push(InstalledPackageOutcome {
|
||||||
package: plan.spec.package.clone(),
|
package: plan.spec.package.clone(),
|
||||||
@@ -2046,12 +2084,18 @@ fn run_search_command(
|
|||||||
} else {
|
} else {
|
||||||
format!(" provides={}", hit.provides.join(","))
|
format!(" provides={}", hit.provides.join(","))
|
||||||
};
|
};
|
||||||
|
let replaces = if hit.replaces.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" replaces={}", hit.replaces.join(","))
|
||||||
|
};
|
||||||
ui::info(format!(
|
ui::info(format!(
|
||||||
" {} [{}] {}{}",
|
" {} [{}] {}{}{}",
|
||||||
hit.name,
|
hit.name,
|
||||||
source_hit_origin(config, &hit.path),
|
source_hit_origin(config, &hit.path),
|
||||||
hit.path.display(),
|
hit.path.display(),
|
||||||
provides
|
provides,
|
||||||
|
replaces
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2242,6 +2286,7 @@ enum UpdateOrigin {
|
|||||||
struct UpdateCandidate {
|
struct UpdateCandidate {
|
||||||
installed_package: String,
|
installed_package: String,
|
||||||
candidate_package: String,
|
candidate_package: String,
|
||||||
|
replaces_installed: bool,
|
||||||
installed_version: String,
|
installed_version: String,
|
||||||
installed_revision: u32,
|
installed_revision: u32,
|
||||||
installed_completed_at: Option<i64>,
|
installed_completed_at: Option<i64>,
|
||||||
@@ -2264,6 +2309,66 @@ struct SourceUpdateCandidate {
|
|||||||
spec: package::PackageSpec,
|
spec: package::PackageSpec,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
fn compare_package_release(
|
||||||
left_version: &str,
|
left_version: &str,
|
||||||
left_revision: u32,
|
left_revision: u32,
|
||||||
@@ -2482,30 +2587,7 @@ fn collect_best_source_update_candidates(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let replace = match best.get(&stream_name) {
|
let replace = match best.get(&stream_name) {
|
||||||
Some(current) => match compare_package_release(
|
Some(current) => source_update_candidate_is_better(&candidate, current),
|
||||||
&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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2518,6 +2600,46 @@ fn collect_best_source_update_candidates(
|
|||||||
Ok(best)
|
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(
|
fn collect_best_binary_update_candidates(
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
rootfs: &Path,
|
rootfs: &Path,
|
||||||
@@ -2548,30 +2670,13 @@ fn collect_best_binary_update_candidates(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let replace = match best.get(&stream_name) {
|
let replace = match best.get(&stream_name) {
|
||||||
Some((current_priority, current)) => match compare_package_release(
|
Some((current_priority, current)) => binary_update_candidate_is_better(
|
||||||
&record.version,
|
repo_name,
|
||||||
record.revision,
|
repo_cfg.priority,
|
||||||
¤t.version,
|
&record,
|
||||||
current.revision,
|
*current_priority,
|
||||||
) {
|
current,
|
||||||
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_cfg.priority != *current_priority {
|
|
||||||
repo_cfg.priority < *current_priority
|
|
||||||
} else if repo_name != ¤t.repo_name {
|
|
||||||
repo_name < ¤t.repo_name
|
|
||||||
} else {
|
|
||||||
record.filename < current.filename
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2584,9 +2689,59 @@ fn collect_best_binary_update_candidates(
|
|||||||
Ok(best)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
fn select_update_candidate(
|
fn select_update_candidate(
|
||||||
installed: &db::InstalledPackageRecord,
|
installed: &db::InstalledPackageRecord,
|
||||||
installed_completed_at: Option<i64>,
|
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>,
|
source_candidates: &HashMap<String, SourceUpdateCandidate>,
|
||||||
binary_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
|
binary_candidates: &HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>,
|
||||||
prefer_binary: bool,
|
prefer_binary: bool,
|
||||||
@@ -2594,6 +2749,60 @@ fn select_update_candidate(
|
|||||||
let mut best: Option<UpdateCandidate> = None;
|
let mut best: Option<UpdateCandidate> = None;
|
||||||
let stream_name = installed.effective_real_name();
|
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)
|
if let Some(candidate) = source_candidates.get(stream_name)
|
||||||
&& update_candidate_is_newer_than_installed(
|
&& update_candidate_is_newer_than_installed(
|
||||||
&candidate.spec.package.version,
|
&candidate.spec.package.version,
|
||||||
@@ -2607,6 +2816,7 @@ fn select_update_candidate(
|
|||||||
best = Some(UpdateCandidate {
|
best = Some(UpdateCandidate {
|
||||||
installed_package: installed.name.clone(),
|
installed_package: installed.name.clone(),
|
||||||
candidate_package: candidate.spec.package.name.clone(),
|
candidate_package: candidate.spec.package.name.clone(),
|
||||||
|
replaces_installed: false,
|
||||||
installed_version: installed.version.clone(),
|
installed_version: installed.version.clone(),
|
||||||
installed_revision: installed.revision,
|
installed_revision: installed.revision,
|
||||||
installed_completed_at,
|
installed_completed_at,
|
||||||
@@ -2637,6 +2847,7 @@ fn select_update_candidate(
|
|||||||
let binary_candidate = UpdateCandidate {
|
let binary_candidate = UpdateCandidate {
|
||||||
installed_package: installed.name.clone(),
|
installed_package: installed.name.clone(),
|
||||||
candidate_package: record.name.clone(),
|
candidate_package: record.name.clone(),
|
||||||
|
replaces_installed: false,
|
||||||
installed_version: installed.version.clone(),
|
installed_version: installed.version.clone(),
|
||||||
installed_revision: installed.revision,
|
installed_revision: installed.revision,
|
||||||
installed_completed_at,
|
installed_completed_at,
|
||||||
@@ -2707,14 +2918,37 @@ fn collect_update_candidates(
|
|||||||
}
|
}
|
||||||
targets
|
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 {
|
let source_candidates = if config.repo_settings.prefer_binary {
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
} else {
|
} else {
|
||||||
collect_best_source_update_candidates(config, &target_real_names)?
|
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 =
|
let binary_candidates =
|
||||||
collect_best_binary_update_candidates(config, rootfs, &target_real_names)?;
|
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 updates = Vec::new();
|
||||||
let mut targets: Vec<_> = target_real_names.into_iter().collect();
|
let mut targets: Vec<_> = target_real_names.into_iter().collect();
|
||||||
@@ -2727,6 +2961,8 @@ fn collect_update_candidates(
|
|||||||
if let Some(candidate) = select_update_candidate(
|
if let Some(candidate) = select_update_candidate(
|
||||||
installed,
|
installed,
|
||||||
installed_completed_at,
|
installed_completed_at,
|
||||||
|
&source_replacement_candidates,
|
||||||
|
&binary_replacement_candidates,
|
||||||
&source_candidates,
|
&source_candidates,
|
||||||
&binary_candidates,
|
&binary_candidates,
|
||||||
config.repo_settings.prefer_binary,
|
config.repo_settings.prefer_binary,
|
||||||
@@ -2791,6 +3027,28 @@ fn run_update_install_command(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn confirm_update_replacements(candidates: &[UpdateCandidate]) -> Result<()> {
|
||||||
|
for candidate in candidates {
|
||||||
|
if !candidate.replaces_installed {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !ui::prompt_yes_no(
|
||||||
|
&format!(
|
||||||
|
"replace {} with {}?",
|
||||||
|
candidate.installed_package, candidate.candidate_package
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)? {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Replacement declined: {} -> {}",
|
||||||
|
candidate.installed_package,
|
||||||
|
candidate.candidate_package
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn run_update_command(
|
fn run_update_command(
|
||||||
packages: &[String],
|
packages: &[String],
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
@@ -2860,6 +3118,10 @@ fn run_update_command(
|
|||||||
anyhow::bail!("Aborted");
|
anyhow::bail!("Aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !options.dry_run {
|
||||||
|
confirm_update_replacements(&updates)?;
|
||||||
|
}
|
||||||
|
|
||||||
resolve_installed_conflicts_for_subjects(
|
resolve_installed_conflicts_for_subjects(
|
||||||
&conflict_subjects,
|
&conflict_subjects,
|
||||||
options.rootfs,
|
options.rootfs,
|
||||||
@@ -5671,6 +5933,7 @@ mod tests {
|
|||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
}
|
}
|
||||||
@@ -5862,6 +6125,7 @@ optional = []
|
|||||||
license: Some("MIT".into()),
|
license: Some("MIT".into()),
|
||||||
provides: vec!["pkg-virtual".into()],
|
provides: vec!["pkg-virtual".into()],
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: vec!["glibc".into()],
|
runtime_dependencies: vec!["glibc".into()],
|
||||||
optional_dependencies: vec!["manpages".into()],
|
optional_dependencies: vec!["manpages".into()],
|
||||||
};
|
};
|
||||||
@@ -6163,6 +6427,7 @@ optional = []
|
|||||||
license: Some("ISC".into()),
|
license: Some("ISC".into()),
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -6515,6 +6780,8 @@ optional = []
|
|||||||
let selected = select_update_candidate(
|
let selected = select_update_candidate(
|
||||||
&installed_records[0],
|
&installed_records[0],
|
||||||
installed_records[0].completed_at,
|
installed_records[0].completed_at,
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
&source_candidates,
|
&source_candidates,
|
||||||
&HashMap::new(),
|
&HashMap::new(),
|
||||||
false,
|
false,
|
||||||
@@ -6601,6 +6868,7 @@ optional = []
|
|||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -6610,6 +6878,8 @@ optional = []
|
|||||||
let selected = select_update_candidate(
|
let selected = select_update_candidate(
|
||||||
&installed,
|
&installed,
|
||||||
None,
|
None,
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
&source_candidates,
|
&source_candidates,
|
||||||
&binary_candidates,
|
&binary_candidates,
|
||||||
true,
|
true,
|
||||||
@@ -6666,6 +6936,8 @@ optional = []
|
|||||||
let selected = select_update_candidate(
|
let selected = select_update_candidate(
|
||||||
&installed,
|
&installed,
|
||||||
Some(100),
|
Some(100),
|
||||||
|
&HashMap::new(),
|
||||||
|
&HashMap::new(),
|
||||||
&source_candidates,
|
&source_candidates,
|
||||||
&HashMap::new(),
|
&HashMap::new(),
|
||||||
true,
|
true,
|
||||||
@@ -6675,6 +6947,245 @@ optional = []
|
|||||||
assert_eq!(selected.candidate_completed_at, Some(200));
|
assert_eq!(selected.candidate_completed_at, Some(200));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_update_candidate_prefers_replacement_candidate() {
|
||||||
|
let installed = db::InstalledPackageRecord {
|
||||||
|
name: "findutils".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "4.9.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
abi_breaking: false,
|
||||||
|
completed_at: Some(100),
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: "findutils".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "5.0.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "findutils".into(),
|
||||||
|
homepage: "https://example.test/findutils".into(),
|
||||||
|
abi_breaking: false,
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Custom,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
let replacement_spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: "busybox".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "1.36.1".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "busybox".into(),
|
||||||
|
homepage: "https://example.test/busybox".into(),
|
||||||
|
abi_breaking: false,
|
||||||
|
license: vec!["GPL-2.0-only".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives {
|
||||||
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
|
replaces: vec!["findutils".into()],
|
||||||
|
lib32: None,
|
||||||
|
},
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Custom,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
|
||||||
|
let source_candidates = HashMap::from([(
|
||||||
|
"findutils".to_string(),
|
||||||
|
SourceUpdateCandidate {
|
||||||
|
repo_name: "source".into(),
|
||||||
|
repo_priority: 5,
|
||||||
|
path: PathBuf::from("/tmp/findutils.toml"),
|
||||||
|
completed_at: Some(200),
|
||||||
|
spec: source_spec,
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
let source_replacement_candidates = HashMap::from([(
|
||||||
|
"findutils".to_string(),
|
||||||
|
SourceUpdateCandidate {
|
||||||
|
repo_name: "source".into(),
|
||||||
|
repo_priority: 0,
|
||||||
|
path: PathBuf::from("/tmp/busybox.toml"),
|
||||||
|
completed_at: Some(150),
|
||||||
|
spec: replacement_spec,
|
||||||
|
},
|
||||||
|
)]);
|
||||||
|
|
||||||
|
let selected = select_update_candidate(
|
||||||
|
&installed,
|
||||||
|
installed.completed_at,
|
||||||
|
&source_replacement_candidates,
|
||||||
|
&HashMap::new(),
|
||||||
|
&source_candidates,
|
||||||
|
&HashMap::new(),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.expect("expected replacement update candidate");
|
||||||
|
|
||||||
|
assert!(selected.replaces_installed);
|
||||||
|
assert_eq!(selected.installed_package, "findutils");
|
||||||
|
assert_eq!(selected.candidate_package, "busybox");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn install_planned_packages_to_rootfs_defers_replacement_removal_until_replacing_plan()
|
||||||
|
-> Result<()> {
|
||||||
|
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||||
|
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||||
|
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
||||||
|
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||||
|
|
||||||
|
let old_spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: "findutils".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "findutils".into(),
|
||||||
|
homepage: "https://example.test/findutils".into(),
|
||||||
|
abi_breaking: false,
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Bin,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
let old_dest = rootfs.path().join("old-dest");
|
||||||
|
fs::create_dir_all(old_dest.join("usr/bin"))?;
|
||||||
|
fs::write(old_dest.join("usr/bin/find"), "old-find")?;
|
||||||
|
install_package_outputs_to_rootfs(&old_spec, &old_dest, rootfs.path(), &cfg)?;
|
||||||
|
|
||||||
|
let alpha_spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: "alpha".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "alpha".into(),
|
||||||
|
homepage: "https://example.test/alpha".into(),
|
||||||
|
abi_breaking: false,
|
||||||
|
license: vec!["MIT".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives::default(),
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Bin,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
let alpha_dest = rootfs.path().join("alpha-dest");
|
||||||
|
fs::create_dir_all(alpha_dest.join("usr/bin"))?;
|
||||||
|
fs::create_dir_all(alpha_dest.join("scripts"))?;
|
||||||
|
fs::write(alpha_dest.join("usr/bin/alpha"), "alpha")?;
|
||||||
|
fs::write(
|
||||||
|
alpha_dest.join("scripts/post_install"),
|
||||||
|
"if [ -e \"$DEPOT_ROOTFS/usr/bin/find\" ]; then printf '%s' present > \"$DEPOT_ROOTFS/alpha-marker\"; else printf '%s' missing > \"$DEPOT_ROOTFS/alpha-marker\"; fi\n",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let replacement_spec = package::PackageSpec {
|
||||||
|
package: package::PackageInfo {
|
||||||
|
name: "busybox".into(),
|
||||||
|
real_name: None,
|
||||||
|
version: "1.0".into(),
|
||||||
|
revision: 1,
|
||||||
|
description: "busybox".into(),
|
||||||
|
homepage: "https://example.test/busybox".into(),
|
||||||
|
abi_breaking: false,
|
||||||
|
license: vec!["GPL-2.0-only".into()],
|
||||||
|
},
|
||||||
|
packages: Vec::new(),
|
||||||
|
alternatives: package::Alternatives {
|
||||||
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
|
replaces: vec!["findutils".into()],
|
||||||
|
lib32: None,
|
||||||
|
},
|
||||||
|
manual_sources: Vec::new(),
|
||||||
|
source: Vec::new(),
|
||||||
|
build: package::Build {
|
||||||
|
build_type: package::BuildType::Bin,
|
||||||
|
flags: package::BuildFlags::default(),
|
||||||
|
},
|
||||||
|
dependencies: package::Dependencies::default(),
|
||||||
|
package_alternatives: Default::default(),
|
||||||
|
package_dependencies: Default::default(),
|
||||||
|
spec_dir: PathBuf::from("."),
|
||||||
|
};
|
||||||
|
let replacement_dest = rootfs.path().join("replacement-dest");
|
||||||
|
fs::create_dir_all(replacement_dest.join("usr/bin"))?;
|
||||||
|
fs::write(replacement_dest.join("usr/bin/find"), "new-find")?;
|
||||||
|
|
||||||
|
let mut plans = Vec::new();
|
||||||
|
plans.extend(plan_package_outputs_for_install(
|
||||||
|
&alpha_spec,
|
||||||
|
&alpha_dest,
|
||||||
|
rootfs.path(),
|
||||||
|
&cfg,
|
||||||
|
)?);
|
||||||
|
plans.extend(plan_package_outputs_for_install(
|
||||||
|
&replacement_spec,
|
||||||
|
&replacement_dest,
|
||||||
|
rootfs.path(),
|
||||||
|
&cfg,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
install_planned_packages_to_rootfs(&plans, rootfs.path(), &cfg)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(rootfs.path().join("alpha-marker"))?,
|
||||||
|
"present"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(rootfs.path().join("usr/bin/find"))?,
|
||||||
|
"new-find"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
db::get_package_version(&cfg.installed_db_path(rootfs.path()), "findutils")?.is_none()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
db::get_package_version(&cfg.installed_db_path(rootfs.path()), "busybox")?,
|
||||||
|
Some("1.0".into())
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn collect_update_candidates_skips_source_when_prefer_binary_is_enabled() -> Result<()> {
|
fn collect_update_candidates_skips_source_when_prefer_binary_is_enabled() -> Result<()> {
|
||||||
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
|
let temp = tempfile::tempdir().context("Failed to create temp dir")?;
|
||||||
@@ -6908,6 +7419,7 @@ optional = []
|
|||||||
UpdateCandidate {
|
UpdateCandidate {
|
||||||
installed_package: "pkg".into(),
|
installed_package: "pkg".into(),
|
||||||
candidate_package: "pkg".into(),
|
candidate_package: "pkg".into(),
|
||||||
|
replaces_installed: false,
|
||||||
installed_version: "1.0".into(),
|
installed_version: "1.0".into(),
|
||||||
installed_revision: 1,
|
installed_revision: 1,
|
||||||
installed_completed_at: None,
|
installed_completed_at: None,
|
||||||
@@ -6926,6 +7438,7 @@ optional = []
|
|||||||
UpdateCandidate {
|
UpdateCandidate {
|
||||||
installed_package: "helper".into(),
|
installed_package: "helper".into(),
|
||||||
candidate_package: "helper".into(),
|
candidate_package: "helper".into(),
|
||||||
|
replaces_installed: false,
|
||||||
installed_version: "1.0".into(),
|
installed_version: "1.0".into(),
|
||||||
installed_revision: 1,
|
installed_revision: 1,
|
||||||
installed_completed_at: None,
|
installed_completed_at: None,
|
||||||
@@ -6944,6 +7457,7 @@ optional = []
|
|||||||
UpdateCandidate {
|
UpdateCandidate {
|
||||||
installed_package: "tool".into(),
|
installed_package: "tool".into(),
|
||||||
candidate_package: "tool".into(),
|
candidate_package: "tool".into(),
|
||||||
|
replaces_installed: false,
|
||||||
installed_version: "1.0".into(),
|
installed_version: "1.0".into(),
|
||||||
installed_revision: 1,
|
installed_revision: 1,
|
||||||
installed_completed_at: None,
|
installed_completed_at: None,
|
||||||
@@ -7511,7 +8025,7 @@ optional = []
|
|||||||
"lib32-compiler-rt-1.0-1-x86_64.depot.pkg.tar.zst",
|
"lib32-compiler-rt-1.0-1-x86_64.depot.pkg.tar.zst",
|
||||||
);
|
);
|
||||||
|
|
||||||
let steps = vec![
|
let steps = [
|
||||||
planner::PlannedStep {
|
planner::PlannedStep {
|
||||||
package: "expat".into(),
|
package: "expat".into(),
|
||||||
action: planner::PlanAction::InstallBinary,
|
action: planner::PlanAction::InstallBinary,
|
||||||
|
|||||||
+21
-21
@@ -220,27 +220,6 @@ pub fn lib32_target_triple(host: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::lib32_target_triple;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn lib32_target_triple_preserves_explicit_x86_variant() {
|
|
||||||
assert_eq!(
|
|
||||||
lib32_target_triple("x86_64-sfg-linux-gnu"),
|
|
||||||
"i686-sfg-linux-gnu"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
lib32_target_triple("i686-sfg-linux-gnu"),
|
|
||||||
"i686-sfg-linux-gnu"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
lib32_target_triple("i586-sfg-linux-gnu"),
|
|
||||||
"i586-sfg-linux-gnu"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a cross-compilation tool in PATH and return its absolute path
|
/// Find a cross-compilation tool in PATH and return its absolute path
|
||||||
fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String> {
|
fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String> {
|
||||||
for suffix in suffixes {
|
for suffix in suffixes {
|
||||||
@@ -264,3 +243,24 @@ fn find_tool(prefix: &str, suffixes: &[&str], required: bool) -> Result<String>
|
|||||||
|
|
||||||
Err(anyhow::anyhow!("Tool not found"))
|
Err(anyhow::anyhow!("Tool not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::lib32_target_triple;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lib32_target_triple_preserves_explicit_x86_variant() {
|
||||||
|
assert_eq!(
|
||||||
|
lib32_target_triple("x86_64-sfg-linux-gnu"),
|
||||||
|
"i686-sfg-linux-gnu"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lib32_target_triple("i686-sfg-linux-gnu"),
|
||||||
|
"i686-sfg-linux-gnu"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
lib32_target_triple("i586-sfg-linux-gnu"),
|
||||||
|
"i586-sfg-linux-gnu"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+3
-2
@@ -877,6 +877,7 @@ mod tests {
|
|||||||
provides: vec![format!("{}-virtual", name)],
|
provides: vec![format!("{}-virtual", name)],
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
|
lib32: None,
|
||||||
},
|
},
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
source: vec![Source {
|
source: vec![Source {
|
||||||
@@ -979,8 +980,8 @@ mod tests {
|
|||||||
|
|
||||||
let ts = filetime::FileTime::from_unix_time(1_700_000_000, 0);
|
let ts = filetime::FileTime::from_unix_time(1_700_000_000, 0);
|
||||||
filetime::set_file_mtime(&file, ts).unwrap();
|
filetime::set_file_mtime(&file, ts).unwrap();
|
||||||
filetime::set_file_mtime(&dest.join("usr"), ts).unwrap();
|
filetime::set_file_mtime(dest.join("usr"), ts).unwrap();
|
||||||
filetime::set_file_mtime(&dest.join("usr/bin"), ts).unwrap();
|
filetime::set_file_mtime(dest.join("usr/bin"), ts).unwrap();
|
||||||
filetime::set_file_mtime(&dest, ts).unwrap();
|
filetime::set_file_mtime(&dest, ts).unwrap();
|
||||||
|
|
||||||
register_package(&db_path, &spec, &dest).unwrap();
|
register_package(&db_path, &spec, &dest).unwrap();
|
||||||
|
|||||||
+65
-1
@@ -87,6 +87,7 @@ struct IndexedPackage {
|
|||||||
sha512: String,
|
sha512: String,
|
||||||
provides: Vec<String>,
|
provides: Vec<String>,
|
||||||
conflicts: Vec<String>,
|
conflicts: Vec<String>,
|
||||||
|
replaces: Vec<String>,
|
||||||
runtime_dependencies: Vec<String>,
|
runtime_dependencies: Vec<String>,
|
||||||
optional_dependencies: Vec<String>,
|
optional_dependencies: Vec<String>,
|
||||||
archive_files: Vec<String>,
|
archive_files: Vec<String>,
|
||||||
@@ -125,6 +126,7 @@ pub struct BinaryRepoPackageRecord {
|
|||||||
pub license: Option<String>,
|
pub license: Option<String>,
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
pub conflicts: Vec<String>,
|
pub conflicts: Vec<String>,
|
||||||
|
pub replaces: Vec<String>,
|
||||||
pub runtime_dependencies: Vec<String>,
|
pub runtime_dependencies: Vec<String>,
|
||||||
pub optional_dependencies: Vec<String>,
|
pub optional_dependencies: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -317,6 +319,11 @@ impl RepoManager {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
FOREIGN KEY(package_id) REFERENCES packages(id)
|
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||||
);
|
);
|
||||||
|
CREATE TABLE replaces (
|
||||||
|
package_id INTEGER,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||||
|
);
|
||||||
CREATE TABLE dependencies (
|
CREATE TABLE dependencies (
|
||||||
package_id INTEGER,
|
package_id INTEGER,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
@@ -338,6 +345,7 @@ impl RepoManager {
|
|||||||
"CREATE INDEX idx_packages_name ON packages(name);
|
"CREATE INDEX idx_packages_name ON packages(name);
|
||||||
CREATE INDEX idx_provides_name ON provides(name);
|
CREATE INDEX idx_provides_name ON provides(name);
|
||||||
CREATE INDEX idx_conflicts_name ON conflicts(name);
|
CREATE INDEX idx_conflicts_name ON conflicts(name);
|
||||||
|
CREATE INDEX idx_replaces_name ON replaces(name);
|
||||||
CREATE INDEX idx_dependencies_name ON dependencies(name);
|
CREATE INDEX idx_dependencies_name ON dependencies(name);
|
||||||
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
|
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
|
||||||
CREATE INDEX idx_repo_files_path ON files(path);",
|
CREATE INDEX idx_repo_files_path ON files(path);",
|
||||||
@@ -369,6 +377,7 @@ impl RepoManager {
|
|||||||
let mut license = None;
|
let mut license = None;
|
||||||
let mut provides = Vec::new();
|
let mut provides = Vec::new();
|
||||||
let mut conflicts = Vec::new();
|
let mut conflicts = Vec::new();
|
||||||
|
let mut replaces = Vec::new();
|
||||||
let mut runtime_dependencies = Vec::new();
|
let mut runtime_dependencies = Vec::new();
|
||||||
let mut optional_dependencies = Vec::new();
|
let mut optional_dependencies = Vec::new();
|
||||||
let mut archive_files = Vec::new();
|
let mut archive_files = Vec::new();
|
||||||
@@ -439,6 +448,14 @@ impl RepoManager {
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
if let Some(replaces_arr) = metadata.get("replaces").and_then(|v| v.as_array())
|
||||||
|
{
|
||||||
|
replaces = replaces_arr
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.map(String::from)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
if let Some(runtime_arr) = metadata
|
if let Some(runtime_arr) = metadata
|
||||||
.get("dependencies")
|
.get("dependencies")
|
||||||
.and_then(|v| v.get("runtime"))
|
.and_then(|v| v.get("runtime"))
|
||||||
@@ -506,6 +523,7 @@ impl RepoManager {
|
|||||||
sha512,
|
sha512,
|
||||||
provides,
|
provides,
|
||||||
conflicts,
|
conflicts,
|
||||||
|
replaces,
|
||||||
runtime_dependencies,
|
runtime_dependencies,
|
||||||
optional_dependencies,
|
optional_dependencies,
|
||||||
archive_files,
|
archive_files,
|
||||||
@@ -529,6 +547,7 @@ impl RepoManager {
|
|||||||
sha512,
|
sha512,
|
||||||
provides,
|
provides,
|
||||||
conflicts,
|
conflicts,
|
||||||
|
replaces,
|
||||||
runtime_dependencies,
|
runtime_dependencies,
|
||||||
optional_dependencies,
|
optional_dependencies,
|
||||||
archive_files,
|
archive_files,
|
||||||
@@ -570,6 +589,12 @@ impl RepoManager {
|
|||||||
params![package_id, conflict],
|
params![package_id, conflict],
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
for replacement in replaces {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO replaces (package_id, name) VALUES (?1, ?2)",
|
||||||
|
params![package_id, replacement],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
for dep in runtime_dependencies {
|
for dep in runtime_dependencies {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
@@ -1592,6 +1617,26 @@ fn query_package_conflicts(conn: &Connection, package_id: i64) -> Result<Vec<Str
|
|||||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn query_package_replaces(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
||||||
|
let has_replaces_table: bool = conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='replaces'",
|
||||||
|
[],
|
||||||
|
|r| {
|
||||||
|
let n: i64 = r.get(0)?;
|
||||||
|
Ok(n > 0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_replaces_table {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare("SELECT name FROM replaces WHERE package_id = ?1 ORDER BY name")?;
|
||||||
|
let rows = stmt.query_map(params![package_id], |row| row.get(0))?;
|
||||||
|
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
fn query_package_runtime_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
fn query_package_runtime_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
||||||
let has_dependencies_table: bool = conn
|
let has_dependencies_table: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
@@ -1677,13 +1722,26 @@ fn find_cached_binary_repo_packages(
|
|||||||
p.license
|
p.license
|
||||||
FROM packages p
|
FROM packages p
|
||||||
WHERE lower(p.name) = lower(?1)
|
WHERE lower(p.name) = lower(?1)
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM replaces rp
|
||||||
|
WHERE rp.package_id = p.id
|
||||||
|
AND lower(rp.name) = lower(?1)
|
||||||
|
)
|
||||||
OR EXISTS (
|
OR EXISTS (
|
||||||
SELECT 1 FROM provides pr
|
SELECT 1 FROM provides pr
|
||||||
WHERE pr.package_id = p.id
|
WHERE pr.package_id = p.id
|
||||||
AND lower(pr.name) = lower(?1)
|
AND lower(pr.name) = lower(?1)
|
||||||
)
|
)
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE WHEN lower(p.name) = lower(?1) THEN 0 ELSE 1 END,
|
CASE
|
||||||
|
WHEN EXISTS (
|
||||||
|
SELECT 1 FROM replaces rp
|
||||||
|
WHERE rp.package_id = p.id
|
||||||
|
AND lower(rp.name) = lower(?1)
|
||||||
|
) THEN 0
|
||||||
|
WHEN lower(p.name) = lower(?1) THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END,
|
||||||
p.name ASC"
|
p.name ASC"
|
||||||
);
|
);
|
||||||
let mut stmt = conn.prepare(&sql)?;
|
let mut stmt = conn.prepare(&sql)?;
|
||||||
@@ -1709,6 +1767,7 @@ fn find_cached_binary_repo_packages(
|
|||||||
license: row.get(13)?,
|
license: row.get(13)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -1720,6 +1779,7 @@ fn find_cached_binary_repo_packages(
|
|||||||
let (package_id, mut rec) = row?;
|
let (package_id, mut rec) = row?;
|
||||||
rec.provides = query_package_provides(&conn, package_id)?;
|
rec.provides = query_package_provides(&conn, package_id)?;
|
||||||
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||||
|
rec.replaces = query_package_replaces(&conn, package_id)?;
|
||||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||||
out.push(rec);
|
out.push(rec);
|
||||||
@@ -1791,6 +1851,7 @@ fn list_cached_binary_repo_packages(
|
|||||||
license: row.get(13)?,
|
license: row.get(13)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -1802,6 +1863,7 @@ fn list_cached_binary_repo_packages(
|
|||||||
let (package_id, mut rec) = row?;
|
let (package_id, mut rec) = row?;
|
||||||
rec.provides = query_package_provides(&conn, package_id)?;
|
rec.provides = query_package_provides(&conn, package_id)?;
|
||||||
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||||
|
rec.replaces = query_package_replaces(&conn, package_id)?;
|
||||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||||
out.push(rec);
|
out.push(rec);
|
||||||
@@ -2754,6 +2816,7 @@ revision = 1
|
|||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -2792,6 +2855,7 @@ revision = 1
|
|||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -715,6 +715,7 @@ mod tests {
|
|||||||
provides: vec!["libfoo".into()],
|
provides: vec!["libfoo".into()],
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
|
lib32: None,
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
package_dependencies: Default::default(),
|
package_dependencies: Default::default(),
|
||||||
|
|||||||
+114
-5
@@ -14,9 +14,11 @@ use walkdir::WalkDir;
|
|||||||
pub const SOURCE_REPO_INDEX_FILENAME: &str = "depot-index.tsv";
|
pub const SOURCE_REPO_INDEX_FILENAME: &str = "depot-index.tsv";
|
||||||
const SOURCE_REPO_INDEX_HEADER_V1: &str = "depot-source-index-v1";
|
const SOURCE_REPO_INDEX_HEADER_V1: &str = "depot-source-index-v1";
|
||||||
const SOURCE_REPO_INDEX_HEADER_V2: &str = "depot-source-index-v2";
|
const SOURCE_REPO_INDEX_HEADER_V2: &str = "depot-source-index-v2";
|
||||||
|
const SOURCE_REPO_INDEX_HEADER_V3: &str = "depot-source-index-v3";
|
||||||
const SOURCE_REPO_INDEX_KIND_PACKAGE: &str = "P";
|
const SOURCE_REPO_INDEX_KIND_PACKAGE: &str = "P";
|
||||||
const SOURCE_REPO_INDEX_KIND_PROVIDES: &str = "V";
|
const SOURCE_REPO_INDEX_KIND_PROVIDES: &str = "V";
|
||||||
const SOURCE_REPO_INDEX_KIND_CONFLICTS: &str = "C";
|
const SOURCE_REPO_INDEX_KIND_CONFLICTS: &str = "C";
|
||||||
|
const SOURCE_REPO_INDEX_KIND_REPLACES: &str = "X";
|
||||||
const SOURCE_REPO_INDEX_KIND_DEP_BUILD: &str = "B";
|
const SOURCE_REPO_INDEX_KIND_DEP_BUILD: &str = "B";
|
||||||
const SOURCE_REPO_INDEX_KIND_DEP_RUNTIME: &str = "R";
|
const SOURCE_REPO_INDEX_KIND_DEP_RUNTIME: &str = "R";
|
||||||
const SOURCE_REPO_INDEX_KIND_DEP_TEST: &str = "T";
|
const SOURCE_REPO_INDEX_KIND_DEP_TEST: &str = "T";
|
||||||
@@ -50,6 +52,8 @@ pub struct PackageIndex {
|
|||||||
by_name: HashMap<String, PathBuf>,
|
by_name: HashMap<String, PathBuf>,
|
||||||
/// Provided name -> spec paths (can be multiple)
|
/// Provided name -> spec paths (can be multiple)
|
||||||
by_provides: HashMap<String, Vec<PathBuf>>,
|
by_provides: HashMap<String, Vec<PathBuf>>,
|
||||||
|
/// Replaced package name -> replacement spec paths (can be multiple)
|
||||||
|
by_replaces: HashMap<String, Vec<PathBuf>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Source package search result from `PackageIndex`.
|
/// Source package search result from `PackageIndex`.
|
||||||
@@ -58,6 +62,7 @@ pub struct SourceSearchHit {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
|
pub replaces: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -66,6 +71,7 @@ struct IndexedSpecRows {
|
|||||||
rel: String,
|
rel: String,
|
||||||
provides: Vec<String>,
|
provides: Vec<String>,
|
||||||
conflicts: Vec<String>,
|
conflicts: Vec<String>,
|
||||||
|
replaces: Vec<String>,
|
||||||
deps: Vec<(String, String)>,
|
deps: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,10 +83,11 @@ pub fn source_repo_index_path(repo_root: &Path) -> PathBuf {
|
|||||||
/// Create/update the source-repo package index at `repo_root`.
|
/// Create/update the source-repo package index at `repo_root`.
|
||||||
///
|
///
|
||||||
/// The file format is line-based TSV with deterministic ordering:
|
/// The file format is line-based TSV with deterministic ordering:
|
||||||
/// - Header: `depot-source-index-v2`
|
/// - Header: `depot-source-index-v3`
|
||||||
/// - Package name row: `P<TAB><name><TAB><relative-spec-path>`
|
/// - Package name row: `P<TAB><name><TAB><relative-spec-path>`
|
||||||
/// - Provides row: `V<TAB><feature><TAB><relative-spec-path>`
|
/// - Provides row: `V<TAB><feature><TAB><relative-spec-path>`
|
||||||
/// - Conflicts row: `C<TAB><name><TAB><relative-spec-path>`
|
/// - Conflicts row: `C<TAB><name><TAB><relative-spec-path>`
|
||||||
|
/// - Replaces row: `X<TAB><name><TAB><relative-spec-path>`
|
||||||
/// - Dependency rows:
|
/// - Dependency rows:
|
||||||
/// - `B<TAB><dep><TAB><relative-spec-path>` for build dependencies
|
/// - `B<TAB><dep><TAB><relative-spec-path>` for build dependencies
|
||||||
/// - `R<TAB><dep><TAB><relative-spec-path>` for runtime dependencies
|
/// - `R<TAB><dep><TAB><relative-spec-path>` for runtime dependencies
|
||||||
@@ -140,6 +147,16 @@ pub fn create_source_repo_index(
|
|||||||
provides.insert(provide.clone());
|
provides.insert(provide.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut replaces = BTreeSet::new();
|
||||||
|
for alternatives in spec.package_alternatives.values() {
|
||||||
|
for replacement in &alternatives.replaces {
|
||||||
|
replaces.insert(replacement.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for replacement in &spec.alternatives.replaces {
|
||||||
|
replaces.insert(replacement.clone());
|
||||||
|
}
|
||||||
|
|
||||||
let mut deps = BTreeSet::new();
|
let mut deps = BTreeSet::new();
|
||||||
for dep in &spec.dependencies.build {
|
for dep in &spec.dependencies.build {
|
||||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_BUILD.to_string(), dep.clone()));
|
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_BUILD.to_string(), dep.clone()));
|
||||||
@@ -173,6 +190,7 @@ pub fn create_source_repo_index(
|
|||||||
rel,
|
rel,
|
||||||
provides: provides.into_iter().collect(),
|
provides: provides.into_iter().collect(),
|
||||||
conflicts: conflicts.into_iter().collect(),
|
conflicts: conflicts.into_iter().collect(),
|
||||||
|
replaces: replaces.into_iter().collect(),
|
||||||
deps: deps.into_iter().collect(),
|
deps: deps.into_iter().collect(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -191,6 +209,13 @@ pub fn create_source_repo_index(
|
|||||||
spec_row.rel.clone(),
|
spec_row.rel.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
for replacement in &spec_row.replaces {
|
||||||
|
rows.push((
|
||||||
|
SOURCE_REPO_INDEX_KIND_REPLACES.to_string(),
|
||||||
|
replacement.clone(),
|
||||||
|
spec_row.rel.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
for conflict in &spec_row.conflicts {
|
for conflict in &spec_row.conflicts {
|
||||||
rows.push((
|
rows.push((
|
||||||
SOURCE_REPO_INDEX_KIND_CONFLICTS.to_string(),
|
SOURCE_REPO_INDEX_KIND_CONFLICTS.to_string(),
|
||||||
@@ -209,7 +234,7 @@ pub fn create_source_repo_index(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(SOURCE_REPO_INDEX_HEADER_V2);
|
out.push_str(SOURCE_REPO_INDEX_HEADER_V3);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
for (kind, name, rel) in &rows {
|
for (kind, name, rel) in &rows {
|
||||||
if name.contains('\n') || name.contains('\r') || name.contains('\t') {
|
if name.contains('\n') || name.contains('\r') || name.contains('\t') {
|
||||||
@@ -359,7 +384,10 @@ impl PackageIndex {
|
|||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing source index header"))?;
|
.ok_or_else(|| anyhow::anyhow!("Missing source index header"))?;
|
||||||
let header = header.trim();
|
let header = header.trim();
|
||||||
if header != SOURCE_REPO_INDEX_HEADER_V1 && header != SOURCE_REPO_INDEX_HEADER_V2 {
|
if header != SOURCE_REPO_INDEX_HEADER_V1
|
||||||
|
&& header != SOURCE_REPO_INDEX_HEADER_V2
|
||||||
|
&& header != SOURCE_REPO_INDEX_HEADER_V3
|
||||||
|
{
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"Unsupported source index header '{}' in {}",
|
"Unsupported source index header '{}' in {}",
|
||||||
header,
|
header,
|
||||||
@@ -396,6 +424,12 @@ impl PackageIndex {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(path);
|
.push(path);
|
||||||
}
|
}
|
||||||
|
SOURCE_REPO_INDEX_KIND_REPLACES => {
|
||||||
|
self.by_replaces
|
||||||
|
.entry(name.to_string())
|
||||||
|
.or_default()
|
||||||
|
.push(path);
|
||||||
|
}
|
||||||
SOURCE_REPO_INDEX_KIND_CONFLICTS
|
SOURCE_REPO_INDEX_KIND_CONFLICTS
|
||||||
| SOURCE_REPO_INDEX_KIND_DEP_BUILD
|
| SOURCE_REPO_INDEX_KIND_DEP_BUILD
|
||||||
| SOURCE_REPO_INDEX_KIND_DEP_RUNTIME
|
| SOURCE_REPO_INDEX_KIND_DEP_RUNTIME
|
||||||
@@ -452,10 +486,30 @@ impl PackageIndex {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(path.clone());
|
.push(path.clone());
|
||||||
}
|
}
|
||||||
|
for replacement in &spec.alternatives.replaces {
|
||||||
|
self.by_replaces
|
||||||
|
.entry(replacement.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(path.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a spec by package name or provides
|
/// Find a spec by package name or provides
|
||||||
pub fn find(&self, name: &str) -> Option<PathBuf> {
|
pub fn find(&self, name: &str) -> Option<PathBuf> {
|
||||||
|
if let Some(paths) = self.by_replaces.get(name) {
|
||||||
|
if paths.len() > 1 {
|
||||||
|
crate::log_warn!(
|
||||||
|
"Multiple packages replace '{}': {:?}",
|
||||||
|
name,
|
||||||
|
paths
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return paths.first().cloned();
|
||||||
|
}
|
||||||
|
|
||||||
// First try by name
|
// First try by name
|
||||||
if let Some(path) = self.by_name.get(name) {
|
if let Some(path) = self.by_name.get(name) {
|
||||||
return Some(path.clone());
|
return Some(path.clone());
|
||||||
@@ -479,6 +533,11 @@ impl PackageIndex {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return all source specs that replace the requested package name.
|
||||||
|
pub fn find_replacements(&self, name: &str) -> Vec<PathBuf> {
|
||||||
|
self.by_replaces.get(name).cloned().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Return all source specs that provide the requested feature/package name.
|
/// Return all source specs that provide the requested feature/package name.
|
||||||
pub fn find_providers(&self, name: &str) -> Vec<PathBuf> {
|
pub fn find_providers(&self, name: &str) -> Vec<PathBuf> {
|
||||||
self.by_provides.get(name).cloned().unwrap_or_default()
|
self.by_provides.get(name).cloned().unwrap_or_default()
|
||||||
@@ -496,17 +555,29 @@ impl PackageIndex {
|
|||||||
.push(provided.clone());
|
.push(provided.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let mut replaces_by_path: HashMap<PathBuf, Vec<String>> = HashMap::new();
|
||||||
|
for (replacement, paths) in &self.by_replaces {
|
||||||
|
for path in paths {
|
||||||
|
replaces_by_path
|
||||||
|
.entry(path.clone())
|
||||||
|
.or_default()
|
||||||
|
.push(replacement.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
for (name, path) in &self.by_name {
|
for (name, path) in &self.by_name {
|
||||||
let provides = provides_by_path.remove(path).unwrap_or_default();
|
let provides = provides_by_path.remove(path).unwrap_or_default();
|
||||||
|
let replaces = replaces_by_path.remove(path).unwrap_or_default();
|
||||||
let name_match = name.to_ascii_lowercase().contains(&q);
|
let name_match = name.to_ascii_lowercase().contains(&q);
|
||||||
let provides_match = provides.iter().any(|p| p.to_ascii_lowercase().contains(&q));
|
let provides_match = provides.iter().any(|p| p.to_ascii_lowercase().contains(&q));
|
||||||
if name_match || provides_match {
|
let replaces_match = replaces.iter().any(|r| r.to_ascii_lowercase().contains(&q));
|
||||||
|
if name_match || provides_match || replaces_match {
|
||||||
hits.push(SourceSearchHit {
|
hits.push(SourceSearchHit {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
provides,
|
provides,
|
||||||
|
replaces,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -639,7 +710,7 @@ mod tests {
|
|||||||
assert!(stats.index_path.exists());
|
assert!(stats.index_path.exists());
|
||||||
|
|
||||||
let index_text = std::fs::read_to_string(stats.index_path).unwrap();
|
let index_text = std::fs::read_to_string(stats.index_path).unwrap();
|
||||||
assert!(index_text.starts_with("depot-source-index-v2\n"));
|
assert!(index_text.starts_with("depot-source-index-v3\n"));
|
||||||
assert!(index_text.contains("C\tbusybox\tcore/hello/hello.toml\n"));
|
assert!(index_text.contains("C\tbusybox\tcore/hello/hello.toml\n"));
|
||||||
assert!(index_text.contains("R\tglibc\tcore/hello/hello.toml\n"));
|
assert!(index_text.contains("R\tglibc\tcore/hello/hello.toml\n"));
|
||||||
|
|
||||||
@@ -712,4 +783,42 @@ mod tests {
|
|||||||
assert_eq!(providers.len(), 1);
|
assert_eq!(providers.len(), 1);
|
||||||
assert!(providers[0].ends_with(Path::new("core/base/base.toml")));
|
assert!(providers[0].ends_with(Path::new("core/base/base.toml")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replacement_index_entries_take_precedence_over_exact_package_names() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let repo_root = tmp.path().join("depot");
|
||||||
|
write_meta_spec(
|
||||||
|
&repo_root.join("core/busybox/busybox.toml"),
|
||||||
|
"busybox",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
write_meta_spec(
|
||||||
|
&repo_root.join("core/findutils/findutils.toml"),
|
||||||
|
"findutils",
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
let busybox_spec = repo_root.join("core/busybox/busybox.toml");
|
||||||
|
let text = std::fs::read_to_string(&busybox_spec).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
&busybox_spec,
|
||||||
|
format!(
|
||||||
|
"{}\n[alternatives]\nreplaces = [\"findutils\"]\n",
|
||||||
|
text.trim_end()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let index = PackageIndex::build_with_repo_dir(Some(repo_root));
|
||||||
|
let resolved = index.find("findutils").expect("replacement should resolve");
|
||||||
|
assert!(resolved.ends_with(Path::new("core/busybox/busybox.toml")));
|
||||||
|
let replacements = index.find_replacements("findutils");
|
||||||
|
assert_eq!(replacements.len(), 1);
|
||||||
|
assert!(replacements[0].ends_with(Path::new("core/busybox/busybox.toml")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -665,7 +665,11 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
|||||||
"Conflicting package/provide",
|
"Conflicting package/provide",
|
||||||
"Installed package or provided name that must be removed before install",
|
"Installed package or provided name that must be removed before install",
|
||||||
)?,
|
)?,
|
||||||
replaces: Vec::new(),
|
replaces: prompt_repeating_list(
|
||||||
|
"Replaced package",
|
||||||
|
"Package name that should resolve to this package and be replaced during install/update",
|
||||||
|
)?,
|
||||||
|
lib32: None,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Alternatives::default()
|
Alternatives::default()
|
||||||
@@ -848,7 +852,11 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
|||||||
root.insert("source".into(), Value::Array(arr));
|
root.insert("source".into(), Value::Array(arr));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !spec.alternatives.provides.is_empty() || !spec.alternatives.conflicts.is_empty() {
|
if !spec.alternatives.provides.is_empty()
|
||||||
|
|| !spec.alternatives.conflicts.is_empty()
|
||||||
|
|| !spec.alternatives.replaces.is_empty()
|
||||||
|
|| spec.alternatives.lib32.is_some()
|
||||||
|
{
|
||||||
let mut alternatives_tbl = Table::new();
|
let mut alternatives_tbl = Table::new();
|
||||||
if !spec.alternatives.provides.is_empty() {
|
if !spec.alternatives.provides.is_empty() {
|
||||||
alternatives_tbl.insert(
|
alternatives_tbl.insert(
|
||||||
@@ -874,6 +882,60 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if !spec.alternatives.replaces.is_empty() {
|
||||||
|
alternatives_tbl.insert(
|
||||||
|
"replaces".into(),
|
||||||
|
Value::Array(
|
||||||
|
spec.alternatives
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.map(|s| Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(lib32) = &spec.alternatives.lib32 {
|
||||||
|
let mut lib32_tbl = Table::new();
|
||||||
|
if !lib32.provides.is_empty() {
|
||||||
|
lib32_tbl.insert(
|
||||||
|
"provides".into(),
|
||||||
|
Value::Array(
|
||||||
|
lib32
|
||||||
|
.provides
|
||||||
|
.iter()
|
||||||
|
.map(|s| Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !lib32.conflicts.is_empty() {
|
||||||
|
lib32_tbl.insert(
|
||||||
|
"conflicts".into(),
|
||||||
|
Value::Array(
|
||||||
|
lib32
|
||||||
|
.conflicts
|
||||||
|
.iter()
|
||||||
|
.map(|s| Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !lib32.replaces.is_empty() {
|
||||||
|
lib32_tbl.insert(
|
||||||
|
"replaces".into(),
|
||||||
|
Value::Array(
|
||||||
|
lib32
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.map(|s| Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if !lib32_tbl.is_empty() {
|
||||||
|
alternatives_tbl.insert("lib32".into(), Value::Table(lib32_tbl));
|
||||||
|
}
|
||||||
|
}
|
||||||
root.insert("alternatives".into(), Value::Table(alternatives_tbl));
|
root.insert("alternatives".into(), Value::Table(alternatives_tbl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1861,7 +1923,12 @@ mod tests {
|
|||||||
alternatives: Alternatives {
|
alternatives: Alternatives {
|
||||||
provides: vec!["editor".into(), "sh".into()],
|
provides: vec!["editor".into(), "sh".into()],
|
||||||
conflicts: vec!["nano".into(), "busybox-sh".into()],
|
conflicts: vec!["nano".into(), "busybox-sh".into()],
|
||||||
replaces: Vec::new(),
|
replaces: vec!["vi".into()],
|
||||||
|
lib32: Some(crate::package::AlternativeGroup {
|
||||||
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
|
replaces: vec!["lib32-vi".into()],
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
source: vec![Source {
|
source: vec![Source {
|
||||||
@@ -1896,6 +1963,18 @@ mod tests {
|
|||||||
.get("conflicts")
|
.get("conflicts")
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.expect("expected alternatives.conflicts array");
|
.expect("expected alternatives.conflicts array");
|
||||||
|
let replaces = alternatives
|
||||||
|
.get("replaces")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("expected alternatives.replaces array");
|
||||||
|
let lib32 = alternatives
|
||||||
|
.get("lib32")
|
||||||
|
.and_then(|v| v.as_table())
|
||||||
|
.expect("expected alternatives.lib32 table");
|
||||||
|
let lib32_replaces = lib32
|
||||||
|
.get("replaces")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("expected alternatives.lib32.replaces array");
|
||||||
|
|
||||||
assert_eq!(provides.len(), 2);
|
assert_eq!(provides.len(), 2);
|
||||||
assert_eq!(provides[0].as_str(), Some("editor"));
|
assert_eq!(provides[0].as_str(), Some("editor"));
|
||||||
@@ -1903,6 +1982,10 @@ mod tests {
|
|||||||
assert_eq!(conflicts.len(), 2);
|
assert_eq!(conflicts.len(), 2);
|
||||||
assert_eq!(conflicts[0].as_str(), Some("nano"));
|
assert_eq!(conflicts[0].as_str(), Some("nano"));
|
||||||
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
|
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
|
||||||
|
assert_eq!(replaces.len(), 1);
|
||||||
|
assert_eq!(replaces[0].as_str(), Some("vi"));
|
||||||
|
assert_eq!(lib32_replaces.len(), 1);
|
||||||
|
assert_eq!(lib32_replaces[0].as_str(), Some("lib32-vi"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -192,6 +192,17 @@ impl Packager {
|
|||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
map.insert(
|
||||||
|
"replaces".to_string(),
|
||||||
|
toml::Value::Array(
|
||||||
|
self.spec
|
||||||
|
.alternatives
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.map(|s| toml::Value::String(s.clone()))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Add install-relevant dependency kinds for repo/runtime consumers.
|
// Add install-relevant dependency kinds for repo/runtime consumers.
|
||||||
let mut deps = toml::map::Map::new();
|
let mut deps = toml::map::Map::new();
|
||||||
@@ -434,6 +445,12 @@ mod tests {
|
|||||||
assert_eq!(val.get("version").and_then(|v| v.as_str()), Some("1.0"));
|
assert_eq!(val.get("version").and_then(|v| v.as_str()), Some("1.0"));
|
||||||
assert_eq!(val.get("revision").and_then(|v| v.as_integer()), Some(1));
|
assert_eq!(val.get("revision").and_then(|v| v.as_integer()), Some(1));
|
||||||
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
assert_eq!(val.get("license").and_then(|v| v.as_str()), Some("MIT"));
|
||||||
|
assert!(
|
||||||
|
val.get("replaces")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("replaces should be an array")
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
crate::metadata_time::parse_completed_at_value(&val).is_some(),
|
crate::metadata_time::parse_completed_at_value(&val).is_some(),
|
||||||
"expected RFC3339 UTC completed_at"
|
"expected RFC3339 UTC completed_at"
|
||||||
@@ -508,6 +525,27 @@ mod tests {
|
|||||||
assert_eq!(keep[1].as_str(), Some("etc/passwd"));
|
assert_eq!(keep[1].as_str(), Some("etc/passwd"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generate_metadata_toml_includes_replaces() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let dest = tmp.path();
|
||||||
|
|
||||||
|
let mut packager = mk_packager(dest.to_path_buf());
|
||||||
|
packager.spec.alternatives.replaces = vec!["findutils".into(), "diffutils".into()];
|
||||||
|
packager.generate_metadata_toml().unwrap();
|
||||||
|
|
||||||
|
let meta_path = dest.join(".metadata.toml");
|
||||||
|
let content = fs::read_to_string(meta_path).unwrap();
|
||||||
|
let val: toml::Value = toml::from_str(&content).unwrap();
|
||||||
|
let replaces = val
|
||||||
|
.get("replaces")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("replaces should be an array");
|
||||||
|
assert_eq!(replaces.len(), 2);
|
||||||
|
assert_eq!(replaces[0].as_str(), Some("findutils"));
|
||||||
|
assert_eq!(replaces[1].as_str(), Some("diffutils"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_create_package_skips_purged_payload_paths() {
|
fn test_create_package_skips_purged_payload_paths() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
+107
-4
@@ -318,6 +318,18 @@ impl PackageSpec {
|
|||||||
///
|
///
|
||||||
/// If no per-output override exists, returns the top-level alternatives.
|
/// If no per-output override exists, returns the top-level alternatives.
|
||||||
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
|
pub fn alternatives_for_output(&self, pkg_name: &str) -> Alternatives {
|
||||||
|
if pkg_name == self.lib32_package_name() {
|
||||||
|
return self
|
||||||
|
.package_alternatives
|
||||||
|
.get(pkg_name)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.alternatives
|
||||||
|
.lib32_alternatives()
|
||||||
|
.unwrap_or_else(|| self.alternatives.primary_alternatives())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if self.docs_parent_output_name(pkg_name).is_some() {
|
if self.docs_parent_output_name(pkg_name).is_some() {
|
||||||
return self
|
return self
|
||||||
.package_alternatives
|
.package_alternatives
|
||||||
@@ -2001,6 +2013,60 @@ provides = ["binutils"]
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_lib32_alternatives_override() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let path = tmp.path().join("pkg.toml");
|
||||||
|
|
||||||
|
std::fs::write(
|
||||||
|
&path,
|
||||||
|
r#"
|
||||||
|
[package]
|
||||||
|
name = "llvm"
|
||||||
|
version = "1.0"
|
||||||
|
description = "d"
|
||||||
|
homepage = "h"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[source]
|
||||||
|
url = "https://example.com/llvm.tar.gz"
|
||||||
|
sha256 = "skip"
|
||||||
|
extract_dir = "llvm"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
type = "custom"
|
||||||
|
|
||||||
|
[alternatives]
|
||||||
|
provides = ["toolchain"]
|
||||||
|
replaces = ["clang"]
|
||||||
|
|
||||||
|
[alternatives.lib32]
|
||||||
|
provides = ["lib32-toolchain"]
|
||||||
|
conflicts = ["lib32-gcc"]
|
||||||
|
replaces = ["lib32-clang"]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let spec = PackageSpec::from_file(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("llvm").replaces,
|
||||||
|
vec!["clang".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("lib32-llvm").provides,
|
||||||
|
vec!["lib32-toolchain".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("lib32-llvm").conflicts,
|
||||||
|
vec!["lib32-gcc".to_string()]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("lib32-llvm").replaces,
|
||||||
|
vec!["lib32-clang".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_python_build_type() {
|
fn parse_python_build_type() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
@@ -3648,6 +3714,9 @@ impl fmt::Display for PackageSpec {
|
|||||||
if !self.alternatives.conflicts.is_empty() {
|
if !self.alternatives.conflicts.is_empty() {
|
||||||
writeln!(f, "Conflicts: {}", self.alternatives.conflicts.join(", "))?;
|
writeln!(f, "Conflicts: {}", self.alternatives.conflicts.join(", "))?;
|
||||||
}
|
}
|
||||||
|
if !self.alternatives.replaces.is_empty() {
|
||||||
|
writeln!(f, "Replaces: {}", self.alternatives.replaces.join(", "))?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3724,17 +3793,51 @@ impl PackageSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Package alternatives such as virtual provides and install conflicts.
|
/// Nested alternatives override group used for output-specific variants such as `lib32-*`.
|
||||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct AlternativeGroup {
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub provides: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub conflicts: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub replaces: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Package alternatives such as virtual provides, install conflicts, and replacements.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Alternatives {
|
pub struct Alternatives {
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub conflicts: Vec<String>,
|
pub conflicts: Vec<String>,
|
||||||
/// Reserved for future package replacement feature
|
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
#[allow(dead_code)]
|
|
||||||
pub replaces: Vec<String>,
|
pub replaces: Vec<String>,
|
||||||
|
/// Optional alternatives override used only for the generated `lib32-*` companion package.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub lib32: Option<AlternativeGroup>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Alternatives {
|
||||||
|
/// Return the top-level alternatives set without any nested output-specific overrides.
|
||||||
|
pub fn primary_alternatives(&self) -> Alternatives {
|
||||||
|
Alternatives {
|
||||||
|
provides: self.provides.clone(),
|
||||||
|
conflicts: self.conflicts.clone(),
|
||||||
|
replaces: self.replaces.clone(),
|
||||||
|
lib32: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the optional lib32-specific alternatives override set.
|
||||||
|
pub fn lib32_alternatives(&self) -> Option<Alternatives> {
|
||||||
|
self.lib32.as_ref().map(|group| Alternatives {
|
||||||
|
provides: group.provides.clone(),
|
||||||
|
conflicts: group.conflicts.clone(),
|
||||||
|
replaces: group.replaces.clone(),
|
||||||
|
lib32: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Source tarball information
|
/// Source tarball information
|
||||||
|
|||||||
+91
-40
@@ -122,6 +122,7 @@ enum CandidateKind {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
enum MatchKind {
|
enum MatchKind {
|
||||||
|
Replaces,
|
||||||
Exact,
|
Exact,
|
||||||
Provides,
|
Provides,
|
||||||
}
|
}
|
||||||
@@ -145,6 +146,7 @@ struct LocalSpecHit {
|
|||||||
spec_name: String,
|
spec_name: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
provides: Vec<String>,
|
provides: Vec<String>,
|
||||||
|
replaces: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Resolver<'a> {
|
struct Resolver<'a> {
|
||||||
@@ -419,6 +421,55 @@ impl<'a> Resolver<'a> {
|
|||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let mut seen = HashSet::<String>::new();
|
let mut seen = HashSet::<String>::new();
|
||||||
|
|
||||||
|
// Local sibling fallback (e.g. ../foo/*.toml when building from a local tree).
|
||||||
|
// Prefer these candidates before probing configured repos so local development
|
||||||
|
// remains deterministic and does not block on external repository I/O.
|
||||||
|
let local_sibling_root = requester_spec_path
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.and_then(|p| p.parent())
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.or_else(|| self.opts.local_sibling_root.clone());
|
||||||
|
if let Some(root) = local_sibling_root {
|
||||||
|
for hit in self.local_sibling_hits(&root)? {
|
||||||
|
let exact = hit.spec_name.eq_ignore_ascii_case(dep_name);
|
||||||
|
let replaces = hit
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.eq_ignore_ascii_case(dep_name));
|
||||||
|
let provides = hit
|
||||||
|
.provides
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.eq_ignore_ascii_case(dep_name));
|
||||||
|
if !(exact || provides || replaces) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let key = format!("src:{}", hit.path.display());
|
||||||
|
if !seen.insert(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(Candidate {
|
||||||
|
package: hit.spec_name.clone(),
|
||||||
|
kind: CandidateKind::Source {
|
||||||
|
path: hit.path.clone(),
|
||||||
|
local_sibling: true,
|
||||||
|
},
|
||||||
|
match_kind: if replaces {
|
||||||
|
MatchKind::Replaces
|
||||||
|
} else if exact {
|
||||||
|
MatchKind::Exact
|
||||||
|
} else {
|
||||||
|
MatchKind::Provides
|
||||||
|
},
|
||||||
|
sort_repo_priority: -10,
|
||||||
|
sort_label: "source:local-sibling".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if !out.is_empty() {
|
||||||
|
return Ok(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Binary repos
|
// Binary repos
|
||||||
let host_arch = std::env::consts::ARCH;
|
let host_arch = std::env::consts::ARCH;
|
||||||
let mut binary_repos: Vec<_> = self
|
let mut binary_repos: Vec<_> = self
|
||||||
@@ -441,6 +492,12 @@ impl<'a> Resolver<'a> {
|
|||||||
for rec in records {
|
for rec in records {
|
||||||
let match_kind = if rec.name.eq_ignore_ascii_case(dep_name) {
|
let match_kind = if rec.name.eq_ignore_ascii_case(dep_name) {
|
||||||
MatchKind::Exact
|
MatchKind::Exact
|
||||||
|
} else if rec
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.any(|replacement| replacement.eq_ignore_ascii_case(dep_name))
|
||||||
|
{
|
||||||
|
MatchKind::Replaces
|
||||||
} else {
|
} else {
|
||||||
MatchKind::Provides
|
MatchKind::Provides
|
||||||
};
|
};
|
||||||
@@ -467,7 +524,14 @@ impl<'a> Resolver<'a> {
|
|||||||
// Global source index
|
// Global source index
|
||||||
if let Some(path) = self.pkg_index.find(dep_name) {
|
if let Some(path) = self.pkg_index.find(dep_name) {
|
||||||
let spec = self.load_spec(&path)?;
|
let spec = self.load_spec(&path)?;
|
||||||
let match_kind = if spec.package.name.eq_ignore_ascii_case(dep_name) {
|
let match_kind = if spec
|
||||||
|
.alternatives
|
||||||
|
.replaces
|
||||||
|
.iter()
|
||||||
|
.any(|replacement| replacement.eq_ignore_ascii_case(dep_name))
|
||||||
|
{
|
||||||
|
MatchKind::Replaces
|
||||||
|
} else if spec.package.name.eq_ignore_ascii_case(dep_name) {
|
||||||
MatchKind::Exact
|
MatchKind::Exact
|
||||||
} else {
|
} else {
|
||||||
MatchKind::Provides
|
MatchKind::Provides
|
||||||
@@ -488,6 +552,25 @@ impl<'a> Resolver<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Additional source providers from index (for provider prompt)
|
||||||
|
for path in self.pkg_index.find_replacements(dep_name) {
|
||||||
|
let spec = self.load_spec(&path)?;
|
||||||
|
let key = format!("src:{}", path.display());
|
||||||
|
if !seen.insert(key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(Candidate {
|
||||||
|
package: spec.package.name.clone(),
|
||||||
|
kind: CandidateKind::Source {
|
||||||
|
path: path.clone(),
|
||||||
|
local_sibling: false,
|
||||||
|
},
|
||||||
|
match_kind: MatchKind::Replaces,
|
||||||
|
sort_repo_priority: 0,
|
||||||
|
sort_label: format!("source:{}", source_label_for_path(self.config, &path)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Additional source providers from index (for provider prompt)
|
// Additional source providers from index (for provider prompt)
|
||||||
for path in self.pkg_index.find_providers(dep_name) {
|
for path in self.pkg_index.find_providers(dep_name) {
|
||||||
let spec = self.load_spec(&path)?;
|
let spec = self.load_spec(&path)?;
|
||||||
@@ -511,43 +594,6 @@ impl<'a> Resolver<'a> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local sibling fallback (e.g. ../foo/*.toml when building from a local tree)
|
|
||||||
let local_sibling_root = requester_spec_path
|
|
||||||
.and_then(|p| p.parent())
|
|
||||||
.and_then(|p| p.parent())
|
|
||||||
.map(Path::to_path_buf)
|
|
||||||
.or_else(|| self.opts.local_sibling_root.clone());
|
|
||||||
if let Some(root) = local_sibling_root {
|
|
||||||
for hit in self.local_sibling_hits(&root)? {
|
|
||||||
let exact = hit.spec_name.eq_ignore_ascii_case(dep_name);
|
|
||||||
let provides = hit
|
|
||||||
.provides
|
|
||||||
.iter()
|
|
||||||
.any(|p| p.eq_ignore_ascii_case(dep_name));
|
|
||||||
if !(exact || provides) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let key = format!("src:{}", hit.path.display());
|
|
||||||
if !seen.insert(key) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
out.push(Candidate {
|
|
||||||
package: hit.spec_name.clone(),
|
|
||||||
kind: CandidateKind::Source {
|
|
||||||
path: hit.path.clone(),
|
|
||||||
local_sibling: true,
|
|
||||||
},
|
|
||||||
match_kind: if exact {
|
|
||||||
MatchKind::Exact
|
|
||||||
} else {
|
|
||||||
MatchKind::Provides
|
|
||||||
},
|
|
||||||
sort_repo_priority: -10,
|
|
||||||
sort_label: "source:local-sibling".to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,6 +630,7 @@ impl<'a> Resolver<'a> {
|
|||||||
spec_name: spec.package.name.clone(),
|
spec_name: spec.package.name.clone(),
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
provides: spec.alternatives.provides.clone(),
|
provides: spec.alternatives.provides.clone(),
|
||||||
|
replaces: spec.alternatives.replaces.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,6 +738,7 @@ fn source_label_for_path(config: &Config, path: &Path) -> String {
|
|||||||
|
|
||||||
fn format_candidate_label(c: &Candidate) -> String {
|
fn format_candidate_label(c: &Candidate) -> String {
|
||||||
let match_label = match c.match_kind {
|
let match_label = match c.match_kind {
|
||||||
|
MatchKind::Replaces => "replaces",
|
||||||
MatchKind::Exact => "exact",
|
MatchKind::Exact => "exact",
|
||||||
MatchKind::Provides => "provides",
|
MatchKind::Provides => "provides",
|
||||||
};
|
};
|
||||||
@@ -750,8 +798,9 @@ fn candidate_sort_key(c: &Candidate, prefer_binary: bool) -> (i32, i32, i32, Str
|
|||||||
(false, true) => 1,
|
(false, true) => 1,
|
||||||
};
|
};
|
||||||
let match_rank = match c.match_kind {
|
let match_rank = match c.match_kind {
|
||||||
MatchKind::Exact => 0,
|
MatchKind::Replaces => 0,
|
||||||
MatchKind::Provides => 1,
|
MatchKind::Exact => 1,
|
||||||
|
MatchKind::Provides => 2,
|
||||||
};
|
};
|
||||||
(
|
(
|
||||||
kind_rank,
|
kind_rank,
|
||||||
@@ -852,6 +901,7 @@ mod tests {
|
|||||||
provides: vec!["libfoo".into()],
|
provides: vec!["libfoo".into()],
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
|
lib32: None,
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
package_dependencies: BTreeMap::from([(
|
package_dependencies: BTreeMap::from([(
|
||||||
@@ -897,6 +947,7 @@ mod tests {
|
|||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
conflicts: Vec::new(),
|
conflicts: Vec::new(),
|
||||||
|
replaces: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user