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_blockdev::{Partition, PartitionTable};
12use bootc_mount as mount;
13
14use crate::bootc_composefs::boot::{SecurebootKeys, get_sysroot_parent_dev, mount_esp};
15use crate::{discoverable_partition_specification, utils};
16
17pub(crate) const EFI_DIR: &str = "efi";
19#[allow(dead_code)]
22const BOOTUPD_UPDATES: &str = "usr/lib/bootupd/updates";
23
24const SYSTEMD_KEY_DIR: &str = "loader/keys";
26
27#[allow(dead_code)]
28pub(crate) fn esp_in(device: &PartitionTable) -> Result<&Partition> {
29 device
30 .find_partition_of_type(discoverable_partition_specification::ESP)
31 .ok_or(anyhow::anyhow!("ESP not found in partition table"))
32}
33
34pub(crate) fn get_esp_partition_node(root: &Dir) -> Result<Option<String>> {
36 let device = get_sysroot_parent_dev(&root)?;
37 let base_partitions = bootc_blockdev::partitions_of(Utf8Path::new(&device))?;
38 let esp = base_partitions.find_partition_of_esp()?;
39 Ok(esp.map(|v| v.node.clone()))
40}
41
42pub(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 if let Some(esp_part) = get_esp_partition_node(physical_root)? {
64 bootc_mount::mount(&esp_part, &root_path.join(&efi_path))?;
65 tracing::debug!("Mounted {esp_part} at /boot/efi");
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
83#[context("Installing bootloader")]
84pub(crate) fn install_via_bootupd(
85 device: &PartitionTable,
86 rootfs: &Utf8Path,
87 configopts: &crate::install::InstallConfigOpts,
88 deployment_path: Option<&str>,
89) -> Result<()> {
90 let verbose = std::env::var_os("BOOTC_BOOTLOADER_DEBUG").map(|_| "-vvvv");
91 let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
93
94 let rootfs_mount = if deployment_path.is_none() {
100 rootfs.as_str()
101 } else {
102 "/"
103 };
104
105 println!("Installing bootloader via bootupd");
106
107 let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
109 if configopts.bootupd_skip_boot_uuid {
110 bootupd_args.push("--with-static-configs")
111 } else {
112 bootupd_args.push("--write-uuid");
113 }
114 if let Some(v) = verbose {
115 bootupd_args.push(v);
116 }
117
118 if let Some(ref opts) = bootupd_opts {
119 bootupd_args.extend(opts.iter().copied());
120 }
121 bootupd_args.extend(["--device", device.path().as_str(), rootfs_mount]);
122
123 if let Some(deploy) = deployment_path {
127 let target_root = rootfs.join(deploy);
128 let boot_path = rootfs.join("boot");
129
130 tracing::debug!("Running bootupctl via bwrap in {}", target_root);
131
132 let mut bwrap_args = vec!["bootupctl"];
134 bwrap_args.extend(bootupd_args);
135
136 let mut cmd = BwrapCmd::new(&target_root)
137 .bind(&boot_path, &"/boot")
140 .bind_device(device.path().as_str());
142
143 for partition in &device.partitions {
145 cmd = cmd.bind_device(&partition.node);
146 }
147
148 cmd.setenv(
153 "PATH",
154 "/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
155 )
156 .run(bwrap_args)
157 } else {
158 Command::new("bootupctl")
160 .args(&bootupd_args)
161 .log_debug()
162 .run_inherited_with_cmd_context()
163 }
164}
165
166#[context("Installing bootloader")]
167pub(crate) fn install_systemd_boot(
168 device: &PartitionTable,
169 _rootfs: &Utf8Path,
170 _configopts: &crate::install::InstallConfigOpts,
171 _deployment_path: Option<&str>,
172 autoenroll: Option<SecurebootKeys>,
173) -> Result<()> {
174 let esp_part = device
175 .find_partition_of_type(discoverable_partition_specification::ESP)
176 .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
177
178 let esp_mount = mount_esp(&esp_part.node).context("Mounting ESP")?;
179 let esp_path = Utf8Path::from_path(esp_mount.dir.path())
180 .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
181
182 println!("Installing bootloader via systemd-boot");
183 Command::new("bootctl")
184 .args(["install", "--esp-path", esp_path.as_str()])
185 .log_debug()
186 .run_inherited_with_cmd_context()?;
187
188 if let Some(SecurebootKeys { dir, keys }) = autoenroll {
189 let path = esp_path.join(SYSTEMD_KEY_DIR);
190 create_dir_all(&path)?;
191
192 let keys_dir = esp_mount
193 .fd
194 .open_dir(SYSTEMD_KEY_DIR)
195 .with_context(|| format!("Opening {path}"))?;
196
197 for filename in keys.iter() {
198 let p = path.join(&filename);
199
200 if let Some(parent) = p.parent() {
202 create_dir_all(parent)?;
203 }
204
205 dir.copy(&filename, &keys_dir, &filename)
206 .with_context(|| format!("Copying secure boot key: {p}"))?;
207 println!("Wrote Secure Boot key: {p}");
208 }
209 if keys.is_empty() {
210 tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
211 }
212 }
213
214 Ok(())
215}
216
217#[context("Installing bootloader using zipl")]
218pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> {
219 let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
221 let boot_dir = Utf8Path::new(&fs.target);
222 let maj_min = fs.maj_min;
223
224 let device_path = device.path();
226
227 let partitions = bootc_blockdev::list_dev(device_path)?
228 .children
229 .with_context(|| format!("no partition found on {device_path}"))?;
230 let boot_part = partitions
231 .iter()
232 .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
233 .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
234 let boot_part_offset = boot_part.start.unwrap_or(0);
235
236 let bls_dir = boot_dir.join("boot/loader/entries");
239 let bls_entry = bls_dir
240 .read_dir_utf8()?
241 .try_fold(None, |acc, e| -> Result<_> {
242 let e = e?;
243 let name = Utf8Path::new(e.file_name());
244 if let Some("conf") = name.extension() {
245 if acc.is_some() {
246 bail!("more than one BLS configurations under {bls_dir}");
247 }
248 Ok(Some(e.path().to_owned()))
249 } else {
250 Ok(None)
251 }
252 })?
253 .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
254
255 let bls_path = bls_dir.join(bls_entry);
256 let bls_conf =
257 std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
258
259 let mut kernel = None;
260 let mut initrd = None;
261 let mut options = None;
262
263 for line in bls_conf.lines() {
264 match line.split_once(char::is_whitespace) {
265 Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
266 Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
267 Some(("options", val)) => options = Some(val.trim()),
268 _ => (),
269 }
270 }
271
272 let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
273 let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
274 let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
275
276 let image = boot_dir.join(kernel).canonicalize_utf8()?;
277 let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
278
279 println!("Running zipl on {device_path}");
281 Command::new("zipl")
282 .args(["--target", boot_dir.as_str()])
283 .args(["--image", image.as_str()])
284 .args(["--ramdisk", ramdisk.as_str()])
285 .args(["--parameters", options])
286 .args(["--targetbase", device_path.as_str()])
287 .args(["--targettype", "SCSI"])
288 .args(["--targetblocksize", "512"])
289 .args(["--targetoffset", &boot_part_offset.to_string()])
290 .args(["--add-files", "--verbose"])
291 .log_debug()
292 .run_inherited_with_cmd_context()
293}