feat: add built_against field for ABI-sensitive dependencies

- Introduced a new field `built_against` in the InstalledPackageRecord struct to track concrete package names for ABI-sensitive dependencies.
- Updated database schema to include `built_against` column in the packages table.
- Modified package registration and listing functions to handle the new `built_against` field.
- Implemented helper functions to format and parse the `built_against` data.
- Enhanced tests to verify the correct handling of the `built_against` metadata during package registration and retrieval.
- Updated resolver logic to utilize `built_against` for dependency resolution.
This commit is contained in:
2026-06-16 06:35:08 -05:00
parent ffe6e4868f
commit 65e48ed280
28 changed files with 568 additions and 40 deletions
+5
View File
@@ -1245,6 +1245,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1278,6 +1279,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1652,6 +1654,7 @@ foo: bar
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1686,6 +1689,7 @@ foo: bar
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1722,6 +1726,7 @@ foo: bar
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -49,6 +49,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -1204,6 +1204,7 @@ mod tests {
description: "".into(), description: "".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+3
View File
@@ -412,6 +412,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -555,6 +556,7 @@ depot_install_dev_pkg() {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
@@ -660,6 +662,7 @@ depot_install() {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
+1
View File
@@ -150,6 +150,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -1050,6 +1050,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -1149,6 +1149,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -517,6 +517,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -791,6 +791,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -239,6 +239,7 @@ mod tests {
description: String::new(), description: String::new(),
homepage: String::new(), homepage: String::new(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+13
View File
@@ -603,6 +603,19 @@ fn parse_dependency_list(metadata: &toml::Value, kind: &str) -> Vec<String> {
.unwrap_or_default() .unwrap_or_default()
} }
fn parse_metadata_string_list(metadata: &toml::Value, key: &str) -> Vec<String> {
metadata
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect()
})
.unwrap_or_default()
}
fn parse_keep_list(metadata: &toml::Value) -> Vec<String> { fn parse_keep_list(metadata: &toml::Value) -> Vec<String> {
if let Some(s) = metadata.get("keep").and_then(|v| v.as_str()) { if let Some(s) = metadata.get("keep").and_then(|v| v.as_str()) {
return vec![s.to_string()]; return vec![s.to_string()];
+1
View File
@@ -386,6 +386,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -35,6 +35,7 @@ fn package_spec_from_archive_metadata(metadata: &toml::Value) -> package::Packag
.get("abi_breaking") .get("abi_breaking")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false), .unwrap_or(false),
built_against: super::super::parse_metadata_string_list(metadata, "built_against"),
license: super::super::parse_licenses_from_toml(metadata), license: super::super::parse_licenses_from_toml(metadata),
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -118,6 +119,7 @@ pub(crate) fn package_spec_from_repo_record(
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, abi_breaking: record.abi_breaking,
built_against: record.built_against.clone(),
license: parse_license_list_from_repo(&record.license), license: parse_license_list_from_repo(&record.license),
}, },
packages: Vec::new(), packages: Vec::new(),
+22
View File
@@ -133,6 +133,7 @@ fn test_binary_repo_record(name: &str, filename: &str) -> db::repo::BinaryRepoPa
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
filename: filename.into(), filename: filename.into(),
size: 1, size: 1,
@@ -173,6 +174,7 @@ fn test_package_spec(
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -436,6 +438,7 @@ fn binary_install_path_uses_repo_record_metadata_without_archive_metadata() -> R
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
filename: archive_path filename: archive_path
.file_name() .file_name()
@@ -734,6 +737,7 @@ fn binary_archive_install_preserves_setuid_permissions() -> Result<()> {
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
filename: archive_path filename: archive_path
.file_name() .file_name()
@@ -851,6 +855,7 @@ fn binary_archive_install_honors_replaces_from_metadata() -> Result<()> {
description: "diffutils".into(), description: "diffutils".into(),
homepage: "https://example.test/diffutils".into(), homepage: "https://example.test/diffutils".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["GPL-3.0-or-later".into()], license: vec!["GPL-3.0-or-later".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -955,6 +960,7 @@ fn plan_staged_install_reads_updates_from_rootfs_installed_db() -> Result<()> {
description: "Base filesystem".into(), description: "Base filesystem".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["Unlicense".into()], license: vec!["Unlicense".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -997,6 +1003,7 @@ fn renamed_abi_updates_keep_versioned_shared_libraries() -> Result<()> {
description: "libxml2 2.14".into(), description: "libxml2 2.14".into(),
homepage: "https://example.test/libxml2".into(), homepage: "https://example.test/libxml2".into(),
abi_breaking: true, abi_breaking: true,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1032,6 +1039,7 @@ fn renamed_abi_updates_keep_versioned_shared_libraries() -> Result<()> {
description: "libxml2 2.15".into(), description: "libxml2 2.15".into(),
homepage: "https://example.test/libxml2".into(), homepage: "https://example.test/libxml2".into(),
abi_breaking: true, abi_breaking: true,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1142,6 +1150,7 @@ fn collect_update_candidates_matches_renamed_packages_by_real_name() -> Result<(
description: "icu78".into(), description: "icu78".into(),
homepage: "https://example.test/icu".into(), homepage: "https://example.test/icu".into(),
abi_breaking: true, abi_breaking: true,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1219,6 +1228,7 @@ fn update_candidate_prefers_binary_when_versions_match_and_config_does() {
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
}; };
let source_spec = package::PackageSpec { let source_spec = package::PackageSpec {
@@ -1230,6 +1240,7 @@ fn update_candidate_prefers_binary_when_versions_match_and_config_does() {
description: "test".into(), description: "test".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1273,6 +1284,7 @@ fn update_candidate_prefers_binary_when_versions_match_and_config_does() {
version: "1.1.0".into(), version: "1.1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
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,
@@ -1312,6 +1324,7 @@ fn select_update_candidate_uses_newer_timestamp_when_versions_match() {
version: "1.0.0".into(), version: "1.0.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: Some(100), completed_at: Some(100),
}; };
let source_spec = package::PackageSpec { let source_spec = package::PackageSpec {
@@ -1323,6 +1336,7 @@ fn select_update_candidate_uses_newer_timestamp_when_versions_match() {
description: "test".into(), description: "test".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1371,6 +1385,7 @@ fn select_update_candidate_prefers_replacement_candidate() {
version: "4.9.0".into(), version: "4.9.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: Some(100), completed_at: Some(100),
}; };
@@ -1383,6 +1398,7 @@ fn select_update_candidate_prefers_replacement_candidate() {
description: "findutils".into(), description: "findutils".into(),
homepage: "https://example.test/findutils".into(), homepage: "https://example.test/findutils".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1407,6 +1423,7 @@ fn select_update_candidate_prefers_replacement_candidate() {
description: "busybox".into(), description: "busybox".into(),
homepage: "https://example.test/busybox".into(), homepage: "https://example.test/busybox".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["GPL-2.0-only".into()], license: vec!["GPL-2.0-only".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1481,6 +1498,7 @@ fn install_planned_packages_to_rootfs_runs_post_hooks_after_batch_install() -> R
description: "findutils".into(), description: "findutils".into(),
homepage: "https://example.test/findutils".into(), homepage: "https://example.test/findutils".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1510,6 +1528,7 @@ fn install_planned_packages_to_rootfs_runs_post_hooks_after_batch_install() -> R
description: "alpha".into(), description: "alpha".into(),
homepage: "https://example.test/alpha".into(), homepage: "https://example.test/alpha".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1543,6 +1562,7 @@ fn install_planned_packages_to_rootfs_runs_post_hooks_after_batch_install() -> R
description: "busybox".into(), description: "busybox".into(),
homepage: "https://example.test/busybox".into(), homepage: "https://example.test/busybox".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["GPL-2.0-only".into()], license: vec!["GPL-2.0-only".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1635,6 +1655,7 @@ fn collect_update_candidates_skips_source_when_prefer_binary_is_enabled() -> Res
description: "pkg".into(), description: "pkg".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1806,6 +1827,7 @@ fn collect_missing_update_dependencies_skips_planned_provides_and_installed_deps
description: "glibc".into(), description: "glibc".into(),
homepage: "https://example.test".into(), homepage: "https://example.test".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+63 -4
View File
@@ -41,6 +41,8 @@ pub struct InstalledPackageRecord {
pub revision: u32, pub revision: u32,
/// Whether renamed updates should retain versioned shared libraries. /// Whether renamed updates should retain versioned shared libraries.
pub abi_breaking: bool, pub abi_breaking: bool,
/// Concrete package names this package was built against for ABI-sensitive dependencies.
pub built_against: Vec<String>,
/// Package completion timestamp if known. /// Package completion timestamp if known.
pub completed_at: Option<i64>, pub completed_at: Option<i64>,
} }
@@ -140,8 +142,8 @@ pub fn register_package_with_replacement(
// 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, real_name, version, revision, description, homepage, license, abi_breaking, completed_at) "INSERT INTO packages (name, real_name, version, revision, description, homepage, license, abi_breaking, built_against, completed_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
ON CONFLICT(name) DO UPDATE SET ON CONFLICT(name) DO UPDATE SET
real_name=excluded.real_name, real_name=excluded.real_name,
version=excluded.version, version=excluded.version,
@@ -150,6 +152,7 @@ pub fn register_package_with_replacement(
homepage=excluded.homepage, homepage=excluded.homepage,
license=excluded.license, license=excluded.license,
abi_breaking=excluded.abi_breaking, abi_breaking=excluded.abi_breaking,
built_against=excluded.built_against,
completed_at=excluded.completed_at", completed_at=excluded.completed_at",
params![ params![
spec.package.name, spec.package.name,
@@ -160,6 +163,7 @@ pub fn register_package_with_replacement(
spec.package.homepage, spec.package.homepage,
format_licenses(&spec.package.license), format_licenses(&spec.package.license),
spec.package.abi_breaking, spec.package.abi_breaking,
format_built_against(&spec.package.built_against),
completed_at, completed_at,
], ],
)?; )?;
@@ -575,6 +579,7 @@ fn init_db(conn: &Connection) -> Result<()> {
homepage TEXT, homepage TEXT,
license TEXT, license TEXT,
abi_breaking INTEGER NOT NULL DEFAULT 0, abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER completed_at INTEGER
); );
@@ -632,6 +637,7 @@ 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_real_name_column(conn)?;
ensure_packages_abi_breaking_column(conn)?; ensure_packages_abi_breaking_column(conn)?;
ensure_packages_built_against_column(conn)?;
Ok(()) Ok(())
} }
@@ -692,6 +698,39 @@ fn ensure_packages_abi_breaking_column(conn: &Connection) -> Result<()> {
Ok(()) Ok(())
} }
fn ensure_packages_built_against_column(conn: &Connection) -> Result<()> {
let has_built_against: bool = conn
.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'built_against'",
[],
|row| {
let count: i64 = row.get(0)?;
Ok(count > 0)
},
)
.context("Failed to inspect installed package DB schema")?;
if !has_built_against {
conn.execute(
"ALTER TABLE packages ADD COLUMN built_against TEXT NOT NULL DEFAULT ''",
[],
)
.context("Failed to add built_against column to installed package DB")?;
}
Ok(())
}
fn format_built_against(packages: &[String]) -> String {
packages.join("\n")
}
fn parse_built_against(raw: &str) -> Vec<String> {
raw.lines()
.map(str::trim)
.filter(|name| !name.is_empty())
.map(String::from)
.collect()
}
fn delete_package_rows(conn: &Connection, pkg_id: i64) -> Result<()> { 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 files WHERE package_id = ?1", params![pkg_id])?;
conn.execute( conn.execute(
@@ -877,18 +916,20 @@ 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 = conn.prepare( let mut stmt = conn.prepare(
"SELECT name, real_name, version, revision, abi_breaking, completed_at "SELECT name, real_name, version, revision, abi_breaking, built_against, completed_at
FROM packages FROM packages
ORDER BY name", ORDER BY name",
)?; )?;
let rows = stmt.query_map([], |row| { let rows = stmt.query_map([], |row| {
let built_against: String = row.get(5)?;
Ok(InstalledPackageRecord { Ok(InstalledPackageRecord {
name: row.get(0)?, name: row.get(0)?,
real_name: row.get(1)?, real_name: row.get(1)?,
version: row.get(2)?, version: row.get(2)?,
revision: row.get::<_, i64>(3)? as u32, revision: row.get::<_, i64>(3)? as u32,
abi_breaking: row.get(4)?, abi_breaking: row.get(4)?,
completed_at: row.get(5)?, built_against: parse_built_against(&built_against),
completed_at: row.get(6)?,
}) })
})?; })?;
Ok(rows.filter_map(|row| row.ok()).collect()) Ok(rows.filter_map(|row| row.ok()).collect())
@@ -1133,6 +1174,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1565,6 +1607,23 @@ mod tests {
); );
} }
#[test]
fn register_package_records_built_against_metadata() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("packages.db");
let dest = tmp.path().join("dest");
std::fs::create_dir_all(&dest).unwrap();
std::fs::write(dest.join("payload"), "x").unwrap();
let mut spec = mk_spec("app", "1.0");
spec.package.built_against = vec!["icu78".into()];
register_package(&db_path, &spec, &dest).unwrap();
let records = list_installed_package_records(&db_path).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].built_against, vec!["icu78".to_string()]);
}
#[test] #[test]
fn installed_group_helpers_round_trip_membership() { fn installed_group_helpers_round_trip_membership() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
+170 -28
View File
@@ -32,6 +32,31 @@ fn parse_license_text(metadata: &toml::Value) -> Option<String> {
None None
} }
fn parse_string_array_metadata(metadata: &toml::Value, key: &str) -> Vec<String> {
metadata
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.map(String::from)
.collect()
})
.unwrap_or_default()
}
fn format_built_against(packages: &[String]) -> String {
packages.join("\n")
}
fn parse_built_against(raw: &str) -> Vec<String> {
raw.lines()
.map(str::trim)
.filter(|name| !name.is_empty())
.map(String::from)
.collect()
}
pub struct RepoManager { pub struct RepoManager {
pub repo_dir: PathBuf, pub repo_dir: PathBuf,
} }
@@ -76,6 +101,7 @@ struct IndexedPackage {
version: String, version: String,
revision: u32, revision: u32,
abi_breaking: bool, abi_breaking: bool,
built_against: Vec<String>,
completed_at: Option<i64>, completed_at: Option<i64>,
description: Option<String>, description: Option<String>,
homepage: Option<String>, homepage: Option<String>,
@@ -116,6 +142,7 @@ pub struct BinaryRepoPackageRecord {
pub version: String, pub version: String,
pub revision: u32, pub revision: u32,
pub abi_breaking: bool, pub abi_breaking: bool,
pub built_against: Vec<String>,
pub completed_at: Option<i64>, pub completed_at: Option<i64>,
pub filename: String, pub filename: String,
pub size: u64, pub size: u64,
@@ -301,6 +328,7 @@ impl RepoManager {
version TEXT NOT NULL, version TEXT NOT NULL,
revision INTEGER NOT NULL, revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0, abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER, completed_at INTEGER,
description TEXT, description TEXT,
homepage TEXT, homepage TEXT,
@@ -378,6 +406,7 @@ impl RepoManager {
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 abi_breaking = false;
let mut built_against = Vec::new();
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;
@@ -427,6 +456,7 @@ impl RepoManager {
.get("abi_breaking") .get("abi_breaking")
.and_then(|v| v.as_bool()) .and_then(|v| v.as_bool())
.unwrap_or(false); .unwrap_or(false);
built_against = parse_string_array_metadata(&metadata, "built_against");
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
@@ -532,6 +562,7 @@ impl RepoManager {
version, version,
revision, revision,
abi_breaking, abi_breaking,
built_against,
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -557,6 +588,7 @@ impl RepoManager {
version, version,
revision, revision,
abi_breaking, abi_breaking,
built_against,
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -576,14 +608,15 @@ impl RepoManager {
// Insert into database // Insert into database
conn.execute( conn.execute(
"INSERT INTO packages (name, real_name, version, revision, abi_breaking, completed_at, description, homepage, license, filename, size, sha256, sha512) "INSERT INTO packages (name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
params![ params![
name, name,
real_name, real_name,
version, version,
revision as i64, revision as i64,
abi_breaking, abi_breaking,
format_built_against(&built_against),
completed_at, completed_at,
description, description,
homepage, homepage,
@@ -1675,6 +1708,19 @@ fn query_package_runtime_deps(conn: &Connection, package_id: i64) -> Result<Vec<
Ok(rows.filter_map(|r| r.ok()).collect()) Ok(rows.filter_map(|r| r.ok()).collect())
} }
fn query_package_built_against(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
if !repo_packages_have_built_against(conn)? {
return Ok(Vec::new());
}
let raw: String = conn.query_row(
"SELECT built_against FROM packages WHERE id = ?1",
params![package_id],
|row| row.get(0),
)?;
Ok(parse_built_against(&raw))
}
fn query_package_optional_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> { fn query_package_optional_deps(conn: &Connection, package_id: i64) -> Result<Vec<String>> {
let has_dependencies_table: bool = conn let has_dependencies_table: bool = conn
.query_row( .query_row(
@@ -1740,6 +1786,11 @@ fn find_cached_binary_repo_packages(
} else { } else {
"0" "0"
}; };
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!( let sql = format!(
"SELECT "SELECT
p.id, p.id,
@@ -1748,6 +1799,7 @@ fn find_cached_binary_repo_packages(
p.version, p.version,
p.revision, p.revision,
{abi_breaking_expr}, {abi_breaking_expr},
{built_against_expr},
{completed_at_expr}, {completed_at_expr},
p.filename, p.filename,
p.size, p.size,
@@ -1758,6 +1810,7 @@ fn find_cached_binary_repo_packages(
p.license p.license
FROM packages p FROM packages p
WHERE lower(p.name) = lower(?1) WHERE lower(p.name) = lower(?1)
OR lower({real_name_expr}) = lower(?1)
OR EXISTS ( OR EXISTS (
SELECT 1 FROM replaces rp SELECT 1 FROM replaces rp
WHERE rp.package_id = p.id WHERE rp.package_id = p.id
@@ -1776,6 +1829,7 @@ fn find_cached_binary_repo_packages(
AND lower(rp.name) = lower(?1) AND lower(rp.name) = lower(?1)
) THEN 0 ) THEN 0
WHEN lower(p.name) = lower(?1) THEN 1 WHEN lower(p.name) = lower(?1) THEN 1
WHEN lower({real_name_expr}) = lower(?1) THEN 2
ELSE 2 ELSE 2
END, END,
p.name ASC" p.name ASC"
@@ -1793,14 +1847,15 @@ fn find_cached_binary_repo_packages(
version: row.get(3)?, version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32, revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?, abi_breaking: row.get(5)?,
completed_at: row.get(6)?, built_against: parse_built_against(&row.get::<_, String>(6)?),
filename: row.get(7)?, completed_at: row.get(7)?,
size: row.get::<_, i64>(8)? as u64, filename: row.get(8)?,
sha256: row.get(9)?, size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?, sha256: row.get(10)?,
description: row.get(11)?, sha512: row.get(11)?,
homepage: row.get(12)?, description: row.get(12)?,
license: row.get(13)?, homepage: row.get(13)?,
license: row.get(14)?,
provides: Vec::new(), provides: Vec::new(),
conflicts: Vec::new(), conflicts: Vec::new(),
replaces: Vec::new(), replaces: Vec::new(),
@@ -1818,6 +1873,7 @@ fn find_cached_binary_repo_packages(
rec.conflicts = query_package_conflicts(&conn, package_id)?; rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?; rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?; rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?; rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?; rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec); out.push(rec);
@@ -1862,6 +1918,11 @@ fn find_cached_binary_repo_packages_by_group(
} else { } else {
"0" "0"
}; };
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!( let sql = format!(
"SELECT "SELECT
p.id, p.id,
@@ -1870,6 +1931,7 @@ fn find_cached_binary_repo_packages_by_group(
p.version, p.version,
p.revision, p.revision,
{abi_breaking_expr}, {abi_breaking_expr},
{built_against_expr},
{completed_at_expr}, {completed_at_expr},
p.filename, p.filename,
p.size, p.size,
@@ -1899,14 +1961,15 @@ fn find_cached_binary_repo_packages_by_group(
version: row.get(3)?, version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32, revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?, abi_breaking: row.get(5)?,
completed_at: row.get(6)?, built_against: parse_built_against(&row.get::<_, String>(6)?),
filename: row.get(7)?, completed_at: row.get(7)?,
size: row.get::<_, i64>(8)? as u64, filename: row.get(8)?,
sha256: row.get(9)?, size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?, sha256: row.get(10)?,
description: row.get(11)?, sha512: row.get(11)?,
homepage: row.get(12)?, description: row.get(12)?,
license: row.get(13)?, homepage: row.get(13)?,
license: row.get(14)?,
provides: Vec::new(), provides: Vec::new(),
conflicts: Vec::new(), conflicts: Vec::new(),
replaces: Vec::new(), replaces: Vec::new(),
@@ -1924,6 +1987,7 @@ fn find_cached_binary_repo_packages_by_group(
rec.conflicts = query_package_conflicts(&conn, package_id)?; rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?; rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?; rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?; rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?; rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec); out.push(rec);
@@ -1953,6 +2017,11 @@ fn list_cached_binary_repo_packages(
} else { } else {
"0" "0"
}; };
let built_against_expr = if repo_packages_have_built_against(&conn)? {
"p.built_against"
} else {
"''"
};
let sql = format!( let sql = format!(
"SELECT "SELECT
p.id, p.id,
@@ -1961,6 +2030,7 @@ fn list_cached_binary_repo_packages(
p.version, p.version,
p.revision, p.revision,
{abi_breaking_expr}, {abi_breaking_expr},
{built_against_expr},
{completed_at_expr}, {completed_at_expr},
p.filename, p.filename,
p.size, p.size,
@@ -1985,14 +2055,15 @@ fn list_cached_binary_repo_packages(
version: row.get(3)?, version: row.get(3)?,
revision: row.get::<_, i64>(4)? as u32, revision: row.get::<_, i64>(4)? as u32,
abi_breaking: row.get(5)?, abi_breaking: row.get(5)?,
completed_at: row.get(6)?, built_against: parse_built_against(&row.get::<_, String>(6)?),
filename: row.get(7)?, completed_at: row.get(7)?,
size: row.get::<_, i64>(8)? as u64, filename: row.get(8)?,
sha256: row.get(9)?, size: row.get::<_, i64>(9)? as u64,
sha512: row.get(10)?, sha256: row.get(10)?,
description: row.get(11)?, sha512: row.get(11)?,
homepage: row.get(12)?, description: row.get(12)?,
license: row.get(13)?, homepage: row.get(13)?,
license: row.get(14)?,
provides: Vec::new(), provides: Vec::new(),
conflicts: Vec::new(), conflicts: Vec::new(),
replaces: Vec::new(), replaces: Vec::new(),
@@ -2010,6 +2081,7 @@ fn list_cached_binary_repo_packages(
rec.conflicts = query_package_conflicts(&conn, package_id)?; rec.conflicts = query_package_conflicts(&conn, package_id)?;
rec.replaces = query_package_replaces(&conn, package_id)?; rec.replaces = query_package_replaces(&conn, package_id)?;
rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?; rec.runtime_dependencies = query_package_runtime_deps(&conn, package_id)?;
rec.built_against = query_package_built_against(&conn, package_id)?;
rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?; rec.optional_dependencies = query_package_optional_deps(&conn, package_id)?;
rec.groups = query_package_groups(&conn, package_id)?; rec.groups = query_package_groups(&conn, package_id)?;
out.push(rec); out.push(rec);
@@ -2053,6 +2125,18 @@ fn repo_packages_have_abi_breaking(conn: &Connection) -> Result<bool> {
.context("Failed to inspect binary repo DB schema") .context("Failed to inspect binary repo DB schema")
} }
fn repo_packages_have_built_against(conn: &Connection) -> Result<bool> {
conn.query_row(
"SELECT COUNT(*) FROM pragma_table_info('packages') WHERE name = 'built_against'",
[],
|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()))?;
@@ -2655,6 +2739,7 @@ real_name = "icu"
version = "1.0" version = "1.0"
revision = 1 revision = 1
abi_breaking = true abi_breaking = true
built_against = ["icu78"]
description = "test description" description = "test description"
homepage = "https://example.com" homepage = "https://example.com"
license = "MIT" license = "MIT"
@@ -2692,6 +2777,7 @@ optional = []
String, String,
i64, i64,
i64, i64,
String,
Option<String>, Option<String>,
Option<String>, Option<String>,
Option<String>, Option<String>,
@@ -2699,9 +2785,21 @@ optional = []
String, String,
); );
let (name, real_name, version, revision, abi_breaking, desc, home, lic, sha256, sha512): PackageRow = conn let (
name,
real_name,
version,
revision,
abi_breaking,
built_against,
desc,
home,
lic,
sha256,
sha512,
): PackageRow = conn
.query_row( .query_row(
"SELECT name, real_name, version, revision, abi_breaking, description, homepage, license, sha256, sha512 FROM packages", "SELECT name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, sha256, sha512 FROM packages",
[], [],
|r| { |r| {
Ok(( Ok((
@@ -2715,6 +2813,7 @@ optional = []
r.get(7)?, r.get(7)?,
r.get(8)?, r.get(8)?,
r.get(9)?, r.get(9)?,
r.get(10)?,
)) ))
}, },
) )
@@ -2725,6 +2824,7 @@ optional = []
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!(abi_breaking, 1);
assert_eq!(built_against, "icu78");
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()));
@@ -2965,6 +3065,46 @@ revision = 1
assert_eq!(rec.filename, "foo-1.0-1.depot.pkg.tar.zst"); assert_eq!(rec.filename, "foo-1.0-1.depot.pkg.tar.zst");
} }
#[test]
fn test_find_cached_binary_repo_packages_matches_real_name_and_built_against() {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("repo.db");
let mut conn = Connection::open(&db_path).unwrap();
let manager = RepoManager::new(tmp.path().to_path_buf());
manager.init_repo_schema(&mut conn).unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, 'icu78.pkg', 10, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, 'app.pkg', 10, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (2, 'runtime', 'icu')",
[],
)
.unwrap();
drop(conn);
let icu_matches = find_cached_binary_repo_packages("repo", &db_path, "icu").unwrap();
assert_eq!(icu_matches.len(), 1);
assert_eq!(icu_matches[0].name, "icu78");
assert_eq!(icu_matches[0].real_name.as_deref(), Some("icu"));
let app = find_cached_binary_repo_packages("repo", &db_path, "app")
.unwrap()
.pop()
.unwrap();
assert_eq!(app.runtime_dependencies, vec!["icu".to_string()]);
assert_eq!(app.built_against, vec!["icu78".to_string()]);
}
#[test] #[test]
fn test_verify_binary_package_record_checksums_accepts_valid_hashes() { fn test_verify_binary_package_record_checksums_accepts_valid_hashes() {
use sha2::{Digest, Sha256, Sha512}; use sha2::{Digest, Sha256, Sha512};
@@ -2991,6 +3131,7 @@ revision = 1
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
filename: "pkg.depot.pkg.tar.zst".into(), filename: "pkg.depot.pkg.tar.zst".into(),
size: 7, size: 7,
@@ -3031,6 +3172,7 @@ revision = 1
version: "1.0".into(), version: "1.0".into(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
completed_at: None, completed_at: None,
filename: filename.to_string(), filename: filename.to_string(),
size: payload.len() as u64, size: payload.len() as u64,
+5
View File
@@ -398,6 +398,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -475,6 +476,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -525,6 +527,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -575,6 +578,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![crate::package::PackageInfo { packages: vec![crate::package::PackageInfo {
@@ -585,6 +589,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Default::default(), alternatives: Default::default(),
+1
View File
@@ -1567,6 +1567,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+11
View File
@@ -692,6 +692,7 @@ pub fn create_interactive() -> Result<PackageSpec> {
description, description,
homepage, homepage,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license, license,
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1757,6 +1758,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1802,6 +1804,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![PackageInfo { packages: vec![PackageInfo {
@@ -1812,6 +1815,7 @@ mod tests {
description: "dev files".into(), description: "dev files".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Alternatives::default(), alternatives: Alternatives::default(),
@@ -1850,6 +1854,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into(), "Apache-2.0".into()], license: vec!["MIT".into(), "Apache-2.0".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1938,6 +1943,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -2019,6 +2025,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -2057,6 +2064,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -2125,6 +2133,7 @@ mod tests {
description: "A test".into(), description: "A test".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -2207,6 +2216,7 @@ mod tests {
description: "Meta package".into(), description: "Meta package".into(),
homepage: "".into(), homepage: "".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -2249,6 +2259,7 @@ mod tests {
description: "keyring".into(), description: "keyring".into(),
homepage: "https://www.vertexlinux.net".into(), homepage: "https://www.vertexlinux.net".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+20
View File
@@ -184,6 +184,19 @@ impl Packager {
"abi_breaking".to_string(), "abi_breaking".to_string(),
toml::Value::Boolean(self.spec.package.abi_breaking), toml::Value::Boolean(self.spec.package.abi_breaking),
); );
if !self.spec.package.built_against.is_empty() {
map.insert(
"built_against".to_string(),
toml::Value::Array(
self.spec
.package
.built_against
.iter()
.map(|s| toml::Value::String(s.clone()))
.collect(),
),
);
}
map.insert( map.insert(
"license".to_string(), "license".to_string(),
license_value(&self.spec.package.license), license_value(&self.spec.package.license),
@@ -357,6 +370,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -528,6 +542,7 @@ mod tests {
let mut packager = mk_packager(dest.to_path_buf()); let mut packager = mk_packager(dest.to_path_buf());
packager.spec.package.real_name = Some("icu".into()); packager.spec.package.real_name = Some("icu".into());
packager.spec.package.abi_breaking = true; packager.spec.package.abi_breaking = true;
packager.spec.package.built_against = vec!["icu78".into()];
packager.generate_metadata_toml().unwrap(); packager.generate_metadata_toml().unwrap();
let meta_path = dest.join(".metadata.toml"); let meta_path = dest.join(".metadata.toml");
@@ -538,6 +553,11 @@ mod tests {
val.get("abi_breaking").and_then(|v| v.as_bool()), val.get("abi_breaking").and_then(|v| v.as_bool()),
Some(true) Some(true)
); );
let built_against = val
.get("built_against")
.and_then(|v| v.as_array())
.expect("built_against should be an array");
assert_eq!(built_against[0].as_str(), Some("icu78"));
} }
#[test] #[test]
+7
View File
@@ -17,6 +17,13 @@ pub struct PackageInfo {
/// When true, renamed updates retain versioned shared libraries from the old package. /// When true, renamed updates retain versioned shared libraries from the old package.
#[serde(default, alias = "abi-breaking")] #[serde(default, alias = "abi-breaking")]
pub abi_breaking: bool, pub abi_breaking: bool,
/// Concrete package names this package was built against for ABI-sensitive dependencies.
#[serde(
default,
alias = "built-against",
deserialize_with = "deserialize_string_or_array"
)]
pub built_against: Vec<String>,
#[serde( #[serde(
deserialize_with = "deserialize_licenses", deserialize_with = "deserialize_licenses",
serialize_with = "serialize_licenses" serialize_with = "serialize_licenses"
+32
View File
@@ -40,6 +40,37 @@ type = "custom"
assert_eq!(spec.spec_dir, tmp.path()); assert_eq!(spec.spec_dir, tmp.path());
} }
#[test]
fn parse_package_built_against_metadata() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("pkg.toml");
std::fs::write(
&path,
r#"
[package]
name = "foo"
version = "1.0"
description = "d"
homepage = "h"
license = "MIT"
built-against = ["icu78"]
[source]
url = "https://example.com/foo.tar.gz"
sha256 = "skip"
extract_dir = "foo"
[build]
type = "custom"
"#,
)
.unwrap();
let spec = PackageSpec::from_file(&path).unwrap();
assert_eq!(spec.package.built_against, vec!["icu78".to_string()]);
}
#[test] #[test]
fn parse_source_array() { fn parse_source_array() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
@@ -2358,6 +2389,7 @@ fn mk_spec(name: &str, version: &str) -> PackageSpec {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+2
View File
@@ -472,6 +472,7 @@ impl ParsedStarbuild {
description: primary_description, description: primary_description,
homepage: homepage.clone(), homepage: homepage.clone(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: self license: self
.output_licenses .output_licenses
.get(self.main_package_name()) .get(self.main_package_name())
@@ -497,6 +498,7 @@ impl ParsedStarbuild {
.unwrap_or_else(|| primary.description.clone()), .unwrap_or_else(|| primary.description.clone()),
homepage: homepage.clone(), homepage: homepage.clone(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: self license: self
.output_licenses .output_licenses
.get(pkg_name) .get(pkg_name)
+194 -8
View File
@@ -284,9 +284,24 @@ impl<'a> Resolver<'a> {
requester_spec_path: Option<&Path>, requester_spec_path: Option<&Path>,
requested_by: String, requested_by: String,
) -> Result<Option<NodeIndex>> { ) -> Result<Option<NodeIndex>> {
if deps::is_dep_satisfied_in_db(dep, &self.db_path)? { self.resolve_dep_node_with_preferred(dep, requester_spec_path, requested_by, &[])
}
fn resolve_dep_node_with_preferred(
&mut self,
dep: &str,
requester_spec_path: Option<&Path>,
requested_by: String,
preferred_packages: &[String],
) -> Result<Option<NodeIndex>> {
if preferred_packages.is_empty() && deps::is_dep_satisfied_in_db(dep, &self.db_path)? {
return Ok(None); return Ok(None);
} }
for preferred in preferred_packages {
if deps::is_dep_satisfied_in_db(preferred, &self.db_path)? {
return Ok(None);
}
}
let dep_name = deps::dep_name(dep); let dep_name = deps::dep_name(dep);
let candidates = self.collect_candidates(dep_name, requester_spec_path)?; let candidates = self.collect_candidates(dep_name, requester_spec_path)?;
@@ -304,7 +319,7 @@ impl<'a> Resolver<'a> {
); );
} }
let chosen = self.choose_candidate(dep, &candidates)?; let chosen = self.choose_candidate(dep, &candidates, preferred_packages)?;
let idx = match chosen.kind { let idx = match chosen.kind {
CandidateKind::Source { CandidateKind::Source {
ref path, ref path,
@@ -345,9 +360,18 @@ impl<'a> Resolver<'a> {
self.by_package.insert(record.name.clone(), idx); self.by_package.insert(record.name.clone(), idx);
for dep in &record.runtime_dependencies { for dep in &record.runtime_dependencies {
if let Some(dep_idx) = let preferred = self.preferred_built_against_packages(dep, &record.built_against)?;
self.resolve_dep_node(dep, None, format!("{} needs {}", record.name, dep))? let dep_label = if preferred.is_empty() {
{ dep.clone()
} else {
format!("{} (built against {})", dep, preferred.join(", "))
};
if let Some(dep_idx) = self.resolve_dep_node_with_preferred(
dep,
None,
format!("{} needs {}", record.name, dep_label),
&preferred,
)? {
self.add_dependency_edge(dep_idx, idx, &record.name)?; self.add_dependency_edge(dep_idx, idx, &record.name)?;
} }
} }
@@ -356,11 +380,50 @@ impl<'a> Resolver<'a> {
Ok(idx) Ok(idx)
} }
fn choose_candidate(&self, dep: &str, candidates: &[Candidate]) -> Result<Candidate> { fn preferred_built_against_packages(
&mut self,
dep: &str,
built_against: &[String],
) -> Result<Vec<String>> {
if built_against.is_empty() {
return Ok(Vec::new());
}
let dep_name = deps::dep_name(dep);
let candidates = self.collect_candidates(dep_name, None)?;
let mut preferred = Vec::new();
for built in built_against {
let built_name = deps::dep_name(built);
if candidates
.iter()
.any(|candidate| candidate.package.eq_ignore_ascii_case(built_name))
{
push_unique(&mut preferred, built.to_string());
}
}
Ok(preferred)
}
fn choose_candidate(
&self,
dep: &str,
candidates: &[Candidate],
preferred_packages: &[String],
) -> Result<Candidate> {
let mut sorted = prune_replacement_fallback_candidates(dedupe_candidate_packages( let mut sorted = prune_replacement_fallback_candidates(dedupe_candidate_packages(
sort_candidates(candidates, self.opts.prefer_binary), sort_candidates(candidates, self.opts.prefer_binary),
)); ));
for preferred in preferred_packages {
let preferred_name = deps::dep_name(preferred);
if let Some(position) = sorted
.iter()
.position(|candidate| candidate.package.eq_ignore_ascii_case(preferred_name))
{
return Ok(sorted.remove(position));
}
}
if sorted.len() == 1 { if sorted.len() == 1 {
return Ok(sorted.remove(0)); return Ok(sorted.remove(0));
} }
@@ -457,7 +520,12 @@ impl<'a> Resolver<'a> {
) { ) {
Ok(records) => { Ok(records) => {
for rec in records { for rec in records {
let match_kind = if rec.name.eq_ignore_ascii_case(dep_name) { let match_kind = if rec.name.eq_ignore_ascii_case(dep_name)
|| rec
.real_name
.as_deref()
.is_some_and(|name| name.eq_ignore_ascii_case(dep_name))
{
MatchKind::Exact MatchKind::Exact
} else if rec } else if rec
.replaces .replaces
@@ -862,7 +930,7 @@ mod tests {
dedupe_candidate_packages, prune_replacement_fallback_candidates, sort_candidates, dedupe_candidate_packages, prune_replacement_fallback_candidates, sort_candidates,
source_deps_for_install, source_deps_for_install,
}; };
use crate::config::Config; use crate::config::{BinaryRepo, Config};
use crate::db; use crate::db;
use crate::package::{ use crate::package::{
Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source, Alternatives, Build, BuildFlags, BuildType, Dependencies, PackageInfo, PackageSpec, Source,
@@ -881,6 +949,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: vec![PackageInfo { packages: vec![PackageInfo {
@@ -891,6 +960,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}], }],
alternatives: Alternatives::default(), alternatives: Alternatives::default(),
@@ -958,6 +1028,7 @@ mod tests {
version: "1.0.0".to_string(), version: "1.0.0".to_string(),
revision: 1, revision: 1,
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
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,
@@ -1269,6 +1340,121 @@ version = "4.3.2"
)); ));
} }
fn write_compressed_repo_db(db_path: &Path, zst_path: &Path) {
let mut input = fs::File::open(db_path).unwrap();
let output = fs::File::create(zst_path).unwrap();
let mut encoder = zstd::stream::write::Encoder::new(output, 3).unwrap();
std::io::copy(&mut input, &mut encoder).unwrap();
encoder.finish().unwrap();
}
#[test]
fn binary_plan_uses_built_against_concrete_dependency() {
let rootfs = tempfile::tempdir().unwrap();
let repo_dir = tempfile::tempdir().unwrap();
let cache_dir = tempfile::tempdir().unwrap();
let mut config = Config::for_rootfs(rootfs.path());
config.package_cache_dir = cache_dir.path().to_path_buf();
config.repo_settings.prefer_binary = true;
config.binary_repos.insert(
"core".into(),
BinaryRepo {
url: url::Url::from_directory_path(repo_dir.path())
.expect("file URL")
.to_string(),
allow_unsigned: true,
..BinaryRepo::default()
},
);
let installed_dest = rootfs.path().join("installed");
fs::create_dir_all(&installed_dest).unwrap();
let mut installed = mk_installed_spec("icu79", "79.1");
installed.package.real_name = Some("icu".into());
db::register_package(
&config.installed_db_path(rootfs.path()),
&installed,
&installed_dest,
)
.unwrap();
let db_path = repo_dir.path().join("repo.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch(
"
CREATE TABLE packages (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
real_name TEXT,
version TEXT NOT NULL,
revision INTEGER NOT NULL,
abi_breaking INTEGER NOT NULL DEFAULT 0,
built_against TEXT NOT NULL DEFAULT '',
completed_at INTEGER,
description TEXT,
homepage TEXT,
license TEXT,
filename TEXT NOT NULL,
size INTEGER NOT NULL,
sha256 TEXT NOT NULL,
sha512 TEXT NOT NULL
);
CREATE TABLE provides (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE replaces (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE conflicts (package_id INTEGER, name TEXT NOT NULL);
CREATE TABLE dependencies (package_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL);
CREATE TABLE groups (package_id INTEGER, name TEXT NOT NULL);
",
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (1, 'app', NULL, '1.0', 1, 0, 'icu78', NULL, NULL, NULL, NULL, 'app.pkg', 1, 'aa', 'bb')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO dependencies (package_id, kind, name) VALUES (1, 'runtime', 'icu')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (2, 'icu78', 'icu', '78.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu78.pkg', 1, 'cc', 'dd')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO packages (id, name, real_name, version, revision, abi_breaking, built_against, completed_at, description, homepage, license, filename, size, sha256, sha512)
VALUES (3, 'icu79', 'icu', '79.1', 1, 0, '', NULL, NULL, NULL, NULL, 'icu79.pkg', 1, 'ee', 'ff')",
[],
)
.unwrap();
drop(conn);
write_compressed_repo_db(&db_path, &repo_dir.path().join("repo.db.zst"));
fs::remove_file(&db_path).unwrap();
let plan = super::build_install_plan(
&config,
rootfs.path(),
super::InstallTarget::PackageName("app".into()),
PlannerOptions {
assume_yes: true,
prefer_binary: true,
local_sibling_root: None,
include_test_deps: false,
lib32_only_requested_specs: false,
},
)
.unwrap();
let actionable: Vec<_> = plan
.actionable_steps()
.map(|step| step.package.as_str())
.collect();
assert_eq!(actionable, vec!["icu78", "app"]);
}
#[test] #[test]
fn add_dependency_edge_skips_active_binary_cycle_back_edge() { fn add_dependency_edge_skips_active_binary_cycle_back_edge() {
let rootfs = tempfile::tempdir().unwrap(); let rootfs = tempfile::tempdir().unwrap();
+1
View File
@@ -712,6 +712,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+1
View File
@@ -329,6 +329,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+3
View File
@@ -758,6 +758,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -817,6 +818,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -886,6 +888,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
+4
View File
@@ -1753,6 +1753,7 @@ mod tests {
description: "d".into(), description: "d".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}, },
packages: Vec::new(), packages: Vec::new(),
@@ -1859,6 +1860,7 @@ mod tests {
description: "dev".into(), description: "dev".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
spec.build.flags.split_docs = true; spec.build.flags.split_docs = true;
@@ -1930,6 +1932,7 @@ mod tests {
description: "dev".into(), description: "dev".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["MIT".into()], license: vec!["MIT".into()],
}); });
spec.packages.push(PackageInfo { spec.packages.push(PackageInfo {
@@ -1940,6 +1943,7 @@ mod tests {
description: "extras".into(), description: "extras".into(),
homepage: "h".into(), homepage: "h".into(),
abi_breaking: false, abi_breaking: false,
built_against: Vec::new(),
license: vec!["Apache-2.0".into()], license: vec!["Apache-2.0".into()],
}); });