1use std::fs::create_dir_all;
2use std::process::Command;
3
4use anyhow::{Context, Result, anyhow, bail};
5use bootc_utils::{BwrapCmd, CommandRunExt};
6use camino::Utf8Path;
7use cap_std_ext::cap_std::fs::Dir;
8use cap_std_ext::dirext::CapStdExtDirExt;
9use fn_error_context::context;
10
11use bootc_mount as mount;
12
13use crate::bootc_composefs::boot::{SecurebootKeys, mount_esp};
14use crate::utils;
15
16pub(crate) const EFI_DIR: &str = "efi";
18#[allow(dead_code)]
21const BOOTUPD_UPDATES: &str = "usr/lib/bootupd/updates";
22
23const SYSTEMD_KEY_DIR: &str = "loader/keys";
25
26pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) -> Result<()> {
34 let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR);
35 let Some(esp_fd) = root
36 .open_dir_optional(&efi_path)
37 .context("Opening /boot/efi")?
38 else {
39 return Ok(());
40 };
41
42 let Some(false) = esp_fd.is_mountpoint(".")? else {
43 return Ok(());
44 };
45
46 tracing::debug!("Not a mountpoint: /boot/efi");
47 let physical_root = if is_ostree {
49 &root.open_dir("sysroot").context("Opening /sysroot")?
50 } else {
51 root
52 };
53
54 let roots = bootc_blockdev::list_dev_by_dir(physical_root)?.find_all_roots()?;
55 for dev in &roots {
56 if let Some(esp_dev) = dev.find_partition_of_esp_optional()? {
57 let esp_path = esp_dev.path();
58 bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
59 tracing::debug!("Mounted {esp_path} at /boot/efi");
60 return Ok(());
61 }
62 }
63 tracing::debug!(
64 "No ESP partition found among {} root device(s)",
65 roots.len()
66 );
67 Ok(())
68}
69
70#[context("Querying for bootupd")]
73pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
74 if !utils::have_executable("bootupctl")? {
75 tracing::trace!("No bootupctl binary found");
76 return Ok(false);
77 };
78 let r = root.try_exists(BOOTUPD_UPDATES)?;
79 tracing::trace!("bootupd updates: {r}");
80 Ok(r)
81}
82
83fn bootupd_supports_filesystem(rootfs: &Utf8Path, deployment_path: Option<&str>) -> Result<bool> {
89 let help_args = ["bootupctl", "backend", "install", "--help"];
90 let output = if let Some(deploy) = deployment_path {
91 let target_root = rootfs.join(deploy);
92 BwrapCmd::new(&target_root)
93 .set_default_path()
94 .run_get_string(help_args)?
95 } else {
96 Command::new("bootupctl")
97 .args(&help_args[1..])
98 .log_debug()
99 .run_get_string()?
100 };
101
102 let use_filesystem = output.contains("--filesystem");
103
104 if use_filesystem {
105 tracing::debug!("bootupd supports --filesystem");
106 } else {
107 tracing::debug!("bootupd does not support --filesystem, falling back to --device");
108 }
109
110 Ok(use_filesystem)
111}
112
113#[context("Installing bootloader")]
123pub(crate) fn install_via_bootupd(
124 device: &bootc_blockdev::Device,
125 rootfs: &Utf8Path,
126 configopts: &crate::install::InstallConfigOpts,
127 deployment_path: Option<&str>,
128) -> Result<()> {
129 let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
130 let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
132
133 let rootfs_mount = if deployment_path.is_none() {
139 rootfs.as_str()
140 } else {
141 "/"
142 };
143
144 println!("Installing bootloader via bootupd");
145
146 let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
148 if configopts.bootupd_skip_boot_uuid {
149 bootupd_args.push("--with-static-configs")
150 } else {
151 bootupd_args.push("--write-uuid");
152 }
153 if let Some(v) = verbose {
154 bootupd_args.push(v);
155 }
156
157 if let Some(ref opts) = bootupd_opts {
158 bootupd_args.extend(opts.iter().copied());
159 }
160
161 let root_device_path = if bootupd_supports_filesystem(rootfs, deployment_path)
168 .context("Probing bootupd --filesystem support")?
169 {
170 None
171 } else {
172 Some(device.require_single_root()?.path())
173 };
174 if let Some(ref dev) = root_device_path {
175 tracing::debug!("bootupd does not support --filesystem, falling back to --device {dev}");
176 bootupd_args.extend(["--device", dev]);
177 bootupd_args.push(rootfs_mount);
178 } else {
179 tracing::debug!("bootupd supports --filesystem");
180 bootupd_args.extend(["--filesystem", rootfs_mount]);
181 bootupd_args.push(rootfs_mount);
182 }
183
184 if let Some(deploy) = deployment_path {
188 let target_root = rootfs.join(deploy);
189 let boot_path = rootfs.join("boot");
190 let rootfs_path = rootfs.to_path_buf();
191
192 tracing::debug!("Running bootupctl via bwrap in {}", target_root);
193
194 let mut bwrap_args = vec!["bootupctl"];
196 bwrap_args.extend(bootupd_args);
197
198 let mut cmd = BwrapCmd::new(&target_root)
199 .bind(&boot_path, &"/boot");
202
203 if root_device_path.is_none() {
206 cmd = cmd.bind(&rootfs_path, &"/sysroot");
207 }
208
209 cmd.set_default_path().run(bwrap_args)
212 } else {
213 Command::new("bootupctl")
215 .args(&bootupd_args)
216 .log_debug()
217 .run_inherited_with_cmd_context()
218 }
219}
220
221#[context("Installing bootloader")]
227pub(crate) fn install_systemd_boot(
228 device: &bootc_blockdev::Device,
229 _rootfs: &Utf8Path,
230 _configopts: &crate::install::InstallConfigOpts,
231 _deployment_path: Option<&str>,
232 autoenroll: Option<SecurebootKeys>,
233) -> Result<()> {
234 let roots = device.find_all_roots()?;
235 let mut esp_part = None;
236 for root in &roots {
237 if let Some(esp) = root.find_partition_of_esp_optional()? {
238 esp_part = Some(esp);
239 break;
240 }
241 }
242 let esp_part = esp_part.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
243
244 let esp_mount = mount_esp(&esp_part.path()).context("Mounting ESP")?;
245 let esp_path = Utf8Path::from_path(esp_mount.dir.path())
246 .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
247
248 println!("Installing bootloader via systemd-boot");
249 Command::new("bootctl")
250 .args(["install", "--esp-path", esp_path.as_str()])
251 .log_debug()
252 .run_inherited_with_cmd_context()?;
253
254 if let Some(SecurebootKeys { dir, keys }) = autoenroll {
255 let path = esp_path.join(SYSTEMD_KEY_DIR);
256 create_dir_all(&path)?;
257
258 let keys_dir = esp_mount
259 .fd
260 .open_dir(SYSTEMD_KEY_DIR)
261 .with_context(|| format!("Opening {path}"))?;
262
263 for filename in keys.iter() {
264 let p = path.join(&filename);
265
266 if let Some(parent) = p.parent() {
268 create_dir_all(parent)?;
269 }
270
271 dir.copy(&filename, &keys_dir, &filename)
272 .with_context(|| format!("Copying secure boot key: {p}"))?;
273 println!("Wrote Secure Boot key: {p}");
274 }
275 if keys.is_empty() {
276 tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
277 }
278 }
279
280 Ok(())
281}
282
283#[context("Installing bootloader using zipl")]
284pub(crate) fn install_via_zipl(device: &bootc_blockdev::Device, boot_uuid: &str) -> Result<()> {
285 let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
287 let boot_dir = Utf8Path::new(&fs.target);
288 let maj_min = fs.maj_min;
289
290 let device_path = device.path();
292
293 let partitions = bootc_blockdev::list_dev(Utf8Path::new(&device_path))?
294 .children
295 .with_context(|| format!("no partition found on {device_path}"))?;
296 let boot_part = partitions
297 .iter()
298 .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
299 .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
300 let boot_part_offset = boot_part.start.unwrap_or(0);
301
302 let bls_dir = boot_dir.join("boot/loader/entries");
305 let bls_entry = bls_dir
306 .read_dir_utf8()?
307 .try_fold(None, |acc, e| -> Result<_> {
308 let e = e?;
309 let name = Utf8Path::new(e.file_name());
310 if let Some("conf") = name.extension() {
311 if acc.is_some() {
312 bail!("more than one BLS configurations under {bls_dir}");
313 }
314 Ok(Some(e.path().to_owned()))
315 } else {
316 Ok(None)
317 }
318 })?
319 .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
320
321 let bls_path = bls_dir.join(bls_entry);
322 let bls_conf =
323 std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
324
325 let mut kernel = None;
326 let mut initrd = None;
327 let mut options = None;
328
329 for line in bls_conf.lines() {
330 match line.split_once(char::is_whitespace) {
331 Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
332 Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
333 Some(("options", val)) => options = Some(val.trim()),
334 _ => (),
335 }
336 }
337
338 let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
339 let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
340 let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
341
342 let image = boot_dir.join(kernel).canonicalize_utf8()?;
343 let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
344
345 println!("Running zipl on {device_path}");
347 Command::new("zipl")
348 .args(["--target", boot_dir.as_str()])
349 .args(["--image", image.as_str()])
350 .args(["--ramdisk", ramdisk.as_str()])
351 .args(["--parameters", options])
352 .args(["--targetbase", &device_path])
353 .args(["--targettype", "SCSI"])
354 .args(["--targetblocksize", "512"])
355 .args(["--targetoffset", &boot_part_offset.to_string()])
356 .args(["--add-files", "--verbose"])
357 .log_debug()
358 .run_inherited_with_cmd_context()
359}