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:
2026-03-10 16:16:20 -05:00
parent d3d885f54b
commit dcbe38ea2b
12 changed files with 889 additions and 68 deletions
Generated
+1 -1
View File
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]]
name = "depot"
version = "0.14.0"
version = "0.15.0"
dependencies = [
"anyhow",
"ar",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "depot"
version = "0.14.0"
version = "0.15.0"
edition = "2024"
[lints.rust]
+1 -1
View File
@@ -1,6 +1,6 @@
project(
'depot',
version: '0.14.0',
version: '0.15.0',
meson_version: '>=0.60.0',
)
+487 -43
View File
@@ -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!(
+20
View File
@@ -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(),
+53
View File
@@ -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(),
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+11
View File
@@ -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
View File
@@ -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)]
+2
View File
@@ -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(),
}),