1use std::fs::create_dir_all;
2use std::process::Command;
3
4use anyhow::{Context, Result, anyhow, bail};
5use bootc_utils::{ChrootCmd, 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::{MountedImageRoot, SecurebootKeys};
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
26const KERNEL_INSTALL_CONF_ROOT: &str = "/tmp";
35
36pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) -> Result<()> {
44 let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR);
45 let Some(esp_fd) = root
46 .open_dir_optional(&efi_path)
47 .context("Opening /boot/efi")?
48 else {
49 return Ok(());
50 };
51
52 let Some(false) = esp_fd.is_mountpoint(".")? else {
53 return Ok(());
54 };
55
56 tracing::debug!("Not a mountpoint: /boot/efi");
57 let physical_root = if is_ostree {
59 &root.open_dir("sysroot").context("Opening /sysroot")?
60 } else {
61 root
62 };
63
64 let roots = bootc_blockdev::list_dev_by_dir(physical_root)?.find_all_roots()?;
65 for dev in &roots {
66 if let Some(esp_dev) = dev.find_partition_of_esp_optional()? {
67 let esp_path = esp_dev.path();
68 bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
69 tracing::debug!("Mounted {esp_path} at /boot/efi");
70 return Ok(());
71 }
72 }
73 tracing::debug!(
74 "No ESP partition found among {} root device(s)",
75 roots.len()
76 );
77 Ok(())
78}
79
80#[context("Querying for bootupd")]
83pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
84 if !utils::have_executable("bootupctl")? {
85 tracing::trace!("No bootupctl binary found");
86 return Ok(false);
87 };
88 let r = root.try_exists(BOOTUPD_UPDATES)?;
89 tracing::trace!("bootupd updates: {r}");
90 Ok(r)
91}
92
93fn bootupd_supports_filesystem(rootfs: &Utf8Path, deployment_path: Option<&str>) -> Result<bool> {
99 let help_args = ["bootupctl", "backend", "install", "--help"];
100 let output = if let Some(deploy) = deployment_path {
101 let target_root = rootfs.join(deploy);
102 ChrootCmd::new(&target_root)
103 .set_default_path()
104 .run_get_string(help_args)?
105 } else {
106 Command::new("bootupctl")
107 .args(&help_args[1..])
108 .log_debug()
109 .run_get_string()?
110 };
111
112 let use_filesystem = output.contains("--filesystem");
113
114 if use_filesystem {
115 tracing::debug!("bootupd supports --filesystem");
116 } else {
117 tracing::debug!("bootupd does not support --filesystem, falling back to --device");
118 }
119
120 Ok(use_filesystem)
121}
122
123#[context("Installing bootloader")]
133pub(crate) fn install_via_bootupd(
134 device: &bootc_blockdev::Device,
135 rootfs: &Utf8Path,
136 configopts: &crate::install::InstallConfigOpts,
137 deployment_path: Option<&str>,
138) -> Result<()> {
139 let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
140 let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
142
143 let rootfs_mount = if deployment_path.is_none() {
149 rootfs.as_str()
150 } else {
151 "/"
152 };
153
154 println!("Installing bootloader via bootupd");
155
156 let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
158 if configopts.bootupd_skip_boot_uuid {
159 bootupd_args.push("--with-static-configs")
160 } else {
161 bootupd_args.push("--write-uuid");
162 }
163 if let Some(v) = verbose {
164 bootupd_args.push(v);
165 }
166
167 if let Some(ref opts) = bootupd_opts {
168 bootupd_args.extend(opts.iter().copied());
169 }
170
171 let root_device_path = if bootupd_supports_filesystem(rootfs, deployment_path)
178 .context("Probing bootupd --filesystem support")?
179 {
180 None
181 } else {
182 Some(device.require_single_root()?.path())
183 };
184 if let Some(ref dev) = root_device_path {
185 tracing::debug!("bootupd does not support --filesystem, falling back to --device {dev}");
186 bootupd_args.extend(["--device", dev]);
187 bootupd_args.push(rootfs_mount);
188 } else {
189 tracing::debug!("bootupd supports --filesystem");
190 bootupd_args.extend(["--filesystem", rootfs_mount]);
191 bootupd_args.push(rootfs_mount);
192 }
193
194 if let Some(deploy) = deployment_path {
199 let target_root = rootfs.join(deploy);
200 let boot_path = rootfs.join("boot");
201 let rootfs_path = rootfs.to_path_buf();
202
203 tracing::debug!("Running bootupctl via chroot in {}", target_root);
204
205 let mut chroot_args = vec!["bootupctl"];
208 chroot_args.extend(bootupd_args);
209
210 let mut cmd = ChrootCmd::new(&target_root)
211 .bind(&boot_path, &"/boot");
214
215 if root_device_path.is_none() {
218 cmd = cmd.bind(&rootfs_path, &"/sysroot");
219 }
220
221 cmd.set_default_path().run(chroot_args)
224 } else {
225 Command::new("bootupctl")
227 .args(&bootupd_args)
228 .log_debug()
229 .run_inherited_with_cmd_context()
230 }
231}
232
233#[context("Installing bootloader")]
235pub(crate) fn install_systemd_boot(
236 prepared_root: &MountedImageRoot,
237 configopts: &crate::install::InstallConfigOpts,
238 autoenroll: Option<SecurebootKeys>,
239) -> Result<()> {
240 println!("Installing bootloader via systemd-boot");
241
242 let root_path = prepared_root
244 .root_path()
245 .to_str()
246 .ok_or_else(|| anyhow::anyhow!("composefs tmpdir path is not UTF-8"))?;
247 let esp_path_in_root = format!("/{}", prepared_root.esp_subdir);
248
249 let mut bootctl_args = vec![
250 "install",
251 "--root",
252 root_path,
253 "--esp-path",
254 esp_path_in_root.as_str(),
255 ];
257
258 if configopts.generic_image {
259 bootctl_args.extend(["--random-seed", "no", "--no-variables"]);
260 }
261
262 Command::new("bootctl")
263 .args(bootctl_args)
264 .env("SYSTEMD_RELAX_ESP_CHECKS", "1")
267 .env("KERNEL_INSTALL_CONF_ROOT", KERNEL_INSTALL_CONF_ROOT)
271 .log_debug()
272 .run_capture_stderr()?;
274
275 if let Some(SecurebootKeys { dir, keys }) = autoenroll {
276 let esp_dir = prepared_root.open_esp_dir()?;
277 let keys_path = prepared_root
278 .root_path()
279 .join(prepared_root.esp_subdir)
280 .join(SYSTEMD_KEY_DIR);
281 create_dir_all(&keys_path).with_context(|| {
282 format!("Creating secureboot key directory {}", keys_path.display())
283 })?;
284
285 let keys_dir = esp_dir
286 .open_dir(SYSTEMD_KEY_DIR)
287 .with_context(|| format!("Opening {SYSTEMD_KEY_DIR}"))?;
288
289 for filename in keys.iter() {
290 if let Some(parent) = filename.parent() {
293 if !parent.as_str().is_empty() {
294 keys_dir
295 .create_dir_all(parent)
296 .with_context(|| format!("Creating key subdirectory {parent}"))?;
297 }
298 }
299 dir.copy(filename, &keys_dir, filename)
300 .with_context(|| format!("Copying secure boot key {filename:?}"))?;
301 println!(
302 "Wrote Secure Boot key: {}/{}",
303 keys_path.display(),
304 filename.as_str()
305 );
306 }
307 if keys.is_empty() {
308 tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
309 }
310 }
311
312 Ok(())
313}
314
315#[context("Installing bootloader using zipl")]
316pub(crate) fn install_via_zipl(device: &bootc_blockdev::Device, boot_uuid: &str) -> Result<()> {
317 let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
319 let boot_dir = Utf8Path::new(&fs.target);
320 let maj_min = fs.maj_min;
321
322 let device_path = device.path();
324
325 let partitions = bootc_blockdev::list_dev(Utf8Path::new(&device_path))?
326 .children
327 .with_context(|| format!("no partition found on {device_path}"))?;
328 let boot_part = partitions
329 .iter()
330 .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
331 .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
332 let boot_part_offset = boot_part.start.unwrap_or(0);
333
334 let bls_dir = boot_dir.join("boot/loader/entries");
337 let bls_entry = bls_dir
338 .read_dir_utf8()?
339 .try_fold(None, |acc, e| -> Result<_> {
340 let e = e?;
341 let name = Utf8Path::new(e.file_name());
342 if let Some("conf") = name.extension() {
343 if acc.is_some() {
344 bail!("more than one BLS configurations under {bls_dir}");
345 }
346 Ok(Some(e.path().to_owned()))
347 } else {
348 Ok(None)
349 }
350 })?
351 .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
352
353 let bls_path = bls_dir.join(bls_entry);
354 let bls_conf =
355 std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
356
357 let mut kernel = None;
358 let mut initrd = None;
359 let mut options = None;
360
361 for line in bls_conf.lines() {
362 match line.split_once(char::is_whitespace) {
363 Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
364 Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
365 Some(("options", val)) => options = Some(val.trim()),
366 _ => (),
367 }
368 }
369
370 let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
371 let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
372 let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
373
374 let image = boot_dir.join(kernel).canonicalize_utf8()?;
375 let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
376
377 println!("Running zipl on {device_path}");
379 Command::new("zipl")
380 .args(["--target", boot_dir.as_str()])
381 .args(["--image", image.as_str()])
382 .args(["--ramdisk", ramdisk.as_str()])
383 .args(["--parameters", options])
384 .args(["--targetbase", &device_path])
385 .args(["--targettype", "SCSI"])
386 .args(["--targetblocksize", "512"])
387 .args(["--targetoffset", &boot_part_offset.to_string()])
388 .args(["--add-files", "--verbose"])
389 .log_debug()
390 .run_inherited_with_cmd_context()
391}