From 028752b591a9329e304c94186fa404d34744fb4a Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 15:24:43 +0200 Subject: [PATCH 01/15] bootupd: make new packagesystem, using a file, no rpm -q Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 341 ++++++++++++++++++++--------------- src/packagesystem_legacy.rs | 344 ++++++++++++++++++++++++++++++++++++ 2 files changed, 540 insertions(+), 145 deletions(-) create mode 100644 src/packagesystem_legacy.rs diff --git a/src/packagesystem.rs b/src/packagesystem.rs index f20e5e81..70183c63 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -1,6 +1,5 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, BTreeSet}; -use std::io::Write; use std::path::Path; use anyhow::{bail, Context, Result}; @@ -9,7 +8,31 @@ use serde::{Deserialize, Serialize}; use uapi_version::Version; use crate::model::*; -use crate::ostreeutil; + +//Manifest file is a alternative to rpm structure, its compatiable with rpm -q --qwertyformat structure. This file need to be generated first and it looks like this: +// grub2-1:2.12-28.fc42,1710000000 shim-15.8-3,170000000 +const MANIFEST_PATH: &str = "usr/lib/bootupd/manifest"; + +//If any package starts with grub** shim**, this will make in one name in ANY distros +const CANONICAL_NAMES: &[&str] = &["grub", "shim"]; + +fn normalize_package_name(name: &str) -> &str { + for canonical in CANONICAL_NAMES { + if name == *canonical { + return canonical; + } + + // if package name is grub-efi -> grub, grub2-efi -> grub + if let Some(rest) = name.strip_prefix(canonical) { + let next = rest.chars().next(); + match next { + Some(c) if c.is_ascii_digit() || !c.is_ascii_alphabetic() => return canonical, + _ => {} + } + } + } + name +} #[derive(Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] pub(crate) struct Module { @@ -21,102 +44,94 @@ impl Module { pub(crate) fn rpm_evr(&self) -> Version { Version::from(&self.rpm_evr) } + + fn canonical_name(&self) -> &str { + normalize_package_name(&self.name) + } } impl Ord for Module { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.name - .cmp(&other.name) // Compare names first - .then_with(|| self.rpm_evr().cmp(&other.rpm_evr())) // If names equal, compare versions + fn cmp(&self, other: &Self) -> Ordering { + self.canonical_name() + .cmp(&other.canonical_name()) + .then_with(|| self.rpm_evr().cmp(&other.rpm_evr())) } } impl PartialOrd for Module { - fn partial_cmp(&self, other: &Self) -> Option { + fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -/// Parse the output of `rpm -q` -fn rpm_parse_metadata(stdout: &[u8]) -> Result { - let pkgs = std::str::from_utf8(stdout)? +fn parse_manifest(data: &[u8]) -> Result { + let pkgs = std::str::from_utf8(data) + .context("Manifest is not valid UTF-8")? .split_whitespace() .map(|s| -> Result<_> { - let parts: Vec<_> = s.splitn(2, ',').collect(); - let name = parts[0]; - if let Some(ts) = parts.get(1) { - let nt = DateTime::parse_from_str(ts, "%s") - .context("Failed to parse rpm buildtime")? - .with_timezone(&chrono::Utc); - Ok((name, nt)) - } else { - bail!("Failed to parse: {}", s); - } + let mut parts = s.splitn(2, ','); + let name = parts + .next() + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow::anyhow!("Missing package name in entry: {}", s))?; + let ts_str = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Missing buildtime in entry: {}", s))?; + let ts = DateTime::parse_from_str(ts_str, "%s") + .with_context(|| format!("Invalid buildtime in entry: {}", s))? + .with_timezone(&Utc); + Ok((name, ts)) }) .collect::>>>()?; + if pkgs.is_empty() { - bail!("Failed to find any RPM packages matching files in source efidir"); + bail!("Manifest contains no entries"); } - let timestamps: BTreeSet<&DateTime> = pkgs.values().collect(); - // Unwrap safety: We validated pkgs has at least one value above - let largest_timestamp = timestamps.iter().last().unwrap(); - let version = pkgs.keys().fold("".to_string(), |mut s, n| { - if !s.is_empty() { - s.push(','); - } - s.push_str(n); - s - }); - // Map the version into Module struct - let mut modules_vec: Vec = pkgs.keys().map(|pkg_str| parse_evr(pkg_str)).collect(); - modules_vec.sort_unstable(); + let largest_timestamp = pkgs + .values() + .collect::>() + .into_iter() + .last() + .expect("pkgs is non-empty"); + + let version = pkgs.keys().cloned().collect::>().join(","); + + let mut modules: Vec = pkgs.keys().map(|s| parse_evr(s)).collect(); + modules.sort_unstable(); + modules.dedup(); + Ok(ContentMetadata { - timestamp: **largest_timestamp, + timestamp: *largest_timestamp, version, - versions: Some(modules_vec), + versions: Some(modules), }) } -/// Query the rpm database and list the package and build times. pub(crate) fn query_files( sysroot_path: &str, - paths: impl IntoIterator, + _paths: impl IntoIterator, ) -> Result where T: AsRef, { - let mut c = ostreeutil::rpm_cmd(sysroot_path)?; - c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]); - for arg in paths { - c.arg(arg.as_ref()); - } - - let rpmout = c.output()?; - if !rpmout.status.success() { - std::io::stderr().write_all(&rpmout.stderr)?; - bail!("Failed to invoke rpm -qf"); - } - - rpm_parse_metadata(&rpmout.stdout) + let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); + let data = std::fs::read(&manifest_path) + .with_context(|| format!("Failed to read manifest: {}", manifest_path.display()))?; + parse_manifest(&data) } fn split_name_version(input: &str) -> Option<(String, String)> { - // assume it is "grub2-tools-1:2.06-110.el9.x86_64" - // strip .arch let main = input.rsplit_once('.')?.0; - - // find last two '-' let mut parts = main.rsplitn(3, '-'); - let release = parts.next()?; // after last '-' - let version = parts.next()?; // between last two '-' - let name = parts.next()?; // the rest (may contain '-') - + let release = parts.next()?; + let version = parts.next()?; + let name = parts.next()?; Some((name.to_string(), format!("{version}-{release}"))) } +//In this function if it using rpm use rpm_rs fn parse_evr(pkg: &str) -> Module { - // assume it is "grub2-1:2.12-28.fc42" (from usr/lib/efi) if !pkg.ends_with(std::env::consts::ARCH) { let (name, evr) = pkg.split_once('-').unwrap_or((pkg, "")); return Module { @@ -145,14 +160,8 @@ fn parse_evr(pkg: &str) -> Module { } fn parse_evr_vec(input: &str) -> Vec { - let mut pkgs: Vec = input - .split(',') - .map(|pkg| parse_evr(pkg)) // parse_evr returns owned Package - .collect(); - // Sort packages to ensure a consistent order for comparison, which is - // required by `compare_package_slices`. + let mut pkgs: Vec = input.split(',').map(|pkg| parse_evr(pkg)).collect(); pkgs.sort_unstable(); - // Now that it's sorted, we can efficiently remove duplicates. pkgs.dedup(); pkgs } @@ -160,21 +169,21 @@ fn parse_evr_vec(input: &str) -> Vec { pub(crate) fn compare_package_slices(a: &[Module], b: &[Module]) -> Ordering { let mut has_greater = false; - // Assume it is in order for (pkg_a, pkg_b) in a.iter().zip(b.iter()) { + // Compare only versions - names are already normalized via canonical_name() + // in Ord so sort order is consistent across distros. match pkg_a.cmp(pkg_b) { - Ordering::Less => return Ordering::Less, // upgradable - Ordering::Greater => has_greater = true, // downgrade + Ordering::Less => return Ordering::Less, + Ordering::Greater => has_greater = true, Ordering::Equal => {} } } - // If all compared equal, longer slice wins if a.len() < b.len() { - return Ordering::Less; // extra packages in b → upgrade + return Ordering::Less; } if a.len() > b.len() { - return Ordering::Greater; // extra packages in a → downgrade + return Ordering::Greater; } if has_greater { @@ -184,12 +193,7 @@ pub(crate) fn compare_package_slices(a: &[Module], b: &[Module]) -> Ordering { } } -// Compare package versions: -// If any package is Ordering::Less, return Ordering::Less, means upgradable, -// Else if any package is Ordering::Greater, return Ordering::Greater, -// Else (all equal), return Ordering::Equal. pub(crate) fn compare_package_versions(a: &str, b: &str) -> Ordering { - // Fast path: if the two values are equal, skip detailed comparison if a == b { return Ordering::Equal; } @@ -201,27 +205,111 @@ pub(crate) fn compare_package_versions(a: &str, b: &str) -> Ordering { #[cfg(test)] mod tests { use super::*; + use tempfile::TempDir; + + fn write_manifest(dir: &TempDir, content: &str) { + let manifest_dir = dir.path().join("usr/lib/bootupd"); + std::fs::create_dir_all(&manifest_dir).unwrap(); + std::fs::write(manifest_dir.join("manifest"), content).unwrap(); + } + + #[test] + fn test_normalize_package_name() { + // Canonic names without change + assert_eq!(normalize_package_name("grub"), "grub"); + assert_eq!(normalize_package_name("shim"), "shim"); + // Grub variants + assert_eq!(normalize_package_name("grub2"), "grub"); + assert_eq!(normalize_package_name("grub2-tools"), "grub"); + assert_eq!(normalize_package_name("grub2-efi-x64"), "grub"); + assert_eq!(normalize_package_name("grub2-efi-ia32"), "grub"); + assert_eq!(normalize_package_name("grub2-common"), "grub"); + assert_eq!(normalize_package_name("grub-efi-amd64"), "grub"); + assert_eq!(normalize_package_name("grub-efi-arm64"), "grub"); + assert_eq!(normalize_package_name("grub-pc"), "grub"); + // Shim Variants + assert_eq!(normalize_package_name("shim-x64"), "shim"); + assert_eq!(normalize_package_name("shim-ia32"), "shim"); + assert_eq!(normalize_package_name("shim-signed"), "shim"); + assert_eq!(normalize_package_name("shim-unsigned"), "shim"); + // Should not (this is not grub, or shim) + assert_eq!(normalize_package_name("grubby"), "grubby"); + assert_eq!(normalize_package_name("shimmer"), "shimmer"); + // Unkown packages, no changes + assert_eq!(normalize_package_name("unknown-pkg"), "unknown-pkg"); + } #[test] - fn test_parse_rpmout() { - let testdata = "grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566"; - let parsed = rpm_parse_metadata(testdata.as_bytes()).unwrap(); + fn test_parse_manifest() { + let data = + b"grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 shim-x64-15.6-2.x86_64,1657222566 "; + let parsed = parse_manifest(data).unwrap(); assert_eq!( parsed.version, "grub2-efi-x64-1:2.06-95.fc38.x86_64,shim-x64-15.6-2.x86_64" ); - let expected_modules = vec![ + let modules = parsed.versions.unwrap(); + assert_eq!(modules[0].name, "grub2"); + assert_eq!(modules[0].rpm_evr, "1:2.06-95.fc38"); + assert_eq!(modules[1].name, "shim"); + assert_eq!(modules[1].rpm_evr, "15.6-2"); + } + + #[test] + fn test_query_files_reads_manifest() { + let dir = TempDir::new().unwrap(); + write_manifest( + &dir, + "grub2-1:2.12-28.fc42,1710000000 shim-15.8-3,1700000000 ", + ); + let meta = query_files(dir.path().to_str().unwrap(), std::iter::empty::<&Path>()).unwrap(); + let modules = meta.versions.unwrap(); + assert_eq!(modules[0].name, "grub2"); + assert_eq!(modules[1].name, "shim"); + } + + #[test] + fn test_query_files_missing_manifest() { + let dir = TempDir::new().unwrap(); + let result = query_files(dir.path().to_str().unwrap(), std::iter::empty::<&Path>()); + assert!(result.is_err()); + } + + #[test] + fn test_compare_cross_distro() { + // grub2-efi-x64 (Fedora) vs grub (Arch) - that same version + let fedora = vec![ Module { - name: "grub2".to_string(), - rpm_evr: "1:2.06-95.fc38".to_string(), + name: "grub2-efi-x64".into(), + rpm_evr: "1:2.12-28.fc42".into(), }, Module { - name: "shim".to_string(), - rpm_evr: "15.6-2".to_string(), + name: "shim-x64".into(), + rpm_evr: "15.8-3".into(), }, ]; - - assert_eq!(parsed.versions, Some(expected_modules)); + let arch = vec![ + Module { + name: "grub".into(), + rpm_evr: "1:2.12-28.fc42".into(), + }, + Module { + name: "shim-signed".into(), + rpm_evr: "15.8-3".into(), + }, + ]; + assert_eq!(compare_package_slices(&fedora, &arch), Ordering::Equal); + + // grub2-tools (fedora) vs Arch (grub) + let rhel = vec![Module { + name: "grub2-tools".into(), + rpm_evr: "1:2.06-86.el9".into(), + }]; + let arch_newer = vec![Module { + name: "grub".into(), + rpm_evr: "1:2.12-28.fc42".into(), + }]; + assert_eq!(compare_package_slices(&rhel, &arch_newer), Ordering::Less); } #[test] @@ -246,99 +334,62 @@ mod tests { rpm_evr: "15.8-3".into(), }, ]; - let ord = compare_package_slices(&a, &b); - assert_eq!(ord, Ordering::Less); - - let ord = compare_package_slices(&b, &a); - assert_eq!(ord, Ordering::Greater); - - let ord = compare_package_slices(&a, &a); - assert_eq!(ord, Ordering::Equal); + assert_eq!(compare_package_slices(&a, &b), Ordering::Less); + assert_eq!(compare_package_slices(&b, &a), Ordering::Greater); + assert_eq!(compare_package_slices(&a, &a), Ordering::Equal); } #[test] fn test_compare_package_versions() { let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-efi-x64-1:2.12-29.fc42.x86_64,shim-x64-15.8-3.x86_64"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); // current < target - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-1:2.12-29.fc42,shim-15.8-3"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); // current < target - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; let target = "grub2-1:2.12-28.fc42,shim-15.8-4"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); // current < target - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); - // The target includes new package, should upgrade let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64,test"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); - - // The target missed some package - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); - // Not sure if this would happen - // current_grub2 > target_grub2 - // current_shim < target_shim - // In this case there is Ordering::Less, return Ordering::Less { let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; let target = "grub2-1:2.12-27.fc42,shim-15.8-4"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Less); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Less); } - // Test Equal { let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Equal); + assert_eq!(compare_package_versions(current, target), Ordering::Equal); let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Equal); + assert_eq!(compare_package_versions(current, target), Ordering::Equal); let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Equal); + assert_eq!(compare_package_versions(current, target), Ordering::Equal); } - // Test only grub2 let current = "grub2-tools-1:2.06-86.el9_4.3.x86_64"; let target = "grub2-tools-1:2.06-110.el9.x86_64"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); let current = "grub2-efi-ia32-1:2.12-21.fc41.x86_64,grub2-efi-x64-1:2.12-21.fc41.x86_64,shim-ia32-15.8-3.x86_64,shim-x64-15.8-3.x86_64"; let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; - let ord = compare_package_versions(current, target); - assert_eq!(ord, Ordering::Less); - - let ord = compare_package_versions(target, current); - assert_eq!(ord, Ordering::Greater); + assert_eq!(compare_package_versions(current, target), Ordering::Less); + assert_eq!(compare_package_versions(target, current), Ordering::Greater); } } diff --git a/src/packagesystem_legacy.rs b/src/packagesystem_legacy.rs new file mode 100644 index 00000000..f20e5e81 --- /dev/null +++ b/src/packagesystem_legacy.rs @@ -0,0 +1,344 @@ +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet}; +use std::io::Write; +use std::path::Path; + +use anyhow::{bail, Context, Result}; +use chrono::prelude::*; +use serde::{Deserialize, Serialize}; +use uapi_version::Version; + +use crate::model::*; +use crate::ostreeutil; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)] +pub(crate) struct Module { + pub(crate) name: String, + pub(crate) rpm_evr: String, +} + +impl Module { + pub(crate) fn rpm_evr(&self) -> Version { + Version::from(&self.rpm_evr) + } +} + +impl Ord for Module { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name + .cmp(&other.name) // Compare names first + .then_with(|| self.rpm_evr().cmp(&other.rpm_evr())) // If names equal, compare versions + } +} + +impl PartialOrd for Module { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Parse the output of `rpm -q` +fn rpm_parse_metadata(stdout: &[u8]) -> Result { + let pkgs = std::str::from_utf8(stdout)? + .split_whitespace() + .map(|s| -> Result<_> { + let parts: Vec<_> = s.splitn(2, ',').collect(); + let name = parts[0]; + if let Some(ts) = parts.get(1) { + let nt = DateTime::parse_from_str(ts, "%s") + .context("Failed to parse rpm buildtime")? + .with_timezone(&chrono::Utc); + Ok((name, nt)) + } else { + bail!("Failed to parse: {}", s); + } + }) + .collect::>>>()?; + if pkgs.is_empty() { + bail!("Failed to find any RPM packages matching files in source efidir"); + } + let timestamps: BTreeSet<&DateTime> = pkgs.values().collect(); + // Unwrap safety: We validated pkgs has at least one value above + let largest_timestamp = timestamps.iter().last().unwrap(); + let version = pkgs.keys().fold("".to_string(), |mut s, n| { + if !s.is_empty() { + s.push(','); + } + s.push_str(n); + s + }); + + // Map the version into Module struct + let mut modules_vec: Vec = pkgs.keys().map(|pkg_str| parse_evr(pkg_str)).collect(); + modules_vec.sort_unstable(); + Ok(ContentMetadata { + timestamp: **largest_timestamp, + version, + versions: Some(modules_vec), + }) +} + +/// Query the rpm database and list the package and build times. +pub(crate) fn query_files( + sysroot_path: &str, + paths: impl IntoIterator, +) -> Result +where + T: AsRef, +{ + let mut c = ostreeutil::rpm_cmd(sysroot_path)?; + c.args(["-q", "--queryformat", "%{nevra},%{buildtime} ", "-f"]); + for arg in paths { + c.arg(arg.as_ref()); + } + + let rpmout = c.output()?; + if !rpmout.status.success() { + std::io::stderr().write_all(&rpmout.stderr)?; + bail!("Failed to invoke rpm -qf"); + } + + rpm_parse_metadata(&rpmout.stdout) +} + +fn split_name_version(input: &str) -> Option<(String, String)> { + // assume it is "grub2-tools-1:2.06-110.el9.x86_64" + // strip .arch + let main = input.rsplit_once('.')?.0; + + // find last two '-' + let mut parts = main.rsplitn(3, '-'); + let release = parts.next()?; // after last '-' + let version = parts.next()?; // between last two '-' + let name = parts.next()?; // the rest (may contain '-') + + Some((name.to_string(), format!("{version}-{release}"))) +} + +fn parse_evr(pkg: &str) -> Module { + // assume it is "grub2-1:2.12-28.fc42" (from usr/lib/efi) + if !pkg.ends_with(std::env::consts::ARCH) { + let (name, evr) = pkg.split_once('-').unwrap_or((pkg, "")); + return Module { + name: name.to_string(), + rpm_evr: evr.to_string(), + }; + } + + let (name_str, rpm_evr) = { + #[cfg(not(feature = "rpm"))] + { + split_name_version(pkg).unwrap() + } + #[cfg(feature = "rpm")] + { + let nevra = rpm_rs::Nevra::parse(pkg); + (nevra.name().to_string(), nevra.evr().to_string()) + } + }; + + let (name, _) = name_str.split_once('-').unwrap_or((&name_str, "")); + Module { + name: name.to_string(), + rpm_evr, + } +} + +fn parse_evr_vec(input: &str) -> Vec { + let mut pkgs: Vec = input + .split(',') + .map(|pkg| parse_evr(pkg)) // parse_evr returns owned Package + .collect(); + // Sort packages to ensure a consistent order for comparison, which is + // required by `compare_package_slices`. + pkgs.sort_unstable(); + // Now that it's sorted, we can efficiently remove duplicates. + pkgs.dedup(); + pkgs +} + +pub(crate) fn compare_package_slices(a: &[Module], b: &[Module]) -> Ordering { + let mut has_greater = false; + + // Assume it is in order + for (pkg_a, pkg_b) in a.iter().zip(b.iter()) { + match pkg_a.cmp(pkg_b) { + Ordering::Less => return Ordering::Less, // upgradable + Ordering::Greater => has_greater = true, // downgrade + Ordering::Equal => {} + } + } + + // If all compared equal, longer slice wins + if a.len() < b.len() { + return Ordering::Less; // extra packages in b → upgrade + } + if a.len() > b.len() { + return Ordering::Greater; // extra packages in a → downgrade + } + + if has_greater { + Ordering::Greater + } else { + Ordering::Equal + } +} + +// Compare package versions: +// If any package is Ordering::Less, return Ordering::Less, means upgradable, +// Else if any package is Ordering::Greater, return Ordering::Greater, +// Else (all equal), return Ordering::Equal. +pub(crate) fn compare_package_versions(a: &str, b: &str) -> Ordering { + // Fast path: if the two values are equal, skip detailed comparison + if a == b { + return Ordering::Equal; + } + let pkg_a = parse_evr_vec(a); + let pkg_b = parse_evr_vec(b); + compare_package_slices(&pkg_a, &pkg_b) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_rpmout() { + let testdata = "grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 grub2-efi-x64-1:2.06-95.fc38.x86_64,1681321788 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566 shim-x64-15.6-2.x86_64,1657222566"; + let parsed = rpm_parse_metadata(testdata.as_bytes()).unwrap(); + assert_eq!( + parsed.version, + "grub2-efi-x64-1:2.06-95.fc38.x86_64,shim-x64-15.6-2.x86_64" + ); + let expected_modules = vec![ + Module { + name: "grub2".to_string(), + rpm_evr: "1:2.06-95.fc38".to_string(), + }, + Module { + name: "shim".to_string(), + rpm_evr: "15.6-2".to_string(), + }, + ]; + + assert_eq!(parsed.versions, Some(expected_modules)); + } + + #[test] + fn test_compare_package_slices() { + let a = vec![ + Module { + name: "grub2".into(), + rpm_evr: "1:2.12-21.fc41".into(), + }, + Module { + name: "shim".into(), + rpm_evr: "15.8-3".into(), + }, + ]; + let b = vec![ + Module { + name: "grub2".into(), + rpm_evr: "1:2.12-28.fc41".into(), + }, + Module { + name: "shim".into(), + rpm_evr: "15.8-3".into(), + }, + ]; + let ord = compare_package_slices(&a, &b); + assert_eq!(ord, Ordering::Less); + + let ord = compare_package_slices(&b, &a); + assert_eq!(ord, Ordering::Greater); + + let ord = compare_package_slices(&a, &a); + assert_eq!(ord, Ordering::Equal); + } + + #[test] + fn test_compare_package_versions() { + let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-efi-x64-1:2.12-29.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); // current < target + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + + let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-1:2.12-29.fc42,shim-15.8-3"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); // current < target + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + + let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let target = "grub2-1:2.12-28.fc42,shim-15.8-4"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); // current < target + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + + // The target includes new package, should upgrade + let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64,test"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); + + // The target missed some package + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + + // Not sure if this would happen + // current_grub2 > target_grub2 + // current_shim < target_shim + // In this case there is Ordering::Less, return Ordering::Less + { + let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let target = "grub2-1:2.12-27.fc42,shim-15.8-4"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Less); + } + + // Test Equal + { + let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Equal); + + let current = "grub2-efi-x64-1:2.12-28.fc42.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Equal); + + let current = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Equal); + } + + // Test only grub2 + let current = "grub2-tools-1:2.06-86.el9_4.3.x86_64"; + let target = "grub2-tools-1:2.06-110.el9.x86_64"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + + let current = "grub2-efi-ia32-1:2.12-21.fc41.x86_64,grub2-efi-x64-1:2.12-21.fc41.x86_64,shim-ia32-15.8-3.x86_64,shim-x64-15.8-3.x86_64"; + let target = "grub2-1:2.12-28.fc42,shim-15.8-3"; + let ord = compare_package_versions(current, target); + assert_eq!(ord, Ordering::Less); + + let ord = compare_package_versions(target, current); + assert_eq!(ord, Ordering::Greater); + } +} From aaf67714f8fa4f96ba3bedd9415034a6ba4ddcf5 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 18:17:54 +0200 Subject: [PATCH 02/15] bootupd: add manifest generator Signed-off-by: krolmiki2011 --- src/main.rs | 1 + src/manifest.rs | 523 +++++++++++++++++++++++++++++++++++++++++++ src/packagesystem.rs | 13 ++ 3 files changed, 537 insertions(+) create mode 100644 src/manifest.rs diff --git a/src/main.rs b/src/main.rs index 56e9ef99..c18d7d57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,7 @@ mod ostreeutil; mod packagesystem; mod sha512string; mod util; +mod manifest; use clap::crate_name; diff --git a/src/manifest.rs b/src/manifest.rs new file mode 100644 index 00000000..42942c91 --- /dev/null +++ b/src/manifest.rs @@ -0,0 +1,523 @@ +use std::collections::BTreeMap; +use std::path::Path; +use std::process::Command; + +use anyhow::{bail, Context, Result}; +use log::debug; + + +//RPM (Fedora, Opensuse) +const LEGACY_RPMOSTREE_DBPATH: &str = "usr/share/rpm"; +const SYSIMAGE_RPM_DBPATH: &str = "usr/lib/sysimage/rpm"; + + +//DPKG (Debian, Ubuntu) +const LEGACY_APT_DBPATH: &str = "var/lib/dpkg"; +const SYSIMAGE_APT_DBPATH: &str = "usr/lib/sysimage/dpkg"; + +//PACMAN (Arch Linux) +const LEGACY_PACMAN_DBPATH: &str = "var/lib/pacman"; +const SYSIMAGE_PACMAN_DBPATH: &str = "usr/lib/sysimage/pacman"; + +//APK (Alpine Linux) +const APK_DBPATH: &str = "usr/lib/apk/db/installed"; + + +pub(crate) struct ManifestEntry { + package: String, + time: i64, +} + +pub(crate) const MANIFEST_PATH: &str = "usr/lib/bootupd/manifest"; + +#[derive(Debug, PartialEq)] +enum PackageManager { + Rpm, + Dpkg, + Pacman, + Apk, +} + + +fn is_nonempty_file(path: &Path) -> bool { + path.metadata().map(|m| m.len() > 0).unwrap_or(false) +} + +fn is_nonempty_dir(path: &Path) -> bool { + path.read_dir() + .map(|mut d| d.next().is_some()) + .unwrap_or(false) +} + +fn find_rpm_dbpath(sysroot_path: &str) -> Option { + let sysroot = Path::new(sysroot_path); + for dbpath in [SYSIMAGE_RPM_DBPATH, LEGACY_RPMOSTREE_DBPATH] { + let p = sysroot.join(dbpath); + if is_nonempty_dir(&p) { + return Some(p); + } + } + None +} + +fn find_dpkg_dbpath(sysroot_path: &str) -> Option { + let sysroot = Path::new(sysroot_path); + for dbpath in [SYSIMAGE_APT_DBPATH, LEGACY_APT_DBPATH] { + let p = sysroot.join(dbpath); + if is_nonempty_file(&p) { + return Some(p); + } + } + None +} + +fn find_pacman_dbpath(sysroot_path: &str) -> Option { + let sysroot = Path::new(sysroot_path); + for dbpath in [SYSIMAGE_PACMAN_DBPATH, LEGACY_PACMAN_DBPATH] { + let p = sysroot.join(dbpath); + if p.exists() { + return Some(p); + } + } + None +} + +fn detect_package_manager(sysroot_path: &str) -> Result { + if let Some(p) = find_rpm_dbpath(sysroot_path) { + debug!("Detected RPM (dbpath: {})", p.display()); + return Ok(PackageManager::Rpm); + } + + if let Some(p) = find_dpkg_dbpath(sysroot_path) { + debug!("Detected DPKG (dbpath: {})", p.display()); + return Ok(PackageManager::Dpkg); + } + + if let Some(p) = find_pacman_dbpath(sysroot_path) { + debug!("Detected Pacman (dbpath: {})", p.display()); + return Ok(PackageManager::Pacman); + } + + if Path::new(sysroot_path).join(APK_DBPATH).exists() { + debug!("Detected APK"); + return Ok(PackageManager::Apk); + } + + bail!( + "No supported package manager found in sysroot '{}' \ + (checked: rpm, dpkg, pacman, apk)", + sysroot_path + ) +} + + +fn query_rpm(sysroot_path: &str, file: &Path) -> Result { + let dbpath = find_rpm_dbpath(sysroot_path) + .ok_or_else(|| anyhow::anyhow!("RPM database not found in sysroot '{}'", sysroot_path))?; + + let out = Command::new("rpm") + .arg(format!("--dbpath={}", dbpath.display())) + .args(["-qf", "--queryformat", "%{nevra},%{buildtime}"]) + .arg(file) + .output() + .context("Failed to run rpm")?; + + if !out.status.success() { + bail!( + "rpm -qf failed for {}: {}", + file.display(), + String::from_utf8_lossy(&out.stderr).trim() + ); + } + + let line = std::str::from_utf8(&out.stdout) + .context("rpm output is not valid UTF-8")? + .trim() + .to_string(); + + parse_manifest_entry(&line) + .with_context(|| format!("Failed to parse rpm output: '{}'", line)) +} + +fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { + // Uzywa tej samej sciezki bazy co detect_package_manager + let dbpath = find_dpkg_dbpath(sysroot_path) + .ok_or_else(|| anyhow::anyhow!("DPKG database not found in sysroot '{}'", sysroot_path))?; + + let out = Command::new("dpkg") + .arg(format!( + "--admindir={}", + dbpath.parent().unwrap().display() + )) + .args(["-S", &file.to_string_lossy()]) + .output() + .context("Failed to run dpkg -S")?; + + if !out.status.success() { + bail!("dpkg -S found no package owning {}", file.display()); + } + + // Format: "pakiet: /sciezka" + let stdout = String::from_utf8_lossy(&out.stdout); + let pkg = stdout + .lines() + .next() + .and_then(|l| l.split(':').next()) + .map(|s| s.trim().to_string()) + .ok_or_else(|| anyhow::anyhow!("Failed to parse dpkg -S output: '{}'", stdout.trim()))?; + + let ver_out = Command::new("dpkg-query") + .arg(format!( + "--admindir={}", + dbpath.parent().unwrap().display() + )) + .args(["-W", "-f=${Package}-${Version}", &pkg]) + .output() + .context("Failed to run dpkg-query")?; + + let package = std::str::from_utf8(&ver_out.stdout) + .context("dpkg-query output is not valid UTF-8")? + .trim() + .to_string(); + + // dpkg nie przechowuje buildtime — uzywa mtime pliku jako przyblizenia + let time = std::fs::metadata(file) + .and_then(|m| m.modified()) + .map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 + }) + .unwrap_or(0); + + Ok(ManifestEntry { package, time }) +} + +fn query_pacman(sysroot_path: &str, file: &Path) -> Result { + let dbpath = find_pacman_dbpath(sysroot_path) + .ok_or_else(|| anyhow::anyhow!("Pacman database not found in sysroot '{}'", sysroot_path))?; + + let out = Command::new("pacman") + .arg(format!("--dbpath={}", dbpath.display())) + .args(["-Qo", &file.to_string_lossy()]) + .output() + .context("Failed to run pacman -Qo")?; + + if !out.status.success() { + bail!("pacman -Qo found no package owning {}", file.display()); + } + + // Format: "/usr/sbin/grub-install is owned by grub 2:2.12-1" + let stdout = String::from_utf8_lossy(&out.stdout); + let words: Vec<&str> = stdout.trim().split_whitespace().collect(); + if words.len() < 2 { + bail!("Unexpected pacman -Qo output: '{}'", stdout.trim()); + } + let pkg = words[words.len() - 2]; + let ver = words[words.len() - 1]; + let package = format!("{}-{}", pkg, ver); + + // BUILDDATE z /local/-/desc to Unix timestamp + // Uzywa tej samej sciezki bazy co wykrycie + let desc_path = dbpath + .join("local") + .join(format!("{}-{}", pkg, ver)) + .join("desc"); + + let time = if desc_path.exists() { + let content = std::fs::read_to_string(&desc_path) + .with_context(|| format!("Failed to read {}", desc_path.display()))?; + let mut found = false; + let mut ts = 0i64; + for line in content.lines() { + if line == "%BUILDDATE%" { + found = true; + continue; + } + if found { + ts = line.parse().unwrap_or(0); + break; + } + } + ts + } else { + std::fs::metadata(file) + .and_then(|m| m.modified()) + .map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() as i64 + }) + .unwrap_or(0) + }; + + Ok(ManifestEntry { package, time }) +} + +fn query_apk(sysroot_path: &str, file: &Path) -> Result { + let out = Command::new("apk") + .arg(format!("--root={}", sysroot_path)) + .args(["info", "--who-owns", &file.to_string_lossy()]) + .output() + .context("Failed to run apk info --who-owns")?; + + if !out.status.success() { + bail!("apk found no package owning {}", file.display()); + } + + // Format: "/usr/sbin/grub-install is owned by grub-2.12-r0" + let stdout = String::from_utf8_lossy(&out.stdout); + let pkg_ver = stdout + .trim() + .split(" is owned by ") + .nth(1) + .map(|s| s.trim().to_string()) + .ok_or_else(|| anyhow::anyhow!("Unexpected apk output: '{}'", stdout.trim()))?; + + let pkg_name = pkg_ver + .rsplitn(3, '-') + .last() + .unwrap_or(&pkg_ver) + .to_string(); + + let ts_out = Command::new("apk") + .arg(format!("--root={}", sysroot_path)) + .args(["info", "-t", &pkg_name]) + .output() + .context("Failed to run apk info -t")?; + + let time = std::str::from_utf8(&ts_out.stdout) + .unwrap_or("") + .trim() + .parse::() + .unwrap_or(0); + + Ok(ManifestEntry { + package: pkg_ver, + time, + }) +} + +fn parse_manifest_entry(entry: &str) -> Result { + let mut parts = entry.splitn(2, ','); + let package = parts + .next() + .filter(|s| !s.is_empty()) + .ok_or_else(|| anyhow::anyhow!("Missing package name"))? + .to_string(); + let time = parts + .next() + .ok_or_else(|| anyhow::anyhow!("Missing time"))? + .trim() + .parse::() + .context("Failed to parse time as integer")?; + Ok(ManifestEntry { package, time }) +} + +fn query_file_owner( + sysroot_path: &str, + pm: &PackageManager, + file: &Path, +) -> Result { + match pm { + PackageManager::Rpm => query_rpm(sysroot_path, file), + PackageManager::Dpkg => query_dpkg(sysroot_path, file), + PackageManager::Pacman => query_pacman(sysroot_path, file), + PackageManager::Apk => query_apk(sysroot_path, file), + } +} + +pub(crate) fn generate_manifest(sysroot_path: &str, files: &[&Path]) -> Result<()> { + if files.is_empty() { + bail!("No files specified for manifest generation"); + } + + let pm = detect_package_manager(sysroot_path) + .context("Failed to detect package manager")?; + println!("Detected package manager: {:?}", pm); + + let mut entries: BTreeMap = BTreeMap::new(); + + for file in files { + if !file.exists() { + println!("File not found, skipping: {}", file.display()); + continue; + } + match query_file_owner(sysroot_path, &pm, file) { + Ok(entry) => { + println!( + " {} -> {} (buildtime: {})", + file.display(), + entry.package, + entry.time + ); + entries + .entry(entry.package) + .and_modify(|ts| { + if entry.time > *ts { + *ts = entry.time; + } + }) + .or_insert(entry.time); + } + Err(e) => { + println!("Warning: failed to query owner of {}: {:#}", file.display(), e); + } + } + } + + if entries.is_empty() { + bail!("No packages found for the given files, manifest not written"); + } + + let content: String = entries + .iter() + .map(|(pkg, ts)| format!("{},{} ", pkg, ts)) + .collect(); + + let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); + std::fs::create_dir_all(manifest_path.parent().expect("manifest has parent dir")) + .with_context(|| format!("Failed to create manifest directory"))?; + std::fs::write(&manifest_path, content.trim_end()) + .with_context(|| format!("Failed to write manifest: {}", manifest_path.display()))?; + + println!("Manifest written to: {}", manifest_path.display()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn make_sysroot_with_rpm(dir: &TempDir) { + let dbpath = dir.path().join(SYSIMAGE_RPM_DBPATH); + std::fs::create_dir_all(&dbpath).unwrap(); + std::fs::write(dbpath.join("Packages"), b"dummy").unwrap(); + } + + fn make_sysroot_with_dpkg(dir: &TempDir) { + let dbpath = dir.path().join(LEGACY_APT_DBPATH); + std::fs::create_dir_all(dbpath.parent().unwrap()).unwrap(); + std::fs::write(&dbpath, b"Package: grub-efi-amd64\n").unwrap(); + } + + fn make_sysroot_with_pacman(dir: &TempDir) { + let dbpath = dir.path().join(LEGACY_PACMAN_DBPATH); + std::fs::create_dir_all(&dbpath).unwrap(); + // Musi byc niepusty zeby is_nonempty_dir zwrocilo true + std::fs::write(dbpath.join("dummy"), b"dummy").unwrap(); + } + + fn make_sysroot_with_apk(dir: &TempDir) { + let dbpath = dir.path().join(APK_DBPATH); + std::fs::create_dir_all(dbpath.parent().unwrap()).unwrap(); + std::fs::write(&dbpath, b"P:grub\n").unwrap(); + } + + #[test] + fn test_detect_rpm() { + let dir = TempDir::new().unwrap(); + make_sysroot_with_rpm(&dir); + assert_eq!( + detect_package_manager(dir.path().to_str().unwrap()).unwrap(), + PackageManager::Rpm + ); + } + + #[test] + fn test_detect_dpkg() { + let dir = TempDir::new().unwrap(); + make_sysroot_with_dpkg(&dir); + assert_eq!( + detect_package_manager(dir.path().to_str().unwrap()).unwrap(), + PackageManager::Dpkg + ); + } + + #[test] + fn test_detect_pacman() { + let dir = TempDir::new().unwrap(); + make_sysroot_with_pacman(&dir); + assert_eq!( + detect_package_manager(dir.path().to_str().unwrap()).unwrap(), + PackageManager::Pacman + ); + } + + #[test] + fn test_detect_apk() { + let dir = TempDir::new().unwrap(); + make_sysroot_with_apk(&dir); + assert_eq!( + detect_package_manager(dir.path().to_str().unwrap()).unwrap(), + PackageManager::Apk + ); + } + + #[test] + fn test_detect_none() { + let dir = TempDir::new().unwrap(); + assert!(detect_package_manager(dir.path().to_str().unwrap()).is_err()); + } + + #[test] + fn test_find_rpm_dbpath_sysimage_wins() { + let dir = TempDir::new().unwrap(); + // Oba istnieja — sysimage powinien wygrac (jest pierwszy w liscie) + let sysimage = dir.path().join(SYSIMAGE_RPM_DBPATH); + std::fs::create_dir_all(&sysimage).unwrap(); + std::fs::write(sysimage.join("Packages"), b"dummy").unwrap(); + let legacy = dir.path().join(LEGACY_RPMOSTREE_DBPATH); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::write(legacy.join("Packages"), b"dummy").unwrap(); + + let found = find_rpm_dbpath(dir.path().to_str().unwrap()).unwrap(); + assert_eq!(found, sysimage); + } + + #[test] + fn test_find_rpm_dbpath_falls_back_to_legacy() { + let dir = TempDir::new().unwrap(); + // Tylko legacy istnieje + let legacy = dir.path().join(LEGACY_RPMOSTREE_DBPATH); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::write(legacy.join("Packages"), b"dummy").unwrap(); + + let found = find_rpm_dbpath(dir.path().to_str().unwrap()).unwrap(); + assert_eq!(found, legacy); + } + + #[test] + fn test_find_dpkg_dbpath_sysimage_wins() { + let dir = TempDir::new().unwrap(); + let sysimage = dir.path().join(SYSIMAGE_APT_DBPATH); + std::fs::create_dir_all(sysimage.parent().unwrap()).unwrap(); + std::fs::write(&sysimage, b"Package: grub\n").unwrap(); + let legacy = dir.path().join(LEGACY_APT_DBPATH); + std::fs::create_dir_all(legacy.parent().unwrap()).unwrap(); + std::fs::write(&legacy, b"Package: grub\n").unwrap(); + + let found = find_dpkg_dbpath(dir.path().to_str().unwrap()).unwrap(); + assert_eq!(found, sysimage); + } + + #[test] + fn test_parse_manifest_entry() { + let e = parse_manifest_entry("grub2-1:2.12-28.fc42,1710000000").unwrap(); + assert_eq!(e.package, "grub2-1:2.12-28.fc42"); + assert_eq!(e.time, 1710000000); + + let e = parse_manifest_entry("shim-x64-15.8-3.x86_64,1700000000").unwrap(); + assert_eq!(e.package, "shim-x64-15.8-3.x86_64"); + assert_eq!(e.time, 1700000000); + } + + #[test] + fn test_generate_manifest_no_files() { + let dir = TempDir::new().unwrap(); + make_sysroot_with_rpm(&dir); + assert!(generate_manifest(dir.path().to_str().unwrap(), &[]).is_err()); + } +} + diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 70183c63..ff8ef8d0 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -9,6 +9,8 @@ use uapi_version::Version; use crate::model::*; +use crate::manifest::*; + //Manifest file is a alternative to rpm structure, its compatiable with rpm -q --qwertyformat structure. This file need to be generated first and it looks like this: // grub2-1:2.12-28.fc42,1710000000 shim-15.8-3,170000000 const MANIFEST_PATH: &str = "usr/lib/bootupd/manifest"; @@ -116,8 +118,19 @@ where T: AsRef, { let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); + + let paths: Vec<_> = _paths.into_iter().collect(); + let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect(); + + //Generate Manifest + generate_manifest(sysroot_path, &path_refs) + .context("Failed to generate manifest")?; + + let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); let data = std::fs::read(&manifest_path) .with_context(|| format!("Failed to read manifest: {}", manifest_path.display()))?; + + parse_manifest(&data) } From 216ba4d5e66c3d8629039b71f16d1bc17ef94239 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 18:20:07 +0200 Subject: [PATCH 03/15] bootupd: add manifest generator Signed-off-by: krolmiki2011 --- src/main.rs | 2 +- src/manifest.rs | 39 +++++++++++++-------------------------- src/packagesystem.rs | 4 +--- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index c18d7d57..defc36c1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,13 +40,13 @@ mod freezethaw; target_arch = "riscv64" ))] mod grubconfigs; +mod manifest; mod model; mod model_legacy; mod ostreeutil; mod packagesystem; mod sha512string; mod util; -mod manifest; use clap::crate_name; diff --git a/src/manifest.rs b/src/manifest.rs index 42942c91..a4fdf574 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -5,12 +5,10 @@ use std::process::Command; use anyhow::{bail, Context, Result}; use log::debug; - //RPM (Fedora, Opensuse) const LEGACY_RPMOSTREE_DBPATH: &str = "usr/share/rpm"; const SYSIMAGE_RPM_DBPATH: &str = "usr/lib/sysimage/rpm"; - //DPKG (Debian, Ubuntu) const LEGACY_APT_DBPATH: &str = "var/lib/dpkg"; const SYSIMAGE_APT_DBPATH: &str = "usr/lib/sysimage/dpkg"; @@ -22,7 +20,6 @@ const SYSIMAGE_PACMAN_DBPATH: &str = "usr/lib/sysimage/pacman"; //APK (Alpine Linux) const APK_DBPATH: &str = "usr/lib/apk/db/installed"; - pub(crate) struct ManifestEntry { package: String, time: i64, @@ -38,7 +35,6 @@ enum PackageManager { Apk, } - fn is_nonempty_file(path: &Path) -> bool { path.metadata().map(|m| m.len() > 0).unwrap_or(false) } @@ -110,7 +106,6 @@ fn detect_package_manager(sysroot_path: &str) -> Result { ) } - fn query_rpm(sysroot_path: &str, file: &Path) -> Result { let dbpath = find_rpm_dbpath(sysroot_path) .ok_or_else(|| anyhow::anyhow!("RPM database not found in sysroot '{}'", sysroot_path))?; @@ -135,8 +130,7 @@ fn query_rpm(sysroot_path: &str, file: &Path) -> Result { .trim() .to_string(); - parse_manifest_entry(&line) - .with_context(|| format!("Failed to parse rpm output: '{}'", line)) + parse_manifest_entry(&line).with_context(|| format!("Failed to parse rpm output: '{}'", line)) } fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { @@ -145,10 +139,7 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { .ok_or_else(|| anyhow::anyhow!("DPKG database not found in sysroot '{}'", sysroot_path))?; let out = Command::new("dpkg") - .arg(format!( - "--admindir={}", - dbpath.parent().unwrap().display() - )) + .arg(format!("--admindir={}", dbpath.parent().unwrap().display())) .args(["-S", &file.to_string_lossy()]) .output() .context("Failed to run dpkg -S")?; @@ -167,10 +158,7 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { .ok_or_else(|| anyhow::anyhow!("Failed to parse dpkg -S output: '{}'", stdout.trim()))?; let ver_out = Command::new("dpkg-query") - .arg(format!( - "--admindir={}", - dbpath.parent().unwrap().display() - )) + .arg(format!("--admindir={}", dbpath.parent().unwrap().display())) .args(["-W", "-f=${Package}-${Version}", &pkg]) .output() .context("Failed to run dpkg-query")?; @@ -194,8 +182,9 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { } fn query_pacman(sysroot_path: &str, file: &Path) -> Result { - let dbpath = find_pacman_dbpath(sysroot_path) - .ok_or_else(|| anyhow::anyhow!("Pacman database not found in sysroot '{}'", sysroot_path))?; + let dbpath = find_pacman_dbpath(sysroot_path).ok_or_else(|| { + anyhow::anyhow!("Pacman database not found in sysroot '{}'", sysroot_path) + })?; let out = Command::new("pacman") .arg(format!("--dbpath={}", dbpath.display())) @@ -314,11 +303,7 @@ fn parse_manifest_entry(entry: &str) -> Result { Ok(ManifestEntry { package, time }) } -fn query_file_owner( - sysroot_path: &str, - pm: &PackageManager, - file: &Path, -) -> Result { +fn query_file_owner(sysroot_path: &str, pm: &PackageManager, file: &Path) -> Result { match pm { PackageManager::Rpm => query_rpm(sysroot_path, file), PackageManager::Dpkg => query_dpkg(sysroot_path, file), @@ -332,8 +317,7 @@ pub(crate) fn generate_manifest(sysroot_path: &str, files: &[&Path]) -> Result<( bail!("No files specified for manifest generation"); } - let pm = detect_package_manager(sysroot_path) - .context("Failed to detect package manager")?; + let pm = detect_package_manager(sysroot_path).context("Failed to detect package manager")?; println!("Detected package manager: {:?}", pm); let mut entries: BTreeMap = BTreeMap::new(); @@ -361,7 +345,11 @@ pub(crate) fn generate_manifest(sysroot_path: &str, files: &[&Path]) -> Result<( .or_insert(entry.time); } Err(e) => { - println!("Warning: failed to query owner of {}: {:#}", file.display(), e); + println!( + "Warning: failed to query owner of {}: {:#}", + file.display(), + e + ); } } } @@ -520,4 +508,3 @@ mod tests { assert!(generate_manifest(dir.path().to_str().unwrap(), &[]).is_err()); } } - diff --git a/src/packagesystem.rs b/src/packagesystem.rs index ff8ef8d0..17ad75b4 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -123,14 +123,12 @@ where let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect(); //Generate Manifest - generate_manifest(sysroot_path, &path_refs) - .context("Failed to generate manifest")?; + generate_manifest(sysroot_path, &path_refs).context("Failed to generate manifest")?; let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); let data = std::fs::read(&manifest_path) .with_context(|| format!("Failed to read manifest: {}", manifest_path.display()))?; - parse_manifest(&data) } From 5f8bcbd8a4dc717d6f6724ca27a2d08ea594d895 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 18:34:03 +0200 Subject: [PATCH 04/15] fix manifest generator Signed-off-by: krolmiki2011 --- src/manifest.rs | 47 +++++++++++++++++++++++++++++++------------- src/packagesystem.rs | 11 +++++++---- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/manifest.rs b/src/manifest.rs index a4fdf574..f4591a4a 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -111,6 +111,7 @@ fn query_rpm(sysroot_path: &str, file: &Path) -> Result { .ok_or_else(|| anyhow::anyhow!("RPM database not found in sysroot '{}'", sysroot_path))?; let out = Command::new("rpm") + .env("LC_ALL", "C") .arg(format!("--dbpath={}", dbpath.display())) .args(["-qf", "--queryformat", "%{nevra},%{buildtime}"]) .arg(file) @@ -134,11 +135,11 @@ fn query_rpm(sysroot_path: &str, file: &Path) -> Result { } fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { - // Uzywa tej samej sciezki bazy co detect_package_manager let dbpath = find_dpkg_dbpath(sysroot_path) .ok_or_else(|| anyhow::anyhow!("DPKG database not found in sysroot '{}'", sysroot_path))?; let out = Command::new("dpkg") + .env("LC_ALL", "C") .arg(format!("--admindir={}", dbpath.parent().unwrap().display())) .args(["-S", &file.to_string_lossy()]) .output() @@ -148,7 +149,7 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { bail!("dpkg -S found no package owning {}", file.display()); } - // Format: "pakiet: /sciezka" + // Format: "package: /path" let stdout = String::from_utf8_lossy(&out.stdout); let pkg = stdout .lines() @@ -158,6 +159,7 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { .ok_or_else(|| anyhow::anyhow!("Failed to parse dpkg -S output: '{}'", stdout.trim()))?; let ver_out = Command::new("dpkg-query") + .env("LC_ALL", "C") .arg(format!("--admindir={}", dbpath.parent().unwrap().display())) .args(["-W", "-f=${Package}-${Version}", &pkg]) .output() @@ -168,7 +170,7 @@ fn query_dpkg(sysroot_path: &str, file: &Path) -> Result { .trim() .to_string(); - // dpkg nie przechowuje buildtime — uzywa mtime pliku jako przyblizenia + // dpkg dont use buildtime, use installtime instead let time = std::fs::metadata(file) .and_then(|m| m.modified()) .map(|t| { @@ -187,6 +189,7 @@ fn query_pacman(sysroot_path: &str, file: &Path) -> Result { })?; let out = Command::new("pacman") + .env("LC_ALL", "C") .arg(format!("--dbpath={}", dbpath.display())) .args(["-Qo", &file.to_string_lossy()]) .output() @@ -206,8 +209,8 @@ fn query_pacman(sysroot_path: &str, file: &Path) -> Result { let ver = words[words.len() - 1]; let package = format!("{}-{}", pkg, ver); - // BUILDDATE z /local/-/desc to Unix timestamp - // Uzywa tej samej sciezki bazy co wykrycie + // BUILDDATE z /local/-/desc is Unix timestamp + // Use the same base after detecting let desc_path = dbpath .join("local") .join(format!("{}-{}", pkg, ver)) @@ -245,6 +248,7 @@ fn query_pacman(sysroot_path: &str, file: &Path) -> Result { fn query_apk(sysroot_path: &str, file: &Path) -> Result { let out = Command::new("apk") + .env("LC_ALL", "C") .arg(format!("--root={}", sysroot_path)) .args(["info", "--who-owns", &file.to_string_lossy()]) .output() @@ -269,17 +273,32 @@ fn query_apk(sysroot_path: &str, file: &Path) -> Result { .unwrap_or(&pkg_ver) .to_string(); - let ts_out = Command::new("apk") + let info_out = Command::new("apk") + .env("LC_ALL", "C") .arg(format!("--root={}", sysroot_path)) - .args(["info", "-t", &pkg_name]) + .args(["info", "-a", &pkg_name]) .output() - .context("Failed to run apk info -t")?; - - let time = std::str::from_utf8(&ts_out.stdout) - .unwrap_or("") - .trim() - .parse::() - .unwrap_or(0); + .context("Failed to run apk info -a")?; + + let time = if info_out.status.success() { + let stdout = String::from_utf8_lossy(&info_out.stdout); + + stdout + .lines() + .find(|l| l.to_ascii_lowercase().contains("build")) + .and_then(|l| l.split_once(':')) + .map(|(_, v)| v.trim()) + .and_then(|v| { + v.parse::().ok().or_else(|| { + chrono::DateTime::parse_from_rfc2822(v) + .ok() + .map(|dt| dt.timestamp()) + }) + }) + .unwrap_or(0) + } else { + 0 + }; Ok(ManifestEntry { package: pkg_ver, diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 17ad75b4..b1dd3faf 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -120,10 +120,13 @@ where let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); let paths: Vec<_> = _paths.into_iter().collect(); - let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect(); - - //Generate Manifest - generate_manifest(sysroot_path, &path_refs).context("Failed to generate manifest")?; + + if !paths.is_empty() { + // If theres a files, generate manifest + let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect(); + generate_manifest(sysroot_path, &path_refs) + .context("Failed to generate manifest")?; + } let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); let data = std::fs::read(&manifest_path) From 8f64206d2a74aad64a569eae986915787b79c144 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 18:35:11 +0200 Subject: [PATCH 05/15] fix fmt Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index b1dd3faf..f173c39f 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -120,12 +120,11 @@ where let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); let paths: Vec<_> = _paths.into_iter().collect(); - + if !paths.is_empty() { // If theres a files, generate manifest let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect(); - generate_manifest(sysroot_path, &path_refs) - .context("Failed to generate manifest")?; + generate_manifest(sysroot_path, &path_refs).context("Failed to generate manifest")?; } let manifest_path = Path::new(sysroot_path).join(MANIFEST_PATH); From 001dc58f7fe493aaade5b0e15355f6720dc7a1f5 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sat, 27 Jun 2026 21:33:02 +0200 Subject: [PATCH 06/15] bootupd: add generate-manifest subcommand Signed-off-by: krolmiki2011 --- src/cli/bootupctl.rs | 5 +++++ src/cli/bootupd.rs | 30 ++++++++++++++++++++++++++++++ src/manifest.rs | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/cli/bootupctl.rs b/src/cli/bootupctl.rs index 1c30b24e..61d1b3da 100644 --- a/src/cli/bootupctl.rs +++ b/src/cli/bootupctl.rs @@ -73,6 +73,8 @@ pub enum CtlBackend { Generate(super::bootupd::GenerateOpts), #[clap(name = "install", hide = true)] Install(super::bootupd::InstallOpts), + #[clap(name = "generate-manifest", hide = true)] + GenerateManifest(super::bootupd::GenerateManifestOpts), } #[derive(Debug, Parser)] @@ -109,6 +111,9 @@ impl CtlCommand { CtlVerb::Backend(CtlBackend::Install(opts)) => { super::bootupd::DCommand::run_install(opts) } + CtlVerb::Backend(CtlBackend::GenerateManifest(opts)) => { + super::bootupd::DCommand::run_generate_manifest(opts) + } CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(), } } diff --git a/src/cli/bootupd.rs b/src/cli/bootupd.rs index b560910f..8b09319e 100644 --- a/src/cli/bootupd.rs +++ b/src/cli/bootupd.rs @@ -1,4 +1,5 @@ use crate::bootupd::{self, ConfigMode}; +use crate::manifest::{self, generate_manifest}; use anyhow::{Context, Result}; use camino::Utf8Path; use cap_std::ambient_authority; @@ -6,6 +7,7 @@ use cap_std::fs::Dir; use cap_std_ext::cap_std; use clap::Parser; use log::LevelFilter; +use std::path::{Path, PathBuf}; /// `bootupd` sub-commands. #[derive(Debug, Parser)] @@ -39,6 +41,11 @@ pub enum DVerb { GenerateUpdateMetadata(GenerateOpts), #[clap(name = "install", about = "Install components")] Install(InstallOpts), + #[clap( + name = "generate-manifest", + about = "Generate manifest for grub and shim packages (supported distros: Ubuntu/Debian, Arch Linux, Fedora/CentOS, OpenSuse and Alpine Linux" + )] + GenerateManifest(GenerateManifestOpts), } #[derive(Debug, Parser)] @@ -92,12 +99,24 @@ pub struct GenerateOpts { sysroot: Option, } +#[derive(Debug, Parser)] +pub struct GenerateManifestOpts { + /// Physical root mountpoint + #[clap(value_parser)] + sysroot: Option, + + /// Grub or shim filepaths + #[clap(value_parser)] + files: Vec, +} + impl DCommand { /// Run CLI application. pub fn run(self) -> Result<()> { match self.cmd { DVerb::Install(opts) => Self::run_install(opts), DVerb::GenerateUpdateMetadata(opts) => Self::run_generate_meta(opts), + DVerb::GenerateManifest(opts) => Self::run_generate_manifest(opts), } } @@ -111,6 +130,17 @@ impl DCommand { Ok(()) } + /// Runner for 'generate-manifest' verb + pub(crate) fn run_generate_manifest(opts: GenerateManifestOpts) -> Result<()> { + let sysroot = opts.sysroot.as_deref().unwrap_or("/"); + let path_refs: Vec<&Path> = opts.files.iter().map(|p| p.as_path()).collect(); + if sysroot != "/" { + anyhow::bail!("Using a non-default sysroot is not supported: {}", sysroot); + } + manifest::generate_manifest(sysroot, &path_refs)?; + Ok(()) + } + /// Runner for `install` verb. pub(crate) fn run_install(opts: InstallOpts) -> Result<()> { let configmode = if opts.write_uuid { diff --git a/src/manifest.rs b/src/manifest.rs index f4591a4a..fbeb66cc 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -337,7 +337,7 @@ pub(crate) fn generate_manifest(sysroot_path: &str, files: &[&Path]) -> Result<( } let pm = detect_package_manager(sysroot_path).context("Failed to detect package manager")?; - println!("Detected package manager: {:?}", pm); + debug!("Detected package manager: {:?}", pm); let mut entries: BTreeMap = BTreeMap::new(); From 273fbc805c19a5646e2b1fbc978404caa19ebf98 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sun, 28 Jun 2026 08:27:47 +0200 Subject: [PATCH 07/15] bootupd: fixing packagesystem Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index f173c39f..e0ee76cd 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -79,9 +79,11 @@ fn parse_manifest(data: &[u8]) -> Result { let ts_str = parts .next() .ok_or_else(|| anyhow::anyhow!("Missing buildtime in entry: {}", s))?; - let ts = DateTime::parse_from_str(ts_str, "%s") - .with_context(|| format!("Invalid buildtime in entry: {}", s))? - .with_timezone(&Utc); + let secs = ts_str.trim().parse::() + .with_context(|| format!("Invalid buildtime integer in entry: {}", s))?; + let ts = Utc.timestamp_opt(secs, 0) + .single() + .ok_or_else(|| anyhow::anyhow!("Invalid timestamp value: {}", secs))?; Ok((name, ts)) }) .collect::>>>()?; @@ -92,9 +94,7 @@ fn parse_manifest(data: &[u8]) -> Result { let largest_timestamp = pkgs .values() - .collect::>() - .into_iter() - .last() + .max() .expect("pkgs is non-empty"); let version = pkgs.keys().cloned().collect::>().join(","); From 4d1af2aaf9a990fcf6c28b9eb8132a9c5786522e Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sun, 28 Jun 2026 08:29:57 +0200 Subject: [PATCH 08/15] forgot fmt :( Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index e0ee76cd..7d7efdc4 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -79,9 +79,12 @@ fn parse_manifest(data: &[u8]) -> Result { let ts_str = parts .next() .ok_or_else(|| anyhow::anyhow!("Missing buildtime in entry: {}", s))?; - let secs = ts_str.trim().parse::() + let secs = ts_str + .trim() + .parse::() .with_context(|| format!("Invalid buildtime integer in entry: {}", s))?; - let ts = Utc.timestamp_opt(secs, 0) + let ts = Utc + .timestamp_opt(secs, 0) .single() .ok_or_else(|| anyhow::anyhow!("Invalid timestamp value: {}", secs))?; Ok((name, ts)) @@ -92,10 +95,7 @@ fn parse_manifest(data: &[u8]) -> Result { bail!("Manifest contains no entries"); } - let largest_timestamp = pkgs - .values() - .max() - .expect("pkgs is non-empty"); + let largest_timestamp = pkgs.values().max().expect("pkgs is non-empty"); let version = pkgs.keys().cloned().collect::>().join(","); From fe40d8a8bc222fcf16550ed1e818fdaffce0c63d Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Sun, 28 Jun 2026 09:08:29 +0200 Subject: [PATCH 09/15] reverse gemini changes, because its not working Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 7d7efdc4..f173c39f 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -79,14 +79,9 @@ fn parse_manifest(data: &[u8]) -> Result { let ts_str = parts .next() .ok_or_else(|| anyhow::anyhow!("Missing buildtime in entry: {}", s))?; - let secs = ts_str - .trim() - .parse::() - .with_context(|| format!("Invalid buildtime integer in entry: {}", s))?; - let ts = Utc - .timestamp_opt(secs, 0) - .single() - .ok_or_else(|| anyhow::anyhow!("Invalid timestamp value: {}", secs))?; + let ts = DateTime::parse_from_str(ts_str, "%s") + .with_context(|| format!("Invalid buildtime in entry: {}", s))? + .with_timezone(&Utc); Ok((name, ts)) }) .collect::>>>()?; @@ -95,7 +90,12 @@ fn parse_manifest(data: &[u8]) -> Result { bail!("Manifest contains no entries"); } - let largest_timestamp = pkgs.values().max().expect("pkgs is non-empty"); + let largest_timestamp = pkgs + .values() + .collect::>() + .into_iter() + .last() + .expect("pkgs is non-empty"); let version = pkgs.keys().cloned().collect::>().join(","); From df028401e2ec2869fb0ec48aa50e9c9fb485d516 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 30 Jun 2026 11:05:28 +0200 Subject: [PATCH 10/15] fixes Signed-off-by: krolmiki2011 --- src/manifest.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/manifest.rs b/src/manifest.rs index fbeb66cc..2840550c 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -412,7 +412,7 @@ mod tests { fn make_sysroot_with_pacman(dir: &TempDir) { let dbpath = dir.path().join(LEGACY_PACMAN_DBPATH); std::fs::create_dir_all(&dbpath).unwrap(); - // Musi byc niepusty zeby is_nonempty_dir zwrocilo true + // Must be empty std::fs::write(dbpath.join("dummy"), b"dummy").unwrap(); } @@ -471,7 +471,7 @@ mod tests { #[test] fn test_find_rpm_dbpath_sysimage_wins() { let dir = TempDir::new().unwrap(); - // Oba istnieja — sysimage powinien wygrac (jest pierwszy w liscie) + // Two exists, but sysimage is first one let sysimage = dir.path().join(SYSIMAGE_RPM_DBPATH); std::fs::create_dir_all(&sysimage).unwrap(); std::fs::write(sysimage.join("Packages"), b"dummy").unwrap(); @@ -486,7 +486,7 @@ mod tests { #[test] fn test_find_rpm_dbpath_falls_back_to_legacy() { let dir = TempDir::new().unwrap(); - // Tylko legacy istnieje + // Only legacy exists let legacy = dir.path().join(LEGACY_RPMOSTREE_DBPATH); std::fs::create_dir_all(&legacy).unwrap(); std::fs::write(legacy.join("Packages"), b"dummy").unwrap(); From 7b2bb79d54e3c48be7108e7199ecc198f90f53d2 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 14 Jul 2026 15:12:06 +0200 Subject: [PATCH 11/15] Merge fixes Signed-off-by: krolmiki2011 --- src/cli/bootupctl.rs | 1 + src/cli/bootupd.rs | 3 +++ src/packagesystem.rs | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/bootupctl.rs b/src/cli/bootupctl.rs index d555c718..0f58c174 100644 --- a/src/cli/bootupctl.rs +++ b/src/cli/bootupctl.rs @@ -116,6 +116,7 @@ impl CtlCommand { } CtlVerb::Backend(CtlBackend::GenerateManifest(opts)) => { super::bootupd::DCommand::run_generate_manifest(opts) + } #[cfg(efi_arch)] CtlVerb::Backend(CtlBackend::SetDefaultBootloader(opts)) => { super::bootupd::DCommand::set_default_bootloader(opts) diff --git a/src/cli/bootupd.rs b/src/cli/bootupd.rs index c8528ac6..3fede3e8 100644 --- a/src/cli/bootupd.rs +++ b/src/cli/bootupd.rs @@ -115,6 +115,9 @@ pub struct GenerateManifestOpts { /// Grub or shim filepaths #[clap(value_parser)] files: Vec, +} + +#[derive(Debug, Parser)] pub struct DefaultBootloaderOpts { /// Physical root mountpoint #[clap(long)] diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 3e93f636..3e2e343b 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -107,7 +107,6 @@ fn parse_manifest(data: &[u8]) -> Result { timestamp: *largest_timestamp, version, versions: Some(modules), - versions: Some(modules_vec), #[cfg(efi_arch)] default_bootloader: None, }) From e1617fcc03e8f2f05d156388b13a39fb5a020b82 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 14 Jul 2026 16:58:44 +0200 Subject: [PATCH 12/15] try to integrate with PR: #1113 changing update a little Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 147 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 28 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 3e2e343b..4ecfd24f 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -18,7 +18,48 @@ const MANIFEST_PATH: &str = "usr/lib/bootupd/manifest"; //If any package starts with grub** shim**, this will make in one name in ANY distros const CANONICAL_NAMES: &[&str] = &["grub", "shim"]; +fn is_grub_cc_name(name: &str) -> bool { + // Handle raw package names and full NEVRA strings. + if !(name.starts_with("grub-") || name.starts_with("grub2-")) { + return false; + } + + let main_name = if let Some((main, arch)) = name.rsplit_once('.') { + if arch == std::env::consts::ARCH { + main + } else { + name + } + } else { + name + }; + + let cc_pos = match main_name.find("-cc") { + Some(pos) => pos, + None => return false, + }; + + let version_boundary = main_name + .char_indices() + .find(|(idx, ch)| { + *ch == '-' + && main_name[*idx + 1..] + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + }) + .map(|(idx, _)| idx) + .unwrap_or(main_name.len()); + + cc_pos < version_boundary +} + fn normalize_package_name(name: &str) -> &str { + if is_grub_cc_name(name) { + return "grub-cc"; + } + for canonical in CANONICAL_NAMES { if name == *canonical { return canonical; @@ -137,7 +178,14 @@ where } fn split_name_version(input: &str) -> Option<(String, String)> { - let main = input.rsplit_once('.')?.0; + let main = if input.ends_with(std::env::consts::ARCH) { + input + .rsplit_once('.') + .map(|(main, _)| main) + .unwrap_or(input) + } else { + input + }; let mut parts = main.rsplitn(3, '-'); let release = parts.next()?; let version = parts.next()?; @@ -147,31 +195,44 @@ fn split_name_version(input: &str) -> Option<(String, String)> { //In this function if it using rpm use rpm_rs fn parse_evr(pkg: &str) -> Module { - if !pkg.ends_with(std::env::consts::ARCH) { - let (name, evr) = pkg.split_once('-').unwrap_or((pkg, "")); - return Module { - name: name.to_string(), - rpm_evr: evr.to_string(), + let (name_str, rpm_evr) = if is_grub_cc_name(pkg) { + let pkg_no_arch = if let Some((main, arch)) = pkg.rsplit_once('.') { + if arch == std::env::consts::ARCH { + main + } else { + pkg + } + } else { + pkg }; - } - let (name_str, rpm_evr) = { - #[cfg(not(feature = "rpm"))] - { - split_name_version(pkg).unwrap() - } - #[cfg(feature = "rpm")] - { - let nevra = rpm_rs::Nevra::parse(pkg); - (nevra.name().to_string(), nevra.evr().to_string()) + if pkg_no_arch.ends_with("-cc") { + (pkg_no_arch.to_string(), String::new()) + } else { + split_name_version(pkg).unwrap_or_else(|| { + let (name, evr) = pkg.split_once('-').unwrap_or((pkg, "")); + (name.to_string(), evr.to_string()) + }) } + } else if !pkg.ends_with(std::env::consts::ARCH) { + split_name_version(pkg).unwrap_or_else(|| { + let (name, evr) = pkg.split_once('-').unwrap_or((pkg, "")); + (name.to_string(), evr.to_string()) + }) + } else { + split_name_version(pkg).unwrap() }; - let (name, _) = name_str.split_once('-').unwrap_or((&name_str, "")); - Module { - name: name.to_string(), - rpm_evr, - } + let name = if is_grub_cc_name(&name_str) || name_str.starts_with("systemd-boot") { + "grub-cc".to_string() + } else { + name_str + .split_once('-') + .map(|(name, _)| name.to_string()) + .unwrap_or(name_str.clone()) + }; + + Module { name, rpm_evr } } fn parse_evr_vec(input: &str) -> Vec { @@ -242,15 +303,24 @@ mod tests { assert_eq!(normalize_package_name("grub-efi-amd64"), "grub"); assert_eq!(normalize_package_name("grub-efi-arm64"), "grub"); assert_eq!(normalize_package_name("grub-pc"), "grub"); - // Shim Variants - assert_eq!(normalize_package_name("shim-x64"), "shim"); - assert_eq!(normalize_package_name("shim-ia32"), "shim"); - assert_eq!(normalize_package_name("shim-signed"), "shim"); - assert_eq!(normalize_package_name("shim-unsigned"), "shim"); - // Should not (this is not grub, or shim) + // Grub CC variants should all canonicalize to grub-cc + assert_eq!(normalize_package_name("grub-cc"), "grub-cc"); + assert_eq!(normalize_package_name("grub-cc-1:2.12-28.fc42"), "grub-cc"); + assert_eq!(normalize_package_name("grub2-efi-x64-cc"), "grub-cc"); + assert_eq!( + normalize_package_name("grub2-efi-x64-cc-1:2.12-28.fc42.x86_64"), + "grub-cc" + ); + assert_eq!(normalize_package_name("grub2-efi-ia32-cc"), "grub-cc"); + assert_eq!( + normalize_package_name("grub2-efi-ia32-cc-1:2.12-28.fc42.x86_64"), + "grub-cc" + ); + assert_eq!(normalize_package_name("grub-efi-x64-cc"), "grub-cc"); + assert_eq!(normalize_package_name("grub-efi-x64-cc-1:2.12"), "grub-cc"); + // Should not be treated as grub-cc assert_eq!(normalize_package_name("grubby"), "grubby"); assert_eq!(normalize_package_name("shimmer"), "shimmer"); - // Unkown packages, no changes assert_eq!(normalize_package_name("unknown-pkg"), "unknown-pkg"); } @@ -283,6 +353,27 @@ mod tests { assert_eq!(modules[1].name, "shim"); } + #[test] + fn test_parse_evr_grub_cc() { + let module = parse_evr("grub-cc-1:2.12-28.fc42.x86_64"); + assert_eq!(module.name, "grub-cc"); + assert_eq!(module.rpm_evr, "1:2.12-28.fc42"); + } + + #[test] + fn test_parse_evr_grub2_efi_x64_cc() { + let module = parse_evr("grub2-efi-x64-cc-1:2.12-28.fc42.x86_64"); + assert_eq!(module.name, "grub-cc"); + assert_eq!(module.rpm_evr, "1:2.12-28.fc42"); + } + + #[test] + fn test_parse_evr_grub2_efi_x64_cc_name_only() { + let module = parse_evr("grub2-efi-x64-cc"); + assert_eq!(module.name, "grub-cc"); + assert_eq!(module.rpm_evr, ""); + } + #[test] fn test_query_files_missing_manifest() { let dir = TempDir::new().unwrap(); From 92e3b852a543cdfeadfac9e1f1abf65b529d67f2 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 14 Jul 2026 17:32:05 +0200 Subject: [PATCH 13/15] new-packagesystem: fixes Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 4ecfd24f..72cab183 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -223,13 +223,19 @@ fn parse_evr(pkg: &str) -> Module { split_name_version(pkg).unwrap() }; - let name = if is_grub_cc_name(&name_str) || name_str.starts_with("systemd-boot") { + // Exceptions for grub**-cc and systemd-boot + let name = if is_grub_cc_name(&name_str) { "grub-cc".to_string() + } else if name_str.contains("systemd-boot") { + "systemd-boot".to_string() } else { - name_str - .split_once('-') - .map(|(name, _)| name.to_string()) - .unwrap_or(name_str.clone()) + normalize_package_name( + name_str + .split_once('-') + .map(|(name, _)| name) + .unwrap_or(&name_str), + ) + .to_string() }; Module { name, rpm_evr } From 30aaa8e5bd1432199e874b3b7d78cf56edf477fa Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 14 Jul 2026 17:41:33 +0200 Subject: [PATCH 14/15] new-packagesystem: fixes Signed-off-by: krolmiki2011 --- src/packagesystem.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/packagesystem.rs b/src/packagesystem.rs index 72cab183..b8c0f365 100644 --- a/src/packagesystem.rs +++ b/src/packagesystem.rs @@ -85,7 +85,11 @@ pub(crate) struct Module { impl Module { pub(crate) fn rpm_evr(&self) -> Version { - Version::from(&self.rpm_evr) + if self.rpm_evr.is_empty() { + Version::from("0") + } else { + Version::from(&self.rpm_evr) + } } fn canonical_name(&self) -> &str { @@ -340,7 +344,7 @@ mod tests { "grub2-efi-x64-1:2.06-95.fc38.x86_64,shim-x64-15.6-2.x86_64" ); let modules = parsed.versions.unwrap(); - assert_eq!(modules[0].name, "grub2"); + assert_eq!(modules[0].name, "grub"); assert_eq!(modules[0].rpm_evr, "1:2.06-95.fc38"); assert_eq!(modules[1].name, "shim"); assert_eq!(modules[1].rpm_evr, "15.6-2"); @@ -355,7 +359,7 @@ mod tests { ); let meta = query_files(dir.path().to_str().unwrap(), std::iter::empty::<&Path>()).unwrap(); let modules = meta.versions.unwrap(); - assert_eq!(modules[0].name, "grub2"); + assert_eq!(modules[0].name, "grub"); assert_eq!(modules[1].name, "shim"); } From 1526de3a0893c199e95d8094d2aab5c61b146743 Mon Sep 17 00:00:00 2001 From: krolmiki2011 Date: Tue, 14 Jul 2026 17:48:26 +0200 Subject: [PATCH 15/15] Add normalization to EFI Signed-off-by: krolmiki2011 --- src/efi.rs | 2 +- src/packagesystem.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/efi.rs b/src/efi.rs index fceecd98..6cc03ec4 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -881,7 +881,7 @@ fn generate_meta_from_usr_efi(sysroot_path: &Utf8Path) -> Result bool { cc_pos < version_boundary } -fn normalize_package_name(name: &str) -> &str { +pub fn normalize_package_name(name: &str) -> &str { if is_grub_cc_name(name) { return "grub-cc"; }