Skip to content

Commit a350c97

Browse files
committed
feat: add systemd-boot support
Signed-off-by: Robert Sturla <[email protected]>
1 parent 42f5536 commit a350c97

File tree

8 files changed

+161
-16
lines changed

8 files changed

+161
-16
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ include = ["src", "LICENSE", "Makefile", "systemd"]
1515
platforms = ["*-unknown-linux-gnu"]
1616
tier = "2"
1717

18+
[features]
19+
default = []
20+
systemd-boot = []
21+
1822
[[bin]]
1923
name = "bootupd"
2024
path = "src/main.rs"

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ that's for tools like `grubby` and `ostree`.
3434
bootupd supports updating GRUB and shim for UEFI firmware on x86_64, aarch64,
3535
and riscv64, and GRUB for BIOS firmware on x86_64 and ppc64le.
3636

37+
bootupd only supports installing the systemd-boot shim currently, though may be
38+
updated to also handle updates in future. systemd-boot support just proxies
39+
to the relevant `bootctl` commands.
40+
3741
The project is used in Bootable Containers and ostree/rpm-ostree based systems:
3842
- [`bootc install`](https://github.com/containers/bootc/#using-bootc-install)
3943
- [Fedora CoreOS](https://docs.fedoraproject.org/en-US/fedora-coreos/bootloader-updates/)
@@ -78,7 +82,7 @@ care of GRUB and shim. See discussion in [this issue](https://github.com/coreos
7882
### systemd bootctl
7983

8084
[systemd bootctl](https://man7.org/linux/man-pages/man1/bootctl.1.html) can update itself;
81-
this project would probably just proxy that if we detect systemd-boot is in use.
85+
this project just proxies that if we detect systemd-boot is present.
8286

8387
## Other goals
8488

@@ -151,4 +155,3 @@ bootupd now uses `systemd-run` instead to guarantee the following:
151155
- If we want a non-CLI API (whether that's DBus or Cap'n Proto or varlink or
152156
something else), we will create an independent daemon with a stable API for
153157
this specific need.
154-

src/bios.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl Component for Bios {
112112
dest_root: &str,
113113
device: &str,
114114
_update_firmware: bool,
115+
_bootloader: &crate::bootupd::Bootloader,
115116
) -> Result<InstalledContent> {
116117
let Some(meta) = get_component_update(src_root, self)? else {
117118
anyhow::bail!("No update metadata for component {} found", self.name());

src/bootupd.rs

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ use std::fs::{self, File};
2525
use std::io::{BufRead, BufReader, BufWriter, Write};
2626
use std::path::{Path, PathBuf};
2727

28+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29+
pub(crate) enum Bootloader {
30+
Grub2,
31+
#[cfg(feature = "systemd-boot")]
32+
SystemdBoot,
33+
}
34+
2835
pub(crate) enum ConfigMode {
2936
None,
3037
Static,
@@ -81,6 +88,25 @@ pub(crate) fn install(
8188
anyhow::bail!("No components specified");
8289
}
8390

91+
// If Grub2 binaries are present, use Grub2
92+
// Else if systemd-boot EFI binaries are present, use SystemdBoot
93+
// Else fall back to Grub2
94+
let bootloader = if has_grub(&source_root) {
95+
Bootloader::Grub2
96+
} else if has_systemd_boot(&source_root) {
97+
#[cfg(feature = "systemd-boot")]
98+
{
99+
Bootloader::SystemdBoot
100+
}
101+
#[cfg(not(feature = "systemd-boot"))]
102+
{
103+
anyhow::bail!("systemd-boot support is not enabled in this build");
104+
}
105+
} else {
106+
log::warn!("No bootloader binaries found, defaulting to Grub2");
107+
Bootloader::Grub2
108+
};
109+
84110
let mut state = SavedState::default();
85111
let mut installed_efi_vendor = None;
86112
for &component in target_components.iter() {
@@ -93,20 +119,51 @@ pub(crate) fn install(
93119
continue;
94120
}
95121

122+
#[cfg(feature = "systemd-boot")]
123+
if bootloader == Bootloader::SystemdBoot && component.name() == "BIOS" {
124+
log::warn!("Skip installing BIOS component when using systemd-boot");
125+
continue;
126+
}
127+
128+
let update_firmware = match bootloader {
129+
Bootloader::Grub2 => update_firmware,
130+
#[cfg(feature = "systemd-boot")]
131+
Bootloader::SystemdBoot => false,
132+
};
133+
96134
let meta = component
97-
.install(&source_root, dest_root, device, update_firmware)
135+
.install(
136+
&source_root,
137+
dest_root,
138+
device,
139+
update_firmware,
140+
&bootloader,
141+
)
98142
.with_context(|| format!("installing component {}", component.name()))?;
99143
log::info!("Installed {} {}", component.name(), meta.meta.version);
100144
state.installed.insert(component.name().into(), meta);
101-
// Yes this is a hack...the Component thing just turns out to be too generic.
102-
if let Some(vendor) = component.get_efi_vendor(&source_root)? {
103-
assert!(installed_efi_vendor.is_none());
104-
installed_efi_vendor = Some(vendor);
145+
146+
match bootloader {
147+
Bootloader::Grub2 => {
148+
// Yes this is a hack...the Component thing just turns out to be too generic.
149+
if let Some(vendor) = component.get_efi_vendor(&source_root)? {
150+
assert!(installed_efi_vendor.is_none());
151+
installed_efi_vendor = Some(vendor);
152+
}
153+
}
154+
#[cfg(feature = "systemd-boot")]
155+
_ => {}
105156
}
106157
}
107158
let sysroot = &openat::Dir::open(dest_root)?;
108159

109-
match configs.enabled_with_uuid() {
160+
// If systemd-boot is enabled, do not run grubconfigs::install
161+
let configs_with_uuid = match bootloader {
162+
Bootloader::Grub2 => configs.enabled_with_uuid(),
163+
#[cfg(feature = "systemd-boot")]
164+
_ => None,
165+
};
166+
match configs_with_uuid {
110167
Some(uuid) => {
111168
let meta = get_static_config_meta()?;
112169
state.static_configs = Some(meta);
@@ -715,6 +772,16 @@ fn strip_grub_config_file(
715772
Ok(())
716773
}
717774

775+
/// Determine whether the necessary bootloader files are present for GRUB.
776+
fn has_grub(source_root: &openat::Dir) -> bool {
777+
source_root.open_file(bios::GRUB_BIN).is_ok()
778+
}
779+
780+
/// Determine whether the necessary bootloader files are present for systemd-boot.
781+
fn has_systemd_boot(source_root: &openat::Dir) -> bool {
782+
source_root.open_file(efi::SYSTEMD_BOOT_EFI).is_ok()
783+
}
784+
718785
#[cfg(test)]
719786
mod tests {
720787
use super::*;

src/component.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ pub(crate) trait Component {
5555
dest_root: &str,
5656
device: &str,
5757
update_firmware: bool,
58+
bootloader: &crate::bootupd::Bootloader,
5859
) -> Result<InstalledContent>;
5960

6061
/// Implementation of `bootupd generate-update-metadata` for a given component.

src/efi.rs

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use rustix::fd::BorrowedFd;
2222
use walkdir::WalkDir;
2323
use widestring::U16CString;
2424

25-
use crate::bootupd::RootContext;
25+
use crate::bootupd::{Bootloader, RootContext};
2626
use crate::freezethaw::fsfreeze_thaw_cycle;
2727
use crate::model::*;
2828
use crate::ostreeutil;
@@ -50,6 +50,10 @@ pub(crate) const SHIM: &str = "shimriscv64.efi";
5050
/// Systemd boot loader info EFI variable names
5151
const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
5252
const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
53+
#[cfg(target_arch = "aarch64")]
54+
pub(crate) const SYSTEMD_BOOT_EFI: &str = "usr/lib/systemd/boot/efi/systemd-bootaarch64.efi";
55+
#[cfg(target_arch = "x86_64")]
56+
pub(crate) const SYSTEMD_BOOT_EFI: &str = "usr/lib/systemd/boot/efi/systemd-bootx64.efi";
5357

5458
/// Return `true` if the system is booted via EFI
5559
pub(crate) fn is_efi_booted() -> Result<bool> {
@@ -342,14 +346,8 @@ impl Component for Efi {
342346
dest_root: &str,
343347
device: &str,
344348
update_firmware: bool,
349+
bootloader: &Bootloader,
345350
) -> Result<InstalledContent> {
346-
let Some(meta) = get_component_update(src_root, self)? else {
347-
anyhow::bail!("No update metadata for component {} found", self.name());
348-
};
349-
log::debug!("Found metadata {}", meta.version);
350-
let srcdir_name = component_updatedirname(self);
351-
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
352-
353351
// Let's attempt to use an already mounted ESP at the target
354352
// dest_root if one is already mounted there in a known ESP location.
355353
let destpath = if let Some(destdir) = self.get_mounted_esp(Path::new(dest_root))? {
@@ -365,6 +363,31 @@ impl Component for Efi {
365363
self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device))?
366364
};
367365

366+
match bootloader {
367+
#[cfg(feature = "systemd-boot")]
368+
Bootloader::SystemdBoot => {
369+
log::info!("Installing systemd-boot via bootctl");
370+
let esp_dir = openat::Dir::open(&destpath)?;
371+
crate::systemdbootconfigs::install(&esp_dir)?;
372+
return Ok(InstalledContent {
373+
meta: ContentMetadata {
374+
timestamp: Utc::now(),
375+
version: "systemd-boot".to_string(),
376+
},
377+
filetree: None,
378+
adopted_from: None,
379+
});
380+
}
381+
_ => {}
382+
}
383+
384+
let Some(meta) = get_component_update(src_root, self)? else {
385+
anyhow::bail!("No update metadata for component {} found", self.name());
386+
};
387+
log::debug!("Found metadata {}", meta.version);
388+
let srcdir_name = component_updatedirname(self);
389+
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
390+
368391
let destd = &openat::Dir::open(&destpath)
369392
.with_context(|| format!("opening dest dir {}", destpath.display()))?;
370393
validate_esp_fstype(destd)?;

src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ mod packagesystem;
4848
mod sha512string;
4949
mod util;
5050

51+
#[cfg(feature = "systemd-boot")]
52+
mod systemdbootconfigs;
53+
5154
use clap::crate_name;
5255

5356
/// Binary entrypoint, for both daemon and client logic.

src/systemdbootconfigs.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
use std::path::Path;
2+
3+
use anyhow::{Context, Result};
4+
use fn_error_context::context;
5+
6+
/// Install files required for systemd-boot
7+
/// This mostly proxies the bootctl install command
8+
#[context("Installing systemd-boot")]
9+
pub(crate) fn install(esp_path: &openat::Dir) -> Result<()> {
10+
let esp_path = esp_path.recover_path().context("ESP path is not valid")?;
11+
let status = std::process::Command::new("bootctl")
12+
.args([
13+
"install",
14+
"--esp-path",
15+
esp_path.to_str().context("ESP path is not valid UTF-8")?,
16+
])
17+
.status()
18+
.context("Failed to execute bootctl")?;
19+
20+
if !status.success() {
21+
anyhow::bail!(
22+
"bootctl install failed with status code {}",
23+
status.code().unwrap_or(-1)
24+
);
25+
}
26+
27+
// If loader.conf is present in the bootupd configuration, replace the original config with it
28+
let src_loader_conf = "/usr/lib/bootupd/systemd-boot/loader.conf";
29+
let dst_loader_conf = Path::new(&esp_path).join("loader/loader.conf");
30+
if Path::new(src_loader_conf).exists() {
31+
std::fs::copy(src_loader_conf, &dst_loader_conf)
32+
.context("Failed to copy loader.conf to ESP")?;
33+
log::info!(
34+
"Copied {} to {}",
35+
src_loader_conf,
36+
dst_loader_conf.display()
37+
);
38+
} else {
39+
log::warn!("{} does not exist, skipping copy", src_loader_conf);
40+
}
41+
42+
Ok(())
43+
}

0 commit comments

Comments
 (0)