diff --git a/Cargo.lock b/Cargo.lock index 84d6b11..8e3c2d3 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2" [[package]] name = "depot" -version = "0.23.1" +version = "0.24.0" dependencies = [ "anyhow", "ar", diff --git a/Cargo.toml b/Cargo.toml index a62699b..e412a68 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "depot" -version = "0.23.1" +version = "0.24.0" edition = "2024" [lints.rust] diff --git a/meson.build b/meson.build index 20c646e..90a1e28 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'depot', - version: '0.23.1', + version: '0.24.0', meson_version: '>=0.60.0', default_options: ['buildtype=release'], ) diff --git a/src/builder/autotools.rs b/src/builder/autotools.rs index 9f794d8..172bd2a 100755 --- a/src/builder/autotools.rs +++ b/src/builder/autotools.rs @@ -879,10 +879,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.2.3".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1172,10 +1174,12 @@ foo: bar let spec = PackageSpec { package: PackageInfo { name: "expect".into(), + real_name: None, version: "5.45.4".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1204,10 +1208,12 @@ foo: bar let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1238,10 +1244,12 @@ foo: bar let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/bin.rs b/src/builder/bin.rs index b812ede..8c3eede 100644 --- a/src/builder/bin.rs +++ b/src/builder/bin.rs @@ -66,10 +66,12 @@ mod tests { PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/cmake.rs b/src/builder/cmake.rs index 3bb8e80..80bf7ea 100755 --- a/src/builder/cmake.rs +++ b/src/builder/cmake.rs @@ -319,58 +319,20 @@ fn cmake_cache_entry_value<'a>(flags: &'a [String], variable: &str) -> Option<&' } fn cmake_install_dir_args(flags: &crate::package::BuildFlags) -> Vec { - let prefix = cmake_cache_entry_value(&flags.configure, "CMAKE_INSTALL_PREFIX") - .unwrap_or(flags.prefix.as_str()); let dirs = crate::builder::install_dirs(flags); let defaults = [ - ( - "CMAKE_INSTALL_BINDIR", - cmake_install_dir_value(prefix, &dirs.bindir), - ), - ( - "CMAKE_INSTALL_SBINDIR", - cmake_install_dir_value(prefix, &dirs.sbindir), - ), - ( - "CMAKE_INSTALL_LIBDIR", - cmake_install_dir_value(prefix, &dirs.libdir), - ), - ( - "CMAKE_INSTALL_LIBEXECDIR", - cmake_install_dir_value(prefix, &dirs.libexecdir), - ), - ( - "CMAKE_INSTALL_SYSCONFDIR", - cmake_install_dir_value(prefix, &dirs.sysconfdir), - ), - ( - "CMAKE_INSTALL_LOCALSTATEDIR", - cmake_install_dir_value(prefix, &dirs.localstatedir), - ), - ( - "CMAKE_INSTALL_SHAREDSTATEDIR", - cmake_install_dir_value(prefix, &dirs.sharedstatedir), - ), - ( - "CMAKE_INSTALL_INCLUDEDIR", - cmake_install_dir_value(prefix, &dirs.includedir), - ), - ( - "CMAKE_INSTALL_DATAROOTDIR", - cmake_install_dir_value(prefix, &dirs.datarootdir), - ), - ( - "CMAKE_INSTALL_DATADIR", - cmake_install_dir_value(prefix, &dirs.datadir), - ), - ( - "CMAKE_INSTALL_MANDIR", - cmake_install_dir_value(prefix, &dirs.mandir), - ), - ( - "CMAKE_INSTALL_INFODIR", - cmake_install_dir_value(prefix, &dirs.infodir), - ), + ("CMAKE_INSTALL_BINDIR", dirs.bindir), + ("CMAKE_INSTALL_SBINDIR", dirs.sbindir), + ("CMAKE_INSTALL_LIBDIR", dirs.libdir), + ("CMAKE_INSTALL_LIBEXECDIR", dirs.libexecdir), + ("CMAKE_INSTALL_SYSCONFDIR", dirs.sysconfdir), + ("CMAKE_INSTALL_LOCALSTATEDIR", dirs.localstatedir), + ("CMAKE_INSTALL_SHAREDSTATEDIR", dirs.sharedstatedir), + ("CMAKE_INSTALL_INCLUDEDIR", dirs.includedir), + ("CMAKE_INSTALL_DATAROOTDIR", dirs.datarootdir), + ("CMAKE_INSTALL_DATADIR", dirs.datadir), + ("CMAKE_INSTALL_MANDIR", dirs.mandir), + ("CMAKE_INSTALL_INFODIR", dirs.infodir), ]; defaults @@ -429,26 +391,6 @@ fn lib32_target_triple( host.map(|host| crate::cross::lib32_target_triple(&host)) } -fn cmake_install_dir_value(prefix: &str, value: &str) -> String { - let trimmed_prefix = prefix.trim(); - let trimmed_value = value.trim(); - if trimmed_prefix.is_empty() || trimmed_value.is_empty() { - return trimmed_value.to_string(); - } - - let prefix_path = Path::new(trimmed_prefix); - let value_path = Path::new(trimmed_value); - if !prefix_path.is_absolute() || !value_path.is_absolute() { - return trimmed_value.to_string(); - } - - match value_path.strip_prefix(prefix_path) { - Ok(rel) if rel.as_os_str().is_empty() => ".".to_string(), - Ok(rel) => rel.to_string_lossy().replace('\\', "/"), - Err(_) => trimmed_value.to_string(), - } -} - fn num_cpus() -> usize { std::thread::available_parallelism() .map(|n| n.get()) @@ -632,10 +574,13 @@ mod tests { #[test] fn test_cmake_install_dir_args_include_defaults() { let args = cmake_install_dir_args(&BuildFlags::default()); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin")); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=bin")); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib")); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=lib")); + assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin")); + assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=/usr/bin")); + assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib")); + assert!( + args.iter() + .any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib") + ); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc")); assert!( args.iter() @@ -647,17 +592,41 @@ mod tests { ); assert!( args.iter() - .any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include") + .any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=/usr/include") ); assert!( args.iter() - .any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=share") + .any(|a| a == "-DCMAKE_INSTALL_DATAROOTDIR=/usr/share") ); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_DATADIR=share")); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_MANDIR=share/man")); assert!( args.iter() - .any(|a| a == "-DCMAKE_INSTALL_INFODIR=share/info") + .any(|a| a == "-DCMAKE_INSTALL_DATADIR=/usr/share") + ); + assert!( + args.iter() + .any(|a| a == "-DCMAKE_INSTALL_MANDIR=/usr/share/man") + ); + assert!( + args.iter() + .any(|a| a == "-DCMAKE_INSTALL_INFODIR=/usr/share/info") + ); + } + + #[test] + fn test_cmake_install_dir_args_use_lib32_defaults() { + let flags = BuildFlags { + lib32_variant: true, + ..BuildFlags::default() + }; + + let args = cmake_install_dir_args(&flags); + assert!( + args.iter() + .any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib32") + ); + assert!( + args.iter() + .any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=/usr/lib32") ); } @@ -741,19 +710,7 @@ mod tests { .iter() .any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR=")) ); - assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin")); - } - - #[test] - fn test_cmake_install_dir_value_makes_prefix_children_relative() { - assert_eq!(cmake_install_dir_value("/usr", "/usr/include"), "include"); - assert_eq!(cmake_install_dir_value("/", "/usr/include"), "usr/include"); - assert_eq!( - cmake_install_dir_value("/opt/depot", "/opt/depot/lib64"), - "lib64" - ); - assert_eq!(cmake_install_dir_value("/usr", "/etc"), "/etc"); - assert_eq!(cmake_install_dir_value("/usr", "include"), "include"); + assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/bin")); } #[test] @@ -772,10 +729,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "x".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/custom.rs b/src/builder/custom.rs index c3ab589..1e6d0c7 100755 --- a/src/builder/custom.rs +++ b/src/builder/custom.rs @@ -273,10 +273,12 @@ mod tests { PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -373,10 +375,12 @@ depot_install_dev_pkg() { let mut spec = mk_spec("demo", "1.0"); spec.packages.push(crate::package::PackageInfo { name: "dev-pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }); @@ -421,10 +425,12 @@ depot_install() { let mut spec = mk_spec("demo", "1.0"); spec.packages.push(crate::package::PackageInfo { name: "dev-pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }); diff --git a/src/builder/makefile.rs b/src/builder/makefile.rs index b297c10..6e54e84 100644 --- a/src/builder/makefile.rs +++ b/src/builder/makefile.rs @@ -127,10 +127,12 @@ mod tests { PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/meson.rs b/src/builder/meson.rs index 5c21f09..e650610 100755 --- a/src/builder/meson.rs +++ b/src/builder/meson.rs @@ -516,6 +516,18 @@ mod tests { assert!(args.iter().any(|a| a == "--bindir=/usr/bin")); } + #[test] + fn test_meson_setup_args_use_lib32_defaults() { + let flags = BuildFlags { + lib32_variant: true, + ..BuildFlags::default() + }; + + let args = meson_setup_args(&flags, None, &[]); + assert!(args.iter().any(|a| a == "--libdir=/usr/lib32")); + assert!(args.iter().any(|a| a == "--libexecdir=/usr/lib32")); + } + #[test] fn test_meson_setup_args_include_linker_override() { let flags = BuildFlags { @@ -629,10 +641,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/mod.rs b/src/builder/mod.rs index 74dbb6d..4ade7bc 100755 --- a/src/builder/mod.rs +++ b/src/builder/mod.rs @@ -457,10 +457,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "env-test".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/perl.rs b/src/builder/perl.rs index 8c6a0ce..87ac752 100644 --- a/src/builder/perl.rs +++ b/src/builder/perl.rs @@ -348,10 +348,12 @@ mod tests { PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/builder/python.rs b/src/builder/python.rs index 3cb4f8f..3d63740 100644 --- a/src/builder/python.rs +++ b/src/builder/python.rs @@ -781,10 +781,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "py-test".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/commands.rs b/src/commands.rs index f343253..f5f42f4 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -634,6 +634,10 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), + real_name: metadata + .get("real_name") + .and_then(|v| v.as_str()) + .map(String::from), version: metadata .get("version") .and_then(|v| v.as_str()) @@ -653,6 +657,10 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag .and_then(|v| v.as_str()) .unwrap_or("") .to_string(), + abi_breaking: metadata + .get("abi_breaking") + .and_then(|v| v.as_bool()) + .unwrap_or(false), license: parse_licenses_from_toml(metadata), }, packages: Vec::new(), @@ -722,10 +730,12 @@ fn package_spec_from_repo_record( package::PackageSpec { package: package::PackageInfo { name: record.name.clone(), + real_name: record.real_name.clone(), version: record.version.clone(), revision: record.revision, description: record.description.clone().unwrap_or_default(), homepage: record.homepage.clone().unwrap_or_default(), + abi_breaking: record.abi_breaking, license: parse_license_list_from_repo(&record.license), }, packages: Vec::new(), @@ -1193,9 +1203,31 @@ fn build_lib32_companion_package( struct PlannedStagedInstall { is_update: bool, remove_paths: Vec, + renamed_transition: Option, hook_context: install::hooks::HookExecutionContextOwned, } +#[derive(Debug, Clone)] +struct RenamedPackageTransition { + replaced: db::InstalledPackageRecord, + retained_files: Vec, + retained_directories: Vec, +} + +impl RenamedPackageTransition { + fn replacement(&self) -> db::PackageReplacement { + db::PackageReplacement { + old_name: self.replaced.name.clone(), + retained_files: self.retained_files.clone(), + retained_directories: self.retained_directories.clone(), + } + } + + fn retains_old_package(&self) -> bool { + !self.retained_files.is_empty() || !self.retained_directories.is_empty() + } +} + #[derive(Debug, Clone)] struct PlannedPackageInstall { spec: package::PackageSpec, @@ -1468,6 +1500,121 @@ fn resolve_installed_conflicts_for_subjects( Ok(()) } +fn is_versioned_shared_library_path(path: &str) -> bool { + let Some(file_name) = Path::new(path).file_name().and_then(|name| name.to_str()) else { + return false; + }; + let Some(version_suffix) = file_name.split(".so.").nth(1) else { + return false; + }; + !version_suffix.is_empty() + && version_suffix + .chars() + .all(|ch| ch.is_ascii_digit() || ch == '.') + && version_suffix.chars().any(|ch| ch.is_ascii_digit()) +} + +fn retained_abi_files_for_replacement( + old_files: &[String], + new_manifest: &staging::Manifest, +) -> Vec { + let new_files: HashSet<&str> = new_manifest.files.iter().map(String::as_str).collect(); + let mut retained: Vec = old_files + .iter() + .filter(|path| is_versioned_shared_library_path(path)) + .filter(|path| !new_files.contains(path.as_str())) + .cloned() + .collect(); + retained.sort(); + retained +} + +fn retained_directories_for_files( + old_directories: &[String], + retained_files: &[String], +) -> Vec { + let retained_files: HashSet<&str> = retained_files.iter().map(String::as_str).collect(); + let mut directories: Vec = old_directories + .iter() + .filter(|directory| { + let prefix = format!("{}/", directory); + retained_files + .iter() + .any(|file| *file == directory.as_str() || file.starts_with(&prefix)) + }) + .cloned() + .collect(); + directories.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count())); + directories +} + +fn compare_installed_records_for_stream( + left: &db::InstalledPackageRecord, + right: &db::InstalledPackageRecord, +) -> Ordering { + compare_package_release(&left.version, left.revision, &right.version, right.revision) + .then_with(|| compare_completed_at(left.completed_at, right.completed_at)) + .then_with(|| left.name.cmp(&right.name)) +} + +fn select_primary_installed_record<'a>( + records: impl IntoIterator, +) -> Option<&'a db::InstalledPackageRecord> { + let mut best: Option<&db::InstalledPackageRecord> = None; + for record in records { + if best.as_ref().is_none_or(|current| { + compare_installed_records_for_stream(record, current) == Ordering::Greater + }) { + best = Some(record); + } + } + best +} + +fn build_renamed_package_transition( + db_path: &Path, + pkg_spec: &package::PackageSpec, + new_manifest: &staging::Manifest, +) -> Result> { + let installed = db::list_installed_package_records(db_path)?; + if installed + .iter() + .any(|record| record.name == pkg_spec.package.name) + { + return Ok(None); + } + + let stream_name = pkg_spec.package.effective_real_name(); + let Some(replaced) = select_primary_installed_record( + installed + .iter() + .filter(|record| record.effective_real_name() == stream_name) + .filter(|record| record.name != pkg_spec.package.name), + ) + .cloned() else { + return Ok(None); + }; + + let old_files = db::get_package_files(db_path, &replaced.name)?; + let old_directories = db::get_package_directories(db_path, &replaced.name)?; + let retained_files = if replaced.abi_breaking { + retained_abi_files_for_replacement(&old_files, new_manifest) + } else { + Vec::new() + }; + let retained_directories = if retained_files.is_empty() { + Vec::new() + } else { + retained_directories_for_files(&old_directories, &retained_files) + }; + + Ok(Some(RenamedPackageTransition { + replaced, + retained_files, + retained_directories, + })) +} + fn plan_staged_install( pkg_spec: &package::PackageSpec, destdir: &Path, @@ -1482,10 +1629,38 @@ fn plan_staged_install( })?; let db_path = config.installed_db_path(rootfs); - let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some(); let new_manifest = staging::generate_manifest_with_dirs(destdir)?; - let remove_paths = + let renamed_transition = build_renamed_package_transition(&db_path, pkg_spec, &new_manifest)?; + let is_update = db::get_package_version(&db_path, &pkg_spec.package.name)?.is_some() + || renamed_transition.is_some(); + let mut remove_paths = db::calculate_upgrade_paths(&db_path, &pkg_spec.package.name, &new_manifest)?; + if let Some(transition) = &renamed_transition { + let old_files = db::get_package_files(&db_path, &transition.replaced.name)?; + let old_directories = db::get_package_directories(&db_path, &transition.replaced.name)?; + let retained_files: HashSet<&str> = transition + .retained_files + .iter() + .map(String::as_str) + .collect(); + let retained_directories: HashSet<&str> = transition + .retained_directories + .iter() + .map(String::as_str) + .collect(); + remove_paths.extend( + old_files + .into_iter() + .filter(|path| !retained_files.contains(path.as_str())), + ); + remove_paths.extend( + old_directories + .into_iter() + .filter(|path| !retained_directories.contains(path.as_str())), + ); + remove_paths.sort_by_key(|path| std::cmp::Reverse(path.matches('/').count())); + remove_paths.dedup(); + } let operation = if is_update { install::hooks::HookOperation::Update } else { @@ -1499,6 +1674,7 @@ fn plan_staged_install( Ok(PlannedStagedInstall { is_update, remove_paths, + renamed_transition, hook_context: install::hooks::HookExecutionContextOwned { operation, package: pkg_spec.package.name.clone(), @@ -1582,12 +1758,27 @@ fn install_staged_to_rootfs( )?; let db_path = config.installed_db_path(rootfs); - if let Err(e) = db::register_package(&db_path, pkg_spec, destdir) { + let replacement = plan + .renamed_transition + .as_ref() + .map(RenamedPackageTransition::replacement); + let register_result = if let Some(replacement) = replacement.as_ref() { + db::register_package_with_replacement(&db_path, pkg_spec, destdir, Some(replacement)) + } else { + db::register_package(&db_path, pkg_spec, destdir) + }; + if let Err(e) = register_result { let _ = tx.rollback(); return Err(e); } tx.commit()?; + if let Some(transition) = &plan.renamed_transition + && !transition.retains_old_package() + { + install::scripts::remove_installed_scripts(rootfs, &transition.replaced.name)?; + } + install::scripts::sync_staged_scripts_to_rootfs( &staged_scripts_dir, rootfs, @@ -2028,7 +2219,8 @@ enum UpdateOrigin { #[derive(Debug, Clone)] struct UpdateCandidate { - package: String, + installed_package: String, + candidate_package: String, installed_version: String, installed_revision: u32, installed_completed_at: Option, @@ -2243,7 +2435,7 @@ fn configured_source_scan_roots(config: &config::Config) -> Vec<(String, i32, Pa fn collect_best_source_update_candidates( config: &config::Config, - target_names: &HashSet, + target_real_names: &HashSet, ) -> Result> { let mut best: HashMap = HashMap::new(); @@ -2255,7 +2447,8 @@ fn collect_best_source_update_candidates( for spec_path in scan_package_specs(&root)? { let mut spec = package::PackageSpec::from_file(&spec_path)?; spec.apply_config(config); - if !target_names.contains(&spec.package.name) { + let stream_name = spec.package.effective_real_name().to_string(); + if !target_real_names.contains(&stream_name) { continue; } @@ -2267,7 +2460,7 @@ fn collect_best_source_update_candidates( spec, }; - let replace = match best.get(&candidate.spec.package.name) { + let replace = match best.get(&stream_name) { Some(current) => match compare_package_release( &candidate.spec.package.version, candidate.spec.package.revision, @@ -2296,7 +2489,7 @@ fn collect_best_source_update_candidates( }; if replace { - best.insert(candidate.spec.package.name.clone(), candidate); + best.insert(stream_name, candidate); } } } @@ -2307,7 +2500,7 @@ fn collect_best_source_update_candidates( fn collect_best_binary_update_candidates( config: &config::Config, rootfs: &Path, - target_names: &HashSet, + target_real_names: &HashSet, ) -> Result> { let mut best: HashMap = HashMap::new(); let host_arch = std::env::consts::ARCH; @@ -2328,11 +2521,12 @@ fn collect_best_binary_update_candidates( .with_context(|| format!("Failed to list binary repo '{}'", repo_name))?; for record in records { - if !target_names.contains(&record.name) { + let stream_name = record.effective_real_name().to_string(); + if !target_real_names.contains(&stream_name) { continue; } - let replace = match best.get(&record.name) { + let replace = match best.get(&stream_name) { Some((current_priority, current)) => match compare_package_release( &record.version, record.revision, @@ -2361,7 +2555,7 @@ fn collect_best_binary_update_candidates( }; if replace { - best.insert(record.name.clone(), (repo_cfg.priority, record)); + best.insert(stream_name, (repo_cfg.priority, record)); } } } @@ -2377,8 +2571,9 @@ fn select_update_candidate( prefer_binary: bool, ) -> Option { let mut best: Option = None; + let stream_name = installed.effective_real_name(); - if let Some(candidate) = source_candidates.get(&installed.name) + if let Some(candidate) = source_candidates.get(stream_name) && update_candidate_is_newer_than_installed( &candidate.spec.package.version, candidate.spec.package.revision, @@ -2389,7 +2584,8 @@ fn select_update_candidate( ) { best = Some(UpdateCandidate { - package: installed.name.clone(), + installed_package: installed.name.clone(), + candidate_package: candidate.spec.package.name.clone(), installed_version: installed.version.clone(), installed_revision: installed.revision, installed_completed_at, @@ -2407,7 +2603,7 @@ fn select_update_candidate( }); } - if let Some((repo_priority, record)) = binary_candidates.get(&installed.name) + if let Some((repo_priority, record)) = binary_candidates.get(stream_name) && update_candidate_is_newer_than_installed( &record.version, record.revision, @@ -2418,7 +2614,8 @@ fn select_update_candidate( ) { let binary_candidate = UpdateCandidate { - package: installed.name.clone(), + installed_package: installed.name.clone(), + candidate_package: record.name.clone(), installed_version: installed.version.clone(), installed_revision: installed.revision, installed_completed_at, @@ -2456,36 +2653,53 @@ fn collect_update_candidates( return Ok(Vec::new()); } - let installed_by_name: HashMap<_, _> = installed + let mut installed_by_real_name: HashMap> = + HashMap::new(); + for record in &installed { + installed_by_real_name + .entry(record.effective_real_name().to_string()) + .or_default() + .push(record); + } + + let active_by_real_name: HashMap = installed_by_real_name .iter() - .cloned() - .map(|record| (record.name.clone(), record)) + .filter_map(|(real_name, records)| { + select_primary_installed_record(records.iter().copied()) + .cloned() + .map(|record| (real_name.clone(), record)) + }) .collect(); - let target_names: HashSet = if requested_packages.is_empty() { - installed_by_name.keys().cloned().collect() + let target_real_names: HashSet = if requested_packages.is_empty() { + active_by_real_name.keys().cloned().collect() } else { - requested_packages.iter().cloned().collect() - }; - - for package in requested_packages { - if !installed_by_name.contains_key(package) { - ui::warn(format!("Package '{}' is not installed; skipping", package)); + let mut targets = HashSet::new(); + for package in requested_packages { + if let Some(record) = installed.iter().find(|record| record.name == *package) { + targets.insert(record.effective_real_name().to_string()); + } else if active_by_real_name.contains_key(package) { + targets.insert(package.clone()); + } else { + ui::warn(format!("Package '{}' is not installed; skipping", package)); + } } - } + targets + }; let source_candidates = if config.repo_settings.prefer_binary { HashMap::new() } else { - collect_best_source_update_candidates(config, &target_names)? + collect_best_source_update_candidates(config, &target_real_names)? }; - let binary_candidates = collect_best_binary_update_candidates(config, rootfs, &target_names)?; + let binary_candidates = + collect_best_binary_update_candidates(config, rootfs, &target_real_names)?; let mut updates = Vec::new(); - let mut targets: Vec<_> = target_names.into_iter().collect(); + let mut targets: Vec<_> = target_real_names.into_iter().collect(); targets.sort(); for target in targets { - let Some(installed) = installed_by_name.get(&target) else { + let Some(installed) = active_by_real_name.get(&target) else { continue; }; let installed_completed_at = installed_package_completed_at(installed, &db_path, rootfs)?; @@ -2509,7 +2723,7 @@ fn collect_missing_update_dependencies( ) -> Result> { let mut planned_provides = HashSet::new(); for candidate in candidates { - planned_provides.insert(candidate.package.clone()); + planned_provides.insert(candidate.candidate_package.clone()); for provide in &candidate.provides { planned_provides.insert(provide.clone()); } @@ -2571,10 +2785,11 @@ fn run_update_command( .iter() .map(|candidate| { let summary = format!( - "{} v{}-{} -> v{}-{}", - candidate.package, + "{} v{}-{} -> {} v{}-{}", + candidate.installed_package, candidate.installed_version, candidate.installed_revision, + candidate.candidate_package, candidate.candidate_version, candidate.candidate_revision ); @@ -2595,7 +2810,7 @@ fn run_update_command( let conflict_subjects: Vec<_> = updates .iter() .map(|candidate| InstallConflictSubject { - package: candidate.package.clone(), + package: candidate.candidate_package.clone(), provides: candidate.provides.clone(), conflicts: candidate.conflicts.clone(), }) @@ -2673,12 +2888,13 @@ fn run_update_command( let exe = std::env::current_exe().context("Failed to locate depot executable")?; for (idx, candidate) in updates.iter().enumerate() { ui::info(format!( - "[{}/{}] updating {} v{}-{} -> v{}-{}", + "[{}/{}] updating {} v{}-{} -> {} v{}-{}", idx + 1, updates.len(), - candidate.package, + candidate.installed_package, candidate.installed_version, candidate.installed_revision, + candidate.candidate_package, candidate.candidate_version, candidate.candidate_revision )); @@ -5415,8 +5631,10 @@ mod tests { db::repo::BinaryRepoPackageRecord { repo_name: "core".into(), name: name.into(), + real_name: None, version: "1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: filename.into(), size: 1, @@ -5449,10 +5667,12 @@ mod tests { package::PackageSpec { package: package::PackageInfo { name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -5598,8 +5818,10 @@ optional = [] let record = db::repo::BinaryRepoPackageRecord { repo_name: "core".into(), name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: archive_path .file_name() @@ -5897,8 +6119,10 @@ optional = [] let record = db::repo::BinaryRepoPackageRecord { repo_name: "core".into(), name: "sudo".into(), + real_name: None, version: "1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: archive_path .file_name() @@ -6017,10 +6241,12 @@ optional = [] let spec = package::PackageSpec { package: package::PackageInfo { name: "filesystem".into(), + real_name: None, version: "1.0.1".into(), revision: 3, description: "Base filesystem".into(), homepage: "https://example.test".into(), + abi_breaking: false, license: vec!["Unlicense".into()], }, packages: Vec::new(), @@ -6047,6 +6273,117 @@ optional = [] Ok(()) } + #[test] + fn renamed_abi_updates_keep_versioned_shared_libraries() -> Result<()> { + let rootfs = tempfile::tempdir().context("Failed to create temp rootfs")?; + let mut cfg = config::Config::for_rootfs(rootfs.path()); + cfg.db_dir = rootfs.path().join("var/lib/depot"); + cfg.build_dir = rootfs.path().join("var/cache/depot/build"); + + let old_spec = package::PackageSpec { + package: package::PackageInfo { + name: "libxml214".into(), + real_name: Some("libxml2".into()), + version: "2.14.9".into(), + revision: 1, + description: "libxml2 2.14".into(), + homepage: "https://example.test/libxml2".into(), + abi_breaking: true, + license: vec!["MIT".into()], + }, + packages: Vec::new(), + alternatives: package::Alternatives::default(), + manual_sources: Vec::new(), + source: Vec::new(), + build: package::Build { + build_type: package::BuildType::Bin, + flags: package::BuildFlags::default(), + }, + dependencies: package::Dependencies::default(), + package_alternatives: Default::default(), + package_dependencies: Default::default(), + spec_dir: PathBuf::from("."), + }; + let old_dest = rootfs.path().join("old-dest"); + fs::create_dir_all(old_dest.join("usr/lib/pkgconfig"))?; + fs::write(old_dest.join("usr/lib/libxml2.so.14.9.0"), "old-real")?; + std::os::unix::fs::symlink("libxml2.so.14.9.0", old_dest.join("usr/lib/libxml2.so.14"))?; + std::os::unix::fs::symlink("libxml2.so.14", old_dest.join("usr/lib/libxml2.so"))?; + fs::write( + old_dest.join("usr/lib/pkgconfig/libxml-2.0.pc"), + "old-pkgconfig", + )?; + install_package_outputs_to_rootfs(&old_spec, &old_dest, rootfs.path(), &cfg)?; + + let new_spec = package::PackageSpec { + package: package::PackageInfo { + name: "libxml215".into(), + real_name: Some("libxml2".into()), + version: "2.15.1".into(), + revision: 1, + description: "libxml2 2.15".into(), + homepage: "https://example.test/libxml2".into(), + abi_breaking: true, + license: vec!["MIT".into()], + }, + packages: Vec::new(), + alternatives: package::Alternatives::default(), + manual_sources: Vec::new(), + source: Vec::new(), + build: package::Build { + build_type: package::BuildType::Bin, + flags: package::BuildFlags::default(), + }, + dependencies: package::Dependencies::default(), + package_alternatives: Default::default(), + package_dependencies: Default::default(), + spec_dir: PathBuf::from("."), + }; + let new_dest = rootfs.path().join("new-dest"); + fs::create_dir_all(new_dest.join("usr/lib/pkgconfig"))?; + fs::write(new_dest.join("usr/lib/libxml2.so.15.1.0"), "new-real")?; + std::os::unix::fs::symlink("libxml2.so.15.1.0", new_dest.join("usr/lib/libxml2.so.15"))?; + std::os::unix::fs::symlink("libxml2.so.15", new_dest.join("usr/lib/libxml2.so"))?; + fs::write( + new_dest.join("usr/lib/pkgconfig/libxml-2.0.pc"), + "new-pkgconfig", + )?; + + let installed = + install_package_outputs_to_rootfs(&new_spec, &new_dest, rootfs.path(), &cfg)?; + assert_eq!(installed.len(), 1); + assert!(installed[0].is_update); + assert_eq!(installed[0].package.name, "libxml215"); + + assert!(rootfs.path().join("usr/lib/libxml2.so.14.9.0").exists()); + assert!(rootfs.path().join("usr/lib/libxml2.so.14").exists()); + assert_eq!( + fs::read_to_string(rootfs.path().join("usr/lib/libxml2.so.15.1.0"))?, + "new-real" + ); + assert_eq!( + fs::read_to_string(rootfs.path().join("usr/lib/pkgconfig/libxml-2.0.pc"))?, + "new-pkgconfig" + ); + + let db_path = cfg.installed_db_path(rootfs.path()); + let old_files = db::get_package_files(&db_path, "libxml214")?; + assert_eq!( + old_files, + vec![ + "usr/lib/libxml2.so.14".to_string(), + "usr/lib/libxml2.so.14.9.0".to_string(), + ] + ); + + let new_files = db::get_package_files(&db_path, "libxml215")?; + assert!(new_files.contains(&"usr/lib/libxml2.so".to_string())); + assert!(new_files.contains(&"usr/lib/libxml2.so.15".to_string())); + assert!(new_files.contains(&"usr/lib/libxml2.so.15.1.0".to_string())); + assert!(new_files.contains(&"usr/lib/pkgconfig/libxml-2.0.pc".to_string())); + Ok(()) + } + #[test] fn merge_missing_dependencies_preserves_order_and_uniqueness() { let merged = merge_missing_dependencies( @@ -6061,21 +6398,130 @@ optional = [] assert_eq!(merged, vec!["make", "pkgconf", "glibc", "openssl", "zlib"]); } + #[test] + fn collect_update_candidates_matches_renamed_packages_by_real_name() -> Result<()> { + let temp = tempfile::tempdir()?; + let rootfs = temp.path().join("rootfs"); + let repo_clones = temp.path().join("repos"); + let build_dir = temp.path().join("build"); + let db_dir = rootfs.join("var/lib/depot"); + fs::create_dir_all(&rootfs)?; + fs::create_dir_all(&repo_clones)?; + fs::create_dir_all(&build_dir)?; + fs::create_dir_all(&db_dir)?; + + let mut config = config::Config::for_rootfs(&rootfs); + config.repo_clone_dir = repo_clones.clone(); + config.build_dir = build_dir; + config.db_dir = db_dir.clone(); + config.repo_settings.prefer_binary = false; + config.binary_repos.clear(); + config.source_repos.insert( + "private".into(), + config::SourceRepo { + url: "https://example.test/private.git".into(), + enabled: true, + priority: 0, + subdirs: Vec::new(), + }, + ); + + let installed_spec = package::PackageSpec { + package: package::PackageInfo { + name: "icu78".into(), + real_name: Some("icu".into()), + version: "78.2".into(), + revision: 1, + description: "icu78".into(), + homepage: "https://example.test/icu".into(), + abi_breaking: true, + license: vec!["MIT".into()], + }, + packages: Vec::new(), + alternatives: package::Alternatives::default(), + manual_sources: Vec::new(), + source: Vec::new(), + build: package::Build { + build_type: package::BuildType::Bin, + flags: package::BuildFlags::default(), + }, + dependencies: package::Dependencies::default(), + package_alternatives: Default::default(), + package_dependencies: Default::default(), + spec_dir: PathBuf::from("."), + }; + let dest = temp.path().join("dest"); + fs::create_dir_all(dest.join("usr/lib"))?; + fs::write(dest.join("usr/lib/libicuuc.so.78.2"), "icu78")?; + db::register_package(&config.installed_db_path(&rootfs), &installed_spec, &dest)?; + + let repo_root = repo_clones.join("private"); + fs::create_dir_all(&repo_root)?; + fs::write( + repo_root.join("icu79.toml"), + r#"[package] +name = "icu79" +real_name = "icu" +version = "79.1" +revision = 1 +description = "icu79" +homepage = "https://example.test/icu" +abi_breaking = true +license = "MIT" + +[build] +type = "meta" + +[dependencies] +runtime = [] +optional = [] +"#, + )?; + + let installed_records = + db::list_installed_package_records(&config.installed_db_path(&rootfs))?; + assert_eq!(installed_records.len(), 1); + assert_eq!(installed_records[0].real_name.as_deref(), Some("icu")); + + let source_candidates = + collect_best_source_update_candidates(&config, &HashSet::from([String::from("icu")]))?; + assert!(source_candidates.contains_key("icu")); + let selected = select_update_candidate( + &installed_records[0], + installed_records[0].completed_at, + &source_candidates, + &HashMap::new(), + false, + ); + assert!(selected.is_some()); + + let updates = collect_update_candidates(&config, &rootfs, &["icu78".into()])?; + assert_eq!(updates.len(), 1); + assert_eq!(updates[0].installed_package, "icu78"); + assert_eq!(updates[0].candidate_package, "icu79"); + assert_eq!(updates[0].candidate_version, "79.1"); + Ok(()) + } + #[test] fn update_candidate_prefers_binary_when_versions_match_and_config_does() { let installed = db::InstalledPackageRecord { name: "pkg".into(), + real_name: None, version: "1.0.0".into(), revision: 1, + abi_breaking: false, completed_at: None, }; let source_spec = package::PackageSpec { package: package::PackageInfo { name: "pkg".into(), + real_name: None, version: "1.1.0".into(), revision: 1, description: "test".into(), homepage: "https://example.test".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -6115,8 +6561,10 @@ optional = [] db::repo::BinaryRepoPackageRecord { repo_name: "binary".into(), name: "pkg".into(), + real_name: None, version: "1.1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: "pkg-1.1.0-1-x86_64.depot.pkg.tar.zst".into(), size: 1, @@ -6148,17 +6596,21 @@ optional = [] fn select_update_candidate_uses_newer_timestamp_when_versions_match() { let installed = db::InstalledPackageRecord { name: "pkg".into(), + real_name: None, version: "1.0.0".into(), revision: 1, + abi_breaking: false, completed_at: Some(100), }; let source_spec = package::PackageSpec { package: package::PackageInfo { name: "pkg".into(), + real_name: None, version: "1.0.0".into(), revision: 1, description: "test".into(), homepage: "https://example.test".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -6227,10 +6679,12 @@ optional = [] let installed_spec = package::PackageSpec { package: package::PackageInfo { name: "pkg".into(), + real_name: None, version: "1.0.0".into(), revision: 1, description: "pkg".into(), homepage: "https://example.test".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -6397,10 +6851,12 @@ optional = [] let libc_spec = package::PackageSpec { package: package::PackageInfo { name: "glibc".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "glibc".into(), homepage: "https://example.test".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -6424,7 +6880,8 @@ optional = [] let missing = collect_missing_update_dependencies( &[ UpdateCandidate { - package: "pkg".into(), + installed_package: "pkg".into(), + candidate_package: "pkg".into(), installed_version: "1.0".into(), installed_revision: 1, installed_completed_at: None, @@ -6441,7 +6898,8 @@ optional = [] }, }, UpdateCandidate { - package: "helper".into(), + installed_package: "helper".into(), + candidate_package: "helper".into(), installed_version: "1.0".into(), installed_revision: 1, installed_completed_at: None, @@ -6458,7 +6916,8 @@ optional = [] }, }, UpdateCandidate { - package: "tool".into(), + installed_package: "tool".into(), + candidate_package: "tool".into(), installed_version: "1.0".into(), installed_revision: 1, installed_completed_at: None, diff --git a/src/db/mod.rs b/src/db/mod.rs index eee3f2f..fcaf552 100755 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -21,14 +21,57 @@ fn verbose_remove_output() -> bool { /// Installed package row from the local package database. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InstalledPackageRecord { + /// Installed package name. pub name: String, + /// Stable package stream name used for renamed updates. + pub real_name: Option, + /// Installed package version. pub version: String, + /// Installed package revision. pub revision: u32, + /// Whether renamed updates should retain versioned shared libraries. + pub abi_breaking: bool, + /// Package completion timestamp if known. pub completed_at: Option, } +impl InstalledPackageRecord { + /// Return the stable package stream name, defaulting to the package name. + pub fn effective_real_name(&self) -> &str { + self.real_name.as_deref().unwrap_or(&self.name) + } +} + +/// Rename-aware replacement metadata applied while registering a new package. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackageReplacement { + /// Installed package name being replaced. + pub old_name: String, + /// Subset of old-package files to keep installed and owned by the old package. + pub retained_files: Vec, + /// Subset of old-package directories to keep owned by the old package. + pub retained_directories: Vec, +} + +impl PackageReplacement { + /// Return true when the replaced package remains installed after registration. + pub fn retains_old_package(&self) -> bool { + !self.retained_files.is_empty() || !self.retained_directories.is_empty() + } +} + /// Initialize database and register a package pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> { + register_package_with_replacement(db_path, spec, destdir, None) +} + +/// Initialize database and register a package, optionally replacing a renamed predecessor. +pub fn register_package_with_replacement( + db_path: &Path, + spec: &PackageSpec, + destdir: &Path, + replacement: Option<&PackageReplacement>, +) -> Result<()> { // Create parent directory (auto-create db dir if missing) if let Some(parent) = db_path.parent() { fs::create_dir_all(parent)?; @@ -43,24 +86,70 @@ pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> R // Generate manifest with files and directories let manifest = staging::generate_manifest_with_dirs(destdir)?; + if let Some(replacement) = replacement { + if replacement.old_name == spec.package.name { + anyhow::bail!( + "Replacement package '{}' cannot match new package name", + spec.package.name + ); + } + + let old_pkg_id: i64 = tx + .query_row( + "SELECT id FROM packages WHERE name = ?1", + params![replacement.old_name], + |row| row.get(0), + ) + .with_context(|| format!("Package '{}' not found", replacement.old_name))?; + + if replacement.retains_old_package() { + tx.execute( + "DELETE FROM files WHERE package_id = ?1", + params![old_pkg_id], + )?; + tx.execute( + "DELETE FROM directories WHERE package_id = ?1", + params![old_pkg_id], + )?; + for file in &replacement.retained_files { + tx.execute( + "INSERT INTO files (package_id, path) VALUES (?1, ?2)", + params![old_pkg_id, file], + )?; + } + for directory in &replacement.retained_directories { + tx.execute( + "INSERT INTO directories (package_id, path) VALUES (?1, ?2)", + params![old_pkg_id, directory], + )?; + } + } else { + delete_package_rows_tx(&tx, old_pkg_id)?; + } + } + // Insert/update package without changing its primary key (UPSERT keeps the existing row). tx.execute( - "INSERT INTO packages (name, version, revision, description, homepage, license, completed_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + "INSERT INTO packages (name, real_name, version, revision, description, homepage, license, abi_breaking, completed_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ON CONFLICT(name) DO UPDATE SET + real_name=excluded.real_name, version=excluded.version, revision=excluded.revision, description=excluded.description, homepage=excluded.homepage, license=excluded.license, + abi_breaking=excluded.abi_breaking, completed_at=excluded.completed_at", params![ spec.package.name, + spec.package.real_name, spec.package.version, spec.package.revision, spec.package.description, spec.package.homepage, format_licenses(&spec.package.license), + spec.package.abi_breaking, completed_at, ], )?; @@ -330,16 +419,7 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> { } // Remove from database - conn.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?; - conn.execute( - "DELETE FROM directories WHERE package_id = ?1", - params![pkg_id], - )?; - conn.execute( - "DELETE FROM provides WHERE package_id = ?1", - params![pkg_id], - )?; - conn.execute("DELETE FROM packages WHERE id = ?1", params![pkg_id])?; + delete_package_rows(&conn, pkg_id)?; crate::log_info!( "Removed {} files and {} directories", @@ -422,11 +502,13 @@ fn init_db(conn: &Connection) -> Result<()> { CREATE TABLE IF NOT EXISTS packages ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, + real_name TEXT, version TEXT NOT NULL, revision INTEGER NOT NULL DEFAULT 1, description TEXT, homepage TEXT, license TEXT, + abi_breaking INTEGER NOT NULL DEFAULT 0, completed_at INTEGER ); @@ -460,6 +542,8 @@ fn init_db(conn: &Connection) -> Result<()> { ", )?; ensure_packages_completed_at_column(conn)?; + ensure_packages_real_name_column(conn)?; + ensure_packages_abi_breaking_column(conn)?; Ok(()) } @@ -481,6 +565,73 @@ fn ensure_packages_completed_at_column(conn: &Connection) -> Result<()> { Ok(()) } +fn ensure_packages_real_name_column(conn: &Connection) -> Result<()> { + let has_real_name: bool = conn + .query_row( + "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'real_name'", + [], + |row| { + let count: i64 = row.get(0)?; + Ok(count > 0) + }, + ) + .context("Failed to inspect installed package DB schema")?; + if !has_real_name { + conn.execute("ALTER TABLE packages ADD COLUMN real_name TEXT", []) + .context("Failed to add real_name column to installed package DB")?; + } + Ok(()) +} + +fn ensure_packages_abi_breaking_column(conn: &Connection) -> Result<()> { + let has_abi_breaking: bool = conn + .query_row( + "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'abi_breaking'", + [], + |row| { + let count: i64 = row.get(0)?; + Ok(count > 0) + }, + ) + .context("Failed to inspect installed package DB schema")?; + if !has_abi_breaking { + conn.execute( + "ALTER TABLE packages ADD COLUMN abi_breaking INTEGER NOT NULL DEFAULT 0", + [], + ) + .context("Failed to add abi_breaking column to installed package DB")?; + } + Ok(()) +} + +fn delete_package_rows(conn: &Connection, pkg_id: i64) -> Result<()> { + conn.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?; + conn.execute( + "DELETE FROM directories WHERE package_id = ?1", + params![pkg_id], + )?; + conn.execute( + "DELETE FROM provides WHERE package_id = ?1", + params![pkg_id], + )?; + conn.execute("DELETE FROM packages WHERE id = ?1", params![pkg_id])?; + Ok(()) +} + +fn delete_package_rows_tx(tx: &rusqlite::Transaction<'_>, pkg_id: i64) -> Result<()> { + tx.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?; + tx.execute( + "DELETE FROM directories WHERE package_id = ?1", + params![pkg_id], + )?; + tx.execute( + "DELETE FROM provides WHERE package_id = ?1", + params![pkg_id], + )?; + tx.execute("DELETE FROM packages WHERE id = ?1", params![pkg_id])?; + Ok(()) +} + fn package_completed_at(destdir: &Path) -> Result> { let metadata_path = destdir.join(".metadata.toml"); if let Some(completed_at) = @@ -597,14 +748,19 @@ pub fn list_installed_package_records(db_path: &Path) -> Result(2)? as u32, - completed_at: row.get(3)?, + real_name: row.get(1)?, + version: row.get(2)?, + revision: row.get::<_, i64>(3)? as u32, + abi_breaking: row.get(4)?, + completed_at: row.get(5)?, }) })?; Ok(rows.filter_map(|row| row.ok()).collect()) @@ -708,10 +864,12 @@ mod tests { PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/db/repo.rs b/src/db/repo.rs index 87b46b5..d4b387f 100644 --- a/src/db/repo.rs +++ b/src/db/repo.rs @@ -73,8 +73,10 @@ impl Read for HashingReader { struct IndexedPackage { name: String, + real_name: Option, version: String, revision: u32, + abi_breaking: bool, completed_at: Option, description: Option, homepage: Option, @@ -109,8 +111,10 @@ pub struct BinaryRepoSearchHit { pub struct BinaryRepoPackageRecord { pub repo_name: String, pub name: String, + pub real_name: Option, pub version: String, pub revision: u32, + pub abi_breaking: bool, pub completed_at: Option, pub filename: String, pub size: u64, @@ -125,6 +129,13 @@ pub struct BinaryRepoPackageRecord { pub optional_dependencies: Vec, } +impl BinaryRepoPackageRecord { + /// Return the stable package stream name, defaulting to the package name. + pub fn effective_real_name(&self) -> &str { + self.real_name.as_deref().unwrap_or(&self.name) + } +} + /// Local cache paths for a binary package archive and its detached signature. #[derive(Debug, Clone)] pub struct BinaryRepoCachedArchive { @@ -283,8 +294,10 @@ impl RepoManager { "CREATE TABLE packages ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, + real_name TEXT, version TEXT NOT NULL, revision INTEGER NOT NULL, + abi_breaking INTEGER NOT NULL DEFAULT 0, completed_at INTEGER, description TEXT, homepage TEXT, @@ -346,8 +359,10 @@ impl RepoManager { let mut hashing_reader = HashingReader::new(file); let mut name = String::new(); + let mut real_name = None; let mut version = String::new(); let mut revision = 1; + let mut abi_breaking = false; let mut completed_at = path_modified_unix_timestamp(pkg_path)?; let mut description = None; let mut homepage = None; @@ -378,6 +393,10 @@ impl RepoManager { .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); + real_name = metadata + .get("real_name") + .and_then(|v| v.as_str()) + .map(String::from); version = metadata .get("version") .and_then(|v| v.as_str()) @@ -387,6 +406,10 @@ impl RepoManager { .get("revision") .and_then(|v| v.as_integer()) .unwrap_or(1) as u32; + abi_breaking = metadata + .get("abi_breaking") + .and_then(|v| v.as_bool()) + .unwrap_or(false); completed_at = metadata_time::parse_completed_at_value(&metadata).or(completed_at); description = metadata @@ -469,8 +492,10 @@ impl RepoManager { Ok(IndexedPackage { name, + real_name, version, revision, + abi_breaking, completed_at, description, homepage, @@ -490,8 +515,10 @@ impl RepoManager { fn insert_indexed_package(&self, conn: &mut Connection, indexed: IndexedPackage) -> Result<()> { let IndexedPackage { name, + real_name, version, revision, + abi_breaking, completed_at, description, homepage, @@ -509,12 +536,14 @@ impl RepoManager { // Insert into database conn.execute( - "INSERT INTO packages (name, version, revision, completed_at, description, homepage, license, filename, size, sha256, sha512) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "INSERT INTO packages (name, real_name, version, revision, abi_breaking, completed_at, description, homepage, license, filename, size, sha256, sha512) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", params![ name, + real_name, version, revision as i64, + abi_breaking, completed_at, description, homepage, @@ -1620,12 +1649,24 @@ fn find_cached_binary_repo_packages( } else { "NULL" }; + let real_name_expr = if repo_packages_have_real_name(&conn)? { + "p.real_name" + } else { + "NULL" + }; + let abi_breaking_expr = if repo_packages_have_abi_breaking(&conn)? { + "p.abi_breaking" + } else { + "0" + }; let sql = format!( "SELECT p.id, p.name, + {real_name_expr}, p.version, p.revision, + {abi_breaking_expr}, {completed_at_expr}, p.filename, p.size, @@ -1654,16 +1695,18 @@ fn find_cached_binary_repo_packages( BinaryRepoPackageRecord { repo_name: repo_name.to_string(), name: row.get(1)?, - version: row.get(2)?, - revision: row.get::<_, i64>(3)? as u32, - completed_at: row.get(4)?, - filename: row.get(5)?, - size: row.get::<_, i64>(6)? as u64, - sha256: row.get(7)?, - sha512: row.get(8)?, - description: row.get(9)?, - homepage: row.get(10)?, - license: row.get(11)?, + real_name: row.get(2)?, + version: row.get(3)?, + revision: row.get::<_, i64>(4)? as u32, + abi_breaking: row.get(5)?, + completed_at: row.get(6)?, + filename: row.get(7)?, + size: row.get::<_, i64>(8)? as u64, + sha256: row.get(9)?, + sha512: row.get(10)?, + description: row.get(11)?, + homepage: row.get(12)?, + license: row.get(13)?, provides: Vec::new(), conflicts: Vec::new(), runtime_dependencies: Vec::new(), @@ -1696,12 +1739,24 @@ fn list_cached_binary_repo_packages( } else { "NULL" }; + let real_name_expr = if repo_packages_have_real_name(&conn)? { + "p.real_name" + } else { + "NULL" + }; + let abi_breaking_expr = if repo_packages_have_abi_breaking(&conn)? { + "p.abi_breaking" + } else { + "0" + }; let sql = format!( "SELECT p.id, p.name, + {real_name_expr}, p.version, p.revision, + {abi_breaking_expr}, {completed_at_expr}, p.filename, p.size, @@ -1722,16 +1777,18 @@ fn list_cached_binary_repo_packages( BinaryRepoPackageRecord { repo_name: repo_name.to_string(), name: row.get(1)?, - version: row.get(2)?, - revision: row.get::<_, i64>(3)? as u32, - completed_at: row.get(4)?, - filename: row.get(5)?, - size: row.get::<_, i64>(6)? as u64, - sha256: row.get(7)?, - sha512: row.get(8)?, - description: row.get(9)?, - homepage: row.get(10)?, - license: row.get(11)?, + real_name: row.get(2)?, + version: row.get(3)?, + revision: row.get::<_, i64>(4)? as u32, + abi_breaking: row.get(5)?, + completed_at: row.get(6)?, + filename: row.get(7)?, + size: row.get::<_, i64>(8)? as u64, + sha256: row.get(9)?, + sha512: row.get(10)?, + description: row.get(11)?, + homepage: row.get(12)?, + license: row.get(13)?, provides: Vec::new(), conflicts: Vec::new(), runtime_dependencies: Vec::new(), @@ -1764,6 +1821,30 @@ fn repo_packages_have_completed_at(conn: &Connection) -> Result { .context("Failed to inspect binary repo DB schema") } +fn repo_packages_have_real_name(conn: &Connection) -> Result { + conn.query_row( + "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'real_name'", + [], + |row| { + let count: i64 = row.get(0)?; + Ok(count > 0) + }, + ) + .context("Failed to inspect binary repo DB schema") +} + +fn repo_packages_have_abi_breaking(conn: &Connection) -> Result { + conn.query_row( + "SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'abi_breaking'", + [], + |row| { + let count: i64 = row.get(0)?; + Ok(count > 0) + }, + ) + .context("Failed to inspect binary repo DB schema") +} + fn path_modified_unix_timestamp(path: &Path) -> Result> { let metadata = fs::metadata(path) .with_context(|| format!("Failed to read metadata for {}", path.display()))?; @@ -2350,8 +2431,10 @@ mod tests { let metadata = r#" name = "test" +real_name = "icu" version = "1.0" revision = 1 +abi_breaking = true description = "test description" homepage = "https://example.com" license = "MIT" @@ -2385,8 +2468,10 @@ optional = [] type PackageRow = ( String, + Option, String, i64, + i64, Option, Option, Option, @@ -2394,9 +2479,9 @@ optional = [] String, ); - let (name, version, revision, desc, home, lic, sha256, sha512): PackageRow = conn + let (name, real_name, version, revision, abi_breaking, desc, home, lic, sha256, sha512): PackageRow = conn .query_row( - "SELECT name, version, revision, description, homepage, license, sha256, sha512 FROM packages", + "SELECT name, real_name, version, revision, abi_breaking, description, homepage, license, sha256, sha512 FROM packages", [], |r| { Ok(( @@ -2408,14 +2493,18 @@ optional = [] r.get(5)?, r.get(6)?, r.get(7)?, + r.get(8)?, + r.get(9)?, )) }, ) .unwrap(); assert_eq!(name, "test"); + assert_eq!(real_name, Some("icu".to_string())); assert_eq!(version, "1.0"); assert_eq!(revision, 1); + assert_eq!(abi_breaking, 1); assert_eq!(desc, Some("test description".to_string())); assert_eq!(home, Some("https://example.com".to_string())); assert_eq!(lic, Some("MIT".to_string())); @@ -2651,8 +2740,10 @@ revision = 1 let rec = BinaryRepoPackageRecord { repo_name: "repo".into(), name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: "pkg.depot.pkg.tar.zst".into(), size: 7, @@ -2687,8 +2778,10 @@ revision = 1 BinaryRepoPackageRecord { repo_name: "repo".into(), name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, + abi_breaking: false, completed_at: None, filename: filename.to_string(), size: payload.len() as u64, diff --git a/src/deps.rs b/src/deps.rs index 14322f7..cb03881 100755 --- a/src/deps.rs +++ b/src/deps.rs @@ -483,10 +483,12 @@ mod tests { PackageSpec { package: crate::package::PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -558,10 +560,12 @@ mod tests { let spec = PackageSpec { package: crate::package::PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -605,10 +609,12 @@ mod tests { let spec = PackageSpec { package: crate::package::PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -652,18 +658,22 @@ mod tests { let spec = PackageSpec { package: crate::package::PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: vec![crate::package::PackageInfo { name: "foo-libs".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }], alternatives: Default::default(), diff --git a/src/install/scripts.rs b/src/install/scripts.rs index a4ad5fd..7419df7 100644 --- a/src/install/scripts.rs +++ b/src/install/scripts.rs @@ -1033,10 +1033,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/package/interactive.rs b/src/package/interactive.rs index 956d538..65e718d 100644 --- a/src/package/interactive.rs +++ b/src/package/interactive.rs @@ -674,10 +674,12 @@ pub fn create_interactive() -> Result { Ok(PackageSpec { package: PackageInfo { name, + real_name: None, version, revision: 1, description, homepage, + abi_breaking: false, license, }, packages: Vec::new(), @@ -708,6 +710,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { // package block let mut pkg = Table::new(); pkg.insert("name".into(), Value::String(spec.package.name.clone())); + if let Some(real_name) = &spec.package.real_name { + pkg.insert("real_name".into(), Value::String(real_name.clone())); + } pkg.insert( "version".into(), Value::String(spec.package.version.clone()), @@ -724,6 +729,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { Value::String(spec.package.homepage.clone()), ); } + if spec.package.abi_breaking { + pkg.insert("abi_breaking".into(), Value::Boolean(true)); + } if let Some(v) = licenses_to_toml_value(&spec.package.license) { pkg.insert("license".into(), v); } @@ -735,6 +743,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { for p in &spec.packages { let mut pt = Table::new(); pt.insert("name".into(), Value::String(p.name.clone())); + if let Some(real_name) = &p.real_name { + pt.insert("real_name".into(), Value::String(real_name.clone())); + } pt.insert("version".into(), Value::String(p.version.clone())); if !p.description.is_empty() { pt.insert("description".into(), Value::String(p.description.clone())); @@ -742,6 +753,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result { if !p.homepage.is_empty() { pt.insert("homepage".into(), Value::String(p.homepage.clone())); } + if p.abi_breaking { + pt.insert("abi_breaking".into(), Value::Boolean(true)); + } if let Some(v) = licenses_to_toml_value(&p.license) { pt.insert("license".into(), v); } @@ -1483,10 +1497,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1526,18 +1542,22 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: vec![PackageInfo { name: "foo-dev".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "dev files".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }], alternatives: Alternatives::default(), @@ -1570,10 +1590,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into(), "Apache-2.0".into()], }, packages: Vec::new(), @@ -1651,10 +1673,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1728,10 +1752,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1764,10 +1790,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1821,10 +1849,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "A test".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1880,10 +1910,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "foo-meta".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "Meta package".into(), homepage: "".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1919,10 +1951,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "vertex-keyring".into(), + real_name: None, version: "1.0.0".into(), revision: 1, description: "keyring".into(), homepage: "https://www.vertexlinux.net".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/package/packager.rs b/src/package/packager.rs index 7a92197..9ea7952 100644 --- a/src/package/packager.rs +++ b/src/package/packager.rs @@ -138,6 +138,12 @@ impl Packager { "version".to_string(), toml::Value::String(self.spec.package.version.clone()), ); + if let Some(real_name) = &self.spec.package.real_name { + map.insert( + "real_name".to_string(), + toml::Value::String(real_name.clone()), + ); + } map.insert( "revision".to_string(), toml::Value::Integer(self.spec.package.revision as i64), @@ -150,6 +156,10 @@ impl Packager { "homepage".to_string(), toml::Value::String(self.spec.package.homepage.clone()), ); + map.insert( + "abi_breaking".to_string(), + toml::Value::Boolean(self.spec.package.abi_breaking), + ); map.insert( "license".to_string(), license_value(&self.spec.package.license), @@ -294,10 +304,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "test".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -455,6 +467,26 @@ mod tests { assert_eq!(arr[1].as_str(), Some("Apache-2.0")); } + #[test] + fn test_generate_metadata_toml_includes_real_name_and_abi_breaking() { + let tmp = tempfile::tempdir().unwrap(); + let dest = tmp.path(); + + let mut packager = mk_packager(dest.to_path_buf()); + packager.spec.package.real_name = Some("icu".into()); + packager.spec.package.abi_breaking = true; + packager.generate_metadata_toml().unwrap(); + + let meta_path = dest.join(".metadata.toml"); + let content = fs::read_to_string(meta_path).unwrap(); + let val: toml::Value = toml::from_str(&content).unwrap(); + assert_eq!(val.get("real_name").and_then(|v| v.as_str()), Some("icu")); + assert_eq!( + val.get("abi_breaking").and_then(|v| v.as_bool()), + Some(true) + ); + } + #[test] fn test_generate_metadata_toml_includes_keep_paths() { let tmp = tempfile::tempdir().unwrap(); diff --git a/src/package/spec.rs b/src/package/spec.rs index 07840d2..64ca230 100755 --- a/src/package/spec.rs +++ b/src/package/spec.rs @@ -3578,10 +3578,12 @@ type = "custom" PackageSpec { package: PackageInfo { name: name.into(), + real_name: None, version: version.into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -3633,12 +3635,18 @@ impl fmt::Display for PackageSpec { #[derive(Debug, Deserialize, serde::Serialize, Clone)] pub struct PackageInfo { pub name: String, + /// Stable package stream name used to associate renamed ABI-split packages. + #[serde(default, alias = "real-name", skip_serializing_if = "Option::is_none")] + pub real_name: Option, pub version: String, /// Maintenance revision of the package (defaults to 1) #[serde(default = "default_revision")] pub revision: u32, pub description: String, pub homepage: String, + /// When true, renamed updates retain versioned shared libraries from the old package. + #[serde(default, alias = "abi-breaking")] + pub abi_breaking: bool, #[serde( deserialize_with = "deserialize_licenses", serialize_with = "serialize_licenses" @@ -3646,6 +3654,13 @@ pub struct PackageInfo { pub license: Vec, } +impl PackageInfo { + /// Return the stable package stream name, defaulting to the package name. + pub fn effective_real_name(&self) -> &str { + self.real_name.as_deref().unwrap_or(&self.name) + } +} + fn default_revision() -> u32 { 1 } diff --git a/src/planner.rs b/src/planner.rs index 41381ff..9f771a5 100644 --- a/src/planner.rs +++ b/src/planner.rs @@ -806,18 +806,22 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: vec![PackageInfo { name: "foo-libs".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }], alternatives: Alternatives::default(), @@ -878,8 +882,10 @@ mod tests { record: Box::new(db::repo::BinaryRepoPackageRecord { repo_name: repo_name.to_string(), name: name.to_string(), + real_name: None, version: "1.0.0".to_string(), revision: 1, + abi_breaking: false, completed_at: None, filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"), size: 1024, diff --git a/src/source/extractor.rs b/src/source/extractor.rs index 83582f8..917e80e 100755 --- a/src/source/extractor.rs +++ b/src/source/extractor.rs @@ -724,10 +724,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "pkg".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/source/hooks.rs b/src/source/hooks.rs index bc1611d..0c698df 100755 --- a/src/source/hooks.rs +++ b/src/source/hooks.rs @@ -320,10 +320,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/source/mod.rs b/src/source/mod.rs index cdc8c5b..05542a1 100755 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -499,10 +499,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -556,10 +558,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -616,10 +620,12 @@ mod tests { let spec = PackageSpec { package: PackageInfo { name: "json".into(), + real_name: None, version: "3.11.3".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), diff --git a/src/staging/mod.rs b/src/staging/mod.rs index 871469c..c1c5fce 100755 --- a/src/staging/mod.rs +++ b/src/staging/mod.rs @@ -1504,10 +1504,12 @@ mod tests { PackageSpec { package: PackageInfo { name: "foo".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "d".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }, packages: Vec::new(), @@ -1608,10 +1610,12 @@ mod tests { let mut spec = mk_spec_for_stage_processing(); spec.packages.push(PackageInfo { name: "foo-dev".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "dev".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }); spec.build.flags.split_docs = true; @@ -1677,18 +1681,22 @@ mod tests { let mut spec = mk_spec_for_stage_processing(); spec.packages.push(PackageInfo { name: "foo-dev".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "dev".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["MIT".into()], }); spec.packages.push(PackageInfo { name: "foo-extras".into(), + real_name: None, version: "1.0".into(), revision: 1, description: "extras".into(), homepage: "h".into(), + abi_breaking: false, license: vec!["Apache-2.0".into()], });