feat: add support for renamed package updates and ABI-breaking flags

- Introduced `real_name` field in `InstalledPackageRecord` and `PackageInfo` to handle renamed packages.
- Added `abi_breaking` field to manage versioned shared libraries during renamed updates.
- Updated database schema to include `real_name` and `abi_breaking` columns.
- Enhanced package registration logic to support replacement of renamed packages while retaining specified files and directories.
- Modified various functions and tests to accommodate the new fields and ensure backward compatibility.
- Updated TOML serialization to include `real_name` and `abi_breaking` in package metadata.
This commit is contained in:
2026-03-15 16:31:44 -05:00
parent a6a752bf69
commit 167467d169
25 changed files with 1001 additions and 177 deletions
Generated
+1 -1
View File
@@ -418,7 +418,7 @@ checksum = "807800ff3288b621186fe0a8f3392c4652068257302709c24efd918c3dffcdc2"
[[package]] [[package]]
name = "depot" name = "depot"
version = "0.23.1" version = "0.24.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"ar", "ar",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "depot" name = "depot"
version = "0.23.1" version = "0.24.0"
edition = "2024" edition = "2024"
[lints.rust] [lints.rust]
+1 -1
View File
@@ -1,6 +1,6 @@
project( project(
'depot', 'depot',
version: '0.23.1', version: '0.24.0',
meson_version: '>=0.60.0', meson_version: '>=0.60.0',
default_options: ['buildtype=release'], default_options: ['buildtype=release'],
) )
+8
View File
@@ -879,10 +879,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.2.3".into(), version: "1.2.3".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1172,10 +1174,12 @@ foo: bar
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "expect".into(), name: "expect".into(),
real_name: None,
version: "5.45.4".into(), version: "5.45.4".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1204,10 +1208,12 @@ foo: bar
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1238,10 +1244,12 @@ foo: bar
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -66,10 +66,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+51 -92
View File
@@ -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<String> { fn cmake_install_dir_args(flags: &crate::package::BuildFlags) -> Vec<String> {
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 dirs = crate::builder::install_dirs(flags);
let defaults = [ let defaults = [
( ("CMAKE_INSTALL_BINDIR", dirs.bindir),
"CMAKE_INSTALL_BINDIR", ("CMAKE_INSTALL_SBINDIR", dirs.sbindir),
cmake_install_dir_value(prefix, &dirs.bindir), ("CMAKE_INSTALL_LIBDIR", dirs.libdir),
), ("CMAKE_INSTALL_LIBEXECDIR", dirs.libexecdir),
( ("CMAKE_INSTALL_SYSCONFDIR", dirs.sysconfdir),
"CMAKE_INSTALL_SBINDIR", ("CMAKE_INSTALL_LOCALSTATEDIR", dirs.localstatedir),
cmake_install_dir_value(prefix, &dirs.sbindir), ("CMAKE_INSTALL_SHAREDSTATEDIR", dirs.sharedstatedir),
), ("CMAKE_INSTALL_INCLUDEDIR", dirs.includedir),
( ("CMAKE_INSTALL_DATAROOTDIR", dirs.datarootdir),
"CMAKE_INSTALL_LIBDIR", ("CMAKE_INSTALL_DATADIR", dirs.datadir),
cmake_install_dir_value(prefix, &dirs.libdir), ("CMAKE_INSTALL_MANDIR", dirs.mandir),
), ("CMAKE_INSTALL_INFODIR", dirs.infodir),
(
"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),
),
]; ];
defaults defaults
@@ -429,26 +391,6 @@ fn lib32_target_triple(
host.map(|host| crate::cross::lib32_target_triple(&host)) 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 { fn num_cpus() -> usize {
std::thread::available_parallelism() std::thread::available_parallelism()
.map(|n| n.get()) .map(|n| n.get())
@@ -632,10 +574,13 @@ mod tests {
#[test] #[test]
fn test_cmake_install_dir_args_include_defaults() { fn test_cmake_install_dir_args_include_defaults() {
let args = cmake_install_dir_args(&BuildFlags::default()); 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_BINDIR=/usr/bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=bin")); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_SBINDIR=/usr/bin"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=lib")); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBDIR=/usr/lib"));
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_LIBEXECDIR=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().any(|a| a == "-DCMAKE_INSTALL_SYSCONFDIR=/etc"));
assert!( assert!(
args.iter() args.iter()
@@ -647,17 +592,41 @@ mod tests {
); );
assert!( assert!(
args.iter() args.iter()
.any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=include") .any(|a| a == "-DCMAKE_INSTALL_INCLUDEDIR=/usr/include")
); );
assert!( assert!(
args.iter() 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!( assert!(
args.iter() 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() .iter()
.any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR=")) .any(|a| a.starts_with("-DCMAKE_INSTALL_DATADIR="))
); );
assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=bin")); assert!(args.iter().any(|a| a == "-DCMAKE_INSTALL_BINDIR=/usr/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");
} }
#[test] #[test]
@@ -772,10 +729,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "x".into(), name: "x".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "".into(), description: "".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+6
View File
@@ -273,10 +273,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -373,10 +375,12 @@ depot_install_dev_pkg() {
let mut spec = mk_spec("demo", "1.0"); let mut spec = mk_spec("demo", "1.0");
spec.packages.push(crate::package::PackageInfo { spec.packages.push(crate::package::PackageInfo {
name: "dev-pkg".into(), name: "dev-pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
@@ -421,10 +425,12 @@ depot_install() {
let mut spec = mk_spec("demo", "1.0"); let mut spec = mk_spec("demo", "1.0");
spec.packages.push(crate::package::PackageInfo { spec.packages.push(crate::package::PackageInfo {
name: "dev-pkg".into(), name: "dev-pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
+2
View File
@@ -127,10 +127,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+14
View File
@@ -516,6 +516,18 @@ mod tests {
assert!(args.iter().any(|a| a == "--bindir=/usr/bin")); 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] #[test]
fn test_meson_setup_args_include_linker_override() { fn test_meson_setup_args_include_linker_override() {
let flags = BuildFlags { let flags = BuildFlags {
@@ -629,10 +641,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -457,10 +457,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "env-test".into(), name: "env-test".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -348,10 +348,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -781,10 +781,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "py-test".into(), name: "py-test".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+496 -37
View File
@@ -634,6 +634,10 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(), .to_string(),
real_name: metadata
.get("real_name")
.and_then(|v| v.as_str())
.map(String::from),
version: metadata version: metadata
.get("version") .get("version")
.and_then(|v| v.as_str()) .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()) .and_then(|v| v.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(), .to_string(),
abi_breaking: metadata
.get("abi_breaking")
.and_then(|v| v.as_bool())
.unwrap_or(false),
license: parse_licenses_from_toml(metadata), license: parse_licenses_from_toml(metadata),
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -722,10 +730,12 @@ fn package_spec_from_repo_record(
package::PackageSpec { package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: record.name.clone(), name: record.name.clone(),
real_name: record.real_name.clone(),
version: record.version.clone(), version: record.version.clone(),
revision: record.revision, revision: record.revision,
description: record.description.clone().unwrap_or_default(), description: record.description.clone().unwrap_or_default(),
homepage: record.homepage.clone().unwrap_or_default(), homepage: record.homepage.clone().unwrap_or_default(),
abi_breaking: record.abi_breaking,
license: parse_license_list_from_repo(&record.license), license: parse_license_list_from_repo(&record.license),
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1193,9 +1203,31 @@ fn build_lib32_companion_package(
struct PlannedStagedInstall { struct PlannedStagedInstall {
is_update: bool, is_update: bool,
remove_paths: Vec<String>, remove_paths: Vec<String>,
renamed_transition: Option<RenamedPackageTransition>,
hook_context: install::hooks::HookExecutionContextOwned, hook_context: install::hooks::HookExecutionContextOwned,
} }
#[derive(Debug, Clone)]
struct RenamedPackageTransition {
replaced: db::InstalledPackageRecord,
retained_files: Vec<String>,
retained_directories: Vec<String>,
}
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)] #[derive(Debug, Clone)]
struct PlannedPackageInstall { struct PlannedPackageInstall {
spec: package::PackageSpec, spec: package::PackageSpec,
@@ -1468,6 +1500,121 @@ fn resolve_installed_conflicts_for_subjects(
Ok(()) 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<String> {
let new_files: HashSet<&str> = new_manifest.files.iter().map(String::as_str).collect();
let mut retained: Vec<String> = 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<String> {
let retained_files: HashSet<&str> = retained_files.iter().map(String::as_str).collect();
let mut directories: Vec<String> = 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<Item = &'a db::InstalledPackageRecord>,
) -> 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<Option<RenamedPackageTransition>> {
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( fn plan_staged_install(
pkg_spec: &package::PackageSpec, pkg_spec: &package::PackageSpec,
destdir: &Path, destdir: &Path,
@@ -1482,10 +1629,38 @@ fn plan_staged_install(
})?; })?;
let db_path = config.installed_db_path(rootfs); 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 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)?; 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 { let operation = if is_update {
install::hooks::HookOperation::Update install::hooks::HookOperation::Update
} else { } else {
@@ -1499,6 +1674,7 @@ fn plan_staged_install(
Ok(PlannedStagedInstall { Ok(PlannedStagedInstall {
is_update, is_update,
remove_paths, remove_paths,
renamed_transition,
hook_context: install::hooks::HookExecutionContextOwned { hook_context: install::hooks::HookExecutionContextOwned {
operation, operation,
package: pkg_spec.package.name.clone(), package: pkg_spec.package.name.clone(),
@@ -1582,12 +1758,27 @@ fn install_staged_to_rootfs(
)?; )?;
let db_path = config.installed_db_path(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(); let _ = tx.rollback();
return Err(e); return Err(e);
} }
tx.commit()?; 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( install::scripts::sync_staged_scripts_to_rootfs(
&staged_scripts_dir, &staged_scripts_dir,
rootfs, rootfs,
@@ -2028,7 +2219,8 @@ enum UpdateOrigin {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct UpdateCandidate { struct UpdateCandidate {
package: String, installed_package: String,
candidate_package: String,
installed_version: String, installed_version: String,
installed_revision: u32, installed_revision: u32,
installed_completed_at: Option<i64>, installed_completed_at: Option<i64>,
@@ -2243,7 +2435,7 @@ fn configured_source_scan_roots(config: &config::Config) -> Vec<(String, i32, Pa
fn collect_best_source_update_candidates( fn collect_best_source_update_candidates(
config: &config::Config, config: &config::Config,
target_names: &HashSet<String>, target_real_names: &HashSet<String>,
) -> Result<HashMap<String, SourceUpdateCandidate>> { ) -> Result<HashMap<String, SourceUpdateCandidate>> {
let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new(); let mut best: HashMap<String, SourceUpdateCandidate> = HashMap::new();
@@ -2255,7 +2447,8 @@ fn collect_best_source_update_candidates(
for spec_path in scan_package_specs(&root)? { for spec_path in scan_package_specs(&root)? {
let mut spec = package::PackageSpec::from_file(&spec_path)?; let mut spec = package::PackageSpec::from_file(&spec_path)?;
spec.apply_config(config); 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; continue;
} }
@@ -2267,7 +2460,7 @@ fn collect_best_source_update_candidates(
spec, spec,
}; };
let replace = match best.get(&candidate.spec.package.name) { let replace = match best.get(&stream_name) {
Some(current) => match compare_package_release( Some(current) => match compare_package_release(
&candidate.spec.package.version, &candidate.spec.package.version,
candidate.spec.package.revision, candidate.spec.package.revision,
@@ -2296,7 +2489,7 @@ fn collect_best_source_update_candidates(
}; };
if replace { 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( fn collect_best_binary_update_candidates(
config: &config::Config, config: &config::Config,
rootfs: &Path, rootfs: &Path,
target_names: &HashSet<String>, target_real_names: &HashSet<String>,
) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> { ) -> Result<HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)>> {
let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new(); let mut best: HashMap<String, (i32, db::repo::BinaryRepoPackageRecord)> = HashMap::new();
let host_arch = std::env::consts::ARCH; 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))?; .with_context(|| format!("Failed to list binary repo '{}'", repo_name))?;
for record in records { 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; continue;
} }
let replace = match best.get(&record.name) { let replace = match best.get(&stream_name) {
Some((current_priority, current)) => match compare_package_release( Some((current_priority, current)) => match compare_package_release(
&record.version, &record.version,
record.revision, record.revision,
@@ -2361,7 +2555,7 @@ fn collect_best_binary_update_candidates(
}; };
if replace { 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, prefer_binary: bool,
) -> Option<UpdateCandidate> { ) -> Option<UpdateCandidate> {
let mut best: Option<UpdateCandidate> = None; let mut best: Option<UpdateCandidate> = 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( && update_candidate_is_newer_than_installed(
&candidate.spec.package.version, &candidate.spec.package.version,
candidate.spec.package.revision, candidate.spec.package.revision,
@@ -2389,7 +2584,8 @@ fn select_update_candidate(
) )
{ {
best = Some(UpdateCandidate { 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_version: installed.version.clone(),
installed_revision: installed.revision, installed_revision: installed.revision,
installed_completed_at, 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( && update_candidate_is_newer_than_installed(
&record.version, &record.version,
record.revision, record.revision,
@@ -2418,7 +2614,8 @@ fn select_update_candidate(
) )
{ {
let binary_candidate = UpdateCandidate { let binary_candidate = UpdateCandidate {
package: installed.name.clone(), installed_package: installed.name.clone(),
candidate_package: record.name.clone(),
installed_version: installed.version.clone(), installed_version: installed.version.clone(),
installed_revision: installed.revision, installed_revision: installed.revision,
installed_completed_at, installed_completed_at,
@@ -2456,36 +2653,53 @@ fn collect_update_candidates(
return Ok(Vec::new()); return Ok(Vec::new());
} }
let installed_by_name: HashMap<_, _> = installed let mut installed_by_real_name: HashMap<String, Vec<&db::InstalledPackageRecord>> =
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<String, db::InstalledPackageRecord> = installed_by_real_name
.iter() .iter()
.filter_map(|(real_name, records)| {
select_primary_installed_record(records.iter().copied())
.cloned() .cloned()
.map(|record| (record.name.clone(), record)) .map(|record| (real_name.clone(), record))
})
.collect(); .collect();
let target_names: HashSet<String> = if requested_packages.is_empty() { let target_real_names: HashSet<String> = if requested_packages.is_empty() {
installed_by_name.keys().cloned().collect() active_by_real_name.keys().cloned().collect()
} else { } else {
requested_packages.iter().cloned().collect() let mut targets = HashSet::new();
};
for package in requested_packages { for package in requested_packages {
if !installed_by_name.contains_key(package) { 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)); ui::warn(format!("Package '{}' is not installed; skipping", package));
} }
} }
targets
};
let source_candidates = if config.repo_settings.prefer_binary { let source_candidates = if config.repo_settings.prefer_binary {
HashMap::new() HashMap::new()
} else { } else {
collect_best_source_update_candidates(config, &target_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 updates = Vec::new();
let mut targets: Vec<_> = target_names.into_iter().collect(); let mut targets: Vec<_> = target_real_names.into_iter().collect();
targets.sort(); targets.sort();
for target in targets { for target in targets {
let Some(installed) = installed_by_name.get(&target) else { let Some(installed) = active_by_real_name.get(&target) else {
continue; continue;
}; };
let installed_completed_at = installed_package_completed_at(installed, &db_path, rootfs)?; let installed_completed_at = installed_package_completed_at(installed, &db_path, rootfs)?;
@@ -2509,7 +2723,7 @@ fn collect_missing_update_dependencies(
) -> Result<Vec<String>> { ) -> Result<Vec<String>> {
let mut planned_provides = HashSet::new(); let mut planned_provides = HashSet::new();
for candidate in candidates { for candidate in candidates {
planned_provides.insert(candidate.package.clone()); planned_provides.insert(candidate.candidate_package.clone());
for provide in &candidate.provides { for provide in &candidate.provides {
planned_provides.insert(provide.clone()); planned_provides.insert(provide.clone());
} }
@@ -2571,10 +2785,11 @@ fn run_update_command(
.iter() .iter()
.map(|candidate| { .map(|candidate| {
let summary = format!( let summary = format!(
"{} v{}-{} -> v{}-{}", "{} v{}-{} -> {} v{}-{}",
candidate.package, candidate.installed_package,
candidate.installed_version, candidate.installed_version,
candidate.installed_revision, candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version, candidate.candidate_version,
candidate.candidate_revision candidate.candidate_revision
); );
@@ -2595,7 +2810,7 @@ fn run_update_command(
let conflict_subjects: Vec<_> = updates let conflict_subjects: Vec<_> = updates
.iter() .iter()
.map(|candidate| InstallConflictSubject { .map(|candidate| InstallConflictSubject {
package: candidate.package.clone(), package: candidate.candidate_package.clone(),
provides: candidate.provides.clone(), provides: candidate.provides.clone(),
conflicts: candidate.conflicts.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")?; let exe = std::env::current_exe().context("Failed to locate depot executable")?;
for (idx, candidate) in updates.iter().enumerate() { for (idx, candidate) in updates.iter().enumerate() {
ui::info(format!( ui::info(format!(
"[{}/{}] updating {} v{}-{} -> v{}-{}", "[{}/{}] updating {} v{}-{} -> {} v{}-{}",
idx + 1, idx + 1,
updates.len(), updates.len(),
candidate.package, candidate.installed_package,
candidate.installed_version, candidate.installed_version,
candidate.installed_revision, candidate.installed_revision,
candidate.candidate_package,
candidate.candidate_version, candidate.candidate_version,
candidate.candidate_revision candidate.candidate_revision
)); ));
@@ -5415,8 +5631,10 @@ mod tests {
db::repo::BinaryRepoPackageRecord { db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(), repo_name: "core".into(),
name: name.into(), name: name.into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: filename.into(), filename: filename.into(),
size: 1, size: 1,
@@ -5449,10 +5667,12 @@ mod tests {
package::PackageSpec { package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -5598,8 +5818,10 @@ optional = []
let record = db::repo::BinaryRepoPackageRecord { let record = db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(), repo_name: "core".into(),
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: archive_path filename: archive_path
.file_name() .file_name()
@@ -5897,8 +6119,10 @@ optional = []
let record = db::repo::BinaryRepoPackageRecord { let record = db::repo::BinaryRepoPackageRecord {
repo_name: "core".into(), repo_name: "core".into(),
name: "sudo".into(), name: "sudo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: archive_path filename: archive_path
.file_name() .file_name()
@@ -6017,10 +6241,12 @@ optional = []
let spec = package::PackageSpec { let spec = package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "filesystem".into(), name: "filesystem".into(),
real_name: None,
version: "1.0.1".into(), version: "1.0.1".into(),
revision: 3, revision: 3,
description: "Base filesystem".into(), description: "Base filesystem".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["Unlicense".into()], license: vec!["Unlicense".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -6047,6 +6273,117 @@ optional = []
Ok(()) 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] #[test]
fn merge_missing_dependencies_preserves_order_and_uniqueness() { fn merge_missing_dependencies_preserves_order_and_uniqueness() {
let merged = merge_missing_dependencies( let merged = merge_missing_dependencies(
@@ -6061,21 +6398,130 @@ optional = []
assert_eq!(merged, vec!["make", "pkgconf", "glibc", "openssl", "zlib"]); 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] #[test]
fn update_candidate_prefers_binary_when_versions_match_and_config_does() { fn update_candidate_prefers_binary_when_versions_match_and_config_does() {
let installed = db::InstalledPackageRecord { let installed = db::InstalledPackageRecord {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
}; };
let source_spec = package::PackageSpec { let source_spec = package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.1.0".into(), version: "1.1.0".into(),
revision: 1, revision: 1,
description: "test".into(), description: "test".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -6115,8 +6561,10 @@ optional = []
db::repo::BinaryRepoPackageRecord { db::repo::BinaryRepoPackageRecord {
repo_name: "binary".into(), repo_name: "binary".into(),
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.1.0".into(), version: "1.1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: "pkg-1.1.0-1-x86_64.depot.pkg.tar.zst".into(), filename: "pkg-1.1.0-1-x86_64.depot.pkg.tar.zst".into(),
size: 1, size: 1,
@@ -6148,17 +6596,21 @@ optional = []
fn select_update_candidate_uses_newer_timestamp_when_versions_match() { fn select_update_candidate_uses_newer_timestamp_when_versions_match() {
let installed = db::InstalledPackageRecord { let installed = db::InstalledPackageRecord {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: Some(100), completed_at: Some(100),
}; };
let source_spec = package::PackageSpec { let source_spec = package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
description: "test".into(), description: "test".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -6227,10 +6679,12 @@ optional = []
let installed_spec = package::PackageSpec { let installed_spec = package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
description: "pkg".into(), description: "pkg".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -6397,10 +6851,12 @@ optional = []
let libc_spec = package::PackageSpec { let libc_spec = package::PackageSpec {
package: package::PackageInfo { package: package::PackageInfo {
name: "glibc".into(), name: "glibc".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "glibc".into(), description: "glibc".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -6424,7 +6880,8 @@ optional = []
let missing = collect_missing_update_dependencies( let missing = collect_missing_update_dependencies(
&[ &[
UpdateCandidate { UpdateCandidate {
package: "pkg".into(), installed_package: "pkg".into(),
candidate_package: "pkg".into(),
installed_version: "1.0".into(), installed_version: "1.0".into(),
installed_revision: 1, installed_revision: 1,
installed_completed_at: None, installed_completed_at: None,
@@ -6441,7 +6898,8 @@ optional = []
}, },
}, },
UpdateCandidate { UpdateCandidate {
package: "helper".into(), installed_package: "helper".into(),
candidate_package: "helper".into(),
installed_version: "1.0".into(), installed_version: "1.0".into(),
installed_revision: 1, installed_revision: 1,
installed_completed_at: None, installed_completed_at: None,
@@ -6458,7 +6916,8 @@ optional = []
}, },
}, },
UpdateCandidate { UpdateCandidate {
package: "tool".into(), installed_package: "tool".into(),
candidate_package: "tool".into(),
installed_version: "1.0".into(), installed_version: "1.0".into(),
installed_revision: 1, installed_revision: 1,
installed_completed_at: None, installed_completed_at: None,
+175 -17
View File
@@ -21,14 +21,57 @@ fn verbose_remove_output() -> bool {
/// Installed package row from the local package database. /// Installed package row from the local package database.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalledPackageRecord { pub struct InstalledPackageRecord {
/// Installed package name.
pub name: String, pub name: String,
/// Stable package stream name used for renamed updates.
pub real_name: Option<String>,
/// Installed package version.
pub version: String, pub version: String,
/// Installed package revision.
pub revision: u32, pub revision: u32,
/// Whether renamed updates should retain versioned shared libraries.
pub abi_breaking: bool,
/// Package completion timestamp if known.
pub completed_at: Option<i64>, pub completed_at: Option<i64>,
} }
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<String>,
/// Subset of old-package directories to keep owned by the old package.
pub retained_directories: Vec<String>,
}
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 /// Initialize database and register a package
pub fn register_package(db_path: &Path, spec: &PackageSpec, destdir: &Path) -> Result<()> { 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) // Create parent directory (auto-create db dir if missing)
if let Some(parent) = db_path.parent() { if let Some(parent) = db_path.parent() {
fs::create_dir_all(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 // Generate manifest with files and directories
let manifest = staging::generate_manifest_with_dirs(destdir)?; 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). // Insert/update package without changing its primary key (UPSERT keeps the existing row).
tx.execute( tx.execute(
"INSERT INTO packages (name, version, revision, description, homepage, license, completed_at) "INSERT INTO packages (name, real_name, version, revision, description, homepage, license, abi_breaking, completed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
ON CONFLICT(name) DO UPDATE SET ON CONFLICT(name) DO UPDATE SET
real_name=excluded.real_name,
version=excluded.version, version=excluded.version,
revision=excluded.revision, revision=excluded.revision,
description=excluded.description, description=excluded.description,
homepage=excluded.homepage, homepage=excluded.homepage,
license=excluded.license, license=excluded.license,
abi_breaking=excluded.abi_breaking,
completed_at=excluded.completed_at", completed_at=excluded.completed_at",
params![ params![
spec.package.name, spec.package.name,
spec.package.real_name,
spec.package.version, spec.package.version,
spec.package.revision, spec.package.revision,
spec.package.description, spec.package.description,
spec.package.homepage, spec.package.homepage,
format_licenses(&spec.package.license), format_licenses(&spec.package.license),
spec.package.abi_breaking,
completed_at, completed_at,
], ],
)?; )?;
@@ -330,16 +419,7 @@ pub fn remove_package(db_path: &Path, name: &str, rootfs: &Path) -> Result<()> {
} }
// Remove from database // Remove from database
conn.execute("DELETE FROM files WHERE package_id = ?1", params![pkg_id])?; delete_package_rows(&conn, 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])?;
crate::log_info!( crate::log_info!(
"Removed {} files and {} directories", "Removed {} files and {} directories",
@@ -422,11 +502,13 @@ fn init_db(conn: &Connection) -> Result<()> {
CREATE TABLE IF NOT EXISTS packages ( CREATE TABLE IF NOT EXISTS packages (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL, name TEXT UNIQUE NOT NULL,
real_name TEXT,
version TEXT NOT NULL, version TEXT NOT NULL,
revision INTEGER NOT NULL DEFAULT 1, revision INTEGER NOT NULL DEFAULT 1,
description TEXT, description TEXT,
homepage TEXT, homepage TEXT,
license TEXT, license TEXT,
abi_breaking INTEGER NOT NULL DEFAULT 0,
completed_at INTEGER completed_at INTEGER
); );
@@ -460,6 +542,8 @@ fn init_db(conn: &Connection) -> Result<()> {
", ",
)?; )?;
ensure_packages_completed_at_column(conn)?; ensure_packages_completed_at_column(conn)?;
ensure_packages_real_name_column(conn)?;
ensure_packages_abi_breaking_column(conn)?;
Ok(()) Ok(())
} }
@@ -481,6 +565,73 @@ fn ensure_packages_completed_at_column(conn: &Connection) -> Result<()> {
Ok(()) 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<Option<i64>> { fn package_completed_at(destdir: &Path) -> Result<Option<i64>> {
let metadata_path = destdir.join(".metadata.toml"); let metadata_path = destdir.join(".metadata.toml");
if let Some(completed_at) = if let Some(completed_at) =
@@ -597,14 +748,19 @@ pub fn list_installed_package_records(db_path: &Path) -> Result<Vec<InstalledPac
let conn = Connection::open(db_path)?; let conn = Connection::open(db_path)?;
init_db(&conn)?; init_db(&conn)?;
let mut stmt = let mut stmt = conn.prepare(
conn.prepare("SELECT name, version, revision, completed_at FROM packages ORDER BY name")?; "SELECT name, real_name, version, revision, abi_breaking, completed_at
FROM packages
ORDER BY name",
)?;
let rows = stmt.query_map([], |row| { let rows = stmt.query_map([], |row| {
Ok(InstalledPackageRecord { Ok(InstalledPackageRecord {
name: row.get(0)?, name: row.get(0)?,
version: row.get(1)?, real_name: row.get(1)?,
revision: row.get::<_, i64>(2)? as u32, version: row.get(2)?,
completed_at: row.get(3)?, 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()) Ok(rows.filter_map(|row| row.ok()).collect())
@@ -708,10 +864,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+117 -24
View File
@@ -73,8 +73,10 @@ impl<R: Read> Read for HashingReader<R> {
struct IndexedPackage { struct IndexedPackage {
name: String, name: String,
real_name: Option<String>,
version: String, version: String,
revision: u32, revision: u32,
abi_breaking: bool,
completed_at: Option<i64>, completed_at: Option<i64>,
description: Option<String>, description: Option<String>,
homepage: Option<String>, homepage: Option<String>,
@@ -109,8 +111,10 @@ pub struct BinaryRepoSearchHit {
pub struct BinaryRepoPackageRecord { pub struct BinaryRepoPackageRecord {
pub repo_name: String, pub repo_name: String,
pub name: String, pub name: String,
pub real_name: Option<String>,
pub version: String, pub version: String,
pub revision: u32, pub revision: u32,
pub abi_breaking: bool,
pub completed_at: Option<i64>, pub completed_at: Option<i64>,
pub filename: String, pub filename: String,
pub size: u64, pub size: u64,
@@ -125,6 +129,13 @@ pub struct BinaryRepoPackageRecord {
pub optional_dependencies: Vec<String>, pub optional_dependencies: Vec<String>,
} }
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. /// Local cache paths for a binary package archive and its detached signature.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BinaryRepoCachedArchive { pub struct BinaryRepoCachedArchive {
@@ -283,8 +294,10 @@ impl RepoManager {
"CREATE TABLE packages ( "CREATE TABLE packages (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL, version TEXT NOT NULL,
revision INTEGER NOT NULL, revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
completed_at INTEGER, completed_at INTEGER,
description TEXT, description TEXT,
homepage TEXT, homepage TEXT,
@@ -346,8 +359,10 @@ impl RepoManager {
let mut hashing_reader = HashingReader::new(file); let mut hashing_reader = HashingReader::new(file);
let mut name = String::new(); let mut name = String::new();
let mut real_name = None;
let mut version = String::new(); let mut version = String::new();
let mut revision = 1; let mut revision = 1;
let mut abi_breaking = false;
let mut completed_at = path_modified_unix_timestamp(pkg_path)?; let mut completed_at = path_modified_unix_timestamp(pkg_path)?;
let mut description = None; let mut description = None;
let mut homepage = None; let mut homepage = None;
@@ -378,6 +393,10 @@ impl RepoManager {
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(); .to_string();
real_name = metadata
.get("real_name")
.and_then(|v| v.as_str())
.map(String::from);
version = metadata version = metadata
.get("version") .get("version")
.and_then(|v| v.as_str()) .and_then(|v| v.as_str())
@@ -387,6 +406,10 @@ impl RepoManager {
.get("revision") .get("revision")
.and_then(|v| v.as_integer()) .and_then(|v| v.as_integer())
.unwrap_or(1) as u32; .unwrap_or(1) as u32;
abi_breaking = metadata
.get("abi_breaking")
.and_then(|v| v.as_bool())
.unwrap_or(false);
completed_at = completed_at =
metadata_time::parse_completed_at_value(&metadata).or(completed_at); metadata_time::parse_completed_at_value(&metadata).or(completed_at);
description = metadata description = metadata
@@ -469,8 +492,10 @@ impl RepoManager {
Ok(IndexedPackage { Ok(IndexedPackage {
name, name,
real_name,
version, version,
revision, revision,
abi_breaking,
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -490,8 +515,10 @@ impl RepoManager {
fn insert_indexed_package(&self, conn: &mut Connection, indexed: IndexedPackage) -> Result<()> { fn insert_indexed_package(&self, conn: &mut Connection, indexed: IndexedPackage) -> Result<()> {
let IndexedPackage { let IndexedPackage {
name, name,
real_name,
version, version,
revision, revision,
abi_breaking,
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -509,12 +536,14 @@ impl RepoManager {
// Insert into database // Insert into database
conn.execute( conn.execute(
"INSERT INTO packages (name, version, revision, completed_at, description, homepage, license, filename, size, sha256, sha512) "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)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![ params![
name, name,
real_name,
version, version,
revision as i64, revision as i64,
abi_breaking,
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -1620,12 +1649,24 @@ fn find_cached_binary_repo_packages(
} else { } else {
"NULL" "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!( let sql = format!(
"SELECT "SELECT
p.id, p.id,
p.name, p.name,
{real_name_expr},
p.version, p.version,
p.revision, p.revision,
{abi_breaking_expr},
{completed_at_expr}, {completed_at_expr},
p.filename, p.filename,
p.size, p.size,
@@ -1654,16 +1695,18 @@ fn find_cached_binary_repo_packages(
BinaryRepoPackageRecord { BinaryRepoPackageRecord {
repo_name: repo_name.to_string(), repo_name: repo_name.to_string(),
name: row.get(1)?, name: row.get(1)?,
version: row.get(2)?, real_name: row.get(2)?,
revision: row.get::<_, i64>(3)? as u32, version: row.get(3)?,
completed_at: row.get(4)?, revision: row.get::<_, i64>(4)? as u32,
filename: row.get(5)?, abi_breaking: row.get(5)?,
size: row.get::<_, i64>(6)? as u64, completed_at: row.get(6)?,
sha256: row.get(7)?, filename: row.get(7)?,
sha512: row.get(8)?, size: row.get::<_, i64>(8)? as u64,
description: row.get(9)?, sha256: row.get(9)?,
homepage: row.get(10)?, sha512: row.get(10)?,
license: row.get(11)?, description: row.get(11)?,
homepage: row.get(12)?,
license: row.get(13)?,
provides: Vec::new(), provides: Vec::new(),
conflicts: Vec::new(), conflicts: Vec::new(),
runtime_dependencies: Vec::new(), runtime_dependencies: Vec::new(),
@@ -1696,12 +1739,24 @@ fn list_cached_binary_repo_packages(
} else { } else {
"NULL" "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!( let sql = format!(
"SELECT "SELECT
p.id, p.id,
p.name, p.name,
{real_name_expr},
p.version, p.version,
p.revision, p.revision,
{abi_breaking_expr},
{completed_at_expr}, {completed_at_expr},
p.filename, p.filename,
p.size, p.size,
@@ -1722,16 +1777,18 @@ fn list_cached_binary_repo_packages(
BinaryRepoPackageRecord { BinaryRepoPackageRecord {
repo_name: repo_name.to_string(), repo_name: repo_name.to_string(),
name: row.get(1)?, name: row.get(1)?,
version: row.get(2)?, real_name: row.get(2)?,
revision: row.get::<_, i64>(3)? as u32, version: row.get(3)?,
completed_at: row.get(4)?, revision: row.get::<_, i64>(4)? as u32,
filename: row.get(5)?, abi_breaking: row.get(5)?,
size: row.get::<_, i64>(6)? as u64, completed_at: row.get(6)?,
sha256: row.get(7)?, filename: row.get(7)?,
sha512: row.get(8)?, size: row.get::<_, i64>(8)? as u64,
description: row.get(9)?, sha256: row.get(9)?,
homepage: row.get(10)?, sha512: row.get(10)?,
license: row.get(11)?, description: row.get(11)?,
homepage: row.get(12)?,
license: row.get(13)?,
provides: Vec::new(), provides: Vec::new(),
conflicts: Vec::new(), conflicts: Vec::new(),
runtime_dependencies: Vec::new(), runtime_dependencies: Vec::new(),
@@ -1764,6 +1821,30 @@ fn repo_packages_have_completed_at(conn: &Connection) -> Result<bool> {
.context("Failed to inspect binary repo DB schema") .context("Failed to inspect binary repo DB schema")
} }
fn repo_packages_have_real_name(conn: &Connection) -> Result<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 binary repo DB schema")
}
fn repo_packages_have_abi_breaking(conn: &Connection) -> Result<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 binary repo DB schema")
}
fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> { fn path_modified_unix_timestamp(path: &Path) -> Result<Option<i64>> {
let metadata = fs::metadata(path) let metadata = fs::metadata(path)
.with_context(|| format!("Failed to read metadata for {}", path.display()))?; .with_context(|| format!("Failed to read metadata for {}", path.display()))?;
@@ -2350,8 +2431,10 @@ mod tests {
let metadata = r#" let metadata = r#"
name = "test" name = "test"
real_name = "icu"
version = "1.0" version = "1.0"
revision = 1 revision = 1
abi_breaking = true
description = "test description" description = "test description"
homepage = "https://example.com" homepage = "https://example.com"
license = "MIT" license = "MIT"
@@ -2385,8 +2468,10 @@ optional = []
type PackageRow = ( type PackageRow = (
String, String,
Option<String>,
String, String,
i64, i64,
i64,
Option<String>, Option<String>,
Option<String>, Option<String>,
Option<String>, Option<String>,
@@ -2394,9 +2479,9 @@ optional = []
String, 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( .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| { |r| {
Ok(( Ok((
@@ -2408,14 +2493,18 @@ optional = []
r.get(5)?, r.get(5)?,
r.get(6)?, r.get(6)?,
r.get(7)?, r.get(7)?,
r.get(8)?,
r.get(9)?,
)) ))
}, },
) )
.unwrap(); .unwrap();
assert_eq!(name, "test"); assert_eq!(name, "test");
assert_eq!(real_name, Some("icu".to_string()));
assert_eq!(version, "1.0"); assert_eq!(version, "1.0");
assert_eq!(revision, 1); assert_eq!(revision, 1);
assert_eq!(abi_breaking, 1);
assert_eq!(desc, Some("test description".to_string())); assert_eq!(desc, Some("test description".to_string()));
assert_eq!(home, Some("https://example.com".to_string())); assert_eq!(home, Some("https://example.com".to_string()));
assert_eq!(lic, Some("MIT".to_string())); assert_eq!(lic, Some("MIT".to_string()));
@@ -2651,8 +2740,10 @@ revision = 1
let rec = BinaryRepoPackageRecord { let rec = BinaryRepoPackageRecord {
repo_name: "repo".into(), repo_name: "repo".into(),
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: "pkg.depot.pkg.tar.zst".into(), filename: "pkg.depot.pkg.tar.zst".into(),
size: 7, size: 7,
@@ -2687,8 +2778,10 @@ revision = 1
BinaryRepoPackageRecord { BinaryRepoPackageRecord {
repo_name: "repo".into(), repo_name: "repo".into(),
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: filename.to_string(), filename: filename.to_string(),
size: payload.len() as u64, size: payload.len() as u64,
+10
View File
@@ -483,10 +483,12 @@ mod tests {
PackageSpec { PackageSpec {
package: crate::package::PackageInfo { package: crate::package::PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -558,10 +560,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: crate::package::PackageInfo { package: crate::package::PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -605,10 +609,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: crate::package::PackageInfo { package: crate::package::PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -652,18 +658,22 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: crate::package::PackageInfo { package: crate::package::PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![crate::package::PackageInfo { packages: vec![crate::package::PackageInfo {
name: "foo-libs".into(), name: "foo-libs".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Default::default(), alternatives: Default::default(),
+2
View File
@@ -1033,10 +1033,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+34
View File
@@ -674,10 +674,12 @@ pub fn create_interactive() -> Result<PackageSpec> {
Ok(PackageSpec { Ok(PackageSpec {
package: PackageInfo { package: PackageInfo {
name, name,
real_name: None,
version, version,
revision: 1, revision: 1,
description, description,
homepage, homepage,
abi_breaking: false,
license, license,
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -708,6 +710,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
// package block // package block
let mut pkg = Table::new(); let mut pkg = Table::new();
pkg.insert("name".into(), Value::String(spec.package.name.clone())); 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( pkg.insert(
"version".into(), "version".into(),
Value::String(spec.package.version.clone()), Value::String(spec.package.version.clone()),
@@ -724,6 +729,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
Value::String(spec.package.homepage.clone()), 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) { if let Some(v) = licenses_to_toml_value(&spec.package.license) {
pkg.insert("license".into(), v); pkg.insert("license".into(), v);
} }
@@ -735,6 +743,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
for p in &spec.packages { for p in &spec.packages {
let mut pt = Table::new(); let mut pt = Table::new();
pt.insert("name".into(), Value::String(p.name.clone())); 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())); pt.insert("version".into(), Value::String(p.version.clone()));
if !p.description.is_empty() { if !p.description.is_empty() {
pt.insert("description".into(), Value::String(p.description.clone())); pt.insert("description".into(), Value::String(p.description.clone()));
@@ -742,6 +753,9 @@ pub fn spec_to_minimal_toml(spec: &PackageSpec) -> anyhow::Result<String> {
if !p.homepage.is_empty() { if !p.homepage.is_empty() {
pt.insert("homepage".into(), Value::String(p.homepage.clone())); 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) { if let Some(v) = licenses_to_toml_value(&p.license) {
pt.insert("license".into(), v); pt.insert("license".into(), v);
} }
@@ -1483,10 +1497,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1526,18 +1542,22 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![PackageInfo { packages: vec![PackageInfo {
name: "foo-dev".into(), name: "foo-dev".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "dev files".into(), description: "dev files".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Alternatives::default(), alternatives: Alternatives::default(),
@@ -1570,10 +1590,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into(), "Apache-2.0".into()], license: vec!["MIT".into(), "Apache-2.0".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1651,10 +1673,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1728,10 +1752,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1764,10 +1790,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1821,10 +1849,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1880,10 +1910,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo-meta".into(), name: "foo-meta".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "Meta package".into(), description: "Meta package".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1919,10 +1951,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "vertex-keyring".into(), name: "vertex-keyring".into(),
real_name: None,
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
description: "keyring".into(), description: "keyring".into(),
homepage: "https://www.vertexlinux.net".into(), homepage: "https://www.vertexlinux.net".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+32
View File
@@ -138,6 +138,12 @@ impl Packager {
"version".to_string(), "version".to_string(),
toml::Value::String(self.spec.package.version.clone()), 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( map.insert(
"revision".to_string(), "revision".to_string(),
toml::Value::Integer(self.spec.package.revision as i64), toml::Value::Integer(self.spec.package.revision as i64),
@@ -150,6 +156,10 @@ impl Packager {
"homepage".to_string(), "homepage".to_string(),
toml::Value::String(self.spec.package.homepage.clone()), toml::Value::String(self.spec.package.homepage.clone()),
); );
map.insert(
"abi_breaking".to_string(),
toml::Value::Boolean(self.spec.package.abi_breaking),
);
map.insert( map.insert(
"license".to_string(), "license".to_string(),
license_value(&self.spec.package.license), license_value(&self.spec.package.license),
@@ -294,10 +304,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "test".into(), name: "test".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -455,6 +467,26 @@ mod tests {
assert_eq!(arr[1].as_str(), Some("Apache-2.0")); 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] #[test]
fn test_generate_metadata_toml_includes_keep_paths() { fn test_generate_metadata_toml_includes_keep_paths() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+15
View File
@@ -3578,10 +3578,12 @@ type = "custom"
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: name.into(), name: name.into(),
real_name: None,
version: version.into(), version: version.into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -3633,12 +3635,18 @@ impl fmt::Display for PackageSpec {
#[derive(Debug, Deserialize, serde::Serialize, Clone)] #[derive(Debug, Deserialize, serde::Serialize, Clone)]
pub struct PackageInfo { pub struct PackageInfo {
pub name: String, 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<String>,
pub version: String, pub version: String,
/// Maintenance revision of the package (defaults to 1) /// Maintenance revision of the package (defaults to 1)
#[serde(default = "default_revision")] #[serde(default = "default_revision")]
pub revision: u32, pub revision: u32,
pub description: String, pub description: String,
pub homepage: 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( #[serde(
deserialize_with = "deserialize_licenses", deserialize_with = "deserialize_licenses",
serialize_with = "serialize_licenses" serialize_with = "serialize_licenses"
@@ -3646,6 +3654,13 @@ pub struct PackageInfo {
pub license: Vec<String>, pub license: Vec<String>,
} }
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 { fn default_revision() -> u32 {
1 1
} }
+6
View File
@@ -806,18 +806,22 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![PackageInfo { packages: vec![PackageInfo {
name: "foo-libs".into(), name: "foo-libs".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Alternatives::default(), alternatives: Alternatives::default(),
@@ -878,8 +882,10 @@ mod tests {
record: Box::new(db::repo::BinaryRepoPackageRecord { record: Box::new(db::repo::BinaryRepoPackageRecord {
repo_name: repo_name.to_string(), repo_name: repo_name.to_string(),
name: name.to_string(), name: name.to_string(),
real_name: None,
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
revision: 1, revision: 1,
abi_breaking: false,
completed_at: None, completed_at: None,
filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"), filename: format!("{name}-1.0.0-1-x86_64.depot.pkg.tar.zst"),
size: 1024, size: 1024,
+2
View File
@@ -724,10 +724,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "pkg".into(), name: "pkg".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -320,10 +320,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+6
View File
@@ -499,10 +499,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -556,10 +558,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -616,10 +620,12 @@ mod tests {
let spec = PackageSpec { let spec = PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "json".into(), name: "json".into(),
real_name: None,
version: "3.11.3".into(), version: "3.11.3".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+8
View File
@@ -1504,10 +1504,12 @@ mod tests {
PackageSpec { PackageSpec {
package: PackageInfo { package: PackageInfo {
name: "foo".into(), name: "foo".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1608,10 +1610,12 @@ mod tests {
let mut spec = mk_spec_for_stage_processing(); let mut spec = mk_spec_for_stage_processing();
spec.packages.push(PackageInfo { spec.packages.push(PackageInfo {
name: "foo-dev".into(), name: "foo-dev".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "dev".into(), description: "dev".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
spec.build.flags.split_docs = true; spec.build.flags.split_docs = true;
@@ -1677,18 +1681,22 @@ mod tests {
let mut spec = mk_spec_for_stage_processing(); let mut spec = mk_spec_for_stage_processing();
spec.packages.push(PackageInfo { spec.packages.push(PackageInfo {
name: "foo-dev".into(), name: "foo-dev".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "dev".into(), description: "dev".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
spec.packages.push(PackageInfo { spec.packages.push(PackageInfo {
name: "foo-extras".into(), name: "foo-extras".into(),
real_name: None,
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
description: "extras".into(), description: "extras".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false,
license: vec!["Apache-2.0".into()], license: vec!["Apache-2.0".into()],
}); });