bootc_lib/bootc_composefs/
finalize.rs1use std::path::Path;
2
3use crate::bootc_composefs::boot::BootType;
4use crate::bootc_composefs::rollback::{rename_exchange_bls_entries, rename_exchange_user_cfg};
5use crate::bootc_composefs::status::get_composefs_status;
6use crate::composefs_consts::STATE_DIR_ABS;
7use crate::spec::Bootloader;
8use crate::store::{BootedComposefs, Storage};
9use anyhow::{Context, Result};
10use bootc_initramfs_setup::mount_composefs_image;
11use bootc_mount::tempmount::TempMount;
12use cap_std_ext::cap_std::{ambient_authority, fs::Dir};
13use cap_std_ext::dirext::CapStdExtDirExt;
14use composefs::generic_tree::{Directory, Stat};
15use etc_merge::{compute_diff, merge, print_diff, traverse_etc};
16use rustix::fs::{fsync, renameat};
17use rustix::path::Arg;
18
19use fn_error_context::context;
20
21pub(crate) async fn get_etc_diff(storage: &Storage, booted_cfs: &BootedComposefs) -> Result<()> {
22 let host = get_composefs_status(storage, booted_cfs).await?;
23 let booted_composefs = host.require_composefs_booted()?;
24
25 let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
27 let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?;
28
29 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
30
31 let pristine_etc =
32 Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
33 let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
34
35 let (pristine_files, current_files, _) = traverse_etc(&pristine_etc, ¤t_etc, None)?;
36 let diff = compute_diff(
37 &pristine_files,
38 ¤t_files,
39 &Directory::new(Stat::uninitialized()),
40 )?;
41
42 print_diff(&diff, &mut std::io::stdout());
43
44 Ok(())
45}
46
47pub(crate) async fn composefs_backend_finalize(
48 storage: &Storage,
49 booted_cfs: &BootedComposefs,
50) -> Result<()> {
51 let host = get_composefs_status(storage, booted_cfs).await?;
52
53 let booted_composefs = host.require_composefs_booted()?;
54
55 let Some(staged_depl) = host.status.staged.as_ref() else {
56 tracing::debug!("No staged deployment found");
57 return Ok(());
58 };
59
60 if staged_depl.download_only {
61 tracing::debug!("Staged deployment is marked download only. Won't finalize");
62 return Ok(());
63 }
64
65 let staged_composefs = staged_depl.composefs.as_ref().ok_or(anyhow::anyhow!(
66 "Staged deployment is not a composefs deployment"
67 ))?;
68
69 let sysroot_fd = storage.physical_root.reopen_as_ownedfd()?;
71 let composefs_fd = mount_composefs_image(&sysroot_fd, &booted_composefs.verity, false)?;
72
73 let erofs_tmp_mnt = TempMount::mount_fd(&composefs_fd)?;
74
75 let pristine_etc =
77 Dir::open_ambient_dir(erofs_tmp_mnt.dir.path().join("etc"), ambient_authority())?;
78 let current_etc = Dir::open_ambient_dir("/etc", ambient_authority())?;
79
80 let new_etc_path = Path::new(STATE_DIR_ABS)
81 .join(&staged_composefs.verity)
82 .join("etc");
83
84 let new_etc = Dir::open_ambient_dir(new_etc_path, ambient_authority())?;
85
86 let (pristine_files, current_files, new_files) =
87 traverse_etc(&pristine_etc, ¤t_etc, Some(&new_etc))?;
88
89 let new_files =
90 new_files.ok_or_else(|| anyhow::anyhow!("Failed to get dirtree for new etc"))?;
91
92 let diff = compute_diff(&pristine_files, ¤t_files, &new_files)?;
93 merge(¤t_etc, ¤t_files, &new_etc, &new_files, &diff)?;
94
95 drop(erofs_tmp_mnt);
97
98 let boot_dir = storage.require_boot_dir()?;
99
100 let esp_mount = storage
101 .esp
102 .as_ref()
103 .ok_or_else(|| anyhow::anyhow!("ESP not found"))?;
104
105 match booted_composefs.bootloader {
107 Bootloader::Grub => match staged_composefs.boot_type {
108 BootType::Bls => {
109 let entries_dir = boot_dir.open_dir("loader")?;
110 rename_exchange_bls_entries(&entries_dir)?;
111 }
112 BootType::Uki => finalize_staged_grub_uki(&esp_mount.fd, boot_dir)?,
113 },
114
115 Bootloader::Systemd => {
116 if matches!(staged_composefs.boot_type, BootType::Uki) {
117 rename_staged_uki_entries(&esp_mount.fd)?;
118 }
119
120 let entries_dir = boot_dir.open_dir("loader")?;
121 rename_exchange_bls_entries(&entries_dir)?;
122 }
123 };
124
125 Ok(())
126}
127
128#[context("Grub: Finalizing staged UKI")]
129fn finalize_staged_grub_uki(esp_mount: &Dir, boot_fd: &Dir) -> Result<()> {
130 rename_staged_uki_entries(esp_mount)?;
131
132 let entries_dir = boot_fd.open_dir("grub2")?;
133 rename_exchange_user_cfg(&entries_dir)?;
134
135 let entries_dir = entries_dir.reopen_as_ownedfd()?;
136 fsync(entries_dir).context("fsync")?;
137
138 Ok(())
139}
140
141#[context("Renaming staged UKI entries")]
142fn rename_staged_uki_entries(esp_mount: &Dir) -> Result<()> {
143 for entry in esp_mount.entries()? {
144 let entry = entry?;
145
146 let filename = entry.file_name();
147 let filename = filename.as_str()?;
148
149 if !filename.ends_with(".staged") {
150 continue;
151 }
152
153 renameat(
154 &esp_mount,
155 filename,
156 &esp_mount,
157 filename.strip_suffix(".staged").unwrap(),
159 )
160 .context("Renaming {filename}")?;
161 }
162
163 let esp_mount = esp_mount.reopen_as_ownedfd()?;
164 fsync(esp_mount).context("fsync")?;
165
166 Ok(())
167}