Skip to content

Commit 714b582

Browse files
committed
blockdev: Clean up ESP discovery code
Several improvements to ESP partition discovery: Add find_partition_of_esp_optional() returning Result<Option<&Device>> to cleanly separate three outcomes: found, absent, and genuinely unexpected errors (like unsupported partition table types). The existing find_partition_of_esp() is now a thin wrapper that converts None to Err. Add find_first_colocated_esp() helper to replace a 10-line pattern that was repeated verbatim 5 times across boot.rs and store/mod.rs. Deduplicate roots in find_all_roots() using a seen-set: in complex topologies like multipath, multiple parent branches can converge on the same physical disk. find_colocated_esps() now uses the optional variant to properly propagate real errors while treating absence normally. Also extract the match-on-if-else in setup_composefs_bls_boot into a let binding for readability. Assisted-by: OpenCode (Claude Opus 4)
1 parent 1d2f9fd commit 714b582

4 files changed

Lines changed: 73 additions & 87 deletions

File tree

crates/blockdev/src/blockdev.rs

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashSet;
12
use std::env;
23
use std::path::Path;
34
use std::process::{Command, Stdio};
@@ -123,15 +124,26 @@ impl Device {
123124
/// Calls find_all_roots() to discover physical disks, then searches each for an ESP.
124125
/// Returns None if no ESPs are found.
125126
pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
126-
let esps: Vec<_> = self
127-
.find_all_roots()?
128-
.iter()
129-
.flat_map(|root| root.find_partition_of_esp().ok())
130-
.cloned()
131-
.collect();
127+
let mut esps = Vec::new();
128+
for root in &self.find_all_roots()? {
129+
if let Some(esp) = root.find_partition_of_esp_optional()? {
130+
esps.push(esp.clone());
131+
}
132+
}
132133
Ok((!esps.is_empty()).then_some(esps))
133134
}
134135

136+
/// Find a single ESP partition among all root devices backing this device.
137+
///
138+
/// Walks the parent chain to find all backing disks, then looks for ESP
139+
/// partitions on each. Returns the first ESP found. This is the common
140+
/// case for composefs/UKI boot paths where exactly one ESP is expected.
141+
pub fn find_first_colocated_esp(&self) -> Result<Device> {
142+
self.find_colocated_esps()?
143+
.and_then(|mut v| Some(v.remove(0)))
144+
.ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
145+
}
146+
135147
/// Find all BIOS boot partitions across all root devices backing this device.
136148
/// Calls find_all_roots() to discover physical disks, then searches each for a BIOS boot partition.
137149
/// Returns None if no BIOS boot partitions are found.
@@ -159,34 +171,41 @@ impl Device {
159171
///
160172
/// For GPT disks, this matches by the ESP partition type GUID.
161173
/// For MBR (dos) disks, this matches by the MBR partition type IDs (0x06 or 0xEF).
162-
pub fn find_partition_of_esp(&self) -> Result<&Device> {
163-
let children = self
164-
.children
165-
.as_ref()
166-
.ok_or_else(|| anyhow!("Device has no children"))?;
174+
///
175+
/// Returns `Ok(None)` when there are no children or no ESP partition
176+
/// is present. Returns `Err` only for genuinely unexpected conditions
177+
/// (e.g. an unsupported partition table type).
178+
pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
179+
let Some(children) = self.children.as_ref() else {
180+
return Ok(None);
181+
};
167182
match self.pttype.as_deref() {
168-
Some("dos") => children
169-
.iter()
170-
.find(|child| {
171-
child
172-
.parttype
173-
.as_ref()
174-
.and_then(|pt| {
175-
let pt = pt.strip_prefix("0x").unwrap_or(pt);
176-
u8::from_str_radix(pt, 16).ok()
177-
})
178-
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
179-
})
180-
.ok_or_else(|| anyhow!("ESP not found in MBR partition table")),
183+
Some("dos") => Ok(children.iter().find(|child| {
184+
child
185+
.parttype
186+
.as_ref()
187+
.and_then(|pt| {
188+
let pt = pt.strip_prefix("0x").unwrap_or(pt);
189+
u8::from_str_radix(pt, 16).ok()
190+
})
191+
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
192+
})),
181193
// When pttype is None (e.g. older lsblk or partition devices), default
182194
// to GPT UUID matching which will simply not match MBR hex types.
183-
Some("gpt") | None => self
184-
.find_partition_of_type(ESP)
185-
.ok_or_else(|| anyhow!("ESP not found in GPT partition table")),
195+
Some("gpt") | None => Ok(self.find_partition_of_type(ESP)),
186196
Some(other) => Err(anyhow!("Unsupported partition table type: {other}")),
187197
}
188198
}
189199

200+
/// Find the EFI System Partition (ESP) among children, or error if absent.
201+
///
202+
/// This is a convenience wrapper around [`find_partition_of_esp_optional`]
203+
/// for callers that require an ESP to be present.
204+
pub fn find_partition_of_esp(&self) -> Result<&Device> {
205+
self.find_partition_of_esp_optional()?
206+
.ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
207+
}
208+
190209
/// Find a child partition by partition number (1-indexed).
191210
pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
192211
self.children
@@ -308,15 +327,21 @@ impl Device {
308327
};
309328

310329
let mut roots = Vec::new();
330+
let mut seen = HashSet::new();
311331
let mut queue = parents;
312332
while let Some(mut device) = queue.pop() {
313333
match device.children.take() {
314334
Some(grandparents) if !grandparents.is_empty() => {
315335
queue.extend(grandparents);
316336
}
317337
_ => {
318-
// Found a root; re-query to populate its actual children
319-
roots.push(list_dev(Utf8Path::new(&device.path()))?);
338+
// Deduplicate: in complex topologies (e.g. multipath)
339+
// multiple branches can converge on the same physical disk.
340+
let name = device.name.clone();
341+
if seen.insert(name) {
342+
// Found a new root; re-query to populate its actual children
343+
roots.push(list_dev(Utf8Path::new(&device.path()))?);
344+
}
320345
}
321346
}
322347
}

crates/lib/src/bootc_composefs/boot.rs

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -534,17 +534,7 @@ pub(crate) fn setup_composefs_bls_boot(
534534
cmdline_options.extend(&Cmdline::from(&composefs_cmdline.to_string()));
535535

536536
// Locate ESP partition device by walking up to the root disk(s)
537-
let esp_part = root_setup
538-
.device_info
539-
.find_colocated_esps()?
540-
.and_then(|mut v| {
541-
if v.is_empty() {
542-
None
543-
} else {
544-
Some(v.remove(0))
545-
}
546-
})
547-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
537+
let esp_part = root_setup.device_info.find_first_colocated_esp()?;
548538

549539
(
550540
root_setup.physical_root_path.clone(),
@@ -583,16 +573,7 @@ pub(crate) fn setup_composefs_bls_boot(
583573

584574
// Locate ESP partition device by walking up to the root disk(s)
585575
let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
586-
let esp_dev = root_dev
587-
.find_colocated_esps()?
588-
.and_then(|mut v| {
589-
if v.is_empty() {
590-
None
591-
} else {
592-
Some(v.remove(0))
593-
}
594-
})
595-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
576+
let esp_dev = root_dev.find_first_colocated_esp()?;
596577

597578
(
598579
storage.physical_root_path.clone(),
@@ -1136,17 +1117,7 @@ pub(crate) fn setup_composefs_uki_boot(
11361117
state.require_no_kargs_for_uki()?;
11371118

11381119
// Locate ESP partition device by walking up to the root disk(s)
1139-
let esp_part = root_setup
1140-
.device_info
1141-
.find_colocated_esps()?
1142-
.and_then(|mut v| {
1143-
if v.is_empty() {
1144-
None
1145-
} else {
1146-
Some(v.remove(0))
1147-
}
1148-
})
1149-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
1120+
let esp_part = root_setup.device_info.find_first_colocated_esp()?;
11501121

11511122
(
11521123
root_setup.physical_root_path.clone(),
@@ -1163,16 +1134,7 @@ pub(crate) fn setup_composefs_uki_boot(
11631134

11641135
// Locate ESP partition device by walking up to the root disk(s)
11651136
let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?;
1166-
let esp_dev = root_dev
1167-
.find_colocated_esps()?
1168-
.and_then(|mut v| {
1169-
if v.is_empty() {
1170-
None
1171-
} else {
1172-
Some(v.remove(0))
1173-
}
1174-
})
1175-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
1137+
let esp_dev = root_dev.find_first_colocated_esp()?;
11761138

11771139
(
11781140
sysroot,

crates/lib/src/bootloader.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use fn_error_context::context;
1111
use bootc_mount as mount;
1212

1313
use crate::bootc_composefs::boot::{mount_esp, SecurebootKeys};
14-
use crate::{discoverable_partition_specification, utils};
14+
use crate::utils;
1515

1616
/// The name of the mountpoint for efi (as a subdirectory of /boot, or at the toplevel)
1717
pub(crate) const EFI_DIR: &str = "efi";
@@ -53,13 +53,17 @@ pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool)
5353

5454
let roots = bootc_blockdev::list_dev_by_dir(physical_root)?.find_all_roots()?;
5555
for dev in &roots {
56-
if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) {
56+
if let Some(esp_dev) = dev.find_partition_of_esp_optional()? {
5757
let esp_path = esp_dev.path();
5858
bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
5959
tracing::debug!("Mounted {esp_path} at /boot/efi");
6060
return Ok(());
6161
}
6262
}
63+
tracing::debug!(
64+
"No ESP partition found among {} root device(s)",
65+
roots.len()
66+
);
6367
Ok(())
6468
}
6569

@@ -228,10 +232,14 @@ pub(crate) fn install_systemd_boot(
228232
autoenroll: Option<SecurebootKeys>,
229233
) -> Result<()> {
230234
let roots = device.find_all_roots()?;
231-
let esp_part = roots
232-
.iter()
233-
.find_map(|root| root.find_partition_of_type(discoverable_partition_specification::ESP))
234-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
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"))?;
235243

236244
let esp_mount = mount_esp(&esp_part.path()).context("Mounting ESP")?;
237245
let esp_path = Utf8Path::from_path(esp_mount.dir.path())

crates/lib/src/store/mod.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -200,16 +200,7 @@ impl BootedStorage {
200200

201201
// Locate ESP by walking up to the root disk(s)
202202
let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?;
203-
let esp_dev = root_dev
204-
.find_colocated_esps()?
205-
.and_then(|mut v| {
206-
if v.is_empty() {
207-
None
208-
} else {
209-
Some(v.remove(0))
210-
}
211-
})
212-
.ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?;
203+
let esp_dev = root_dev.find_first_colocated_esp()?;
213204
let esp_mount = mount_esp(&esp_dev.path())?;
214205

215206
let boot_dir = match get_bootloader()? {

0 commit comments

Comments
 (0)