Skip to content

Commit 1c89242

Browse files
committed
feat: enable systemd-boot support
1 parent c7e314e commit 1c89242

File tree

7 files changed

+234
-73
lines changed

7 files changed

+234
-73
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
fastbuild*.qcow2
33
_kola_temp
44
.cosa
5+
*.img
6+
*.tar
7+
bootupctl

src/bootupd.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ pub(crate) fn install(
9696
let meta = component
9797
.install(&source_root, dest_root, device, update_firmware)
9898
.with_context(|| format!("installing component {}", component.name()))?;
99-
log::info!("Installed {} {}", component.name(), meta.meta.version);
99+
log::warn!("Installed {} {}", component.name(), meta.meta.version);
100100
state.installed.insert(component.name().into(), meta);
101101
// Yes this is a hack...the Component thing just turns out to be too generic.
102102
if let Some(vendor) = component.get_efi_vendor(&source_root)? {
@@ -303,7 +303,7 @@ pub(crate) fn adopt_and_update(
303303
return Ok(Some(update));
304304
} else {
305305
// Nothing adopted, skip
306-
log::info!("Component '{}' skipped adoption", component.name());
306+
log::warn!("Component '{}' skipped adoption", component.name());
307307
return Ok(None);
308308
}
309309
}

src/efi.rs

Lines changed: 110 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@ use cap_std::fs::Dir;
1616
use cap_std_ext::cap_std;
1717
use chrono::prelude::*;
1818
use fn_error_context::context;
19+
use log::{info, warn};
1920
use openat_ext::OpenatDirExt;
2021
use os_release::OsRelease;
22+
use std::io::Write;
2123
use rustix::fd::BorrowedFd;
2224
use walkdir::WalkDir;
25+
use widestring::U16CString;
2326

2427
use crate::bootupd::RootContext;
2528
use crate::freezethaw::fsfreeze_thaw_cycle;
@@ -46,10 +49,6 @@ pub(crate) const SHIM: &str = "shimx64.efi";
4649
#[cfg(target_arch = "riscv64")]
4750
pub(crate) const SHIM: &str = "shimriscv64.efi";
4851

49-
// /// Systemd boot loader info EFI variable names
50-
// const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
51-
// const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
52-
5352
/// Return `true` if the system is booted via EFI
5453
pub(crate) fn is_efi_booted() -> Result<bool> {
5554
Path::new("/sys/firmware/efi")
@@ -152,6 +151,35 @@ impl Efi {
152151
clear_efi_target(&product_name)?;
153152
create_efi_boot_entry(device, espdir, vendordir, &product_name)
154153
}
154+
155+
/// Find a kernel and optional initramfs/uki file in the update directory for systemd-boot
156+
fn find_kernel_and_initrd(dir: &openat::Dir) -> Result<(String, Option<String>)> {
157+
let mut kernel: Option<String> = None;
158+
let mut initrd: Option<String> = None;
159+
log::warn!("Searching for kernel and initrd in update dir: {:?}", dir.recover_path());
160+
for entry in dir.list_dir(".")? {
161+
log::warn!("Found entry: {:?}", entry);
162+
let entry = entry?;
163+
let fname = entry.file_name().to_string_lossy();
164+
if fname.starts_with("vmlinuz") || fname.ends_with(".efi") || fname.ends_with(".uki") {
165+
log::warn!("Found kernel/UKI file: {}", fname);
166+
if kernel.is_some() {
167+
log::warn!("Multiple kernel/UKI files found in update dir");
168+
bail!("Multiple kernel/UKI files found in update dir");
169+
}
170+
kernel = Some(fname.to_string());
171+
} else if fname.starts_with("initrd") || fname.ends_with(".img") || fname.ends_with(".cpio.gz") {
172+
log::warn!("Found initramfs file: {}", fname);
173+
if initrd.is_some() {
174+
log::warn!("Multiple initramfs files found in update dir");
175+
bail!("Multiple initramfs files found in update dir");
176+
}
177+
initrd = Some(fname.to_string());
178+
}
179+
}
180+
let kernel = kernel.ok_or_else(|| anyhow::anyhow!("No kernel or UKI file found in update dir"))?;
181+
Ok((kernel, initrd))
182+
}
155183
}
156184

157185
#[context("Get product name")]
@@ -168,70 +196,6 @@ fn get_product_name(sysroot: &Dir) -> Result<String> {
168196
Ok(release.name)
169197
}
170198

171-
// /// Convert a nul-terminated UTF-16 byte array to a String.
172-
// fn string_from_utf16_bytes(slice: &[u8]) -> String {
173-
// // For some reason, systemd appends 3 nul bytes after the string.
174-
// // Drop the last byte if there's an odd number.
175-
// let size = slice.len() / 2;
176-
// let v: Vec<u16> = (0..size)
177-
// .map(|i| u16::from_ne_bytes([slice[2 * i], slice[2 * i + 1]]))
178-
// .collect();
179-
// U16CString::from_vec(v).unwrap().to_string_lossy()
180-
// }
181-
182-
// /// Read a nul-terminated UTF-16 string from an EFI variable.
183-
// fn read_efi_var_utf16_string(name: &str) -> Option<String> {
184-
// let efivars = Path::new("/sys/firmware/efi/efivars");
185-
// if !efivars.exists() {
186-
// log::trace!("No efivars mount at {:?}", efivars);
187-
// return None;
188-
// }
189-
// let path = efivars.join(name);
190-
// if !path.exists() {
191-
// log::trace!("No EFI variable {name}");
192-
// return None;
193-
// }
194-
// match std::fs::read(&path) {
195-
// Ok(buf) => {
196-
// // Skip the first 4 bytes, those are the EFI variable attributes.
197-
// if buf.len() < 4 {
198-
// log::warn!("Read less than 4 bytes from {:?}", path);
199-
// return None;
200-
// }
201-
// Some(string_from_utf16_bytes(&buf[4..]))
202-
// }
203-
// Err(reason) => {
204-
// log::warn!("Failed reading {:?}: {reason}", path);
205-
// None
206-
// }
207-
// }
208-
// }
209-
210-
// /// Read the LoaderInfo EFI variable if it exists.
211-
// fn get_loader_info() -> Option<String> {
212-
// read_efi_var_utf16_string(LOADER_INFO_VAR_STR)
213-
// }
214-
215-
// /// Read the StubInfo EFI variable if it exists.
216-
// fn get_stub_info() -> Option<String> {
217-
// read_efi_var_utf16_string(STUB_INFO_VAR_STR)
218-
// }
219-
220-
// /// Whether to skip adoption if a systemd bootloader is found.
221-
// fn skip_systemd_bootloaders() -> bool {
222-
// if let Some(loader_info) = get_loader_info() {
223-
// if loader_info.starts_with("systemd") {
224-
// log::trace!("Skipping adoption for {:?}", loader_info);
225-
// return true;
226-
// }
227-
// }
228-
// if let Some(stub_info) = get_stub_info() {
229-
// log::trace!("Skipping adoption for {:?}", stub_info);
230-
// return true;
231-
// }
232-
// false
233-
// }
234-
235199
impl Component for Efi {
236200
fn name(&self) -> &'static str {
237201
"EFI"
@@ -342,10 +306,11 @@ impl Component for Efi {
342306
device: &str,
343307
update_firmware: bool,
344308
) -> Result<InstalledContent> {
309+
log::warn!("Installing component: {}", self.name());
345310
let Some(meta) = get_component_update(src_root, self)? else {
346311
anyhow::bail!("No update metadata for component {} found", self.name());
347312
};
348-
log::debug!("Found metadata {}", meta.version);
313+
log::warn!("Found metadata {}", meta.version);
349314
let srcdir_name = component_updatedirname(self);
350315
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
351316

@@ -364,23 +329,94 @@ impl Component for Efi {
364329
self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device))?
365330
};
366331

332+
let destpath_clone = destpath.clone();
333+
367334
let destd = &openat::Dir::open(&destpath)
368335
.with_context(|| format!("opening dest dir {}", destpath.display()))?;
369336
validate_esp_fstype(destd)?;
370337

338+
// let using_systemd_boot = skip_systemd_bootloaders();
339+
let using_systemd_boot = true;
340+
371341
// TODO - add some sort of API that allows directly setting the working
372342
// directory to a file descriptor.
373343
std::process::Command::new("cp")
374344
.args(["-rp", "--reflink=auto"])
375345
.arg(&srcdir_name)
376-
.arg(destpath)
346+
.arg(&destpath)
377347
.current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd()))
378348
.run()?;
379-
if update_firmware {
349+
350+
// Configure systemd-boot loader config/entry
351+
if using_systemd_boot {
352+
let loader_dir = destpath.join("loader");
353+
if !loader_dir.exists() {
354+
log::warn!("Creating systemd-boot loader directory at {}", loader_dir.display());
355+
std::fs::create_dir_all(&loader_dir)
356+
.with_context(|| format!("creating systemd-boot loader directory {}", loader_dir.display()))?;
357+
}
358+
let entries_dir = loader_dir.join("entries");
359+
log::warn!("Using systemd-boot, creating entries dir at {}", entries_dir.display());
360+
std::fs::create_dir_all(&entries_dir)
361+
.with_context(|| format!("creating entries dir {}", entries_dir.display()))?;
362+
log::warn!("Installing systemd-boot entry in {}/bootupd.conf", entries_dir.display());
363+
// Write loader.conf if it doesn't exist
364+
let loader_conf_path = loader_dir.join("loader.conf");
365+
if !loader_conf_path.exists() {
366+
let mut loader_conf = std::fs::File::create(&loader_conf_path)
367+
.with_context(|| format!("creating loader.conf at {}", loader_conf_path.display()))?;
368+
writeln!(loader_conf, "default auto")?;
369+
writeln!(loader_conf, "timeout 20")?;
370+
writeln!(loader_conf, "editor no")?;
371+
}
372+
log::warn!("Installed: {}/bootupd.conf", entries_dir.display());
373+
374+
// let sysroot = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
375+
// let product_name = get_product_name(&sysroot).unwrap_or("Unknown Product".to_string());
376+
// log::warn!("Get product name: '{product_name}'");
377+
378+
// Find the kernel/UKI and optional initramfs in the update dir
379+
log::warn!(
380+
"Searching for kernel and initrd in update dir: {:?}",
381+
crate::model::BOOTUPD_UPDATES_DIR
382+
);
383+
let update_dir = src_root.sub_dir(&srcdir_name)
384+
.context("opening update dir")?;
385+
log::debug!("Searching for kernel and initrd in update dir: {:?}", update_dir.recover_path());
386+
let (kernel_file, initrd_file) = Self::find_kernel_and_initrd(&update_dir)?;
387+
388+
log::warn!(
389+
"Installing systemd-boot entry for kernel: {}, initrd: {:?}",
390+
kernel_file, initrd_file
391+
);
392+
crate::systemd_boot_configs::install(
393+
&destd,
394+
true,
395+
"Unknown Product",
396+
&kernel_file,
397+
initrd_file.as_deref(),
398+
).context("installing systemd-boot entry")?;
399+
}
400+
401+
// If using systemd-boot, run `bootctl install` to set up the bootloader.
402+
if using_systemd_boot {
403+
log::warn!("Using systemd-boot, running bootctl install");
404+
let status = std::process::Command::new("bootctl")
405+
.args(["install", "--esp-path", destpath_clone.to_str().unwrap()])
406+
.status()
407+
.context("running bootctl install")?;
408+
log::warn!("bootctl install status: {}", status);
409+
if !status.success() {
410+
bail!("bootctl install failed with status: {}", status);
411+
}
412+
}
413+
414+
if update_firmware && !using_systemd_boot {
380415
if let Some(vendordir) = self.get_efi_vendor(&src_root)? {
381416
self.update_firmware(device, destd, &vendordir)?
382417
}
383418
}
419+
384420
Ok(InstalledContent {
385421
meta,
386422
filetree: Some(ft),
@@ -714,9 +750,12 @@ fn get_efi_component_from_usr<'a>(
714750
.filter_map(|entry| {
715751
let entry = entry.ok()?;
716752
if !entry.file_type().is_dir() || entry.file_name() != "EFI" {
753+
info!("Skipping non-directory or non-EFI entry: {}", entry.path().display());
717754
return None;
718755
}
719756

757+
info!("Found EFI component at {}", entry.path().display());
758+
720759
let abs_path = entry.path();
721760
let rel_path = abs_path.strip_prefix(sysroot).ok()?;
722761
let utf8_rel_path = Utf8PathBuf::from_path_buf(rel_path.to_path_buf()).ok()?;

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ mod ostreeutil;
4747
mod packagesystem;
4848
mod sha512string;
4949
mod util;
50+
mod systemd_boot_configs;
5051

5152
use clap::crate_name;
5253

src/systemd_boot_configs.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use anyhow::{Context, Result};
2+
use fn_error_context::context;
3+
use openat_ext::OpenatDirExt;
4+
5+
const SYSTEMD_BOOT_ENTRIES_DIR: &str = "loader/entries";
6+
7+
pub(crate) struct SystemdBootEntry {
8+
title: String,
9+
linux: String,
10+
initrd: Option<String>,
11+
options: String,
12+
}
13+
14+
/// Install the systemd-boot entry files
15+
#[context("Installing systemd-boot entries")]
16+
pub(crate) fn install(
17+
target_root: &openat::Dir, // This should be mounted ESP root dir (not /boot inside ESP)
18+
write_uuid: bool,
19+
os_title: &str,
20+
linux_path: &str,
21+
initrd_path: Option<&str>,
22+
) -> Result<()> {
23+
// Ensure /loader/entries exist on ESP root
24+
if !target_root.exists(SYSTEMD_BOOT_ENTRIES_DIR)? {
25+
target_root.create_dir(SYSTEMD_BOOT_ENTRIES_DIR, 0o700)?;
26+
}
27+
28+
// Inspect root filesystem UUID - for root=UUID=... kernel parameter
29+
let rootfs_meta = crate::filesystem::inspect_filesystem(target_root, ".")?;
30+
let root_uuid = rootfs_meta
31+
.uuid
32+
.ok_or_else(|| anyhow::anyhow!("Failed to find UUID for root"))?;
33+
34+
// Compose entry config
35+
let config = SystemdBootEntry {
36+
title: os_title.to_string(),
37+
// For UKI, path is relative to ESP root, e.g. /EFI/ukify.efi
38+
linux: format!("/EFI/{}", linux_path.trim_start_matches('/')),
39+
initrd: initrd_path.map(|p| format!("{}/{}", SYSTEMD_BOOT_ENTRIES_DIR, p)),
40+
options: if write_uuid {
41+
format!("root=UUID={} quiet", root_uuid)
42+
} else {
43+
"quiet".to_string()
44+
},
45+
};
46+
47+
let mut entry_content = format!("title {}\n", config.title);
48+
49+
if linux_path.ends_with(".efi") {
50+
// UKI boot entry
51+
log::warn!("Installing UKI entry: {}", config.linux);
52+
entry_content.push_str(&format!("efi {}\n", config.linux));
53+
} else {
54+
// Kernel/initrd entry
55+
entry_content.push_str(&format!("linux {}\n", config.linux));
56+
if let Some(initrd) = &config.initrd {
57+
entry_content.push_str(&format!("initrd {}\n", initrd));
58+
}
59+
entry_content.push_str(&format!("options {}\n", config.options));
60+
}
61+
62+
// Write the entry file under /loader/entries/bootupd.conf on ESP root
63+
let entries_dir = target_root.sub_dir(SYSTEMD_BOOT_ENTRIES_DIR)?;
64+
entries_dir.write_file_contents(
65+
"bootupd.conf",
66+
0o644,
67+
entry_content.as_bytes(),
68+
).context("Writing systemd-boot loader entry")?;
69+
log::warn!("Installed: {}/bootupd.conf", SYSTEMD_BOOT_ENTRIES_DIR);
70+
71+
Ok(())
72+
}

tmp/Containerfile

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM quay.io/fedora/fedora-bootc:42
2+
3+
# RUN dnf remove -y grub2-efi grub2-common grub2-tools grub2-pc grub2-efi-x64 shim-x64 shim-x64-efi grub2-tools-minimal
4+
RUN dnf install -y \
5+
systemd-boot \
6+
systemd-ukify
7+
8+
RUN set -x && \
9+
KVER=$(rpm -q --qf '%{version}-%{release}.%{arch}\n' kernel | head -n 1) && \
10+
KVER_SIMPLE=$(rpm -q --qf '%{version}' kernel) && \
11+
mkdir -p /usr/lib/efi/sdboot/$KVER_SIMPLE/EFI && \
12+
ukify build \
13+
--linux=/usr/lib/modules/$KVER/vmlinuz \
14+
--initrd=/usr/lib/modules/$KVER/initramfs.img \
15+
--output /usr/lib/efi/sdboot/$KVER_SIMPLE/EFI/ukify.efi
16+
17+
# COPY bootc /usr/bin/bootc
18+
COPY bootupctl /usr/bin/bootupctl
19+
20+
RUN bootupctl backend generate-update-metadata -vvv
21+
# RUN rm /usr/bin/bootupctl

0 commit comments

Comments
 (0)