Skip to content

Commit 23d044c

Browse files
committed
feat: support systemd-boot via config and build flag
1 parent f0fcca8 commit 23d044c

File tree

8 files changed

+182
-49
lines changed

8 files changed

+182
-49
lines changed

Cargo.toml

Lines changed: 12 additions & 1 deletion
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 = ["systemd-boot"]
20+
systemd-boot = []
21+
1822
[[bin]]
1923
name = "bootupd"
2024
path = "src/main.rs"
@@ -27,7 +31,14 @@ bootc-internal-utils = "0.0.0"
2731
cap-std-ext = "4.0.6"
2832
camino = "1.1.11"
2933
chrono = { version = "0.4.41", features = ["serde"] }
30-
clap = { version = "4.5", default-features = false, features = ["cargo", "derive", "std", "help", "usage", "suggestions"] }
34+
clap = { version = "4.5", default-features = false, features = [
35+
"cargo",
36+
"derive",
37+
"std",
38+
"help",
39+
"usage",
40+
"suggestions",
41+
] }
3142
env_logger = "0.11"
3243
fail = { version = "0.5", features = ["failpoints"] }
3344
fn-error-context = "0.2.1"

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: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ 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+
// TODO: Implement dynamic bootloader detection
30+
pub(crate) enum Bootloader {
31+
_Auto,
32+
Grub,
33+
SystemdBoot,
34+
}
35+
2836
pub(crate) enum ConfigMode {
2937
None,
3038
Static,
@@ -46,9 +54,10 @@ pub(crate) fn install(
4654
dest_root: &str,
4755
device: Option<&str>,
4856
configs: ConfigMode,
49-
update_firmware: bool,
57+
mut update_firmware: bool,
5058
target_components: Option<&[String]>,
5159
auto_components: bool,
60+
bootloader: Bootloader,
5261
) -> Result<()> {
5362
// TODO: Change this to an Option<&str>; though this probably balloons into having
5463
// DeviceComponent and FileBasedComponent
@@ -93,15 +102,33 @@ pub(crate) fn install(
93102
continue;
94103
}
95104

105+
if component.name() == "BIOS" && bootloader == Bootloader::SystemdBoot {
106+
println!(
107+
"Skip installing component {} for systemd-boot",
108+
component.name()
109+
);
110+
continue;
111+
}
112+
113+
if bootloader == Bootloader::SystemdBoot {
114+
log::warn!(
115+
"Disabling firmware updates for component {}",
116+
component.name()
117+
);
118+
update_firmware = false;
119+
}
96120
let meta = component
97-
.install(&source_root, dest_root, device, update_firmware)
121+
.install(&source_root, dest_root, device, update_firmware, bootloader)
98122
.with_context(|| format!("installing component {}", component.name()))?;
99123
log::info!("Installed {} {}", component.name(), meta.meta.version);
100124
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);
125+
126+
if bootloader != Bootloader::SystemdBoot {
127+
// Yes this is a hack...the Component thing just turns out to be too generic.
128+
if let Some(vendor) = component.get_efi_vendor(&source_root)? {
129+
assert!(installed_efi_vendor.is_none());
130+
installed_efi_vendor = Some(vendor);
131+
}
105132
}
106133
}
107134
let sysroot = &openat::Dir::open(dest_root)?;
@@ -119,7 +146,9 @@ pub(crate) fn install(
119146
crate::grubconfigs::install(sysroot, installed_efi_vendor.as_deref(), uuid)?;
120147
// On other architectures, assume that there's nothing to do.
121148
}
122-
None => {}
149+
None => {
150+
log::info!("Skipping static config generation");
151+
}
123152
}
124153

125154
// Unmount the ESP, etc.

src/cli/bootupd.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::bootupd::{self, ConfigMode};
1+
use crate::bootupd::{self, Bootloader, ConfigMode};
22
use anyhow::{Context, Result};
33
use clap::Parser;
44
use log::LevelFilter;
@@ -73,6 +73,12 @@ pub struct InstallOpts {
7373
/// then only enable installation to the ESP.
7474
#[clap(long)]
7575
auto: bool,
76+
77+
/// The bootloader to configure
78+
///
79+
/// Defaults to Grub
80+
#[clap(long, default_value = "grub")]
81+
bootloader: String,
7682
}
7783

7884
#[derive(Debug, Parser)]
@@ -103,13 +109,23 @@ impl DCommand {
103109

104110
/// Runner for `install` verb.
105111
pub(crate) fn run_install(opts: InstallOpts) -> Result<()> {
106-
let configmode = if opts.write_uuid {
112+
let bootloader = match opts.bootloader.as_str() {
113+
"grub" => Bootloader::Grub,
114+
"systemd-boot" => Bootloader::SystemdBoot,
115+
_ => anyhow::bail!("Unknown bootloader: {}", opts.bootloader),
116+
};
117+
118+
// If systemd-boot, always use ConfigMode::None
119+
let configmode = if let Bootloader::SystemdBoot = bootloader {
120+
ConfigMode::None
121+
} else if opts.write_uuid {
107122
ConfigMode::WithUUID
108123
} else if opts.with_static_configs {
109124
ConfigMode::Static
110125
} else {
111126
ConfigMode::None
112127
};
128+
113129
bootupd::install(
114130
&opts.src_root,
115131
&opts.dest_root,
@@ -118,6 +134,7 @@ impl DCommand {
118134
opts.update_firmware,
119135
opts.components.as_deref(),
120136
opts.auto,
137+
bootloader,
121138
)
122139
.context("boot data installation failed")?;
123140
Ok(())

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: 69 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -342,51 +342,81 @@ impl Component for Efi {
342342
dest_root: &str,
343343
device: &str,
344344
update_firmware: bool,
345+
bootloader: crate::bootupd::Bootloader,
345346
) -> 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-
353-
// Let's attempt to use an already mounted ESP at the target
354-
// dest_root if one is already mounted there in a known ESP location.
355-
let destpath = if let Some(destdir) = self.get_mounted_esp(Path::new(dest_root))? {
356-
destdir
357-
} else {
358-
// Using `blockdev` to find the partition instead of partlabel because
359-
// we know the target install toplevel device already.
347+
let esp_path = self.get_mounted_esp(Path::new(dest_root))?.or_else(|| {
360348
if device.is_empty() {
361-
anyhow::bail!("Device value not provided");
349+
None
350+
} else {
351+
let esp_device = blockdev::get_esp_partition(device).ok().flatten()?;
352+
self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device))
353+
.ok()
362354
}
363-
let esp_device = blockdev::get_esp_partition(device)?
364-
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
365-
self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device))?
366-
};
355+
});
356+
357+
match bootloader {
358+
crate::bootupd::Bootloader::SystemdBoot => {
359+
log::info!("Installing systemd-boot via bootctl");
360+
let esp_dir = openat::Dir::open(esp_path.as_ref().ok_or_else(|| {
361+
anyhow::anyhow!("No ESP mount found for systemd-boot install")
362+
})?)
363+
.context("Opening mounted ESP directory for systemd-boot")?;
364+
crate::systemdbootconfigs::install(&esp_dir)?;
365+
Ok(InstalledContent {
366+
meta: ContentMetadata {
367+
timestamp: chrono::Utc::now(),
368+
version: "systemd-boot".to_string(),
369+
},
370+
filetree: None,
371+
adopted_from: None,
372+
})
373+
}
374+
crate::bootupd::Bootloader::Grub | crate::bootupd::Bootloader::_Auto => {
375+
let meta = match get_component_update(src_root, self)? {
376+
Some(meta) => meta,
377+
None => {
378+
log::debug!(
379+
"No update metadata for component {} found, continuing (GRUB case)",
380+
self.name()
381+
);
382+
return Ok(InstalledContent {
383+
meta: ContentMetadata {
384+
timestamp: chrono::Utc::now(),
385+
version: "grub".to_string(),
386+
},
387+
filetree: None,
388+
adopted_from: None,
389+
});
390+
}
391+
};
392+
log::debug!("Found metadata {}", meta.version);
393+
let srcdir_name = component_updatedirname(self);
394+
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
395+
396+
let destpath = esp_path
397+
.ok_or_else(|| anyhow::anyhow!("No ESP mount found for GRUB install"))?;
398+
let destd = &openat::Dir::open(&destpath)
399+
.with_context(|| format!("opening dest dir {}", destpath.display()))?;
400+
validate_esp_fstype(destd)?;
367401

368-
let destd = &openat::Dir::open(&destpath)
369-
.with_context(|| format!("opening dest dir {}", destpath.display()))?;
370-
validate_esp_fstype(destd)?;
371-
372-
// TODO - add some sort of API that allows directly setting the working
373-
// directory to a file descriptor.
374-
std::process::Command::new("cp")
375-
.args(["-rp", "--reflink=auto"])
376-
.arg(&srcdir_name)
377-
.arg(destpath)
378-
.current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd()))
379-
.run()?;
380-
if update_firmware {
381-
if let Some(vendordir) = self.get_efi_vendor(&src_root)? {
382-
self.update_firmware(device, destd, &vendordir)?
402+
std::process::Command::new("cp")
403+
.args(["-rp", "--reflink=auto"])
404+
.arg(&srcdir_name)
405+
.arg(&destpath)
406+
.current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd()))
407+
.run()?;
408+
if update_firmware {
409+
if let Some(vendordir) = self.get_efi_vendor(&src_root)? {
410+
self.update_firmware(device, destd, &vendordir)?
411+
}
412+
}
413+
Ok(InstalledContent {
414+
meta,
415+
filetree: Some(ft),
416+
adopted_from: None,
417+
})
383418
}
384419
}
385-
Ok(InstalledContent {
386-
meta,
387-
filetree: Some(ft),
388-
adopted_from: None,
389-
})
390420
}
391421

392422
fn run_update(

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ mod model_legacy;
4646
mod ostreeutil;
4747
mod packagesystem;
4848
mod sha512string;
49+
#[cfg(feature = "systemd-boot")]
50+
mod systemdbootconfigs;
4951
mod util;
5052

5153
use clap::crate_name;

src/systemdbootconfigs.rs

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

0 commit comments

Comments
 (0)