1use std::fs::create_dir_all;
65use std::io::{Read, Seek, SeekFrom, Write};
66use std::path::Path;
67use std::sync::Arc;
68
69use anyhow::{Context, Result, anyhow, bail};
70use bootc_kernel_cmdline::utf8::{Cmdline, Parameter};
71use bootc_mount::tempmount::TempMount;
72use camino::{Utf8Path, Utf8PathBuf};
73use cap_std_ext::{
74 cap_std::{ambient_authority, fs::Dir},
75 dirext::CapStdExtDirExt,
76};
77use clap::ValueEnum;
78use composefs::fs::read_file;
79use composefs::fsverity::{FsVerityHashValue, Sha512HashValue};
80use composefs::tree::RegularFile;
81use composefs_boot::bootloader::{
82 BootEntry as ComposefsBootEntry, EFI_ADDON_DIR_EXT, EFI_ADDON_FILE_EXT, EFI_EXT, PEType,
83 UsrLibModulesVmlinuz, get_boot_resources,
84};
85use composefs_boot::{
86 cmdline::ComposefsCmdline as ComposefsBootCmdline, os_release::OsReleaseInfo, uki,
87};
88use composefs_ctl::composefs;
89use composefs_ctl::composefs_boot;
90use composefs_ctl::composefs_oci;
91use fn_error_context::context;
92use rustix::{mount::MountFlags, path::Arg};
93use schemars::JsonSchema;
94use serde::{Deserialize, Serialize};
95
96use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state};
97use crate::bootc_composefs::status::ComposefsCmdline;
98use crate::bootc_kargs::compute_new_kargs;
99use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, TYPE1_ENT_PATH, TYPE1_ENT_PATH_STAGED};
100use crate::parsers::bls_config::{BLSConfig, BLSConfigType, EFIKey};
101use crate::spec::BootloaderKind;
102use crate::task::Task;
103use crate::{bootc_composefs::repo::open_composefs_repo, store::Storage};
104use crate::{bootc_composefs::status::get_sorted_grub_uki_boot_entries, install::PostFetchState};
105use crate::{
106 composefs_consts::{
107 BOOT_LOADER_ENTRIES, STAGED_BOOT_LOADER_ENTRIES, UKI_NAME_PREFIX, USER_CFG, USER_CFG_STAGED,
108 },
109 spec::{Bootloader, Host},
110};
111use crate::{parsers::grub_menuconfig::MenuEntry, store::BootedComposefs};
112
113use crate::install::{RootSetup, State};
114
115pub(crate) const EFI_UUID_FILE: &str = "efiuuid.cfg";
117pub(crate) const EFI_LINUX: &str = "EFI/Linux";
119
120const SYSTEMD_TIMEOUT: &str = "timeout 5";
122const SYSTEMD_LOADER_CONF_PATH: &str = "loader/loader.conf";
123
124pub(crate) const INITRD: &str = "initrd";
125pub(crate) const VMLINUZ: &str = "vmlinuz";
126
127const BOOTC_AUTOENROLL_PATH: &str = "usr/lib/bootc/install/secureboot-keys";
128
129const AUTH_EXT: &str = "auth";
130
131pub(crate) const BOOTC_UKI_DIR: &str = "EFI/Linux/bootc";
136
137pub(crate) enum BootSetupType<'a> {
138 Setup((&'a RootSetup, &'a State, &'a PostFetchState)),
140 Upgrade((&'a Storage, &'a BootedComposefs, &'a Host)),
142}
143
144#[derive(
145 ValueEnum, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema,
146)]
147pub enum BootType {
148 #[default]
149 Bls,
150 Uki,
151}
152
153impl ::std::fmt::Display for BootType {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let s = match self {
156 BootType::Bls => "bls",
157 BootType::Uki => "uki",
158 };
159
160 write!(f, "{}", s)
161 }
162}
163
164impl TryFrom<&str> for BootType {
165 type Error = anyhow::Error;
166
167 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
168 match value {
169 "bls" => Ok(Self::Bls),
170 "uki" => Ok(Self::Uki),
171 unrecognized => Err(anyhow::anyhow!(
172 "Unrecognized boot option: '{unrecognized}'"
173 )),
174 }
175 }
176}
177
178impl From<&ComposefsBootEntry<Sha512HashValue>> for BootType {
179 fn from(entry: &ComposefsBootEntry<Sha512HashValue>) -> Self {
180 match entry {
181 ComposefsBootEntry::Type1(..) => Self::Bls,
182 ComposefsBootEntry::Type2(..) => Self::Uki,
183 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => Self::Bls,
184 }
185 }
186}
187
188pub(crate) fn get_efi_uuid_source() -> String {
191 format!(
192 r#"
193if [ -f ${{config_directory}}/{EFI_UUID_FILE} ]; then
194 source ${{config_directory}}/{EFI_UUID_FILE}
195fi
196"#
197 )
198}
199
200const ESP_MOUNT_FLAGS: MountFlags =
202 MountFlags::from_bits_retain(MountFlags::NOEXEC.bits() | MountFlags::NOSUID.bits());
203
204const ESP_MOUNT_DATA: &std::ffi::CStr = c"fmask=0177,dmask=0077";
206
207pub fn mount_esp(device: &str) -> Result<TempMount> {
209 TempMount::mount_dev(device, "vfat", ESP_MOUNT_FLAGS, Some(ESP_MOUNT_DATA))
210}
211
212pub(crate) fn mount_esp_at(
215 device: &str,
216 path: std::path::PathBuf,
217) -> Result<bootc_mount::tempmount::MountGuard> {
218 bootc_mount::tempmount::MountGuard::mount(
219 device,
220 path,
221 "vfat",
222 ESP_MOUNT_FLAGS,
223 Some(ESP_MOUNT_DATA),
224 )
225}
226
227pub(crate) const FILENAME_PRIORITY_PRIMARY: &str = "1";
230
231pub(crate) const FILENAME_PRIORITY_SECONDARY: &str = "0";
233
234pub(crate) const SORTKEY_PRIORITY_PRIMARY: &str = "0";
237
238pub(crate) const SORTKEY_PRIORITY_SECONDARY: &str = "1";
240
241pub fn type1_entry_conf_file_name(
253 os_id: &str,
254 version: impl std::fmt::Display,
255 priority: &str,
256) -> String {
257 let os_id_safe = os_id.replace('-', "_");
258 format!("bootc_{os_id_safe}-{version}-{priority}.conf")
259}
260
261pub(crate) fn primary_sort_key(os_id: &str) -> String {
266 format!("bootc-{os_id}-{SORTKEY_PRIORITY_PRIMARY}")
267}
268
269pub(crate) fn secondary_sort_key(os_id: &str) -> String {
272 format!("bootc-{os_id}-{SORTKEY_PRIORITY_SECONDARY}")
273}
274
275pub(crate) fn get_type1_dir_name(depl_verity: &str) -> String {
277 format!("{TYPE1_BOOT_DIR_PREFIX}{depl_verity}")
278}
279
280pub(crate) fn get_uki_name(depl_verity: &str) -> String {
282 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_EXT}")
283}
284
285pub(crate) fn get_uki_addon_dir_name(depl_verity: &str) -> String {
287 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_ADDON_DIR_EXT}")
288}
289
290#[allow(dead_code)]
291pub(crate) fn get_uki_addon_file_name(depl_verity: &str) -> String {
293 format!("{UKI_NAME_PREFIX}{depl_verity}{EFI_ADDON_FILE_EXT}")
294}
295
296#[context("Computing boot digest")]
302fn compute_boot_digest(
303 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
304 repo: &crate::store::ComposefsRepository,
305) -> Result<String> {
306 let vmlinuz = read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?;
307
308 let Some(initramfs) = &entry.initramfs else {
309 anyhow::bail!("initramfs not found");
310 };
311
312 let initramfs = read_file(initramfs, &repo).context("Reading intird")?;
313
314 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
315 .context("Creating hasher")?;
316
317 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
318 hasher.update(&initramfs).context("hashing initrd")?;
319
320 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
321
322 Ok(hex::encode(digest))
323}
324
325#[context("Computing boot digest for Type1 entries")]
326fn compute_boot_digest_type1(dir: &Dir) -> Result<String> {
327 let mut vmlinuz = dir
328 .open(VMLINUZ)
329 .with_context(|| format!("Opening {VMLINUZ}"))?;
330
331 let mut initrd = dir
332 .open(INITRD)
333 .with_context(|| format!("Opening {INITRD}"))?;
334
335 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
336 .context("Creating hasher")?;
337
338 std::io::copy(&mut vmlinuz, &mut hasher)?;
339 std::io::copy(&mut initrd, &mut hasher)?;
340
341 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
342
343 Ok(hex::encode(digest))
344}
345
346#[context("Computing boot digest")]
352pub(crate) fn compute_boot_digest_uki<R: Read + Seek>(uki_reader: &mut R) -> Result<String> {
353 let vmlinuz = uki::get_section_buffered(uki_reader, ".linux").context(".linux not present")?;
354 uki_reader
355 .seek(SeekFrom::Start(0))
356 .context("Moving seek to 0")?;
357 let initramfs =
358 uki::get_section_buffered(uki_reader, ".initrd").context(".initrd not present")?;
359
360 let mut hasher = openssl::hash::Hasher::new(openssl::hash::MessageDigest::sha256())
361 .context("Creating hasher")?;
362
363 hasher.update(&vmlinuz).context("hashing vmlinuz")?;
364 hasher.update(&initramfs).context("hashing initrd")?;
365
366 let digest: &[u8] = &hasher.finish().context("Finishing digest")?;
367
368 Ok(hex::encode(digest))
369}
370
371#[context("Checking boot entry duplicates")]
377pub(crate) fn find_vmlinuz_initrd_duplicate(
378 storage: &Storage,
379 digest: &str,
380) -> Result<Option<String>> {
381 let boot_dir = storage.bls_boot_binaries_dir()?;
382
383 for entry in boot_dir.entries_utf8()? {
384 let entry = entry?;
385 let dir_name = entry.file_name()?;
386
387 if !entry.file_type()?.is_dir() {
388 continue;
389 }
390
391 let Some(..) = dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) else {
392 continue;
393 };
394
395 let entry_digest = compute_boot_digest_type1(&boot_dir.open_dir(&dir_name)?)?;
396
397 if entry_digest == digest {
398 return Ok(Some(dir_name));
399 }
400 }
401
402 Ok(None)
403}
404
405#[context("Writing BLS entries to disk")]
406fn write_bls_boot_entries_to_disk(
407 boot_dir: &Utf8PathBuf,
408 deployment_id: &Sha512HashValue,
409 entry: &UsrLibModulesVmlinuz<Sha512HashValue>,
410 repo: &crate::store::ComposefsRepository,
411) -> Result<()> {
412 let dir_name = get_type1_dir_name(&deployment_id.to_hex());
413
414 let path = boot_dir.join(&dir_name);
416 create_dir_all(&path)?;
417
418 let entries_dir = Dir::open_ambient_dir(&path, ambient_authority())
419 .with_context(|| format!("Opening {path}"))?;
420
421 entries_dir
422 .atomic_write(
423 VMLINUZ,
424 read_file(&entry.vmlinuz, &repo).context("Reading vmlinuz")?,
425 )
426 .context("Writing vmlinuz to path")?;
427
428 let Some(initramfs) = &entry.initramfs else {
429 anyhow::bail!("initramfs not found");
430 };
431
432 entries_dir
433 .atomic_write(
434 INITRD,
435 read_file(initramfs, &repo).context("Reading initrd")?,
436 )
437 .context("Writing initrd to path")?;
438
439 let owned_fd = entries_dir
441 .reopen_as_ownedfd()
442 .context("Reopen as owned fd")?;
443
444 rustix::fs::fsync(owned_fd).context("fsync")?;
445
446 Ok(())
447}
448
449pub fn parse_os_release(root: &Dir) -> Result<Option<(String, Option<String>, Option<String>)>> {
453 let file = root
455 .open_optional("usr/lib/os-release")
456 .context("Opening usr/lib/os-release")?;
457
458 let Some(mut os_rel_file) = file else {
459 return Ok(None);
460 };
461
462 let mut file_contents = String::new();
463 os_rel_file.read_to_string(&mut file_contents)?;
464
465 let parsed = OsReleaseInfo::parse(&file_contents);
466
467 let os_id = parsed
468 .get_value(&["ID"])
469 .unwrap_or_else(|| "bootc".to_string());
470
471 Ok(Some((
472 os_id,
473 parsed.get_pretty_name(),
474 parsed.get_version(),
475 )))
476}
477
478struct BLSEntryPath {
479 entries_path: Utf8PathBuf,
481 abs_entries_path: Utf8PathBuf,
483 config_path: Utf8PathBuf,
485}
486
487#[context("Setting up BLS boot")]
492pub(crate) fn setup_composefs_bls_boot(
493 setup_type: BootSetupType,
494 repo: crate::store::ComposefsRepository,
495 id: &Sha512HashValue,
496 entry: &ComposefsBootEntry<Sha512HashValue>,
497 mounted_erofs: &Dir,
498) -> Result<String> {
499 let id_hex = id.to_hex();
500
501 let (root_path, esp_device, mut cmdline_refs, bootloader) = match setup_type {
502 BootSetupType::Setup((root_setup, state, postfetch)) => {
503 let mut cmdline_options = Cmdline::new();
505
506 cmdline_options.extend(&root_setup.kargs);
507
508 let composefs_cmdline =
509 ComposefsCmdline::build(&id_hex, state.composefs_options.allow_missing_verity);
510 cmdline_options.extend(&Cmdline::from(&composefs_cmdline.to_string()));
511
512 if let Some(boot) = root_setup.boot_mount_spec() {
516 if !boot.source.is_empty() {
517 let mount_extra = format!(
518 "systemd.mount-extra={}:/boot:{}:{}",
519 boot.source,
520 boot.fstype,
521 boot.options.as_deref().unwrap_or("defaults"),
522 );
523 cmdline_options.extend(&Cmdline::from(mount_extra.as_str()));
524 tracing::debug!("Added /boot mount karg: {mount_extra}");
525 }
526 }
527
528 let esp_part = root_setup.device_info.find_first_colocated_esp()?;
530
531 (
532 root_setup.physical_root_path.clone(),
533 esp_part.path(),
534 cmdline_options,
535 postfetch.detected_bootloader.clone(),
536 )
537 }
538
539 BootSetupType::Upgrade((storage, booted_cfs, host)) => {
540 let bootloader = host.require_composefs_booted()?.bootloader.clone();
541
542 let boot_dir = storage.require_boot_dir()?;
543 let current_cfg = get_booted_bls(&boot_dir, booted_cfs)?;
544
545 let mut cmdline = match current_cfg.cfg_type {
546 BLSConfigType::NonEFI { options, .. } => {
547 let options = options
548 .ok_or_else(|| anyhow::anyhow!("No 'options' found in BLS Config"))?;
549
550 Cmdline::from(options)
551 }
552
553 _ => anyhow::bail!("Found NonEFI config"),
554 };
555
556 let cfs_cmdline =
558 ComposefsCmdline::build(&id_hex, booted_cfs.cmdline.allow_missing_fsverity)
559 .to_string();
560
561 let param = Parameter::parse(&cfs_cmdline)
562 .context("Failed to create 'composefs=' parameter")?;
563 cmdline.add_or_modify(¶m);
564
565 let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
567 let esp_dev = root_dev.find_first_colocated_esp()?;
568
569 (
570 Utf8PathBuf::from("/sysroot"),
571 esp_dev.path(),
572 cmdline,
573 bootloader,
574 )
575 }
576 };
577
578 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
579
580 let current_root = if is_upgrade {
581 Some(&Dir::open_ambient_dir("/", ambient_authority()).context("Opening root")? as &Dir)
582 } else {
583 None
584 };
585
586 compute_new_kargs(mounted_erofs, current_root, &mut cmdline_refs)?;
587
588 let (entry_paths, _tmpdir_guard) = match bootloader.kind()? {
589 BootloaderKind::GRUBClassic => {
590 let root = Dir::open_ambient_dir(&root_path, ambient_authority())
591 .context("Opening root path")?;
592
593 let entries_path = match root.is_mountpoint("boot")? {
598 Some(true) => "/",
599 Some(false) | None => "/boot",
601 };
602
603 (
604 BLSEntryPath {
605 entries_path: root_path.join("boot"),
606 config_path: root_path.join("boot"),
607 abs_entries_path: entries_path.into(),
608 },
609 None,
610 )
611 }
612
613 BootloaderKind::BLSCompatible => {
614 let efi_mount = mount_esp(&esp_device).context("Mounting ESP")?;
615
616 let mounted_efi = Utf8PathBuf::from(efi_mount.dir.path().as_str()?);
617 let efi_linux_dir = mounted_efi.join(EFI_LINUX);
618
619 (
620 BLSEntryPath {
621 entries_path: efi_linux_dir,
622 config_path: mounted_efi.clone(),
623 abs_entries_path: Utf8PathBuf::from("/").join(EFI_LINUX),
624 },
625 Some(efi_mount),
626 )
627 }
628 };
629
630 let (bls_config, boot_digest, os_id) = match &entry {
631 ComposefsBootEntry::Type1(..) => anyhow::bail!("Found Type1 entries in /boot"),
632 ComposefsBootEntry::Type2(..) => anyhow::bail!("Found UKI"),
633
634 ComposefsBootEntry::UsrLibModulesVmLinuz(usr_lib_modules_vmlinuz) => {
635 let boot_digest = compute_boot_digest(usr_lib_modules_vmlinuz, &repo)
636 .context("Computing boot digest")?;
637
638 let osrel = parse_os_release(mounted_erofs)?;
639
640 let (os_id, title, version, sort_key) = match osrel {
641 Some((id_str, title_opt, version_opt)) => (
642 id_str.clone(),
643 title_opt.unwrap_or_else(|| id.to_hex()),
644 version_opt.unwrap_or_else(|| id.to_hex()),
645 primary_sort_key(&id_str),
646 ),
647 None => {
648 let default_id = "bootc".to_string();
649 (
650 default_id.clone(),
651 id.to_hex(),
652 id.to_hex(),
653 primary_sort_key(&default_id),
654 )
655 }
656 };
657
658 let mut bls_config = BLSConfig::default();
659
660 let entries_dir = get_type1_dir_name(&id_hex);
661
662 bls_config
663 .with_title(title)
664 .with_version(version)
665 .with_sort_key(sort_key)
666 .with_cfg(BLSConfigType::NonEFI {
667 linux: entry_paths
668 .abs_entries_path
669 .join(&entries_dir)
670 .join(VMLINUZ),
671 initrd: vec![entry_paths.abs_entries_path.join(&entries_dir).join(INITRD)],
672 options: Some(cmdline_refs),
673 });
674
675 let shared_entry = match setup_type {
676 BootSetupType::Setup(_) => None,
677 BootSetupType::Upgrade((storage, ..)) => {
678 find_vmlinuz_initrd_duplicate(storage, &boot_digest)?
679 }
680 };
681
682 match shared_entry {
683 Some(shared_entry) => {
684 match bls_config.cfg_type {
690 BLSConfigType::NonEFI {
691 ref mut linux,
692 ref mut initrd,
693 ..
694 } => {
695 *linux = entry_paths
696 .abs_entries_path
697 .join(&shared_entry)
698 .join(VMLINUZ);
699
700 *initrd = vec![
701 entry_paths
702 .abs_entries_path
703 .join(&shared_entry)
704 .join(INITRD),
705 ];
706 }
707
708 _ => unreachable!(),
709 };
710 }
711
712 None => {
713 write_bls_boot_entries_to_disk(
714 &entry_paths.entries_path,
715 id,
716 usr_lib_modules_vmlinuz,
717 &repo,
718 )?;
719 }
720 };
721
722 (bls_config, boot_digest, os_id)
723 }
724 };
725
726 let loader_path = entry_paths.config_path.join("loader");
727
728 let (config_path, booted_bls) = if is_upgrade {
729 let boot_dir = Dir::open_ambient_dir(&entry_paths.config_path, ambient_authority())?;
730
731 let BootSetupType::Upgrade((_, booted_cfs, ..)) = setup_type else {
732 unreachable!("enum mismatch");
734 };
735
736 let mut booted_bls = get_booted_bls(&boot_dir, booted_cfs)?;
737 booted_bls.sort_key = Some(secondary_sort_key(&os_id));
738
739 let staged_path = loader_path.join(STAGED_BOOT_LOADER_ENTRIES);
740
741 if boot_dir
744 .remove_all_optional(TYPE1_ENT_PATH_STAGED)
745 .context("Failed to remove staged directory")?
746 {
747 tracing::debug!("Removed existing staged entries directory");
748 }
749
750 (staged_path, Some(booted_bls))
752 } else {
753 (loader_path.join(BOOT_LOADER_ENTRIES), None)
754 };
755
756 create_dir_all(&config_path).with_context(|| format!("Creating {:?}", config_path))?;
757
758 let loader_entries_dir = Dir::open_ambient_dir(&config_path, ambient_authority())
759 .with_context(|| format!("Opening {config_path:?}"))?;
760
761 loader_entries_dir.atomic_write(
762 type1_entry_conf_file_name(&os_id, &bls_config.version(), FILENAME_PRIORITY_PRIMARY),
763 bls_config.to_string().as_bytes(),
764 )?;
765
766 if let Some(booted_bls) = booted_bls {
767 loader_entries_dir.atomic_write(
768 type1_entry_conf_file_name(&os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
769 booted_bls.to_string().as_bytes(),
770 )?;
771 }
772
773 let owned_loader_entries_fd = loader_entries_dir
774 .reopen_as_ownedfd()
775 .context("Reopening as owned fd")?;
776
777 rustix::fs::fsync(owned_loader_entries_fd).context("fsync")?;
778
779 Ok(boot_digest)
780}
781
782struct UKIInfo {
783 boot_label: String,
784 version: Option<String>,
785 os_id: Option<String>,
786 boot_digest: String,
787}
788
789#[context("Writing {file_path} to ESP")]
791fn write_pe_to_esp(
792 repo: &crate::store::ComposefsRepository,
793 file: &RegularFile<Sha512HashValue>,
794 file_path: &Utf8Path,
795 pe_type: PEType,
796 uki_id: &Sha512HashValue,
797 missing_fsverity_allowed: bool,
798 mounted_efi: impl AsRef<Path>,
799) -> Result<Option<UKIInfo>> {
800 let mut uki_reader = match file {
801 RegularFile::Inline(..) => {
802 anyhow::bail!("File too small to be UKI/Addon")
804 }
805 RegularFile::External(id, ..) => std::fs::File::from(repo.open_object(id)?),
806 };
807
808 let mut boot_label: Option<UKIInfo> = None;
809
810 if matches!(pe_type, PEType::Uki) {
813 let cmdline = uki::get_cmdline_buffered(&mut uki_reader).context("Getting UKI cmdline")?;
814
815 let composefs_info = ComposefsBootCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
816 .context("Parsing composefs=")?
817 .ok_or_else(|| anyhow::anyhow!("No composefs image in UKI cmdline"))?;
818 let composefs_cmdline = composefs_info.digest();
819 let missing_verity_allowed_cmdline = composefs_info.is_insecure();
820
821 match missing_fsverity_allowed {
824 true if !missing_verity_allowed_cmdline => {
825 tracing::warn!(
826 "--allow-missing-fsverity passed as option but UKI cmdline does not support it"
827 );
828 }
829
830 false if missing_verity_allowed_cmdline => {
831 tracing::warn!("UKI cmdline has composefs set as insecure");
832 }
833
834 _ => { }
835 }
836
837 if *composefs_cmdline != *uki_id {
838 anyhow::bail!(
839 "The UKI has the wrong composefs= parameter (is '{composefs_cmdline:?}', should be {uki_id:?})"
840 );
841 }
842
843 uki_reader.seek(SeekFrom::Start(0))?;
844 let osrel = uki::get_text_section_buffered(&mut uki_reader, ".osrel")?;
845
846 let parsed_osrel = OsReleaseInfo::parse(&osrel);
847
848 uki_reader.seek(SeekFrom::Start(0))?;
849 let boot_digest = compute_boot_digest_uki(&mut uki_reader)?;
850
851 uki_reader.seek(SeekFrom::Start(0))?;
852 boot_label = Some(UKIInfo {
853 boot_label: uki::get_boot_label_buffered(&mut uki_reader)
854 .context("Getting UKI boot label")?,
855 version: parsed_osrel.get_version(),
856 os_id: parsed_osrel.get_value(&["ID"]),
857 boot_digest,
858 });
859 }
860
861 let efi_linux_path = mounted_efi.as_ref().join(BOOTC_UKI_DIR);
862 create_dir_all(&efi_linux_path).context("Creating bootc UKI directory")?;
863
864 let final_pe_path = match file_path.parent() {
865 Some(parent) => {
866 let renamed_path = match parent.as_str().ends_with(EFI_ADDON_DIR_EXT) {
867 true => {
868 let dir_name = get_uki_addon_dir_name(&uki_id.to_hex());
869
870 parent
871 .parent()
872 .map(|p| p.join(&dir_name))
873 .unwrap_or(dir_name.into())
874 }
875
876 false => parent.to_path_buf(),
877 };
878
879 let full_path = efi_linux_path.join(renamed_path);
880 create_dir_all(&full_path)?;
881
882 full_path
883 }
884
885 None => efi_linux_path,
886 };
887
888 let pe_dir = Dir::open_ambient_dir(&final_pe_path, ambient_authority())
889 .with_context(|| format!("Opening {final_pe_path:?}"))?;
890
891 let pe_name = match pe_type {
892 PEType::Uki => &get_uki_name(&uki_id.to_hex()),
893 PEType::UkiAddon => file_path
894 .components()
895 .last()
896 .ok_or_else(|| anyhow::anyhow!("Failed to get UKI Addon file name"))?
897 .as_str(),
898 };
899
900 uki_reader.seek(SeekFrom::Start(0))?;
901 pe_dir
902 .atomic_replace_with(pe_name, |writer| std::io::copy(&mut uki_reader, writer))
903 .context("Writing UKI")?;
904
905 rustix::fs::fsync(
906 pe_dir
907 .reopen_as_ownedfd()
908 .context("Reopening as owned fd")?,
909 )
910 .context("fsync")?;
911
912 Ok(boot_label)
913}
914
915#[context("Writing Grub menuentry")]
916fn write_grub_uki_menuentry(
917 root_path: Utf8PathBuf,
918 setup_type: &BootSetupType,
919 boot_label: String,
920 id: &Sha512HashValue,
921 esp_device: &String,
922) -> Result<()> {
923 let boot_dir = root_path.join("boot");
924 create_dir_all(&boot_dir).context("Failed to create boot dir")?;
925
926 let is_upgrade = matches!(setup_type, BootSetupType::Upgrade(..));
927
928 let efi_uuid_source = get_efi_uuid_source();
929
930 let user_cfg_name = if is_upgrade {
931 USER_CFG_STAGED
932 } else {
933 USER_CFG
934 };
935
936 let grub_dir = Dir::open_ambient_dir(boot_dir.join("grub2"), ambient_authority())
937 .context("opening boot/grub2")?;
938
939 if is_upgrade {
941 let mut str_buf = String::new();
942 let boot_dir =
943 Dir::open_ambient_dir(boot_dir, ambient_authority()).context("Opening boot dir")?;
944 let entries = get_sorted_grub_uki_boot_entries(&boot_dir, &mut str_buf)?;
945
946 grub_dir
947 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
948 f.write_all(efi_uuid_source.as_bytes())?;
949 f.write_all(
950 MenuEntry::new(&boot_label, &id.to_hex())
951 .to_string()
952 .as_bytes(),
953 )?;
954
955 f.write_all(entries[0].to_string().as_bytes())?;
959
960 Ok(())
961 })
962 .with_context(|| format!("Writing to {user_cfg_name}"))?;
963
964 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
965
966 return Ok(());
967 }
968
969 let esp_uuid = Task::new("blkid for ESP UUID", "blkid")
972 .args(["-s", "UUID", "-o", "value", &esp_device])
973 .read()?;
974
975 grub_dir.atomic_write(
976 EFI_UUID_FILE,
977 format!("set EFI_PART_UUID=\"{}\"", esp_uuid.trim()).as_bytes(),
978 )?;
979
980 grub_dir
982 .atomic_replace_with(user_cfg_name, |f| -> std::io::Result<_> {
983 f.write_all(efi_uuid_source.as_bytes())?;
984 f.write_all(
985 MenuEntry::new(&boot_label, &id.to_hex())
986 .to_string()
987 .as_bytes(),
988 )?;
989
990 Ok(())
991 })
992 .with_context(|| format!("Writing to {user_cfg_name}"))?;
993
994 rustix::fs::fsync(grub_dir.reopen_as_ownedfd()?).context("fsync")?;
995
996 Ok(())
997}
998
999#[context("Writing systemd UKI config")]
1000fn write_systemd_uki_config(
1001 esp_dir: &Dir,
1002 setup_type: &BootSetupType,
1003 boot_label: UKIInfo,
1004 id: &Sha512HashValue,
1005 bootloader: &Bootloader,
1006) -> Result<()> {
1007 let os_id = boot_label.os_id.as_deref().unwrap_or("bootc");
1008 let primary_sort_key = primary_sort_key(os_id);
1009
1010 let mut bls_conf = BLSConfig::default();
1011 bls_conf
1012 .with_title(boot_label.boot_label)
1013 .with_cfg(BLSConfigType::EFI {
1014 key: EFIKey::for_bootloader(
1015 format!("/{BOOTC_UKI_DIR}/{}", get_uki_name(&id.to_hex())).into(),
1016 bootloader,
1017 ),
1018 })
1019 .with_sort_key(primary_sort_key.clone())
1020 .with_version(boot_label.version.unwrap_or_else(|| id.to_hex()));
1021
1022 let (entries_dir, booted_bls) = match setup_type {
1023 BootSetupType::Setup(..) => {
1024 esp_dir
1025 .create_dir_all(TYPE1_ENT_PATH)
1026 .with_context(|| format!("Creating {TYPE1_ENT_PATH}"))?;
1027
1028 (esp_dir.open_dir(TYPE1_ENT_PATH)?, None)
1029 }
1030
1031 BootSetupType::Upgrade((_, booted_cfs, ..)) => {
1032 esp_dir
1033 .create_dir_all(TYPE1_ENT_PATH_STAGED)
1034 .with_context(|| format!("Creating {TYPE1_ENT_PATH_STAGED}"))?;
1035
1036 let mut booted_bls = get_booted_bls(&esp_dir, booted_cfs)?;
1037 booted_bls.sort_key = Some(secondary_sort_key(os_id));
1038
1039 (esp_dir.open_dir(TYPE1_ENT_PATH_STAGED)?, Some(booted_bls))
1040 }
1041 };
1042
1043 entries_dir
1044 .atomic_write(
1045 type1_entry_conf_file_name(os_id, &bls_conf.version(), FILENAME_PRIORITY_PRIMARY),
1046 bls_conf.to_string().as_bytes(),
1047 )
1048 .context("Writing conf file")?;
1049
1050 if let Some(booted_bls) = booted_bls {
1051 entries_dir.atomic_write(
1052 type1_entry_conf_file_name(os_id, &booted_bls.version(), FILENAME_PRIORITY_SECONDARY),
1053 booted_bls.to_string().as_bytes(),
1054 )?;
1055 }
1056
1057 if !esp_dir.exists(SYSTEMD_LOADER_CONF_PATH) {
1059 esp_dir
1060 .atomic_write(SYSTEMD_LOADER_CONF_PATH, SYSTEMD_TIMEOUT)
1061 .with_context(|| format!("Writing to {SYSTEMD_LOADER_CONF_PATH}"))?;
1062 }
1063
1064 let esp_dir = esp_dir
1065 .reopen_as_ownedfd()
1066 .context("Reopening as owned fd")?;
1067 rustix::fs::fsync(esp_dir).context("fsync")?;
1068
1069 Ok(())
1070}
1071
1072#[context("Setting up UKI boot")]
1073pub(crate) fn setup_composefs_uki_boot(
1074 setup_type: BootSetupType,
1075 repo: crate::store::ComposefsRepository,
1076 id: &Sha512HashValue,
1077 entries: Vec<ComposefsBootEntry<Sha512HashValue>>,
1078) -> Result<String> {
1079 let (root_path, esp_device, bootloader, missing_fsverity_allowed, uki_addons) = match setup_type
1080 {
1081 BootSetupType::Setup((root_setup, state, postfetch)) => {
1082 state.require_no_kargs_for_uki()?;
1083
1084 let esp_part = root_setup.device_info.find_first_colocated_esp()?;
1086
1087 (
1088 root_setup.physical_root_path.clone(),
1089 esp_part.path(),
1090 postfetch.detected_bootloader.clone(),
1091 state.composefs_options.allow_missing_verity,
1092 state.composefs_options.uki_addon.as_ref(),
1093 )
1094 }
1095
1096 BootSetupType::Upgrade((storage, booted_cfs, host)) => {
1097 let sysroot = Utf8PathBuf::from("/sysroot"); let bootloader = host.require_composefs_booted()?.bootloader.clone();
1099
1100 let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
1102 let esp_dev = root_dev.find_first_colocated_esp()?;
1103
1104 (
1105 sysroot,
1106 esp_dev.path(),
1107 bootloader,
1108 booted_cfs.cmdline.allow_missing_fsverity,
1109 None,
1110 )
1111 }
1112 };
1113
1114 let esp_mount = mount_esp(&esp_device).context("Mounting ESP")?;
1115
1116 let mut uki_info: Option<UKIInfo> = None;
1117
1118 for entry in entries {
1119 match entry {
1120 ComposefsBootEntry::Type1(..) => tracing::debug!("Skipping Type1 Entry"),
1121 ComposefsBootEntry::UsrLibModulesVmLinuz(..) => {
1122 tracing::debug!("Skipping vmlinuz in /usr/lib/modules")
1123 }
1124
1125 ComposefsBootEntry::Type2(entry) => {
1126 if matches!(entry.pe_type, PEType::UkiAddon) {
1128 let Some(addons) = uki_addons else {
1129 continue;
1130 };
1131
1132 let addon_name = entry
1133 .file_path
1134 .components()
1135 .last()
1136 .ok_or_else(|| anyhow::anyhow!("Could not get UKI addon name"))?;
1137
1138 let addon_name = addon_name.as_str()?;
1139
1140 let addon_name =
1141 addon_name.strip_suffix(EFI_ADDON_FILE_EXT).ok_or_else(|| {
1142 anyhow::anyhow!("UKI addon doesn't end with {EFI_ADDON_DIR_EXT}")
1143 })?;
1144
1145 if !addons.iter().any(|passed_addon| passed_addon == addon_name) {
1146 continue;
1147 }
1148 }
1149
1150 let utf8_file_path = Utf8Path::from_path(&entry.file_path)
1151 .ok_or_else(|| anyhow::anyhow!("Path is not valid UTf8"))?;
1152
1153 let ret = write_pe_to_esp(
1154 &repo,
1155 &entry.file,
1156 utf8_file_path,
1157 entry.pe_type,
1158 &id,
1159 missing_fsverity_allowed,
1160 esp_mount.dir.path(),
1161 )?;
1162
1163 if let Some(label) = ret {
1164 uki_info = Some(label);
1165 }
1166 }
1167 };
1168 }
1169
1170 let uki_info =
1171 uki_info.ok_or_else(|| anyhow::anyhow!("Failed to get version and boot label from UKI"))?;
1172
1173 let boot_digest = uki_info.boot_digest.clone();
1174
1175 match bootloader.kind()? {
1176 BootloaderKind::GRUBClassic => {
1177 write_grub_uki_menuentry(root_path, &setup_type, uki_info.boot_label, id, &esp_device)?
1178 }
1179
1180 BootloaderKind::BLSCompatible => {
1181 write_systemd_uki_config(&esp_mount.fd, &setup_type, uki_info, id, &bootloader)?
1182 }
1183 };
1184
1185 Ok(boot_digest)
1186}
1187
1188pub(crate) struct MountedImageRoot {
1202 _esp: bootc_mount::tempmount::MountGuard,
1204 _tmp: bootc_mount::tempmount::MountGuard,
1205 composefs: TempMount,
1206 pub(crate) esp_subdir: &'static str,
1207}
1208
1209impl MountedImageRoot {
1210 #[context("Preparing image root for bootloader installation")]
1214 pub(crate) fn new(
1215 composefs_mnt_fd: std::os::fd::OwnedFd,
1216 device: &bootc_blockdev::Device,
1217 ) -> Result<Self> {
1218 let roots = device.find_all_roots()?;
1219 let mut esp_part = None;
1220 for root in &roots {
1221 if let Some(esp) = root.find_partition_of_esp_optional()? {
1222 esp_part = Some(esp);
1223 break;
1224 }
1225 }
1226 let esp_part = esp_part.ok_or_else(|| anyhow!("ESP partition not found"))?;
1227
1228 let composefs = TempMount::mount_fd(composefs_mnt_fd)
1231 .context("Attaching composefs image to temporary directory")?;
1232
1233 let esp_subdir = "boot";
1238
1239 let esp_path = composefs.dir.path().join(esp_subdir);
1240 let esp =
1241 mount_esp_at(&esp_part.path(), esp_path).context("Mounting ESP into composefs root")?;
1242
1243 let tmp_path = composefs.dir.path().join("tmp");
1246 let tmp = bootc_mount::tempmount::MountGuard::mount(
1247 "tmpfs",
1248 tmp_path,
1249 "tmpfs",
1250 MountFlags::NOEXEC | MountFlags::NOSUID | MountFlags::NODEV,
1251 None::<&std::ffi::CStr>,
1252 )
1253 .context("Mounting tmpfs into composefs root")?;
1254
1255 Ok(Self {
1256 _esp: esp,
1257 _tmp: tmp,
1258 composefs,
1259 esp_subdir,
1260 })
1261 }
1262
1263 pub(crate) fn dir(&self) -> &Dir {
1265 &self.composefs.fd
1266 }
1267
1268 pub(crate) fn root_path(&self) -> &std::path::Path {
1270 self.composefs.dir.path()
1271 }
1272
1273 pub(crate) fn open_esp_dir(&self) -> Result<Dir> {
1275 self.composefs
1276 .fd
1277 .open_dir(self.esp_subdir)
1278 .with_context(|| format!("Opening ESP at /{}", self.esp_subdir))
1279 }
1280}
1281
1282pub struct SecurebootKeys {
1283 pub dir: Dir,
1284 pub keys: Vec<Utf8PathBuf>,
1285}
1286
1287fn get_secureboot_keys(fs: &Dir, p: &str) -> Result<Option<SecurebootKeys>> {
1288 let mut entries = vec![];
1289
1290 let keys_dir = match fs.open_dir_optional(p)? {
1292 Some(d) => d,
1293 _ => return Ok(None),
1294 };
1295
1296 for entry in keys_dir.entries()? {
1299 let dir_e = entry?;
1300 let dirname = dir_e.file_name();
1301 if !dir_e.file_type()?.is_dir() {
1302 bail!("/{p}/{dirname:?} is not a directory");
1303 }
1304
1305 let dir_path: Utf8PathBuf = dirname.try_into()?;
1306 let dir = dir_e.open_dir()?;
1307 for entry in dir.entries()? {
1308 let e = entry?;
1309 let local: Utf8PathBuf = e.file_name().try_into()?;
1310 let path = dir_path.join(local);
1311
1312 if path.extension() != Some(AUTH_EXT) {
1313 continue;
1314 }
1315
1316 if !e.file_type()?.is_file() {
1317 bail!("/{p}/{path:?} is not a file");
1318 }
1319 entries.push(path);
1320 }
1321 }
1322 return Ok(Some(SecurebootKeys {
1323 dir: keys_dir,
1324 keys: entries,
1325 }));
1326}
1327
1328#[context("Setting up composefs boot")]
1329pub(crate) async fn setup_composefs_boot(
1330 root_setup: &RootSetup,
1331 state: &State,
1332 pull_result: &composefs_oci::PullResult<Sha512HashValue>,
1333 allow_missing_fsverity: bool,
1334) -> Result<()> {
1335 const COMPOSEFS_BOOT_SETUP_JOURNAL_ID: &str = "1f0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5";
1336
1337 tracing::info!(
1338 message_id = COMPOSEFS_BOOT_SETUP_JOURNAL_ID,
1339 bootc.operation = "boot_setup",
1340 bootc.config_digest = %pull_result.config_digest,
1341 bootc.allow_missing_fsverity = allow_missing_fsverity,
1342 "Setting up composefs boot",
1343 );
1344
1345 let mut repo = open_composefs_repo(&root_setup.physical_root)?;
1346 if allow_missing_fsverity {
1347 repo.set_insecure();
1348 }
1349
1350 let repo = Arc::new(repo);
1351
1352 let id = composefs_oci::generate_boot_image(&repo, &pull_result.manifest_digest)
1354 .context("Generating bootable EROFS image")?;
1355
1356 let fs = composefs_oci::image::create_filesystem(&*repo, &pull_result.config_digest, None)
1358 .context("Creating composefs filesystem for boot entry discovery")?;
1359 let entries =
1360 get_boot_resources(&fs, &*repo).context("Extracting boot entries from OCI image")?;
1361
1362 let composefs_mnt_fd = repo
1363 .mount(&id.to_hex())
1364 .context("Failed to mount composefs image")?;
1365 let mounted_root = MountedImageRoot::new(composefs_mnt_fd, &root_setup.device_info)?;
1366
1367 let postfetch = PostFetchState::new(state, mounted_root.dir())?;
1368
1369 let boot_uuid = root_setup
1370 .get_boot_uuid()?
1371 .or(root_setup.rootfs_uuid.as_deref())
1372 .ok_or_else(|| anyhow!("No uuid for boot/root"))?;
1373
1374 if cfg!(target_arch = "s390x") {
1375 crate::bootloader::install_via_zipl(
1377 &root_setup.device_info.require_single_root()?,
1378 boot_uuid,
1379 )?;
1380 } else if matches!(
1381 postfetch.detected_bootloader,
1382 Bootloader::Grub | Bootloader::GrubCC
1383 ) {
1384 crate::bootloader::install_via_bootupd(
1385 &root_setup.device_info,
1386 &root_setup.physical_root_path,
1387 &state.config_opts,
1388 None,
1389 )?;
1390
1391 if matches!(postfetch.detected_bootloader, Bootloader::GrubCC) {
1393 root_setup
1394 .physical_root
1395 .remove_dir_all("boot/grub2")
1396 .context("removing grub2")?;
1397
1398 let (os_id, ..) = parse_os_release(mounted_root.dir())?
1399 .ok_or_else(|| anyhow::anyhow!("Failed to parse os-release"))?;
1400
1401 let dir = format!("EFI/{os_id}");
1402
1403 let efis_dir = mounted_root
1405 .open_esp_dir()
1406 .context("opening esp")?
1407 .open_dir(&dir)
1408 .with_context(|| format!("Opening {dir}"))?;
1409
1410 efis_dir
1411 .remove_file_optional("bootuuid.cfg")
1412 .context("Removing bootuuid.cfg")?;
1413 efis_dir
1414 .remove_file_optional("grub.cfg")
1415 .context("Removing grub.cfg")?;
1416
1417 let final_name = match std::env::consts::ARCH {
1418 "x86_64" => "grubx64.efi",
1419 "aarch64" => "grubaa64-cc.efi",
1420 arch => anyhow::bail!("GrubCC not supported for: {arch}"),
1421 };
1422
1423 mounted_root
1424 .dir()
1425 .copy("usr/lib/grub-cc/grub-cc.efi", &efis_dir, final_name)
1426 .context("Copying grub-cc binary")?;
1427 }
1428 } else {
1429 crate::bootloader::install_systemd_boot(
1430 &mounted_root,
1431 &state.config_opts,
1432 get_secureboot_keys(mounted_root.dir(), BOOTC_AUTOENROLL_PATH)?,
1433 )?;
1434 }
1435
1436 let Some(entry) = entries.iter().next() else {
1437 anyhow::bail!("No boot entries!");
1438 };
1439
1440 let boot_type = BootType::from(entry);
1441
1442 let repo = Arc::try_unwrap(repo).map_err(|_| {
1444 anyhow::anyhow!(
1445 "BUG: Arc<Repository> still has other references after boot image generation"
1446 )
1447 })?;
1448
1449 let boot_digest = match boot_type {
1450 BootType::Bls => setup_composefs_bls_boot(
1451 BootSetupType::Setup((&root_setup, &state, &postfetch)),
1452 repo,
1453 &id,
1454 entry,
1455 mounted_root.dir(),
1456 )?,
1457 BootType::Uki => setup_composefs_uki_boot(
1458 BootSetupType::Setup((&root_setup, &state, &postfetch)),
1459 repo,
1460 &id,
1461 entries,
1462 )?,
1463 };
1464
1465 write_composefs_state(
1466 &root_setup.physical_root_path,
1467 &id,
1468 &crate::spec::ImageReference::from(state.target_imgref.clone()),
1469 None,
1470 boot_type,
1471 boot_digest,
1472 &pull_result.manifest_digest.to_string(),
1473 allow_missing_fsverity,
1474 )
1475 .await?;
1476
1477 Ok(())
1478}
1479
1480#[cfg(test)]
1481mod tests {
1482 use super::*;
1483
1484 #[test]
1485 fn test_type1_filename_generation() {
1486 let filename =
1488 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1489 assert_eq!(filename, "bootc_fedora-41.20251125.0-1.conf");
1490
1491 let primary =
1493 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1494 let secondary =
1495 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1496 assert_eq!(primary, "bootc_fedora-41.20251125.0-1.conf");
1497 assert_eq!(secondary, "bootc_fedora-41.20251125.0-0.conf");
1498
1499 let filename =
1501 type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1502 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1503
1504 let filename =
1506 type1_entry_conf_file_name("my-custom-os", "1.0.0", FILENAME_PRIORITY_PRIMARY);
1507 assert_eq!(filename, "bootc_my_custom_os-1.0.0-1.conf");
1508
1509 let filename = type1_entry_conf_file_name("rhel", "9.3.0", FILENAME_PRIORITY_SECONDARY);
1511 assert_eq!(filename, "bootc_rhel-9.3.0-0.conf");
1512 }
1513
1514 #[test]
1515 fn test_grub_filename_parsing() {
1516 let filename = type1_entry_conf_file_name("fedora-coreos", "41.20251125.0", "1");
1525 assert_eq!(filename, "bootc_fedora_coreos-41.20251125.0-1.conf");
1526
1527 let without_ext = filename.strip_suffix(".conf").unwrap();
1533 let parts: Vec<&str> = without_ext.rsplitn(3, '-').collect();
1534 assert_eq!(parts.len(), 3);
1535 assert_eq!(parts[0], "1"); assert_eq!(parts[1], "41.20251125.0"); assert_eq!(parts[2], "bootc_fedora_coreos"); }
1539
1540 #[test]
1541 fn test_sort_keys() {
1542 let primary = primary_sort_key("fedora");
1544 let secondary = secondary_sort_key("fedora");
1545
1546 assert_eq!(primary, "bootc-fedora-0");
1547 assert_eq!(secondary, "bootc-fedora-1");
1548
1549 assert!(primary < secondary);
1551
1552 let primary_coreos = primary_sort_key("fedora-coreos");
1554 assert_eq!(primary_coreos, "bootc-fedora-coreos-0");
1555 }
1556
1557 #[test]
1558 fn test_filename_sorting_grub_style() {
1559 let primary =
1563 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1564 let secondary =
1565 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_SECONDARY);
1566
1567 assert!(
1569 primary > secondary,
1570 "Primary should sort before secondary in descending order"
1571 );
1572
1573 let newer =
1575 type1_entry_conf_file_name("fedora", "42.20251125.0", FILENAME_PRIORITY_PRIMARY);
1576 let older =
1577 type1_entry_conf_file_name("fedora", "41.20251125.0", FILENAME_PRIORITY_PRIMARY);
1578
1579 assert!(
1581 newer > older,
1582 "Newer version should sort before older in descending order"
1583 );
1584
1585 let fedora = type1_entry_conf_file_name("fedora", "41.0", FILENAME_PRIORITY_PRIMARY);
1587 let rhel = type1_entry_conf_file_name("rhel", "9.0", FILENAME_PRIORITY_PRIMARY);
1588
1589 assert!(
1591 rhel > fedora,
1592 "RHEL should sort before Fedora in descending order"
1593 );
1594 }
1595}