Skip to content

Commit f3da9f1

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

File tree

9 files changed

+205
-17
lines changed

9 files changed

+205
-17
lines changed

Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ authors = ["Colin Walters <[email protected]>"]
77
edition = "2021"
88
rust-version = "1.84.1"
99
homepage = "https://github.com/coreos/bootupd"
10+
build = "build.rs"
1011

1112
include = ["src", "LICENSE", "Makefile", "systemd"]
1213

@@ -15,6 +16,10 @@ include = ["src", "LICENSE", "Makefile", "systemd"]
1516
platforms = ["*-unknown-linux-gnu"]
1617
tier = "2"
1718

19+
[features]
20+
default = []
21+
systemd-boot = []
22+
1823
[[bin]]
1924
name = "bootupd"
2025
path = "src/main.rs"
@@ -27,7 +32,14 @@ bootc-internal-utils = "0.0.0"
2732
cap-std-ext = "4.0.6"
2833
camino = "1.1.11"
2934
chrono = { version = "0.4.41", features = ["serde"] }
30-
clap = { version = "4.5", default-features = false, features = ["cargo", "derive", "std", "help", "usage", "suggestions"] }
35+
clap = { version = "4.5", default-features = false, features = [
36+
"cargo",
37+
"derive",
38+
"std",
39+
"help",
40+
"usage",
41+
"suggestions",
42+
] }
3143
env_logger = "0.11"
3244
fail = { version = "0.5", features = ["failpoints"] }
3345
fn-error-context = "0.2.1"

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-

build.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
fn main() {
2+
if std::env::var("CARGO_FEATURE_SYSTEMD_BOOT").is_ok() {
3+
if let Ok(arch) = std::env::var("CARGO_CFG_TARGET_ARCH") {
4+
if arch.starts_with("riscv") {
5+
panic!("The systemd-boot feature is not supported on RISC-V.");
6+
}
7+
}
8+
}
9+
}

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: 81 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,8 @@ pub(crate) fn install(
8188
anyhow::bail!("No components specified");
8289
}
8390

91+
let bootloader = select_bootloader(&source_root);
92+
8493
let mut state = SavedState::default();
8594
let mut installed_efi_vendor = None;
8695
for &component in target_components.iter() {
@@ -93,20 +102,51 @@ pub(crate) fn install(
93102
continue;
94103
}
95104

105+
#[cfg(feature = "systemd-boot")]
106+
if bootloader == Bootloader::SystemdBoot && component.name() == "BIOS" {
107+
log::warn!("Skip installing BIOS component when using systemd-boot");
108+
continue;
109+
}
110+
111+
let update_firmware = match bootloader {
112+
Bootloader::Grub2 => update_firmware,
113+
#[cfg(feature = "systemd-boot")]
114+
Bootloader::SystemdBoot => false,
115+
};
116+
96117
let meta = component
97-
.install(&source_root, dest_root, device, update_firmware)
118+
.install(
119+
&source_root,
120+
dest_root,
121+
device,
122+
update_firmware,
123+
&bootloader,
124+
)
98125
.with_context(|| format!("installing component {}", component.name()))?;
99126
log::info!("Installed {} {}", component.name(), meta.meta.version);
100127
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);
128+
129+
match bootloader {
130+
Bootloader::Grub2 => {
131+
// Yes this is a hack...the Component thing just turns out to be too generic.
132+
if let Some(vendor) = component.get_efi_vendor(&source_root)? {
133+
assert!(installed_efi_vendor.is_none());
134+
installed_efi_vendor = Some(vendor);
135+
}
136+
}
137+
#[cfg(feature = "systemd-boot")]
138+
_ => {}
105139
}
106140
}
107141
let sysroot = &openat::Dir::open(dest_root)?;
108142

109-
match configs.enabled_with_uuid() {
143+
// If systemd-boot is enabled, do not run grubconfigs::install
144+
let configs_with_uuid = match bootloader {
145+
Bootloader::Grub2 => configs.enabled_with_uuid(),
146+
#[cfg(feature = "systemd-boot")]
147+
_ => None,
148+
};
149+
match configs_with_uuid {
110150
Some(uuid) => {
111151
let meta = get_static_config_meta()?;
112152
state.static_configs = Some(meta);
@@ -715,6 +755,41 @@ fn strip_grub_config_file(
715755
Ok(())
716756
}
717757

758+
/// Determine whether the necessary bootloader files are present for GRUB.
759+
fn has_grub(source_root: &openat::Dir) -> bool {
760+
source_root.open_file(bios::GRUB_BIN).is_ok()
761+
}
762+
763+
/// Determine whether the necessary bootloader files are present for systemd-boot.
764+
#[cfg(feature = "systemd-boot")]
765+
fn has_systemd_boot(source_root: &openat::Dir) -> bool {
766+
source_root.open_file(efi::SYSTEMD_BOOT_EFI).is_ok()
767+
}
768+
769+
/// Select the bootloader based on available binaries and feature flags.
770+
fn select_bootloader(source_root: &openat::Dir) -> Bootloader {
771+
#[cfg(not(feature = "systemd-boot"))]
772+
{
773+
if has_grub(source_root) {
774+
Bootloader::Grub2
775+
} else {
776+
log::warn!("No bootloader binaries found, defaulting to Grub2");
777+
Bootloader::Grub2
778+
}
779+
}
780+
#[cfg(feature = "systemd-boot")]
781+
{
782+
if has_grub(source_root) {
783+
Bootloader::Grub2
784+
} else if has_systemd_boot(source_root) {
785+
Bootloader::SystemdBoot
786+
} else {
787+
log::warn!("No bootloader binaries found, defaulting to Grub2");
788+
Bootloader::Grub2
789+
}
790+
}
791+
}
792+
718793
#[cfg(test)]
719794
mod tests {
720795
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(all(feature = "systemd-boot", target_arch = "aarch64"))]
54+
pub(crate) const SYSTEMD_BOOT_EFI: &str = "usr/lib/systemd/boot/efi/systemd-bootaarch64.efi";
55+
#[cfg(all(feature = "systemd-boot", 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(src_root, &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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use std::path::Path;
2+
3+
use anyhow::{Context, Result};
4+
use fn_error_context::context;
5+
6+
const CONFIG_DIR: &str = "usr/lib/bootupd/systemd-boot";
7+
8+
/// Install files required for systemd-boot
9+
/// This mostly proxies the bootctl install command
10+
#[context("Installing systemd-boot")]
11+
pub(crate) fn install(src_root: &openat::Dir, esp_path: &openat::Dir) -> Result<()> {
12+
let esp_path_buf = esp_path.recover_path().context("ESP path is not valid")?;
13+
let esp_path_str = esp_path_buf
14+
.to_str()
15+
.context("ESP path is not valid UTF-8")?;
16+
let status = std::process::Command::new("bootctl")
17+
.args(["install", "--esp-path", esp_path_str])
18+
.status()
19+
.context("Failed to execute bootctl")?;
20+
21+
if !status.success() {
22+
anyhow::bail!(
23+
"bootctl install failed with status code {}",
24+
status.code().unwrap_or(-1)
25+
);
26+
}
27+
28+
// If loader.conf is present in the bootupd configuration, replace the original config with it
29+
let configdir_path = Path::new(CONFIG_DIR);
30+
if let Err(e) = try_copy_loader_conf(src_root, configdir_path, esp_path_str) {
31+
log::debug!("Optional loader.conf copy skipped: {}", e);
32+
}
33+
34+
Ok(())
35+
}
36+
37+
/// Try to copy loader.conf from configdir to ESP, returns error if not present or copy fails
38+
fn try_copy_loader_conf(
39+
src_root: &openat::Dir,
40+
configdir_path: &Path,
41+
esp_path_str: &str,
42+
) -> Result<()> {
43+
let configdir = src_root
44+
.sub_dir(configdir_path)
45+
.context(format!("Config directory '{}' not found", CONFIG_DIR))?;
46+
let dst_loader_conf = Path::new(esp_path_str).join("loader/loader.conf");
47+
match configdir.open_file("loader.conf") {
48+
Ok(mut src_file) => {
49+
let mut dst_file = std::fs::File::create(&dst_loader_conf)
50+
.context("Failed to create loader.conf in ESP")?;
51+
std::io::copy(&mut src_file, &mut dst_file)
52+
.context("Failed to copy loader.conf to ESP")?;
53+
log::info!("loader.conf copied to {}", dst_loader_conf.display());
54+
Ok(())
55+
}
56+
Err(e) => {
57+
log::debug!("loader.conf not found in configdir, skipping: {}", e);
58+
Err(anyhow::anyhow!(e))
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)