|
1 | 1 | use anyhow::{Context, Result}; |
2 | 2 | use chrono::Utc; |
3 | 3 | use serde::{Deserialize, Serialize}; |
4 | | -use std::{fs, os::unix::fs::symlink, path::Path, path::PathBuf, process::Command}; |
| 4 | +use std::{fs, io, os::unix::fs::symlink, path::Path, path::PathBuf, process::Command}; |
5 | 5 |
|
6 | 6 | use crate::{cfsctl, config, signing}; |
7 | 7 |
|
| 8 | +/// Set the systemd-boot default via `bootctl set-default`, writing the |
| 9 | +/// `LoaderEntryDefault` EFI variable (which takes priority over `loader.conf`). |
| 10 | +/// Silently skips if `bootctl` is not found (container/install contexts without |
| 11 | +/// a writable efivarfs). |
| 12 | +pub(crate) fn bootctl_set_default(entry_id: &str) -> Result<()> { |
| 13 | + let id_with_efi = format!("{entry_id}.efi"); |
| 14 | + match Command::new("bootctl") |
| 15 | + .args(["set-default", &id_with_efi]) |
| 16 | + .status() |
| 17 | + { |
| 18 | + Ok(s) if s.success() => Ok(()), |
| 19 | + Ok(s) => anyhow::bail!("bootctl set-default: exited {s}"), |
| 20 | + Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()), |
| 21 | + Err(e) => Err(e).context("spawning bootctl"), |
| 22 | + } |
| 23 | +} |
| 24 | + |
| 25 | +/// Write (or update) the `default <entry_id>` line in |
| 26 | +/// `<esp>/loader/loader.conf`. Used as a fallback for contexts without a |
| 27 | +/// writable efivarfs (container image builds, install from live media). |
| 28 | +pub(crate) fn set_loader_conf_default(esp: &Path, entry_id: &str) -> Result<()> { |
| 29 | + let conf = esp.join("loader/loader.conf"); |
| 30 | + fs::create_dir_all(conf.parent().unwrap()).context("creating loader dir")?; |
| 31 | + let existing = if conf.exists() { |
| 32 | + fs::read_to_string(&conf).with_context(|| format!("reading {}", conf.display()))? |
| 33 | + } else { |
| 34 | + String::new() |
| 35 | + }; |
| 36 | + let mut replaced = false; |
| 37 | + let mut lines: Vec<String> = existing |
| 38 | + .lines() |
| 39 | + .map(|l| { |
| 40 | + if l.starts_with("default ") || l == "default" { |
| 41 | + replaced = true; |
| 42 | + format!("default {entry_id}") |
| 43 | + } else { |
| 44 | + l.to_owned() |
| 45 | + } |
| 46 | + }) |
| 47 | + .collect(); |
| 48 | + if !replaced { |
| 49 | + lines.push(format!("default {entry_id}")); |
| 50 | + } |
| 51 | + fs::write(&conf, lines.join("\n") + "\n").with_context(|| format!("writing {}", conf.display())) |
| 52 | +} |
| 53 | + |
8 | 54 | const EFI_ESP: &str = "/boot/efi"; |
9 | 55 | // UKIs live on the ESP, not XBOOTLDR — systemd-boot always scans its own partition. |
10 | 56 | const EFI_LINUX_DIR: &str = "/boot/efi/EFI/Linux"; |
@@ -65,12 +111,22 @@ pub fn run(reboot: bool) -> Result<()> { |
65 | 111 | println!("Signing UKI ..."); |
66 | 112 | crate::install::sign_efi(&uki_path, Path::new(&sb.key), Path::new(&sb.cert))?; |
67 | 113 | } |
| 114 | + // Make the new UKI the permanent default. Write both the EFI variable |
| 115 | + // (via bootctl, highest priority) and loader.conf (fallback for contexts |
| 116 | + // without a writable efivarfs, e.g. container image builds). |
| 117 | + set_loader_conf_default(Path::new(EFI_ESP), &digest)?; |
| 118 | + bootctl_set_default(&digest)?; |
68 | 119 | } else { |
69 | 120 | patch_bls_entry(Path::new(BOOT_DIR), &digest, &image_ref)?; |
70 | 121 | if !crate::install::has_grub2() { |
71 | 122 | // Ubuntu: regenerate menuentry-based grub.cfg so the new deployment |
72 | 123 | // appears in the menu (blscfg.mod is not available on Ubuntu). |
73 | 124 | write_grub_menuentry_cfg(Path::new(BOOT_DIR), crate::install::grub_dir())?; |
| 125 | + } else { |
| 126 | + // Fedora/RHEL: blscfg.mod reads BLS entries but selects the default |
| 127 | + // based on grubenv `default`. Without this update, GRUB continues |
| 128 | + // to boot the previous entry regardless of the new BLS conf. |
| 129 | + set_grub_default(&digest)?; |
74 | 130 | } |
75 | 131 | } |
76 | 132 |
|
@@ -190,8 +246,9 @@ pub fn patch_bls_entry(bootdir: &Path, digest: &str, image_ref: &str) -> Result< |
190 | 246 | /// `bootdir/loader/entries/`. Used on distros (Ubuntu/Debian) that do not |
191 | 247 | /// ship `blscfg.mod` in their GRUB package. |
192 | 248 | /// |
193 | | -/// Each entry gets `--id <digest>` so that rollback's `next_entry=<digest>` |
194 | | -/// in grubenv correctly selects the previous deployment. |
| 249 | +/// Entries are sorted newest-first; index 0 is the default boot entry. |
| 250 | +/// rollback.rs writes `next_entry=<numeric-index>` (not the digest) for |
| 251 | +/// Ubuntu because GRUB does not reliably match long --id values. |
195 | 252 | pub fn write_grub_menuentry_cfg(bootdir: &Path, grub_subdir: &str) -> Result<()> { |
196 | 253 | let entries_dir = bootdir.join("loader/entries"); |
197 | 254 | let mut bls: Vec<(std::time::SystemTime, String, String, String)> = Vec::new(); |
@@ -333,6 +390,30 @@ fn write_state(digest: &str, manifest_digest: Option<&str>) -> Result<()> { |
333 | 390 | fs::write(STATE_PATH, json).with_context(|| format!("writing {STATE_PATH}")) |
334 | 391 | } |
335 | 392 |
|
| 393 | +/// Set the permanent GRUB default to `entry_id` via grub2-editenv/grub-editenv. |
| 394 | +/// If grubenv does not exist yet, skips silently (blscfg will fall back to its |
| 395 | +/// own sort order). |
| 396 | +fn set_grub_default(entry_id: &str) -> Result<()> { |
| 397 | + let Some(grubenv) = ["/boot/grub2/grubenv", "/boot/grub/grubenv"] |
| 398 | + .iter() |
| 399 | + .find(|p| Path::new(p).exists()) |
| 400 | + else { |
| 401 | + return Ok(()); |
| 402 | + }; |
| 403 | + for cmd in &["grub2-editenv", "grub-editenv"] { |
| 404 | + match Command::new(cmd) |
| 405 | + .args([*grubenv, "set", &format!("default={entry_id}")]) |
| 406 | + .status() |
| 407 | + { |
| 408 | + Ok(s) if s.success() => return Ok(()), |
| 409 | + Ok(s) => anyhow::bail!("{cmd}: exited {s}"), |
| 410 | + Err(e) if e.kind() == io::ErrorKind::NotFound => continue, |
| 411 | + Err(e) => return Err(e).with_context(|| format!("spawning {cmd}")), |
| 412 | + } |
| 413 | + } |
| 414 | + anyhow::bail!("neither grub2-editenv nor grub-editenv found in PATH") |
| 415 | +} |
| 416 | + |
336 | 417 | fn trigger_reboot() -> Result<()> { |
337 | 418 | let status = Command::new("systemctl") |
338 | 419 | .arg("reboot") |
|
0 commit comments