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::{discoverable_partition_specification, 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<()> {
28 let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR);
29 let Some(esp_fd) = root
30 .open_dir_optional(&efi_path)
31 .context("Opening /boot/efi")?
32 else {
33 return Ok(());
34 };
35
36 let Some(false) = esp_fd.is_mountpoint(".")? else {
37 return Ok(());
38 };
39
40 tracing::debug!("Not a mountpoint: /boot/efi");
41 let physical_root = if is_ostree {
43 &root.open_dir("sysroot").context("Opening /sysroot")?
44 } else {
45 root
46 };
47
48 let dev = bootc_blockdev::list_dev_by_dir(physical_root)?.require_single_root()?;
49 if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) {
50 let esp_path = esp_dev.path();
51 bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
52 tracing::debug!("Mounted {esp_path} at /boot/efi");
53 }
54 Ok(())
55}
56
57#[context("Querying for bootupd")]
60pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
61 if !utils::have_executable("bootupctl")? {
62 tracing::trace!("No bootupctl binary found");
63 return Ok(false);
64 };
65 let r = root.try_exists(BOOTUPD_UPDATES)?;
66 tracing::trace!("bootupd updates: {r}");
67 Ok(r)
68}
69
70#[context("Installing bootloader")]
71pub(crate) fn install_via_bootupd(
72 device: &bootc_blockdev::Device,
73 rootfs: &Utf8Path,
74 configopts: &crate::install::InstallConfigOpts,
75 deployment_path: Option<&str>,
76) -> Result<()> {
77 let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
78 let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
80
81 let rootfs_mount = if deployment_path.is_none() {
87 rootfs.as_str()
88 } else {
89 "/"
90 };
91
92 println!("Installing bootloader via bootupd");
93
94 let device_path = device.path();
95
96 let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
98 if configopts.bootupd_skip_boot_uuid {
99 bootupd_args.push("--with-static-configs")
100 } else {
101 bootupd_args.push("--write-uuid");
102 }
103 if let Some(v) = verbose {
104 bootupd_args.push(v);
105 }
106
107 if let Some(ref opts) = bootupd_opts {
108 bootupd_args.extend(opts.iter().copied());
109 }
110 bootupd_args.extend(["--device", &device_path, rootfs_mount]);
111
112 if let Some(deploy) = deployment_path {
116 let target_root = rootfs.join(deploy);
117 let boot_path = rootfs.join("boot");
118
119 tracing::debug!("Running bootupctl via bwrap in {}", target_root);
120
121 let mut bwrap_args = vec!["bootupctl"];
123 bwrap_args.extend(bootupd_args);
124
125 let cmd = BwrapCmd::new(&target_root)
126 .bind(&boot_path, &"/boot");
129
130 cmd.setenv(
135 "PATH",
136 "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
137 )
138 .run(bwrap_args)
139 } else {
140 Command::new("bootupctl")
142 .args(&bootupd_args)
143 .log_debug()
144 .run_inherited_with_cmd_context()
145 }
146}
147
148#[context("Installing bootloader")]
149pub(crate) fn install_systemd_boot(
150 device: &bootc_blockdev::Device,
151 _rootfs: &Utf8Path,
152 _configopts: &crate::install::InstallConfigOpts,
153 _deployment_path: Option<&str>,
154 autoenroll: Option<SecurebootKeys>,
155) -> Result<()> {
156 let esp_part = device
157 .find_partition_of_type(discoverable_partition_specification::ESP)
158 .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
159
160 let esp_mount = mount_esp(&esp_part.path()).context("Mounting ESP")?;
161 let esp_path = Utf8Path::from_path(esp_mount.dir.path())
162 .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
163
164 println!("Installing bootloader via systemd-boot");
165 Command::new("bootctl")
166 .args(["install", "--esp-path", esp_path.as_str()])
167 .log_debug()
168 .run_inherited_with_cmd_context()?;
169
170 if let Some(SecurebootKeys { dir, keys }) = autoenroll {
171 let path = esp_path.join(SYSTEMD_KEY_DIR);
172 create_dir_all(&path)?;
173
174 let keys_dir = esp_mount
175 .fd
176 .open_dir(SYSTEMD_KEY_DIR)
177 .with_context(|| format!("Opening {path}"))?;
178
179 for filename in keys.iter() {
180 let p = path.join(&filename);
181
182 if let Some(parent) = p.parent() {
184 create_dir_all(parent)?;
185 }
186
187 dir.copy(&filename, &keys_dir, &filename)
188 .with_context(|| format!("Copying secure boot key: {p}"))?;
189 println!("Wrote Secure Boot key: {p}");
190 }
191 if keys.is_empty() {
192 tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
193 }
194 }
195
196 Ok(())
197}
198
199#[context("Installing bootloader using zipl")]
200pub(crate) fn install_via_zipl(device: &bootc_blockdev::Device, boot_uuid: &str) -> Result<()> {
201 let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
203 let boot_dir = Utf8Path::new(&fs.target);
204 let maj_min = fs.maj_min;
205
206 let device_path = device.path();
208
209 let partitions = bootc_blockdev::list_dev(Utf8Path::new(&device_path))?
210 .children
211 .with_context(|| format!("no partition found on {device_path}"))?;
212 let boot_part = partitions
213 .iter()
214 .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
215 .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
216 let boot_part_offset = boot_part.start.unwrap_or(0);
217
218 let bls_dir = boot_dir.join("boot/loader/entries");
221 let bls_entry = bls_dir
222 .read_dir_utf8()?
223 .try_fold(None, |acc, e| -> Result<_> {
224 let e = e?;
225 let name = Utf8Path::new(e.file_name());
226 if let Some("conf") = name.extension() {
227 if acc.is_some() {
228 bail!("more than one BLS configurations under {bls_dir}");
229 }
230 Ok(Some(e.path().to_owned()))
231 } else {
232 Ok(None)
233 }
234 })?
235 .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
236
237 let bls_path = bls_dir.join(bls_entry);
238 let bls_conf =
239 std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
240
241 let mut kernel = None;
242 let mut initrd = None;
243 let mut options = None;
244
245 for line in bls_conf.lines() {
246 match line.split_once(char::is_whitespace) {
247 Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
248 Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
249 Some(("options", val)) => options = Some(val.trim()),
250 _ => (),
251 }
252 }
253
254 let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
255 let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
256 let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
257
258 let image = boot_dir.join(kernel).canonicalize_utf8()?;
259 let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
260
261 println!("Running zipl on {device_path}");
263 Command::new("zipl")
264 .args(["--target", boot_dir.as_str()])
265 .args(["--image", image.as_str()])
266 .args(["--ramdisk", ramdisk.as_str()])
267 .args(["--parameters", options])
268 .args(["--targetbase", &device_path])
269 .args(["--targettype", "SCSI"])
270 .args(["--targetblocksize", "512"])
271 .args(["--targetoffset", &boot_part_offset.to_string()])
272 .args(["--add-files", "--verbose"])
273 .log_debug()
274 .run_inherited_with_cmd_context()
275}