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]]
|
[[package]]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"ar",
|
"ar",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "depot"
|
name = "depot"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
project(
|
project(
|
||||||
'depot',
|
'depot',
|
||||||
version: '0.14.0',
|
version: '0.15.0',
|
||||||
meson_version: '>=0.60.0',
|
meson_version: '>=0.60.0',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+487
-43
@@ -7,7 +7,7 @@ use anyhow::{Context, Result};
|
|||||||
use git2::Direction;
|
use git2::Direction;
|
||||||
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -258,6 +258,13 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.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
|
spec
|
||||||
}
|
}
|
||||||
@@ -297,6 +304,7 @@ fn package_spec_from_repo_record(
|
|||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: package::Alternatives {
|
alternatives: package::Alternatives {
|
||||||
provides: record.provides.clone(),
|
provides: record.provides.clone(),
|
||||||
|
conflicts: record.conflicts.clone(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
},
|
},
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
@@ -728,6 +736,265 @@ struct PlannedPackageInstall {
|
|||||||
staged: PlannedStagedInstall,
|
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(
|
fn plan_staged_install(
|
||||||
pkg_spec: &package::PackageSpec,
|
pkg_spec: &package::PackageSpec,
|
||||||
destdir: &Path,
|
destdir: &Path,
|
||||||
@@ -1288,6 +1555,7 @@ struct UpdateCandidate {
|
|||||||
candidate_completed_at: Option<i64>,
|
candidate_completed_at: Option<i64>,
|
||||||
runtime_dependencies: Vec<String>,
|
runtime_dependencies: Vec<String>,
|
||||||
provides: Vec<String>,
|
provides: Vec<String>,
|
||||||
|
conflicts: Vec<String>,
|
||||||
repo_priority: i32,
|
repo_priority: i32,
|
||||||
origin: UpdateOrigin,
|
origin: UpdateOrigin,
|
||||||
}
|
}
|
||||||
@@ -1648,6 +1916,7 @@ fn select_update_candidate(
|
|||||||
candidate_completed_at: candidate.completed_at,
|
candidate_completed_at: candidate.completed_at,
|
||||||
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
|
runtime_dependencies: candidate.spec.dependencies.runtime.clone(),
|
||||||
provides: candidate.spec.alternatives.provides.clone(),
|
provides: candidate.spec.alternatives.provides.clone(),
|
||||||
|
conflicts: candidate.spec.alternatives.conflicts.clone(),
|
||||||
repo_priority: candidate.repo_priority,
|
repo_priority: candidate.repo_priority,
|
||||||
origin: UpdateOrigin::Source {
|
origin: UpdateOrigin::Source {
|
||||||
repo_name: candidate.repo_name.clone(),
|
repo_name: candidate.repo_name.clone(),
|
||||||
@@ -1676,6 +1945,7 @@ fn select_update_candidate(
|
|||||||
candidate_completed_at: record.completed_at,
|
candidate_completed_at: record.completed_at,
|
||||||
runtime_dependencies: record.runtime_dependencies.clone(),
|
runtime_dependencies: record.runtime_dependencies.clone(),
|
||||||
provides: record.provides.clone(),
|
provides: record.provides.clone(),
|
||||||
|
conflicts: record.conflicts.clone(),
|
||||||
repo_priority: *repo_priority,
|
repo_priority: *repo_priority,
|
||||||
origin: UpdateOrigin::Binary {
|
origin: UpdateOrigin::Binary {
|
||||||
repo_name: record.repo_name.clone(),
|
repo_name: record.repo_name.clone(),
|
||||||
@@ -1840,6 +2110,16 @@ fn run_update_command(
|
|||||||
})
|
})
|
||||||
.collect();
|
.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()));
|
ui::info(format!("{} package(s) can be updated:", updates.len()));
|
||||||
for target in &targets {
|
for target in &targets {
|
||||||
ui::info(format!(" {}", target));
|
ui::info(format!(" {}", target));
|
||||||
@@ -1862,6 +2142,13 @@ fn run_update_command(
|
|||||||
anyhow::bail!("Aborted");
|
anyhow::bail!("Aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve_installed_conflicts_for_subjects(
|
||||||
|
&conflict_subjects,
|
||||||
|
options.rootfs,
|
||||||
|
config,
|
||||||
|
options.dry_run,
|
||||||
|
)?;
|
||||||
|
|
||||||
if !missing_deps.is_empty() {
|
if !missing_deps.is_empty() {
|
||||||
let dep_plan = planner::build_dependency_install_plan(
|
let dep_plan = planner::build_dependency_install_plan(
|
||||||
config,
|
config,
|
||||||
@@ -2405,6 +2692,27 @@ fn execute_install_plan_with_child_commands(
|
|||||||
anyhow::bail!("Aborted");
|
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 {
|
if options.dry_run {
|
||||||
ui::info("Dry run enabled, no install/build actions executed.");
|
ui::info("Dry run enabled, no install/build actions executed.");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -2753,6 +3061,17 @@ fn run_direct_archive_install_requests(
|
|||||||
staged_dirs.push(staging_dir);
|
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 {
|
if options.dry_run {
|
||||||
ui::info("Dry run enabled, stopping before install/build work.");
|
ui::info("Dry run enabled, stopping before install/build work.");
|
||||||
return Ok(false);
|
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 {
|
if options.dry_run {
|
||||||
ui::info("Dry run enabled, stopping before install/build work.");
|
ui::info("Dry run enabled, stopping before install/build work.");
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@@ -3222,48 +3556,11 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
let remove_lock_path = locking::lock_path(&config);
|
let remove_lock_path = locking::lock_path(&config);
|
||||||
let _remove_lock_guard =
|
let _remove_lock_guard =
|
||||||
locking::try_write(&mut remove_lock, &remove_lock_path, "remove")?;
|
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()];
|
let removal_targets = vec![package.clone()];
|
||||||
if !ui::prompt_package_action("removal", &removal_targets, true)? {
|
if !ui::prompt_package_action("removal", &removal_targets, true)? {
|
||||||
anyhow::bail!("Aborted");
|
anyhow::bail!("Aborted");
|
||||||
}
|
}
|
||||||
let affected_paths = db::get_package_files(&db_path, &package)?;
|
remove_installed_package_with_hooks(&package, &cli.rootfs, &config)?;
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
Commands::Build {
|
Commands::Build {
|
||||||
spec_pos,
|
spec_pos,
|
||||||
@@ -3736,11 +4033,13 @@ pub fn run(cli: Cli) -> Result<()> {
|
|||||||
stats.index_path.display()
|
stats.index_path.display()
|
||||||
));
|
));
|
||||||
ui::info(format!(
|
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.specs_indexed,
|
||||||
stats.toml_files_scanned,
|
stats.toml_files_scanned,
|
||||||
stats.package_rows,
|
stats.package_rows,
|
||||||
stats.provides_rows,
|
stats.provides_rows,
|
||||||
|
stats.conflicts_rows,
|
||||||
|
stats.dependency_rows,
|
||||||
stats.ignored_toml_files
|
stats.ignored_toml_files
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -4173,6 +4472,7 @@ mod tests {
|
|||||||
homepage: Some("https://example.test".into()),
|
homepage: Some("https://example.test".into()),
|
||||||
license: Some("MIT".into()),
|
license: Some("MIT".into()),
|
||||||
provides: vec!["pkg-virtual".into()],
|
provides: vec!["pkg-virtual".into()],
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: vec!["glibc".into()],
|
runtime_dependencies: vec!["glibc".into()],
|
||||||
optional_dependencies: vec!["manpages".into()],
|
optional_dependencies: vec!["manpages".into()],
|
||||||
};
|
};
|
||||||
@@ -4228,6 +4528,7 @@ mod tests {
|
|||||||
fn write_archive(
|
fn write_archive(
|
||||||
archive_path: &Path,
|
archive_path: &Path,
|
||||||
package_name: &str,
|
package_name: &str,
|
||||||
|
conflicts: &[&str],
|
||||||
payload_path: &str,
|
payload_path: &str,
|
||||||
payload: &[u8],
|
payload: &[u8],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -4244,8 +4545,20 @@ mod tests {
|
|||||||
payload_header.set_cksum();
|
payload_header.set_cksum();
|
||||||
tar.append(&payload_header, payload)?;
|
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!(
|
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();
|
let mut meta_header = tar::Header::new_gnu();
|
||||||
meta_header.set_path(".metadata.toml")?;
|
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 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_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");
|
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_a, "alpha", &[], "usr/bin/alpha", b"alpha")?;
|
||||||
write_archive(&archive_b, "beta", "usr/bin/beta", b"beta")?;
|
write_archive(&archive_b, "beta", &[], "usr/bin/beta", b"beta")?;
|
||||||
|
|
||||||
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
let mut cfg = config::Config::for_rootfs(rootfs.path());
|
||||||
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
cfg.build_dir = rootfs.path().join("var/cache/depot/build");
|
||||||
@@ -4301,6 +4614,110 @@ mod tests {
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn binary_archive_install_preserves_setuid_permissions() -> Result<()> {
|
fn binary_archive_install_preserves_setuid_permissions() -> Result<()> {
|
||||||
@@ -4354,6 +4771,7 @@ mod tests {
|
|||||||
homepage: Some("https://example.test".into()),
|
homepage: Some("https://example.test".into()),
|
||||||
license: Some("ISC".into()),
|
license: Some("ISC".into()),
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -4567,6 +4985,7 @@ optional = []
|
|||||||
homepage: None,
|
homepage: None,
|
||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -4741,6 +5160,7 @@ optional = []
|
|||||||
candidate_completed_at: None,
|
candidate_completed_at: None,
|
||||||
runtime_dependencies: vec!["glibc".into(), "helper-virtual".into()],
|
runtime_dependencies: vec!["glibc".into(), "helper-virtual".into()],
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
repo_priority: 0,
|
repo_priority: 0,
|
||||||
origin: UpdateOrigin::Source {
|
origin: UpdateOrigin::Source {
|
||||||
repo_name: "source".into(),
|
repo_name: "source".into(),
|
||||||
@@ -4757,6 +5177,7 @@ optional = []
|
|||||||
candidate_completed_at: None,
|
candidate_completed_at: None,
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
provides: vec!["helper-virtual".into()],
|
provides: vec!["helper-virtual".into()],
|
||||||
|
conflicts: Vec::new(),
|
||||||
repo_priority: 0,
|
repo_priority: 0,
|
||||||
origin: UpdateOrigin::Source {
|
origin: UpdateOrigin::Source {
|
||||||
repo_name: "source".into(),
|
repo_name: "source".into(),
|
||||||
@@ -4773,6 +5194,7 @@ optional = []
|
|||||||
candidate_completed_at: None,
|
candidate_completed_at: None,
|
||||||
runtime_dependencies: vec!["newdep".into()],
|
runtime_dependencies: vec!["newdep".into()],
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
repo_priority: 0,
|
repo_priority: 0,
|
||||||
origin: UpdateOrigin::Source {
|
origin: UpdateOrigin::Source {
|
||||||
repo_name: "source".into(),
|
repo_name: "source".into(),
|
||||||
@@ -4787,6 +5209,28 @@ optional = []
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn compare_versions_for_updates_handles_semver_and_date_versions() {
|
fn compare_versions_for_updates_handles_semver_and_date_versions() {
|
||||||
assert_eq!(
|
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())
|
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)
|
/// Get set of all provided package names (alternatives)
|
||||||
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
pub fn get_all_provides(db_path: &Path) -> Result<std::collections::HashSet<String>> {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -653,6 +672,7 @@ mod tests {
|
|||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives {
|
alternatives: Alternatives {
|
||||||
provides: vec![format!("{}-virtual", name)],
|
provides: vec![format!("{}-virtual", name)],
|
||||||
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
},
|
},
|
||||||
manual_sources: Vec::new(),
|
manual_sources: Vec::new(),
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ struct IndexedPackage {
|
|||||||
sha256: String,
|
sha256: String,
|
||||||
sha512: String,
|
sha512: String,
|
||||||
provides: Vec<String>,
|
provides: Vec<String>,
|
||||||
|
conflicts: Vec<String>,
|
||||||
runtime_dependencies: Vec<String>,
|
runtime_dependencies: Vec<String>,
|
||||||
optional_dependencies: Vec<String>,
|
optional_dependencies: Vec<String>,
|
||||||
archive_files: Vec<String>,
|
archive_files: Vec<String>,
|
||||||
@@ -118,6 +119,7 @@ pub struct BinaryRepoPackageRecord {
|
|||||||
pub homepage: Option<String>,
|
pub homepage: Option<String>,
|
||||||
pub license: Option<String>,
|
pub license: Option<String>,
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
|
pub conflicts: Vec<String>,
|
||||||
pub runtime_dependencies: Vec<String>,
|
pub runtime_dependencies: Vec<String>,
|
||||||
pub optional_dependencies: Vec<String>,
|
pub optional_dependencies: Vec<String>,
|
||||||
}
|
}
|
||||||
@@ -296,6 +298,11 @@ impl RepoManager {
|
|||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
FOREIGN KEY(package_id) REFERENCES packages(id)
|
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||||
);
|
);
|
||||||
|
CREATE TABLE conflicts (
|
||||||
|
package_id INTEGER,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
FOREIGN KEY(package_id) REFERENCES packages(id)
|
||||||
|
);
|
||||||
CREATE TABLE dependencies (
|
CREATE TABLE dependencies (
|
||||||
package_id INTEGER,
|
package_id INTEGER,
|
||||||
kind TEXT NOT NULL,
|
kind TEXT NOT NULL,
|
||||||
@@ -316,6 +323,7 @@ impl RepoManager {
|
|||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"CREATE INDEX idx_packages_name ON packages(name);
|
"CREATE INDEX idx_packages_name ON packages(name);
|
||||||
CREATE INDEX idx_provides_name ON provides(name);
|
CREATE INDEX idx_provides_name ON provides(name);
|
||||||
|
CREATE INDEX idx_conflicts_name ON conflicts(name);
|
||||||
CREATE INDEX idx_dependencies_name ON dependencies(name);
|
CREATE INDEX idx_dependencies_name ON dependencies(name);
|
||||||
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
|
CREATE INDEX idx_dependencies_kind ON dependencies(kind);
|
||||||
CREATE INDEX idx_repo_files_path ON files(path);",
|
CREATE INDEX idx_repo_files_path ON files(path);",
|
||||||
@@ -344,6 +352,7 @@ impl RepoManager {
|
|||||||
let mut homepage = None;
|
let mut homepage = None;
|
||||||
let mut license = None;
|
let mut license = None;
|
||||||
let mut provides = Vec::new();
|
let mut provides = Vec::new();
|
||||||
|
let mut conflicts = Vec::new();
|
||||||
let mut runtime_dependencies = Vec::new();
|
let mut runtime_dependencies = Vec::new();
|
||||||
let mut optional_dependencies = Vec::new();
|
let mut optional_dependencies = Vec::new();
|
||||||
let mut archive_files = Vec::new();
|
let mut archive_files = Vec::new();
|
||||||
@@ -397,6 +406,15 @@ impl RepoManager {
|
|||||||
.map(String::from)
|
.map(String::from)
|
||||||
.collect();
|
.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
|
if let Some(runtime_arr) = metadata
|
||||||
.get("dependencies")
|
.get("dependencies")
|
||||||
.and_then(|v| v.get("runtime"))
|
.and_then(|v| v.get("runtime"))
|
||||||
@@ -460,6 +478,7 @@ impl RepoManager {
|
|||||||
sha256,
|
sha256,
|
||||||
sha512,
|
sha512,
|
||||||
provides,
|
provides,
|
||||||
|
conflicts,
|
||||||
runtime_dependencies,
|
runtime_dependencies,
|
||||||
optional_dependencies,
|
optional_dependencies,
|
||||||
archive_files,
|
archive_files,
|
||||||
@@ -480,6 +499,7 @@ impl RepoManager {
|
|||||||
sha256,
|
sha256,
|
||||||
sha512,
|
sha512,
|
||||||
provides,
|
provides,
|
||||||
|
conflicts,
|
||||||
runtime_dependencies,
|
runtime_dependencies,
|
||||||
optional_dependencies,
|
optional_dependencies,
|
||||||
archive_files,
|
archive_files,
|
||||||
@@ -513,6 +533,12 @@ impl RepoManager {
|
|||||||
params![package_id, provide],
|
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 {
|
for dep in runtime_dependencies {
|
||||||
conn.execute(
|
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())
|
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>> {
|
fn query_package_runtime_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
|
||||||
let has_dependencies_table: bool = conn
|
let has_dependencies_table: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
@@ -1563,6 +1610,7 @@ fn find_cached_binary_repo_packages(
|
|||||||
homepage: row.get(10)?,
|
homepage: row.get(10)?,
|
||||||
license: row.get(11)?,
|
license: row.get(11)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -1573,6 +1621,7 @@ fn find_cached_binary_repo_packages(
|
|||||||
for row in rows {
|
for row in rows {
|
||||||
let (package_id, mut rec) = row?;
|
let (package_id, mut rec) = row?;
|
||||||
rec.provides = query_package_provides(&conn, package_id)?;
|
rec.provides = query_package_provides(&conn, package_id)?;
|
||||||
|
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||||
out.push(rec);
|
out.push(rec);
|
||||||
@@ -1629,6 +1678,7 @@ fn list_cached_binary_repo_packages(
|
|||||||
homepage: row.get(10)?,
|
homepage: row.get(10)?,
|
||||||
license: row.get(11)?,
|
license: row.get(11)?,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
},
|
},
|
||||||
@@ -1639,6 +1689,7 @@ fn list_cached_binary_repo_packages(
|
|||||||
for row in rows {
|
for row in rows {
|
||||||
let (package_id, mut rec) = row?;
|
let (package_id, mut rec) = row?;
|
||||||
rec.provides = query_package_provides(&conn, package_id)?;
|
rec.provides = query_package_provides(&conn, package_id)?;
|
||||||
|
rec.conflicts = query_package_conflicts(&conn, package_id)?;
|
||||||
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
|
||||||
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
|
||||||
out.push(rec);
|
out.push(rec);
|
||||||
@@ -2463,6 +2514,7 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
homepage: None,
|
homepage: None,
|
||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -2498,6 +2550,7 @@ license = ["MIT", "Apache-2.0"]
|
|||||||
homepage: None,
|
homepage: None,
|
||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -495,6 +495,7 @@ mod tests {
|
|||||||
"foo-libs".to_string(),
|
"foo-libs".to_string(),
|
||||||
crate::package::Alternatives {
|
crate::package::Alternatives {
|
||||||
provides: vec!["libfoo".into()],
|
provides: vec!["libfoo".into()],
|
||||||
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
|
|||||||
+188
-20
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
use crate::package::PackageSpec;
|
use crate::package::PackageSpec;
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::collections::HashMap;
|
use std::collections::{BTreeSet, HashMap};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
@@ -12,9 +12,15 @@ use walkdir::WalkDir;
|
|||||||
|
|
||||||
/// Filename for the source-repo index written at a repo root.
|
/// Filename for the source-repo index written at a repo root.
|
||||||
pub const SOURCE_REPO_INDEX_FILENAME: &str = "depot-index.tsv";
|
pub const SOURCE_REPO_INDEX_FILENAME: &str = "depot-index.tsv";
|
||||||
const SOURCE_REPO_INDEX_HEADER: &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_PACKAGE: &str = "P";
|
||||||
const SOURCE_REPO_INDEX_KIND_PROVIDES: &str = "V";
|
const SOURCE_REPO_INDEX_KIND_PROVIDES: &str = "V";
|
||||||
|
const SOURCE_REPO_INDEX_KIND_CONFLICTS: &str = "C";
|
||||||
|
const SOURCE_REPO_INDEX_KIND_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.
|
/// Statistics for generating a source repository index file.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -29,6 +35,10 @@ pub struct SourceRepoIndexStats {
|
|||||||
pub package_rows: usize,
|
pub package_rows: usize,
|
||||||
/// Number of provides rows written to the index.
|
/// Number of provides rows written to the index.
|
||||||
pub provides_rows: usize,
|
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.
|
/// Number of discovered TOML files ignored because they were not package specs.
|
||||||
pub ignored_toml_files: usize,
|
pub ignored_toml_files: usize,
|
||||||
}
|
}
|
||||||
@@ -50,6 +60,15 @@ pub struct SourceSearchHit {
|
|||||||
pub provides: Vec<String>,
|
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`.
|
/// Return the source index file path for `repo_root`.
|
||||||
pub fn source_repo_index_path(repo_root: &Path) -> PathBuf {
|
pub fn source_repo_index_path(repo_root: &Path) -> PathBuf {
|
||||||
repo_root.join(SOURCE_REPO_INDEX_FILENAME)
|
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`.
|
/// Create/update the source-repo package index at `repo_root`.
|
||||||
///
|
///
|
||||||
/// The file format is line-based TSV with deterministic ordering:
|
/// The file format is line-based TSV with deterministic ordering:
|
||||||
/// - Header: `depot-source-index-v1`
|
/// - Header: `depot-source-index-v2`
|
||||||
/// - Package name row: `P<TAB><name><TAB><relative-spec-path>`
|
/// - Package name row: `P<TAB><name><TAB><relative-spec-path>`
|
||||||
/// - Provides row: `V<TAB><feature><TAB><relative-spec-path>`
|
/// - Provides row: `V<TAB><feature><TAB><relative-spec-path>`
|
||||||
|
/// - Conflicts row: `C<TAB><name><TAB><relative-spec-path>`
|
||||||
|
/// - 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(
|
pub fn create_source_repo_index(
|
||||||
repo_root: &Path,
|
repo_root: &Path,
|
||||||
subdirs: &[String],
|
subdirs: &[String],
|
||||||
@@ -73,7 +98,7 @@ pub fn create_source_repo_index(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let scan_roots = resolve_scan_roots(&repo_root, subdirs)?;
|
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 toml_files_scanned = 0usize;
|
||||||
let mut ignored_toml_files = 0usize;
|
let mut ignored_toml_files = 0usize;
|
||||||
|
|
||||||
@@ -95,23 +120,87 @@ pub fn create_source_repo_index(
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let rel = rel.to_string_lossy().replace('\\', "/");
|
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();
|
let mut rows: Vec<(String, String, String)> = Vec::new();
|
||||||
for (name, rel, provides) in &spec_rows {
|
for spec_row in &spec_rows {
|
||||||
rows.push((
|
rows.push((
|
||||||
SOURCE_REPO_INDEX_KIND_PACKAGE.to_string(),
|
SOURCE_REPO_INDEX_KIND_PACKAGE.to_string(),
|
||||||
name.clone(),
|
spec_row.name.clone(),
|
||||||
rel.clone(),
|
spec_row.rel.clone(),
|
||||||
));
|
));
|
||||||
for provided in provides {
|
for provided in &spec_row.provides {
|
||||||
rows.push((
|
rows.push((
|
||||||
SOURCE_REPO_INDEX_KIND_PROVIDES.to_string(),
|
SOURCE_REPO_INDEX_KIND_PROVIDES.to_string(),
|
||||||
provided.clone(),
|
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| {
|
rows.sort_by(|a, b| {
|
||||||
a.0.cmp(&b.0)
|
a.0.cmp(&b.0)
|
||||||
@@ -120,7 +209,7 @@ pub fn create_source_repo_index(
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str(SOURCE_REPO_INDEX_HEADER);
|
out.push_str(SOURCE_REPO_INDEX_HEADER_V2);
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
for (kind, name, rel) in &rows {
|
for (kind, name, rel) in &rows {
|
||||||
if name.contains('\n') || name.contains('\r') || name.contains('\t') {
|
if name.contains('\n') || name.contains('\r') || name.contains('\t') {
|
||||||
@@ -159,6 +248,22 @@ pub fn create_source_repo_index(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|(kind, _, _)| kind == SOURCE_REPO_INDEX_KIND_PROVIDES)
|
.filter(|(kind, _, _)| kind == SOURCE_REPO_INDEX_KIND_PROVIDES)
|
||||||
.count();
|
.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 {
|
Ok(SourceRepoIndexStats {
|
||||||
index_path,
|
index_path,
|
||||||
@@ -166,6 +271,8 @@ pub fn create_source_repo_index(
|
|||||||
specs_indexed: spec_rows.len(),
|
specs_indexed: spec_rows.len(),
|
||||||
package_rows,
|
package_rows,
|
||||||
provides_rows,
|
provides_rows,
|
||||||
|
conflicts_rows,
|
||||||
|
dependency_rows,
|
||||||
ignored_toml_files,
|
ignored_toml_files,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -251,7 +358,8 @@ impl PackageIndex {
|
|||||||
let header = lines
|
let header = lines
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Missing source index header"))?;
|
.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!(
|
anyhow::bail!(
|
||||||
"Unsupported source index header '{}' in {}",
|
"Unsupported source index header '{}' in {}",
|
||||||
header,
|
header,
|
||||||
@@ -288,6 +396,11 @@ impl PackageIndex {
|
|||||||
.or_default()
|
.or_default()
|
||||||
.push(path);
|
.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!(
|
anyhow::bail!(
|
||||||
"Unknown source index row type '{}' on line {} in {}",
|
"Unknown source index row type '{}' on line {} in {}",
|
||||||
@@ -461,22 +574,51 @@ fn scan_toml_files(scan_roots: &[PathBuf]) -> Result<Vec<PathBuf>> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).unwrap();
|
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()
|
String::new()
|
||||||
} else {
|
} 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()
|
.iter()
|
||||||
.map(|p| format!("\"{p}\""))
|
.map(|p| format!("\"{p}\""))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", ");
|
.join(", ");
|
||||||
format!("[alternatives]\nprovides = [{quoted}]\n\n")
|
format!("[dependencies]\nruntime = [{quoted}]\n\n")
|
||||||
};
|
};
|
||||||
let content = format!(
|
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();
|
std::fs::write(path, content).unwrap();
|
||||||
}
|
}
|
||||||
@@ -486,14 +628,21 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let repo_root = tmp.path().join("depot");
|
let repo_root = tmp.path().join("depot");
|
||||||
let spec_path = repo_root.join("core/hello/hello.toml");
|
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();
|
let stats = create_source_repo_index(&repo_root, &["core".to_string()]).unwrap();
|
||||||
assert_eq!(stats.specs_indexed, 1);
|
assert_eq!(stats.specs_indexed, 1);
|
||||||
assert_eq!(stats.package_rows, 1);
|
assert_eq!(stats.package_rows, 1);
|
||||||
assert_eq!(stats.provides_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());
|
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 index = PackageIndex::build_with_repo_dir(Some(repo_root.clone()));
|
||||||
let hit = index.find("hello").expect("package name should resolve");
|
let hit = index.find("hello").expect("package name should resolve");
|
||||||
assert!(hit.ends_with(Path::new("core/hello/hello.toml")));
|
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_store = tmp.path().join("repo-store");
|
||||||
let repo_root = repo_store.join("vertex");
|
let repo_root = repo_store.join("vertex");
|
||||||
let spec_path = repo_root.join("core/base/base.toml");
|
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 index = PackageIndex::build_with_repo_dir(Some(repo_store));
|
||||||
let hit = index
|
let hit = index
|
||||||
@@ -522,7 +671,7 @@ mod tests {
|
|||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let repo_root = tmp.path().join("depot");
|
let repo_root = tmp.path().join("depot");
|
||||||
let spec_path = repo_root.join("core/curl/curl.toml");
|
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::create_dir_all(&repo_root).unwrap();
|
||||||
std::fs::write(source_repo_index_path(&repo_root), "invalid-header\n").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 '..'")
|
.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",
|
"Optional dependency",
|
||||||
"Package that enables optional runtime functionality",
|
"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 {
|
Ok(PackageSpec {
|
||||||
package: PackageInfo {
|
package: PackageInfo {
|
||||||
@@ -666,7 +681,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
|
|||||||
license,
|
license,
|
||||||
},
|
},
|
||||||
packages: Vec::new(),
|
packages: Vec::new(),
|
||||||
alternatives: Alternatives::default(),
|
alternatives,
|
||||||
manual_sources,
|
manual_sources,
|
||||||
source: sources,
|
source: sources,
|
||||||
build: Build { build_type, flags },
|
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));
|
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)
|
// build (only include set fields)
|
||||||
let mut build_tbl = Table::new();
|
let mut build_tbl = Table::new();
|
||||||
let build_type = match spec.build.build_type {
|
let build_type = match spec.build.build_type {
|
||||||
@@ -1757,6 +1801,65 @@ mod tests {
|
|||||||
assert_eq!(optional_deps[0].as_str(), Some("gtk-doc"));
|
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]
|
#[test]
|
||||||
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
|
fn spec_to_minimal_toml_supports_metapackage_without_sources() {
|
||||||
let spec = PackageSpec {
|
let spec = PackageSpec {
|
||||||
|
|||||||
@@ -171,6 +171,17 @@ impl Packager {
|
|||||||
.collect(),
|
.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.
|
// Add install-relevant dependency kinds for repo/runtime consumers.
|
||||||
let mut deps = toml::map::Map::new();
|
let mut deps = toml::map::Map::new();
|
||||||
|
|||||||
+20
-1
@@ -1557,9 +1557,11 @@ type = "custom"
|
|||||||
|
|
||||||
[alternatives]
|
[alternatives]
|
||||||
provides = ["toolchain"]
|
provides = ["toolchain"]
|
||||||
|
conflicts = ["gcc"]
|
||||||
|
|
||||||
[package_alternatives.clang]
|
[package_alternatives.clang]
|
||||||
provides = ["cc", "c++", "gcc"]
|
provides = ["cc", "c++", "gcc"]
|
||||||
|
conflicts = ["clang-legacy"]
|
||||||
|
|
||||||
[package_alternatives.llvm]
|
[package_alternatives.llvm]
|
||||||
provides = ["binutils"]
|
provides = ["binutils"]
|
||||||
@@ -1572,14 +1574,26 @@ provides = ["binutils"]
|
|||||||
spec.alternatives_for_output("llvm").provides,
|
spec.alternatives_for_output("llvm").provides,
|
||||||
vec!["binutils".to_string()]
|
vec!["binutils".to_string()]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("llvm").conflicts,
|
||||||
|
Vec::<String>::new()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
spec.alternatives_for_output("clang").provides,
|
spec.alternatives_for_output("clang").provides,
|
||||||
vec!["cc".to_string(), "c++".to_string(), "gcc".to_string()]
|
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!(
|
assert_eq!(
|
||||||
spec.alternatives_for_output("other").provides,
|
spec.alternatives_for_output("other").provides,
|
||||||
vec!["toolchain".to_string()]
|
vec!["toolchain".to_string()]
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
spec.alternatives_for_output("other").conflicts,
|
||||||
|
vec!["gcc".to_string()]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3001,6 +3015,9 @@ impl fmt::Display for PackageSpec {
|
|||||||
if !self.alternatives.provides.is_empty() {
|
if !self.alternatives.provides.is_empty() {
|
||||||
writeln!(f, "Provides: {}", self.alternatives.provides.join(", "))?;
|
writeln!(f, "Provides: {}", self.alternatives.provides.join(", "))?;
|
||||||
}
|
}
|
||||||
|
if !self.alternatives.conflicts.is_empty() {
|
||||||
|
writeln!(f, "Conflicts: {}", self.alternatives.conflicts.join(", "))?;
|
||||||
|
}
|
||||||
Ok(())
|
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)]
|
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Alternatives {
|
pub struct Alternatives {
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
pub conflicts: Vec<String>,
|
||||||
/// Reserved for future package replacement feature
|
/// Reserved for future package replacement feature
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
@@ -814,6 +814,7 @@ mod tests {
|
|||||||
"foo-libs".into(),
|
"foo-libs".into(),
|
||||||
Alternatives {
|
Alternatives {
|
||||||
provides: vec!["libfoo".into()],
|
provides: vec!["libfoo".into()],
|
||||||
|
conflicts: Vec::new(),
|
||||||
replaces: Vec::new(),
|
replaces: Vec::new(),
|
||||||
},
|
},
|
||||||
)]),
|
)]),
|
||||||
@@ -856,6 +857,7 @@ mod tests {
|
|||||||
homepage: None,
|
homepage: None,
|
||||||
license: None,
|
license: None,
|
||||||
provides: Vec::new(),
|
provides: Vec::new(),
|
||||||
|
conflicts: Vec::new(),
|
||||||
runtime_dependencies: Vec::new(),
|
runtime_dependencies: Vec::new(),
|
||||||
optional_dependencies: Vec::new(),
|
optional_dependencies: Vec::new(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user