1use std::fs::create_dir_all;
2use std::process::Command;
3
4use anyhow::{Context, Result, anyhow, bail};
5use bootc_utils::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 abs_deployment_path = deployment_path.map(|v| rootfs.join(v));
95 let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() {
96 vec!["--src-root", p.as_str()]
97 } else {
98 vec![]
99 };
100 let devpath = device.path();
101 println!("Installing bootloader via bootupd");
102 Command::new("bootupctl")
103 .args(["backend", "install", "--write-uuid"])
104 .args(verbose)
105 .args(bootupd_opts.iter().copied().flatten())
106 .args(src_root_arg)
107 .args(["--device", devpath.as_str(), rootfs.as_str()])
108 .log_debug()
109 .run_inherited_with_cmd_context()
110}
111
112#[context("Installing bootloader")]
113pub(crate) fn install_systemd_boot(
114 device: &PartitionTable,
115 _rootfs: &Utf8Path,
116 _configopts: &crate::install::InstallConfigOpts,
117 _deployment_path: Option<&str>,
118 autoenroll: Option<SecurebootKeys>,
119) -> Result<()> {
120 let esp_part = device
121 .find_partition_of_type(discoverable_partition_specification::ESP)
122 .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
123
124 let esp_mount = mount_esp(&esp_part.node).context("Mounting ESP")?;
125 let esp_path = Utf8Path::from_path(esp_mount.dir.path())
126 .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?;
127
128 println!("Installing bootloader via systemd-boot");
129 Command::new("bootctl")
130 .args(["install", "--esp-path", esp_path.as_str()])
131 .log_debug()
132 .run_inherited_with_cmd_context()?;
133
134 if let Some(SecurebootKeys { dir, keys }) = autoenroll {
135 let path = esp_path.join(SYSTEMD_KEY_DIR);
136 create_dir_all(&path)?;
137
138 let keys_dir = esp_mount
139 .fd
140 .open_dir(SYSTEMD_KEY_DIR)
141 .with_context(|| format!("Opening {path}"))?;
142
143 for filename in keys.iter() {
144 let p = path.join(&filename);
145
146 if let Some(parent) = p.parent() {
148 create_dir_all(parent)?;
149 }
150
151 dir.copy(&filename, &keys_dir, &filename)
152 .with_context(|| format!("Copying secure boot key: {p}"))?;
153 println!("Wrote Secure Boot key: {p}");
154 }
155 if keys.is_empty() {
156 tracing::debug!("No Secure Boot keys provided for systemd-boot enrollment");
157 }
158 }
159
160 Ok(())
161}
162
163#[context("Installing bootloader using zipl")]
164pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> {
165 let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?;
167 let boot_dir = Utf8Path::new(&fs.target);
168 let maj_min = fs.maj_min;
169
170 let device_path = device.path();
172
173 let partitions = bootc_blockdev::list_dev(device_path)?
174 .children
175 .with_context(|| format!("no partition found on {device_path}"))?;
176 let boot_part = partitions
177 .iter()
178 .find(|part| part.maj_min.as_deref() == Some(maj_min.as_str()))
179 .with_context(|| format!("partition device {maj_min} is not on {device_path}"))?;
180 let boot_part_offset = boot_part.start.unwrap_or(0);
181
182 let bls_dir = boot_dir.join("boot/loader/entries");
185 let bls_entry = bls_dir
186 .read_dir_utf8()?
187 .try_fold(None, |acc, e| -> Result<_> {
188 let e = e?;
189 let name = Utf8Path::new(e.file_name());
190 if let Some("conf") = name.extension() {
191 if acc.is_some() {
192 bail!("more than one BLS configurations under {bls_dir}");
193 }
194 Ok(Some(e.path().to_owned()))
195 } else {
196 Ok(None)
197 }
198 })?
199 .with_context(|| format!("no BLS configuration under {bls_dir}"))?;
200
201 let bls_path = bls_dir.join(bls_entry);
202 let bls_conf =
203 std::fs::read_to_string(&bls_path).with_context(|| format!("reading {bls_path}"))?;
204
205 let mut kernel = None;
206 let mut initrd = None;
207 let mut options = None;
208
209 for line in bls_conf.lines() {
210 match line.split_once(char::is_whitespace) {
211 Some(("linux", val)) => kernel = Some(val.trim().trim_start_matches('/')),
212 Some(("initrd", val)) => initrd = Some(val.trim().trim_start_matches('/')),
213 Some(("options", val)) => options = Some(val.trim()),
214 _ => (),
215 }
216 }
217
218 let kernel = kernel.ok_or_else(|| anyhow!("missing 'linux' key in default BLS config"))?;
219 let initrd = initrd.ok_or_else(|| anyhow!("missing 'initrd' key in default BLS config"))?;
220 let options = options.ok_or_else(|| anyhow!("missing 'options' key in default BLS config"))?;
221
222 let image = boot_dir.join(kernel).canonicalize_utf8()?;
223 let ramdisk = boot_dir.join(initrd).canonicalize_utf8()?;
224
225 println!("Running zipl on {device_path}");
227 Command::new("zipl")
228 .args(["--target", boot_dir.as_str()])
229 .args(["--image", image.as_str()])
230 .args(["--ramdisk", ramdisk.as_str()])
231 .args(["--parameters", options])
232 .args(["--targetbase", device_path.as_str()])
233 .args(["--targettype", "SCSI"])
234 .args(["--targetblocksize", "512"])
235 .args(["--targetoffset", &boot_part_offset.to_string()])
236 .args(["--add-files", "--verbose"])
237 .log_debug()
238 .run_inherited_with_cmd_context()
239}