feat: add support for package conflicts in alternatives
- Introduced a new `conflicts` field in the `Alternatives` struct to track conflicting packages. - Updated database schema to include a `conflicts` table and related queries. - Enhanced package specification handling to include conflicts in TOML serialization and deserialization. - Modified the interactive package creation to allow input for conflicting packages. - Updated source repository index creation to handle conflicts and added relevant statistics. - Adjusted tests to cover new conflicts functionality in package alternatives.
This commit is contained in:
Generated
+1
-1
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
|
||||
|
||||
[[package]]
|
||||
name = "depot"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "depot"
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
edition = "2024"
|
||||
|
||||
[lints.rust]
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
project(
|
||||
'depot',
|
||||
version: '0.14.0',
|
||||
version: '0.15.0',
|
||||
meson_version: '>=0.60.0',
|
||||
)
|
||||
|
||||
|
||||
+487
-43
@@ -7,7 +7,7 @@ use anyhow::{Context, Result};
|
||||
use git2::Direction;
|
||||
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -258,6 +258,13 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
if let Some(conflicts) = metadata.get("conflicts").and_then(|v| v.as_array()) {
|
||||
spec.alternatives.conflicts = conflicts
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
|
||||
spec
|
||||
}
|
||||
@@ -297,6 +304,7 @@ fn package_spec_from_repo_record(
|
||||
packages: Vec::new(),
|
||||
alternatives: package::Alternatives {
|
||||
provides: record.provides.clone(),
|
||||
conflicts: record.conflicts.clone(),
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
manual_sources: Vec::new(),
|
||||
@@ -728,6 +736,265 @@ struct PlannedPackageInstall {
|
||||
staged: PlannedStagedInstall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InstallConflictSubject {
|
||||
package: String,
|
||||
provides: Vec<String>,
|
||||
conflicts: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct InstalledConflictPackage {
|
||||
name: String,
|
||||
provides: Vec<String>,
|
||||
}
|
||||
|
||||
fn install_conflict_subjects_for_output_spec(
|
||||
spec: &package::PackageSpec,
|
||||
) -> Vec<InstallConflictSubject> {
|
||||
spec.outputs()
|
||||
.into_iter()
|
||||
.map(|output| {
|
||||
let alternatives = spec.alternatives_for_output(&output.name);
|
||||
InstallConflictSubject {
|
||||
package: output.name,
|
||||
provides: alternatives.provides,
|
||||
conflicts: alternatives.conflicts,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn install_conflict_subjects_for_spec(
|
||||
spec: &package::PackageSpec,
|
||||
include_primary: bool,
|
||||
include_lib32: bool,
|
||||
) -> Vec<InstallConflictSubject> {
|
||||
let mut subjects = Vec::new();
|
||||
if include_primary {
|
||||
subjects.extend(install_conflict_subjects_for_output_spec(spec));
|
||||
}
|
||||
if include_lib32 {
|
||||
subjects.extend(install_conflict_subjects_for_output_spec(
|
||||
&make_lib32_package_spec(spec),
|
||||
));
|
||||
}
|
||||
subjects
|
||||
}
|
||||
|
||||
fn install_conflict_subject_for_binary_record(
|
||||
record: &db::repo::BinaryRepoPackageRecord,
|
||||
) -> InstallConflictSubject {
|
||||
InstallConflictSubject {
|
||||
package: record.name.clone(),
|
||||
provides: record.provides.clone(),
|
||||
conflicts: record.conflicts.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn matching_conflict_names(
|
||||
conflicts: &[String],
|
||||
package_name: &str,
|
||||
provides: &[String],
|
||||
) -> Vec<String> {
|
||||
let mut matches = Vec::new();
|
||||
for conflict in conflicts {
|
||||
if conflict == package_name || provides.iter().any(|provided| provided == conflict) {
|
||||
matches.push(conflict.clone());
|
||||
}
|
||||
}
|
||||
matches.sort();
|
||||
matches.dedup();
|
||||
matches
|
||||
}
|
||||
|
||||
fn validate_no_transaction_conflicts(subjects: &[InstallConflictSubject]) -> Result<()> {
|
||||
let mut violations = BTreeSet::new();
|
||||
for (idx, left) in subjects.iter().enumerate() {
|
||||
for right in subjects.iter().skip(idx + 1) {
|
||||
let left_hits =
|
||||
matching_conflict_names(&left.conflicts, &right.package, &right.provides);
|
||||
if !left_hits.is_empty() {
|
||||
violations.insert(format!(
|
||||
"{} conflicts with {} via {}",
|
||||
left.package,
|
||||
right.package,
|
||||
left_hits.join(", ")
|
||||
));
|
||||
}
|
||||
let right_hits =
|
||||
matching_conflict_names(&right.conflicts, &left.package, &left.provides);
|
||||
if !right_hits.is_empty() {
|
||||
violations.insert(format!(
|
||||
"{} conflicts with {} via {}",
|
||||
right.package,
|
||||
left.package,
|
||||
right_hits.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if violations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut message =
|
||||
String::from("Cannot install conflicting packages in the same transaction:\n");
|
||||
for violation in violations {
|
||||
message.push_str(" ");
|
||||
message.push_str(&violation);
|
||||
message.push('\n');
|
||||
}
|
||||
anyhow::bail!(message.trim_end().to_string());
|
||||
}
|
||||
|
||||
fn collect_installed_conflict_packages(db_path: &Path) -> Result<Vec<InstalledConflictPackage>> {
|
||||
let mut installed = Vec::new();
|
||||
for record in db::list_installed_package_records(db_path)? {
|
||||
installed.push(InstalledConflictPackage {
|
||||
provides: db::get_package_provides(db_path, &record.name)?,
|
||||
name: record.name,
|
||||
});
|
||||
}
|
||||
Ok(installed)
|
||||
}
|
||||
|
||||
fn collect_conflicting_installed_packages(
|
||||
subjects: &[InstallConflictSubject],
|
||||
installed: &[InstalledConflictPackage],
|
||||
) -> Result<BTreeMap<String, BTreeSet<String>>> {
|
||||
validate_no_transaction_conflicts(subjects)?;
|
||||
let planned_packages: HashSet<_> = subjects
|
||||
.iter()
|
||||
.map(|subject| subject.package.clone())
|
||||
.collect();
|
||||
let mut removals: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||
|
||||
for subject in subjects {
|
||||
for installed_pkg in installed {
|
||||
if installed_pkg.name == subject.package {
|
||||
continue;
|
||||
}
|
||||
let matched = matching_conflict_names(
|
||||
&subject.conflicts,
|
||||
&installed_pkg.name,
|
||||
&installed_pkg.provides,
|
||||
);
|
||||
if matched.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if planned_packages.contains(&installed_pkg.name) {
|
||||
anyhow::bail!(
|
||||
"Cannot install conflicting packages in the same transaction: {} conflicts with {}",
|
||||
subject.package,
|
||||
installed_pkg.name
|
||||
);
|
||||
}
|
||||
removals
|
||||
.entry(installed_pkg.name.clone())
|
||||
.or_default()
|
||||
.insert(subject.package.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(removals)
|
||||
}
|
||||
|
||||
fn remove_installed_package_with_hooks(
|
||||
package: &str,
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
) -> Result<()> {
|
||||
let db_path = config.installed_db_path(rootfs);
|
||||
let affected_paths = db::get_package_files(&db_path, package)?;
|
||||
install::hooks::run_transaction_hooks(
|
||||
rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Pre,
|
||||
operation: install::hooks::HookOperation::Remove,
|
||||
package,
|
||||
affected_paths: &affected_paths,
|
||||
},
|
||||
)?;
|
||||
let script_dir = install::scripts::installed_scripts_dir(rootfs, package);
|
||||
let _ = install::scripts::run_hook_if_present(
|
||||
&script_dir,
|
||||
install::scripts::Hook::PreRemove,
|
||||
rootfs,
|
||||
package,
|
||||
)?;
|
||||
db::remove_package(&db_path, package, rootfs)?;
|
||||
let post_remove = install::scripts::run_hook_if_present(
|
||||
&script_dir,
|
||||
install::scripts::Hook::PostRemove,
|
||||
rootfs,
|
||||
package,
|
||||
);
|
||||
let cleanup_scripts = install::scripts::remove_installed_scripts(rootfs, package);
|
||||
post_remove?;
|
||||
cleanup_scripts?;
|
||||
install::hooks::run_transaction_hooks(
|
||||
rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Post,
|
||||
operation: install::hooks::HookOperation::Remove,
|
||||
package,
|
||||
affected_paths: &affected_paths,
|
||||
},
|
||||
)?;
|
||||
ui::success(format!("Successfully removed {}", package));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_installed_conflicts_for_subjects(
|
||||
subjects: &[InstallConflictSubject],
|
||||
rootfs: &Path,
|
||||
config: &config::Config,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
if subjects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db_path = config.installed_db_path(rootfs);
|
||||
let installed = collect_installed_conflict_packages(&db_path)?;
|
||||
let removals = collect_conflicting_installed_packages(subjects, &installed)?;
|
||||
if removals.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let prompt_entries: Vec<String> = removals
|
||||
.iter()
|
||||
.map(|(package, conflicted_by)| {
|
||||
format!(
|
||||
"{} (conflicts with {})",
|
||||
package,
|
||||
conflicted_by.iter().cloned().collect::<Vec<_>>().join(", ")
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if dry_run {
|
||||
ui::info(format!(
|
||||
"Dry run: would remove conflicting installed package(s): {}",
|
||||
prompt_entries.join(", ")
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if !ui::prompt_package_action("conflict removal", &prompt_entries, true)? {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
for package in removals.keys() {
|
||||
ui::info(format!("Removing conflicting package: {}", package));
|
||||
remove_installed_package_with_hooks(package, rootfs, config)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn plan_staged_install(
|
||||
pkg_spec: &package::PackageSpec,
|
||||
destdir: &Path,
|
||||
@@ -1288,6 +1555,7 @@ struct UpdateCandidate {
|
||||
candidate_completed_at: Option<i64>,
|
||||
runtime_dependencies: Vec<String>,
|
||||
provides: Vec<String>,
|
||||
conflicts: Vec<String>,
|
||||
repo_priority: i32,
|
||||
origin: UpdateOrigin,
|
||||
}
|
||||
@@ -1648,6 +1916,7 @@ fn select_update_candidate(
|
||||
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(),
|
||||
@@ -1676,6 +1945,7 @@ fn select_update_candidate(
|
||||
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(),
|
||||
@@ -1840,6 +2110,16 @@ fn run_update_command(
|
||||
})
|
||||
.collect();
|
||||
|
||||
let conflict_subjects: Vec<_> = updates
|
||||
.iter()
|
||||
.map(|candidate| InstallConflictSubject {
|
||||
package: candidate.package.clone(),
|
||||
provides: candidate.provides.clone(),
|
||||
conflicts: candidate.conflicts.clone(),
|
||||
})
|
||||
.collect();
|
||||
validate_no_transaction_conflicts(&conflict_subjects)?;
|
||||
|
||||
ui::info(format!("{} package(s) can be updated:", updates.len()));
|
||||
for target in &targets {
|
||||
ui::info(format!(" {}", target));
|
||||
@@ -1862,6 +2142,13 @@ fn run_update_command(
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
resolve_installed_conflicts_for_subjects(
|
||||
&conflict_subjects,
|
||||
options.rootfs,
|
||||
config,
|
||||
options.dry_run,
|
||||
)?;
|
||||
|
||||
if !missing_deps.is_empty() {
|
||||
let dep_plan = planner::build_dependency_install_plan(
|
||||
config,
|
||||
@@ -2405,6 +2692,27 @@ fn execute_install_plan_with_child_commands(
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
|
||||
let mut conflict_subjects = Vec::new();
|
||||
for step in &actionable_steps {
|
||||
match &step.origin {
|
||||
planner::PlanOrigin::Source { path, .. } => {
|
||||
let mut spec = package::PackageSpec::from_file(path)
|
||||
.with_context(|| format!("Failed to parse spec {}", path.display()))?;
|
||||
spec.apply_config(config);
|
||||
conflict_subjects.extend(install_conflict_subjects_for_spec(
|
||||
&spec,
|
||||
true,
|
||||
spec.build.flags.build_32,
|
||||
));
|
||||
}
|
||||
planner::PlanOrigin::Binary { record, .. } => {
|
||||
conflict_subjects.push(install_conflict_subject_for_binary_record(record));
|
||||
}
|
||||
planner::PlanOrigin::Installed => {}
|
||||
}
|
||||
}
|
||||
resolve_installed_conflicts_for_subjects(&conflict_subjects, rootfs, config, options.dry_run)?;
|
||||
|
||||
if options.dry_run {
|
||||
ui::info("Dry run enabled, no install/build actions executed.");
|
||||
return Ok(());
|
||||
@@ -2753,6 +3061,17 @@ fn run_direct_archive_install_requests(
|
||||
staged_dirs.push(staging_dir);
|
||||
}
|
||||
|
||||
let mut conflict_subjects = Vec::new();
|
||||
for pkg_spec in &pkg_specs {
|
||||
conflict_subjects.extend(install_conflict_subjects_for_spec(pkg_spec, true, false));
|
||||
}
|
||||
resolve_installed_conflicts_for_subjects(
|
||||
&conflict_subjects,
|
||||
options.rootfs,
|
||||
config,
|
||||
options.dry_run,
|
||||
)?;
|
||||
|
||||
if options.dry_run {
|
||||
ui::info("Dry run enabled, stopping before install/build work.");
|
||||
return Ok(false);
|
||||
@@ -2892,6 +3211,21 @@ fn run_direct_install_request(
|
||||
));
|
||||
}
|
||||
|
||||
let mut conflict_subjects = install_conflict_subjects_for_spec(
|
||||
&pkg_spec,
|
||||
!options.lib32_only,
|
||||
staging_dir.is_none() && (options.lib32_only || pkg_spec.build.flags.build_32),
|
||||
);
|
||||
if staging_dir.is_some() {
|
||||
conflict_subjects = install_conflict_subjects_for_spec(&pkg_spec, true, false);
|
||||
}
|
||||
resolve_installed_conflicts_for_subjects(
|
||||
&conflict_subjects,
|
||||
options.rootfs,
|
||||
config,
|
||||
options.dry_run,
|
||||
)?;
|
||||
|
||||
if options.dry_run {
|
||||
ui::info("Dry run enabled, stopping before install/build work.");
|
||||
return Ok(false);
|
||||
@@ -3222,48 +3556,11 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
let remove_lock_path = locking::lock_path(&config);
|
||||
let _remove_lock_guard =
|
||||
locking::try_write(&mut remove_lock, &remove_lock_path, "remove")?;
|
||||
let db_path = config.db_dir.join("packages.db");
|
||||
let removal_targets = vec![package.clone()];
|
||||
if !ui::prompt_package_action("removal", &removal_targets, true)? {
|
||||
anyhow::bail!("Aborted");
|
||||
}
|
||||
let affected_paths = db::get_package_files(&db_path, &package)?;
|
||||
install::hooks::run_transaction_hooks(
|
||||
&cli.rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Pre,
|
||||
operation: install::hooks::HookOperation::Remove,
|
||||
package: &package,
|
||||
affected_paths: &affected_paths,
|
||||
},
|
||||
)?;
|
||||
let script_dir = install::scripts::installed_scripts_dir(&cli.rootfs, &package);
|
||||
let _ = install::scripts::run_hook_if_present(
|
||||
&script_dir,
|
||||
install::scripts::Hook::PreRemove,
|
||||
&cli.rootfs,
|
||||
&package,
|
||||
)?;
|
||||
db::remove_package(&db_path, &package, &cli.rootfs)?;
|
||||
let post_remove = install::scripts::run_hook_if_present(
|
||||
&script_dir,
|
||||
install::scripts::Hook::PostRemove,
|
||||
&cli.rootfs,
|
||||
&package,
|
||||
);
|
||||
let cleanup_scripts = install::scripts::remove_installed_scripts(&cli.rootfs, &package);
|
||||
post_remove?;
|
||||
cleanup_scripts?;
|
||||
install::hooks::run_transaction_hooks(
|
||||
&cli.rootfs,
|
||||
&install::hooks::HookExecutionContext {
|
||||
phase: install::hooks::HookPhase::Post,
|
||||
operation: install::hooks::HookOperation::Remove,
|
||||
package: &package,
|
||||
affected_paths: &affected_paths,
|
||||
},
|
||||
)?;
|
||||
ui::success(format!("Successfully removed {}", package));
|
||||
remove_installed_package_with_hooks(&package, &cli.rootfs, &config)?;
|
||||
}
|
||||
Commands::Build {
|
||||
spec_pos,
|
||||
@@ -3736,11 +4033,13 @@ pub fn run(cli: Cli) -> Result<()> {
|
||||
stats.index_path.display()
|
||||
));
|
||||
ui::info(format!(
|
||||
"Indexed {} spec(s) from {} TOML file(s): package rows={} provides rows={} ignored_toml={}",
|
||||
"Indexed {} spec(s) from {} TOML file(s): package rows={} provides rows={} conflicts rows={} dependency rows={} ignored_toml={}",
|
||||
stats.specs_indexed,
|
||||
stats.toml_files_scanned,
|
||||
stats.package_rows,
|
||||
stats.provides_rows,
|
||||
stats.conflicts_rows,
|
||||
stats.dependency_rows,
|
||||
stats.ignored_toml_files
|
||||
));
|
||||
}
|
||||
@@ -4173,6 +4472,7 @@ mod tests {
|
||||
homepage: Some("https://example.test".into()),
|
||||
license: Some("MIT".into()),
|
||||
provides: vec!["pkg-virtual".into()],
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: vec!["glibc".into()],
|
||||
optional_dependencies: vec!["manpages".into()],
|
||||
};
|
||||
@@ -4228,6 +4528,7 @@ mod tests {
|
||||
fn write_archive(
|
||||
archive_path: &Path,
|
||||
package_name: &str,
|
||||
conflicts: &[&str],
|
||||
payload_path: &str,
|
||||
payload: &[u8],
|
||||
) -> Result<()> {
|
||||
@@ -4244,8 +4545,20 @@ mod tests {
|
||||
payload_header.set_cksum();
|
||||
tar.append(&payload_header, payload)?;
|
||||
|
||||
let conflicts_toml = if conflicts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
"conflicts = [{}]\n",
|
||||
conflicts
|
||||
.iter()
|
||||
.map(|conflict| format!("\"{conflict}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let metadata = format!(
|
||||
"name = \"{package_name}\"\nversion = \"1.0\"\nrevision = 1\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n\n[dependencies]\nruntime = []\noptional = []\n"
|
||||
"name = \"{package_name}\"\nversion = \"1.0\"\nrevision = 1\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n{conflicts_toml}\n[dependencies]\nruntime = []\noptional = []\n"
|
||||
);
|
||||
let mut meta_header = tar::Header::new_gnu();
|
||||
meta_header.set_path(".metadata.toml")?;
|
||||
@@ -4263,8 +4576,8 @@ mod tests {
|
||||
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
|
||||
let archive_a = pkg_dir.path().join("alpha-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||
let archive_b = pkg_dir.path().join("beta-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||
write_archive(&archive_a, "alpha", "usr/bin/alpha", b"alpha")?;
|
||||
write_archive(&archive_b, "beta", "usr/bin/beta", b"beta")?;
|
||||
write_archive(&archive_a, "alpha", &[], "usr/bin/alpha", b"alpha")?;
|
||||
write_archive(&archive_b, "beta", &[], "usr/bin/beta", b"beta")?;
|
||||
|
||||
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||
@@ -4301,6 +4614,110 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_archive_install_rejects_conflicting_archives_in_same_batch() -> Result<()> {
|
||||
fn write_archive(
|
||||
archive_path: &Path,
|
||||
package_name: &str,
|
||||
conflicts: &[&str],
|
||||
) -> Result<()> {
|
||||
let file = fs::File::create(archive_path)
|
||||
.with_context(|| format!("Failed to create {}", archive_path.display()))?;
|
||||
let encoder = zstd::stream::write::Encoder::new(file, 3)
|
||||
.context("Failed to create zstd encoder")?;
|
||||
let mut tar = tar::Builder::new(encoder);
|
||||
|
||||
let payload = package_name.as_bytes();
|
||||
let mut payload_header = tar::Header::new_gnu();
|
||||
payload_header.set_path(format!("usr/bin/{package_name}"))?;
|
||||
payload_header.set_size(payload.len() as u64);
|
||||
payload_header.set_mode(0o755);
|
||||
payload_header.set_cksum();
|
||||
tar.append(&payload_header, payload)?;
|
||||
|
||||
let conflicts_toml = if conflicts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(
|
||||
"conflicts = [{}]\n",
|
||||
conflicts
|
||||
.iter()
|
||||
.map(|conflict| format!("\"{conflict}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
};
|
||||
let metadata = format!(
|
||||
"name = \"{package_name}\"\nversion = \"1.0\"\nrevision = 1\ndescription = \"test\"\nhomepage = \"https://example.test\"\nlicense = \"MIT\"\n{conflicts_toml}\n[dependencies]\nruntime = []\noptional = []\n"
|
||||
);
|
||||
let mut meta_header = tar::Header::new_gnu();
|
||||
meta_header.set_path(".metadata.toml")?;
|
||||
meta_header.set_size(metadata.len() as u64);
|
||||
meta_header.set_mode(0o644);
|
||||
meta_header.set_cksum();
|
||||
tar.append(&meta_header, metadata.as_bytes())?;
|
||||
|
||||
let encoder = tar.into_inner()?;
|
||||
encoder.finish()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?;
|
||||
let pkg_dir = tempfile::tempdir().context("Failed to create temp package dir")?;
|
||||
let archive_a = pkg_dir.path().join("alpha-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||
let archive_b = pkg_dir.path().join("beta-1.0-1-x86_64.depot.pkg.tar.zst");
|
||||
write_archive(&archive_a, "alpha", &["beta"])?;
|
||||
write_archive(&archive_b, "beta", &[])?;
|
||||
|
||||
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||
cfg.db_dir = rootfs.path().join("var/lib/depot");
|
||||
|
||||
let err = run_direct_archive_install_requests(
|
||||
DirectInstallOptions {
|
||||
rootfs: rootfs.path(),
|
||||
no_deps: true,
|
||||
no_flags: false,
|
||||
cross_prefix: None,
|
||||
clean: false,
|
||||
dry_run: false,
|
||||
lib32_only: false,
|
||||
install_test_deps: false,
|
||||
},
|
||||
&cfg,
|
||||
&[archive_a, archive_b],
|
||||
false,
|
||||
)
|
||||
.expect_err("conflicting archives should be rejected");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("Cannot install conflicting packages in the same transaction")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_conflicting_installed_packages_matches_by_name_and_provide() -> Result<()> {
|
||||
let removals = collect_conflicting_installed_packages(
|
||||
&[InstallConflictSubject {
|
||||
package: "beta".into(),
|
||||
provides: Vec::new(),
|
||||
conflicts: vec!["alpha".into(), "editor".into()],
|
||||
}],
|
||||
&[InstalledConflictPackage {
|
||||
name: "alpha".into(),
|
||||
provides: vec!["editor".into()],
|
||||
}],
|
||||
)?;
|
||||
|
||||
assert_eq!(
|
||||
removals.get("alpha"),
|
||||
Some(&BTreeSet::from(["beta".to_string()]))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn binary_archive_install_preserves_setuid_permissions() -> Result<()> {
|
||||
@@ -4354,6 +4771,7 @@ mod tests {
|
||||
homepage: Some("https://example.test".into()),
|
||||
license: Some("ISC".into()),
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
};
|
||||
@@ -4567,6 +4985,7 @@ optional = []
|
||||
homepage: None,
|
||||
license: None,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
},
|
||||
@@ -4741,6 +5160,7 @@ optional = []
|
||||
candidate_completed_at: None,
|
||||
runtime_dependencies: vec!["glibc".into(), "helper-virtual".into()],
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
repo_priority: 0,
|
||||
origin: UpdateOrigin::Source {
|
||||
repo_name: "source".into(),
|
||||
@@ -4757,6 +5177,7 @@ optional = []
|
||||
candidate_completed_at: None,
|
||||
runtime_dependencies: Vec::new(),
|
||||
provides: vec!["helper-virtual".into()],
|
||||
conflicts: Vec::new(),
|
||||
repo_priority: 0,
|
||||
origin: UpdateOrigin::Source {
|
||||
repo_name: "source".into(),
|
||||
@@ -4773,6 +5194,7 @@ optional = []
|
||||
candidate_completed_at: None,
|
||||
runtime_dependencies: vec!["newdep".into()],
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
repo_priority: 0,
|
||||
origin: UpdateOrigin::Source {
|
||||
repo_name: "source".into(),
|
||||
@@ -4787,6 +5209,28 @@ optional = []
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_no_transaction_conflicts_rejects_conflicting_updates() {
|
||||
let err = validate_no_transaction_conflicts(&[
|
||||
InstallConflictSubject {
|
||||
package: "alpha".into(),
|
||||
provides: Vec::new(),
|
||||
conflicts: vec!["beta".into()],
|
||||
},
|
||||
InstallConflictSubject {
|
||||
package: "beta".into(),
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
},
|
||||
])
|
||||
.expect_err("conflicting update set should be rejected");
|
||||
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("Cannot install conflicting packages in the same transaction")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compare_versions_for_updates_handles_semver_and_date_versions() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -565,6 +565,25 @@ pub fn list_installed_package_records(db_path: &Path) -> Result<Vec<InstalledPac
|
||||
Ok(rows.filter_map(|row| row.ok()).collect())
|
||||
}
|
||||
|
||||
/// Return the alternatives provided by an installed package.
|
||||
pub fn get_package_provides(db_path: &Path, name: &str) -> Result<Vec<String>> {
|
||||
if !db_path.exists() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
init_db(&conn)?;
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT pr.provides_name
|
||||
FROM provides pr
|
||||
JOIN packages p ON p.id = pr.package_id
|
||||
WHERE p.name = ?1
|
||||
ORDER BY pr.provides_name",
|
||||
)?;
|
||||
let rows = stmt.query_map(params![name], |row| row.get(0))?;
|
||||
Ok(rows.filter_map(|row| row.ok()).collect())
|
||||
}
|
||||
|
||||
/// Get set of all provided package names (alternatives)
|
||||
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
||||
use std::collections::HashSet;
|
||||
@@ -653,6 +672,7 @@ mod tests {
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives {
|
||||
provides: vec![format!("{}-virtual", name)],
|
||||
conflicts: Vec::new(),
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
manual_sources: Vec::new(),
|
||||
|
||||
@@ -83,6 +83,7 @@ struct IndexedPackage {
|
||||
sha256: String,
|
||||
sha512: String,
|
||||
provides: Vec<String>,
|
||||
conflicts: Vec<String>,
|
||||
runtime_dependencies: Vec<String>,
|
||||
optional_dependencies: Vec<String>,
|
||||
archive_files: Vec<String>,
|
||||
@@ -118,6 +119,7 @@ pub struct BinaryRepoPackageRecord {
|
||||
pub homepage: Option<String>,
|
||||
pub license: Option<String>,
|
||||
pub provides: Vec<String>,
|
||||
pub conflicts: Vec<String>,
|
||||
pub runtime_dependencies: Vec<String>,
|
||||
pub optional_dependencies: Vec<String>,
|
||||
}
|
||||
@@ -296,6 +298,11 @@ impl RepoManager {
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||
);
|
||||
CREATE TABLE conflicts (
|
||||
package_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||
);
|
||||
CREATE TABLE dependencies (
|
||||
package_id INTEGER,
|
||||
kind TEXT NOT NULL,
|
||||
@@ -316,6 +323,7 @@ impl RepoManager {
|
||||
conn.execute_batch(
|
||||
"CREATE INDEX idx_packages_name ON packages(name);
|
||||
CREATE INDEX idx_provides_name ON provides(name);
|
||||
CREATE INDEX idx_conflicts_name ON conflicts(name);
|
||||
CREATE INDEX idx_dependencies_name ON dependencies(name);
|
||||
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
|
||||
CREATE INDEX idx_repo_files_path ON files(path);",
|
||||
@@ -344,6 +352,7 @@ impl RepoManager {
|
||||
let mut homepage = None;
|
||||
let mut license = None;
|
||||
let mut provides = Vec::new();
|
||||
let mut conflicts = Vec::new();
|
||||
let mut runtime_dependencies = Vec::new();
|
||||
let mut optional_dependencies = Vec::new();
|
||||
let mut archive_files = Vec::new();
|
||||
@@ -397,6 +406,15 @@ impl RepoManager {
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
if let Some(conflicts_arr) =
|
||||
metadata.get("conflicts").and_then(|v| v.as_array())
|
||||
{
|
||||
conflicts = conflicts_arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
}
|
||||
if let Some(runtime_arr) = metadata
|
||||
.get("dependencies")
|
||||
.and_then(|v| v.get("runtime"))
|
||||
@@ -460,6 +478,7 @@ impl RepoManager {
|
||||
sha256,
|
||||
sha512,
|
||||
provides,
|
||||
conflicts,
|
||||
runtime_dependencies,
|
||||
optional_dependencies,
|
||||
archive_files,
|
||||
@@ -480,6 +499,7 @@ impl RepoManager {
|
||||
sha256,
|
||||
sha512,
|
||||
provides,
|
||||
conflicts,
|
||||
runtime_dependencies,
|
||||
optional_dependencies,
|
||||
archive_files,
|
||||
@@ -513,6 +533,12 @@ impl RepoManager {
|
||||
params![package_id, provide],
|
||||
)?;
|
||||
}
|
||||
for conflict in conflicts {
|
||||
conn.execute(
|
||||
"INSERT INTO conflicts (package_id, name) VALUES (?1, ?2)",
|
||||
params![package_id, conflict],
|
||||
)?;
|
||||
}
|
||||
|
||||
for dep in runtime_dependencies {
|
||||
conn.execute(
|
||||
@@ -1461,6 +1487,27 @@ fn query_package_provides(conn: &Connection, package_id: i64) -> Result<Vec<Stri
|
||||
Ok(rows.filter_map(|r| r.ok()).collect())
|
||||
}
|
||||
|
||||
fn query_package_conflicts(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
||||
let has_conflicts_table: bool = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='conflicts'",
|
||||
[],
|
||||
|r| {
|
||||
let n: i64 = r.get(0)?;
|
||||
Ok(n > 0)
|
||||
},
|
||||
)
|
||||
.unwrap_or(false);
|
||||
if !has_conflicts_table {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut stmt =
|
||||
conn.prepare("SELECT name FROM conflicts 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>> {
|
||||
let has_dependencies_table: bool = conn
|
||||
.query_row(
|
||||
@@ -1563,6 +1610,7 @@ fn find_cached_binary_repo_packages(
|
||||
homepage: row.get(10)?,
|
||||
license: row.get(11)?,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
},
|
||||
@@ -1573,6 +1621,7 @@ fn find_cached_binary_repo_packages(
|
||||
for row in rows {
|
||||
let (package_id, mut rec) = row?;
|
||||
rec.provides = query_package_provides(&conn, package_id)?;
|
||||
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||
out.push(rec);
|
||||
@@ -1629,6 +1678,7 @@ fn list_cached_binary_repo_packages(
|
||||
homepage: row.get(10)?,
|
||||
license: row.get(11)?,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
},
|
||||
@@ -1639,6 +1689,7 @@ fn list_cached_binary_repo_packages(
|
||||
for row in rows {
|
||||
let (package_id, mut rec) = row?;
|
||||
rec.provides = query_package_provides(&conn, package_id)?;
|
||||
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||
out.push(rec);
|
||||
@@ -2463,6 +2514,7 @@ license = ["MIT", "Apache-2.0"]
|
||||
homepage: None,
|
||||
license: None,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
};
|
||||
@@ -2498,6 +2550,7 @@ license = ["MIT", "Apache-2.0"]
|
||||
homepage: None,
|
||||
license: None,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
}
|
||||
|
||||
@@ -495,6 +495,7 @@ mod tests {
|
||||
"foo-libs".to_string(),
|
||||
crate::package::Alternatives {
|
||||
provides: vec!["libfoo".into()],
|
||||
conflicts: Vec::new(),
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
|
||||
+188
-20
@@ -4,7 +4,7 @@
|
||||
|
||||
use crate::package::PackageSpec;
|
||||
use anyhow::{Context, Result};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
@@ -12,9 +12,15 @@ use walkdir::WalkDir;
|
||||
|
||||
/// Filename for the source-repo index written at a repo root.
|
||||
pub const SOURCE_REPO_INDEX_FILENAME: &str = "depot-index.tsv";
|
||||
const SOURCE_REPO_INDEX_HEADER: &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_KIND_PACKAGE: &str = "P";
|
||||
const SOURCE_REPO_INDEX_KIND_PROVIDES: &str = "V";
|
||||
const SOURCE_REPO_INDEX_KIND_CONFLICTS: &str = "C";
|
||||
const SOURCE_REPO_INDEX_KIND_DEP_BUILD: &str = "B";
|
||||
const SOURCE_REPO_INDEX_KIND_DEP_RUNTIME: &str = "R";
|
||||
const SOURCE_REPO_INDEX_KIND_DEP_TEST: &str = "T";
|
||||
const SOURCE_REPO_INDEX_KIND_DEP_OPTIONAL: &str = "O";
|
||||
|
||||
/// Statistics for generating a source repository index file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -29,6 +35,10 @@ pub struct SourceRepoIndexStats {
|
||||
pub package_rows: usize,
|
||||
/// Number of provides rows written to the index.
|
||||
pub provides_rows: usize,
|
||||
/// Number of conflict rows written to the index.
|
||||
pub conflicts_rows: usize,
|
||||
/// Number of dependency rows written to the index.
|
||||
pub dependency_rows: usize,
|
||||
/// Number of discovered TOML files ignored because they were not package specs.
|
||||
pub ignored_toml_files: usize,
|
||||
}
|
||||
@@ -50,6 +60,15 @@ pub struct SourceSearchHit {
|
||||
pub provides: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct IndexedSpecRows {
|
||||
name: String,
|
||||
rel: String,
|
||||
provides: Vec<String>,
|
||||
conflicts: Vec<String>,
|
||||
deps: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// Return the source index file path for `repo_root`.
|
||||
pub fn source_repo_index_path(repo_root: &Path) -> PathBuf {
|
||||
repo_root.join(SOURCE_REPO_INDEX_FILENAME)
|
||||
@@ -58,9 +77,15 @@ pub fn source_repo_index_path(repo_root: &Path) -> PathBuf {
|
||||
/// Create/update the source-repo package index at `repo_root`.
|
||||
///
|
||||
/// The file format is line-based TSV with deterministic ordering:
|
||||
/// - Header: `depot-source-index-v1`
|
||||
/// - Header: `depot-source-index-v2`
|
||||
/// - Package name row: `P<TAB><name><TAB><relative-spec-path>`
|
||||
/// - Provides row: `V<TAB><feature><TAB><relative-spec-path>`
|
||||
/// - Conflicts row: `C<TAB><name><TAB><relative-spec-path>`
|
||||
/// - Dependency rows:
|
||||
/// - `B<TAB><dep><TAB><relative-spec-path>` for build dependencies
|
||||
/// - `R<TAB><dep><TAB><relative-spec-path>` for runtime dependencies
|
||||
/// - `T<TAB><dep><TAB><relative-spec-path>` for test dependencies
|
||||
/// - `O<TAB><dep><TAB><relative-spec-path>` for optional dependencies
|
||||
pub fn create_source_repo_index(
|
||||
repo_root: &Path,
|
||||
subdirs: &[String],
|
||||
@@ -73,7 +98,7 @@ pub fn create_source_repo_index(
|
||||
}
|
||||
|
||||
let scan_roots = resolve_scan_roots(&repo_root, subdirs)?;
|
||||
let mut spec_rows: Vec<(String, String, Vec<String>)> = Vec::new();
|
||||
let mut spec_rows: Vec<IndexedSpecRows> = Vec::new();
|
||||
let mut toml_files_scanned = 0usize;
|
||||
let mut ignored_toml_files = 0usize;
|
||||
|
||||
@@ -95,23 +120,87 @@ pub fn create_source_repo_index(
|
||||
)
|
||||
})?;
|
||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
||||
spec_rows.push((spec.package.name.clone(), rel, spec.alternatives.provides));
|
||||
let mut conflicts = BTreeSet::new();
|
||||
for alternatives in spec.package_alternatives.values() {
|
||||
for conflict in &alternatives.conflicts {
|
||||
conflicts.insert(conflict.clone());
|
||||
}
|
||||
}
|
||||
for conflict in &spec.alternatives.conflicts {
|
||||
conflicts.insert(conflict.clone());
|
||||
}
|
||||
|
||||
let mut provides = BTreeSet::new();
|
||||
for alternatives in spec.package_alternatives.values() {
|
||||
for provide in &alternatives.provides {
|
||||
provides.insert(provide.clone());
|
||||
}
|
||||
}
|
||||
for provide in &spec.alternatives.provides {
|
||||
provides.insert(provide.clone());
|
||||
}
|
||||
|
||||
let mut deps = BTreeSet::new();
|
||||
for dep in &spec.dependencies.build {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_BUILD.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &spec.dependencies.runtime {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_RUNTIME.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &spec.dependencies.test {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_TEST.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &spec.dependencies.optional {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_OPTIONAL.to_string(), dep.clone()));
|
||||
}
|
||||
for dep_overrides in spec.package_dependencies.values() {
|
||||
for dep in &dep_overrides.build {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_BUILD.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &dep_overrides.runtime {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_RUNTIME.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &dep_overrides.test {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_TEST.to_string(), dep.clone()));
|
||||
}
|
||||
for dep in &dep_overrides.optional {
|
||||
deps.insert((SOURCE_REPO_INDEX_KIND_DEP_OPTIONAL.to_string(), dep.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
spec_rows.push(IndexedSpecRows {
|
||||
name: spec.package.name.clone(),
|
||||
rel,
|
||||
provides: provides.into_iter().collect(),
|
||||
conflicts: conflicts.into_iter().collect(),
|
||||
deps: deps.into_iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut rows: Vec<(String, String, String)> = Vec::new();
|
||||
for (name, rel, provides) in &spec_rows {
|
||||
for spec_row in &spec_rows {
|
||||
rows.push((
|
||||
SOURCE_REPO_INDEX_KIND_PACKAGE.to_string(),
|
||||
name.clone(),
|
||||
rel.clone(),
|
||||
spec_row.name.clone(),
|
||||
spec_row.rel.clone(),
|
||||
));
|
||||
for provided in provides {
|
||||
for provided in &spec_row.provides {
|
||||
rows.push((
|
||||
SOURCE_REPO_INDEX_KIND_PROVIDES.to_string(),
|
||||
provided.clone(),
|
||||
rel.clone(),
|
||||
spec_row.rel.clone(),
|
||||
));
|
||||
}
|
||||
for conflict in &spec_row.conflicts {
|
||||
rows.push((
|
||||
SOURCE_REPO_INDEX_KIND_CONFLICTS.to_string(),
|
||||
conflict.clone(),
|
||||
spec_row.rel.clone(),
|
||||
));
|
||||
}
|
||||
for (dep_kind, dep_name) in &spec_row.deps {
|
||||
rows.push((dep_kind.clone(), dep_name.clone(), spec_row.rel.clone()));
|
||||
}
|
||||
}
|
||||
rows.sort_by(|a, b| {
|
||||
a.0.cmp(&b.0)
|
||||
@@ -120,7 +209,7 @@ pub fn create_source_repo_index(
|
||||
});
|
||||
|
||||
let mut out = String::new();
|
||||
out.push_str(SOURCE_REPO_INDEX_HEADER);
|
||||
out.push_str(SOURCE_REPO_INDEX_HEADER_V2);
|
||||
out.push('\n');
|
||||
for (kind, name, rel) in &rows {
|
||||
if name.contains('\n') || name.contains('\r') || name.contains('\t') {
|
||||
@@ -159,6 +248,22 @@ pub fn create_source_repo_index(
|
||||
.iter()
|
||||
.filter(|(kind, _, _)| kind == SOURCE_REPO_INDEX_KIND_PROVIDES)
|
||||
.count();
|
||||
let conflicts_rows = rows
|
||||
.iter()
|
||||
.filter(|(kind, _, _)| kind == SOURCE_REPO_INDEX_KIND_CONFLICTS)
|
||||
.count();
|
||||
let dependency_rows = rows
|
||||
.iter()
|
||||
.filter(|(kind, _, _)| {
|
||||
matches!(
|
||||
kind.as_str(),
|
||||
SOURCE_REPO_INDEX_KIND_DEP_BUILD
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_RUNTIME
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_TEST
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_OPTIONAL
|
||||
)
|
||||
})
|
||||
.count();
|
||||
|
||||
Ok(SourceRepoIndexStats {
|
||||
index_path,
|
||||
@@ -166,6 +271,8 @@ pub fn create_source_repo_index(
|
||||
specs_indexed: spec_rows.len(),
|
||||
package_rows,
|
||||
provides_rows,
|
||||
conflicts_rows,
|
||||
dependency_rows,
|
||||
ignored_toml_files,
|
||||
})
|
||||
}
|
||||
@@ -251,7 +358,8 @@ impl PackageIndex {
|
||||
let header = lines
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing source index header"))?;
|
||||
if header.trim() != SOURCE_REPO_INDEX_HEADER {
|
||||
let header = header.trim();
|
||||
if header != SOURCE_REPO_INDEX_HEADER_V1 && header != SOURCE_REPO_INDEX_HEADER_V2 {
|
||||
anyhow::bail!(
|
||||
"Unsupported source index header '{}' in {}",
|
||||
header,
|
||||
@@ -288,6 +396,11 @@ impl PackageIndex {
|
||||
.or_default()
|
||||
.push(path);
|
||||
}
|
||||
SOURCE_REPO_INDEX_KIND_CONFLICTS
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_BUILD
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_RUNTIME
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_TEST
|
||||
| SOURCE_REPO_INDEX_KIND_DEP_OPTIONAL => {}
|
||||
_ => {
|
||||
anyhow::bail!(
|
||||
"Unknown source index row type '{}' on line {} in {}",
|
||||
@@ -461,22 +574,51 @@ fn scan_toml_files(scan_roots: &[PathBuf]) -> Result<Vec<PathBuf>> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn write_meta_spec(path: &Path, name: &str, provides: &[&str]) {
|
||||
fn write_meta_spec(
|
||||
path: &Path,
|
||||
name: &str,
|
||||
provides: &[&str],
|
||||
conflicts: &[&str],
|
||||
runtime_deps: &[&str],
|
||||
) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).unwrap();
|
||||
}
|
||||
let provides_line = if provides.is_empty() {
|
||||
let alternatives_lines = if provides.is_empty() && conflicts.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let quoted = provides
|
||||
let mut section = String::from("[alternatives]\n");
|
||||
if !provides.is_empty() {
|
||||
let quoted = provides
|
||||
.iter()
|
||||
.map(|p| format!("\"{p}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
section.push_str(&format!("provides = [{quoted}]\n"));
|
||||
}
|
||||
if !conflicts.is_empty() {
|
||||
let quoted = conflicts
|
||||
.iter()
|
||||
.map(|c| format!("\"{c}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
section.push_str(&format!("conflicts = [{quoted}]\n"));
|
||||
}
|
||||
section.push('\n');
|
||||
section
|
||||
};
|
||||
let deps_line = if runtime_deps.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
let quoted = runtime_deps
|
||||
.iter()
|
||||
.map(|p| format!("\"{p}\""))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
format!("[alternatives]\nprovides = [{quoted}]\n\n")
|
||||
format!("[dependencies]\nruntime = [{quoted}]\n\n")
|
||||
};
|
||||
let content = format!(
|
||||
"[package]\nname = \"{name}\"\nversion = \"1.0.0\"\ndescription = \"test\"\nhomepage = \"https://example.com\"\nlicense = \"MIT\"\n\n{provides_line}[build]\ntype = \"meta\"\n"
|
||||
"[package]\nname = \"{name}\"\nversion = \"1.0.0\"\ndescription = \"test\"\nhomepage = \"https://example.com\"\nlicense = \"MIT\"\n\n{alternatives_lines}{deps_line}[build]\ntype = \"meta\"\n"
|
||||
);
|
||||
std::fs::write(path, content).unwrap();
|
||||
}
|
||||
@@ -486,14 +628,21 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("depot");
|
||||
let spec_path = repo_root.join("core/hello/hello.toml");
|
||||
write_meta_spec(&spec_path, "hello", &["sh"]);
|
||||
write_meta_spec(&spec_path, "hello", &["sh"], &["busybox"], &["glibc"]);
|
||||
|
||||
let stats = create_source_repo_index(&repo_root, &["core".to_string()]).unwrap();
|
||||
assert_eq!(stats.specs_indexed, 1);
|
||||
assert_eq!(stats.package_rows, 1);
|
||||
assert_eq!(stats.provides_rows, 1);
|
||||
assert_eq!(stats.conflicts_rows, 1);
|
||||
assert_eq!(stats.dependency_rows, 1);
|
||||
assert!(stats.index_path.exists());
|
||||
|
||||
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.contains("C\tbusybox\tcore/hello/hello.toml\n"));
|
||||
assert!(index_text.contains("R\tglibc\tcore/hello/hello.toml\n"));
|
||||
|
||||
let index = PackageIndex::build_with_repo_dir(Some(repo_root.clone()));
|
||||
let hit = index.find("hello").expect("package name should resolve");
|
||||
assert!(hit.ends_with(Path::new("core/hello/hello.toml")));
|
||||
@@ -508,7 +657,7 @@ mod tests {
|
||||
let repo_store = tmp.path().join("repo-store");
|
||||
let repo_root = repo_store.join("vertex");
|
||||
let spec_path = repo_root.join("core/base/base.toml");
|
||||
write_meta_spec(&spec_path, "base", &[]);
|
||||
write_meta_spec(&spec_path, "base", &[], &[], &[]);
|
||||
|
||||
let index = PackageIndex::build_with_repo_dir(Some(repo_store));
|
||||
let hit = index
|
||||
@@ -522,7 +671,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("depot");
|
||||
let spec_path = repo_root.join("core/curl/curl.toml");
|
||||
write_meta_spec(&spec_path, "curl", &["libcurl"]);
|
||||
write_meta_spec(&spec_path, "curl", &["libcurl"], &[], &[]);
|
||||
|
||||
std::fs::create_dir_all(&repo_root).unwrap();
|
||||
std::fs::write(source_repo_index_path(&repo_root), "invalid-header\n").unwrap();
|
||||
@@ -544,4 +693,23 @@ mod tests {
|
||||
.contains("must be a relative path without '..'")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_with_repo_dir_accepts_v1_index_files() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let repo_root = tmp.path().join("depot");
|
||||
std::fs::create_dir_all(repo_root.join("core/base")).unwrap();
|
||||
std::fs::write(
|
||||
source_repo_index_path(&repo_root),
|
||||
"depot-source-index-v1\nP\tbase\tcore/base/base.toml\nV\tsh\tcore/base/base.toml\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let index = PackageIndex::build_with_repo_dir(Some(repo_root.clone()));
|
||||
let hit = index.find("base").expect("package name should resolve");
|
||||
assert!(hit.ends_with(Path::new("core/base/base.toml")));
|
||||
let providers = index.find_providers("sh");
|
||||
assert_eq!(providers.len(), 1);
|
||||
assert!(providers[0].ends_with(Path::new("core/base/base.toml")));
|
||||
}
|
||||
}
|
||||
|
||||
+104
-1
@@ -655,6 +655,21 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
"Optional dependency",
|
||||
"Package that enables optional runtime functionality",
|
||||
)?;
|
||||
let alternatives = if show_advanced {
|
||||
Alternatives {
|
||||
provides: prompt_repeating_list(
|
||||
"Alternative provided name",
|
||||
"Virtual package name(s) this package satisfies, e.g. sh, editor, libfoo",
|
||||
)?,
|
||||
conflicts: prompt_repeating_list(
|
||||
"Conflicting package/provide",
|
||||
"Installed package or provided name that must be removed before install",
|
||||
)?,
|
||||
replaces: Vec::new(),
|
||||
}
|
||||
} else {
|
||||
Alternatives::default()
|
||||
};
|
||||
|
||||
Ok(PackageSpec {
|
||||
package: PackageInfo {
|
||||
@@ -666,7 +681,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
||||
license,
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives::default(),
|
||||
alternatives,
|
||||
manual_sources,
|
||||
source: sources,
|
||||
build: Build { build_type, flags },
|
||||
@@ -818,6 +833,35 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
|
||||
root.insert("source".into(), Value::Array(arr));
|
||||
}
|
||||
|
||||
if !spec.alternatives.provides.is_empty() || !spec.alternatives.conflicts.is_empty() {
|
||||
let mut alternatives_tbl = Table::new();
|
||||
if !spec.alternatives.provides.is_empty() {
|
||||
alternatives_tbl.insert(
|
||||
"provides".into(),
|
||||
Value::Array(
|
||||
spec.alternatives
|
||||
.provides
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if !spec.alternatives.conflicts.is_empty() {
|
||||
alternatives_tbl.insert(
|
||||
"conflicts".into(),
|
||||
Value::Array(
|
||||
spec.alternatives
|
||||
.conflicts
|
||||
.iter()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
root.insert("alternatives".into(), Value::Table(alternatives_tbl));
|
||||
}
|
||||
|
||||
// build (only include set fields)
|
||||
let mut build_tbl = Table::new();
|
||||
let build_type = match spec.build.build_type {
|
||||
@@ -1757,6 +1801,65 @@ mod tests {
|
||||
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_to_minimal_toml_includes_alternatives_conflicts_and_provides() {
|
||||
let spec = PackageSpec {
|
||||
package: PackageInfo {
|
||||
name: "foo".into(),
|
||||
version: "1.0".into(),
|
||||
revision: 1,
|
||||
description: "A test".into(),
|
||||
homepage: "".into(),
|
||||
license: vec!["MIT".into()],
|
||||
},
|
||||
packages: Vec::new(),
|
||||
alternatives: Alternatives {
|
||||
provides: vec!["editor".into(), "sh".into()],
|
||||
conflicts: vec!["nano".into(), "busybox-sh".into()],
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
manual_sources: Vec::new(),
|
||||
source: vec![Source {
|
||||
url: "https://example.com/foo-1.0.tar.gz".into(),
|
||||
sha256: "skip".into(),
|
||||
extract_dir: "foo-1.0".into(),
|
||||
patches: Vec::new(),
|
||||
post_extract: Vec::new(),
|
||||
cherry_pick: Vec::new(),
|
||||
}],
|
||||
build: Build {
|
||||
build_type: BuildType::Autotools,
|
||||
flags: BuildFlags::default(),
|
||||
},
|
||||
dependencies: Dependencies::default(),
|
||||
package_alternatives: Default::default(),
|
||||
package_dependencies: Default::default(),
|
||||
spec_dir: PathBuf::from("."),
|
||||
};
|
||||
|
||||
let toml = spec_to_minimal_toml(&spec).unwrap();
|
||||
let val: toml::Value = toml::from_str(&toml).unwrap();
|
||||
let alternatives = val
|
||||
.get("alternatives")
|
||||
.and_then(|v| v.as_table())
|
||||
.expect("expected alternatives table");
|
||||
let provides = alternatives
|
||||
.get("provides")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("expected alternatives.provides array");
|
||||
let conflicts = alternatives
|
||||
.get("conflicts")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("expected alternatives.conflicts array");
|
||||
|
||||
assert_eq!(provides.len(), 2);
|
||||
assert_eq!(provides[0].as_str(), Some("editor"));
|
||||
assert_eq!(provides[1].as_str(), Some("sh"));
|
||||
assert_eq!(conflicts.len(), 2);
|
||||
assert_eq!(conflicts[0].as_str(), Some("nano"));
|
||||
assert_eq!(conflicts[1].as_str(), Some("busybox-sh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
|
||||
let spec = PackageSpec {
|
||||
|
||||
@@ -171,6 +171,17 @@ impl Packager {
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
map.insert(
|
||||
"conflicts".to_string(),
|
||||
toml::Value::Array(
|
||||
self.spec
|
||||
.alternatives
|
||||
.conflicts
|
||||
.iter()
|
||||
.map(|s| toml::Value::String(s.clone()))
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
// Add install-relevant dependency kinds for repo/runtime consumers.
|
||||
let mut deps = toml::map::Map::new();
|
||||
|
||||
+20
-1
@@ -1557,9 +1557,11 @@ type = "custom"
|
||||
|
||||
[alternatives]
|
||||
provides = ["toolchain"]
|
||||
conflicts = ["gcc"]
|
||||
|
||||
[package_alternatives.clang]
|
||||
provides = ["cc", "c++", "gcc"]
|
||||
conflicts = ["clang-legacy"]
|
||||
|
||||
[package_alternatives.llvm]
|
||||
provides = ["binutils"]
|
||||
@@ -1572,14 +1574,26 @@ provides = ["binutils"]
|
||||
spec.alternatives_for_output("llvm").provides,
|
||||
vec!["binutils".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
spec.alternatives_for_output("llvm").conflicts,
|
||||
Vec::<String>::new()
|
||||
);
|
||||
assert_eq!(
|
||||
spec.alternatives_for_output("clang").provides,
|
||||
vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
spec.alternatives_for_output("clang").conflicts,
|
||||
vec!["clang-legacy".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
spec.alternatives_for_output("other").provides,
|
||||
vec!["toolchain".to_string()]
|
||||
);
|
||||
assert_eq!(
|
||||
spec.alternatives_for_output("other").conflicts,
|
||||
vec!["gcc".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3001,6 +3015,9 @@ impl fmt::Display for PackageSpec {
|
||||
if !self.alternatives.provides.is_empty() {
|
||||
writeln!(f, "Provides: {}", self.alternatives.provides.join(", "))?;
|
||||
}
|
||||
if !self.alternatives.conflicts.is_empty() {
|
||||
writeln!(f, "Conflicts: {}", self.alternatives.conflicts.join(", "))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -3064,11 +3081,13 @@ impl PackageSpec {
|
||||
}
|
||||
}
|
||||
|
||||
/// Package alternatives (provides/replaces)
|
||||
/// Package alternatives such as virtual provides and install conflicts.
|
||||
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||
pub struct Alternatives {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub provides: Vec<String>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub conflicts: Vec<String>,
|
||||
/// Reserved for future package replacement feature
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -814,6 +814,7 @@ mod tests {
|
||||
"foo-libs".into(),
|
||||
Alternatives {
|
||||
provides: vec!["libfoo".into()],
|
||||
conflicts: Vec::new(),
|
||||
replaces: Vec::new(),
|
||||
},
|
||||
)]),
|
||||
@@ -856,6 +857,7 @@ mod tests {
|
||||
homepage: None,
|
||||
license: None,
|
||||
provides: Vec::new(),
|
||||
conflicts: Vec::new(),
|
||||
runtime_dependencies: Vec::new(),
|
||||
optional_dependencies: Vec::new(),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user