Skip to content

Commit 641ee16

Browse files
committed
package mode: use write-alongside + atomic rename and check ESP space
Apply code review
1 parent 40f9692 commit 641ee16

File tree

3 files changed

+69
-15
lines changed

3 files changed

+69
-15
lines changed

src/efi.rs

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -248,35 +248,67 @@ impl Efi {
248248
clear_efi_target(&product_name)?;
249249
create_efi_boot_entry(device, esp_part_num.trim(), &loader, &product_name)
250250
}
251+
/// Copy EFI components to ESP using the same "write alongside + atomic rename" pattern
252+
/// as bootable container updates, so the system stays bootable if any step fails.
251253
fn copy_efi_components_to_esp(
252254
&self,
253255
sysroot_dir: &openat::Dir,
254256
esp_dir: &openat::Dir,
255-
esp_path: &Path,
257+
_esp_path: &Path,
256258
efi_components: &[EFIComponent],
257259
) -> Result<()> {
258-
let dest_str = esp_path
260+
// Build a merged source tree in a temp dir (same layout as desired ESP/EFI)
261+
let temp_dir = tempfile::tempdir().context("Creating temp dir for EFI merge")?;
262+
let temp_efi_path = temp_dir.path().join("EFI");
263+
std::fs::create_dir_all(&temp_efi_path)
264+
.with_context(|| format!("Creating {}", temp_efi_path.display()))?;
265+
let temp_efi_str = temp_efi_path
259266
.to_str()
260-
.with_context(|| format!("Invalid UTF-8: {}", esp_path.display()))?;
267+
.context("Temp EFI path is not valid UTF-8")?;
261268

262269
for efi_comp in efi_components {
263270
log::info!(
264-
"Copying EFI component {} version {} to ESP at {}",
271+
"Merging EFI component {} version {} into update tree",
265272
efi_comp.name,
266-
efi_comp.version,
267-
esp_path.display()
273+
efi_comp.version
268274
);
275+
// Copy contents of component's EFI dir (e.g. fedora/) into temp_efi_path so merged
276+
// layout is EFI/fedora/..., not EFI/EFI/fedora/...
277+
let src_efi_contents = format!("{}/.", efi_comp.path);
278+
filetree::copy_dir_with_args(sysroot_dir, src_efi_contents.as_str(), temp_efi_str, OPTIONS)
279+
.with_context(|| format!("Copying {} to merge dir", efi_comp.path))?;
280+
}
269281

270-
filetree::copy_dir_with_args(sysroot_dir, efi_comp.path.as_str(), dest_str, OPTIONS)
271-
.with_context(|| {
272-
format!(
273-
"Failed to copy {} from {} to {}",
274-
efi_comp.name, efi_comp.path, dest_str
275-
)
276-
})?;
282+
// Ensure ESP/EFI exists (e.g. first install)
283+
esp_dir.ensure_dir_all(std::path::Path::new("EFI"), 0o755)?;
284+
let esp_efi_dir = esp_dir.sub_dir("EFI").context("Opening ESP EFI dir")?;
285+
286+
let source_dir =
287+
openat::Dir::open(&temp_efi_path).context("Opening merged EFI source dir")?;
288+
let source_filetree =
289+
filetree::FileTree::new_from_dir(&source_dir).context("Building source filetree")?;
290+
let current_filetree =
291+
filetree::FileTree::new_from_dir(&esp_efi_dir).context("Building current filetree")?;
292+
let diff = current_filetree
293+
.diff(&source_filetree)
294+
.context("Computing EFI diff")?;
295+
296+
// Check available space before writing to prevent partial updates when the partition is full
297+
let required_bytes = current_filetree.total_size() + source_filetree.total_size();
298+
let available_bytes = util::available_space_bytes(&esp_efi_dir)?;
299+
if available_bytes < required_bytes {
300+
anyhow::bail!(
301+
"ESP has insufficient free space for update: need {} MiB, have {} MiB",
302+
required_bytes / (1024 * 1024),
303+
available_bytes / (1024 * 1024)
304+
);
277305
}
278306

279-
// Sync the whole ESP filesystem
307+
// Same logic as bootable container: write to .btmp.* then atomic rename
308+
filetree::apply_diff(&source_dir, &esp_efi_dir, &diff, None)
309+
.context("Applying EFI update (write alongside + atomic rename)")?;
310+
311+
// Sync the whole ESP filesystem
280312
fsfreeze_thaw_cycle(esp_dir.open_file(".")?)?;
281313

282314
Ok(())

src/filetree.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ impl FileTree {
202202
Ok(Self { children })
203203
}
204204

205+
/// Total size in bytes of all files in the tree (for space checks).
206+
#[cfg(any(
207+
target_arch = "x86_64",
208+
target_arch = "aarch64",
209+
target_arch = "riscv64"
210+
))]
211+
pub(crate) fn total_size(&self) -> u64 {
212+
self.children.values().map(|m| m.size).sum()
213+
}
214+
205215
/// Determine the changes *from* self to the updated tree
206216
#[cfg(any(
207217
target_arch = "x86_64",

src/util.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashSet;
2+
use std::os::unix::io::AsRawFd;
23
use std::path::Path;
34
use std::process::Command;
45

56
use anyhow::{bail, Context, Result};
67
use openat_ext::OpenatDirExt;
8+
use rustix::fd::BorrowedFd;
79

810
/// Parse an environment variable as UTF-8
911
#[allow(dead_code)]
@@ -51,9 +53,18 @@ pub(crate) fn filenames(dir: &openat::Dir) -> Result<HashSet<String>> {
5153
Ok(ret)
5254
}
5355

56+
/// Return the available space in bytes on the filesystem containing the given directory.
57+
/// Uses f_bavail * f_frsize from fstatvfs to avoid partial updates when the partition is full.
58+
pub(crate) fn available_space_bytes(dir: &openat::Dir) -> Result<u64> {
59+
let fd = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) };
60+
let st = rustix::fs::fstatvfs(fd)?;
61+
Ok((st.f_bavail as u64) * (st.f_frsize as u64))
62+
}
63+
5464
pub(crate) fn ensure_writable_mount<P: AsRef<Path>>(p: P) -> Result<()> {
5565
let p = p.as_ref();
56-
let stat = rustix::fs::statvfs(p)?;
66+
let stat = rustix::fs::statvfs(p)
67+
.map_err(|e| std::io::Error::from_raw_os_error(e.raw_os_error()))?;
5768
if !stat.f_flag.contains(rustix::fs::StatVfsMountFlags::RDONLY) {
5869
return Ok(());
5970
}
@@ -121,3 +132,4 @@ impl Drop for SignalTerminationGuard {
121132
signal_hook_registry::unregister(self.0);
122133
}
123134
}
135+

0 commit comments

Comments
 (0)