Skip to content

Commit 17a03fe

Browse files
committed
Add boot-once support to generate
1 parent c4df57f commit 17a03fe

6 files changed

Lines changed: 666 additions & 27 deletions

File tree

‎README.md‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
- [Installation](#installation)
3232
- [Configuration](#configuration)
3333
- [Usage](#usage)
34+
- [Safer First Boot Workflow](#safer-first-boot-workflow)
3435
- [Example Workflow](#example-workflow)
3536
- [⚠️ Backups & Recovery](#%EF%B8%8F-backups--recovery)
3637
- [⚠️ Warnings & Known Failure Modes](#%EF%B8%8F-warnings--known-failure-modes)
@@ -203,9 +204,12 @@ Output is written atomically (temp file → rename) to prevent partial writes fr
203204
```bash
204205
sudo rustyuki generate
205206
sudo rustyuki generate --kernel-version "$(uname -r)"
207+
sudo rustyuki generate --boot-once
206208
sudo rustyuki generate --dry-run # preview only
207209
```
208210

211+
Use `--boot-once` when you want firmware to trial the new UKI exactly once via `efibootmgr --bootnext` before you make it permanent with `rustyuki confirm`.
212+
209213
#### `install` — Generate and sync the ESP
210214

211215
Identical to `generate`, followed by `bootctl update` to synchronise the ESP.
@@ -215,6 +219,12 @@ sudo rustyuki install
215219
sudo rustyuki install --kernel-version "6.12.0-200.fc41.x86_64"
216220
```
217221

222+
Add `--boot-once` to schedule the new EFI entry as the next boot only via `efibootmgr --bootnext`, leaving your permanent `BootOrder` unchanged until you confirm the trial boot succeeded. The same flag is available on `generate` if you only want to register the UKI entry without running `bootctl update`.
223+
224+
```bash
225+
sudo rustyuki install --boot-once
226+
```
227+
218228
#### `reconcile` — Rebuild all installed kernel UKIs and prune stale artifacts
219229

220230
Rebuilds UKIs for every kernel reported by `rpm -q kernel`, prunes stale `linux-*.efi` entries in the output directory, and runs `bootctl update`.
@@ -223,6 +233,14 @@ Rebuilds UKIs for every kernel reported by `rpm -q kernel`, prunes stale `linux-
223233
sudo rustyuki reconcile
224234
```
225235

236+
#### `confirm` — Make a successful trial boot permanent
237+
238+
After booting the one-time UKI successfully, run `confirm` from that booted system. RustyUKI reads `BootCurrent` and moves that entry to the front of `BootOrder`, making the tested UKI your permanent default.
239+
240+
```bash
241+
sudo rustyuki confirm
242+
```
243+
226244
#### `install-hook` — Run reconcile automatically on kernel updates
227245

228246
Installs a `kernel-install` plugin so `rustyuki reconcile` runs on kernel add/remove events.
@@ -233,6 +251,23 @@ sudo rustyuki install-hook
233251

234252
---
235253

254+
## Safer First Boot Workflow
255+
256+
For first-time GRUB replacement or any risky UKI change, prefer a one-time boot trial instead of immediately changing your permanent firmware boot order.
257+
258+
```bash
259+
# Build the UKI, refresh the ESP, and schedule it for the next boot only
260+
sudo rustyuki install --boot-once
261+
262+
# Reboot normally; firmware will use BootNext exactly once
263+
sudo reboot
264+
265+
# If the system came back successfully from the UKI, make it permanent
266+
sudo rustyuki confirm
267+
```
268+
269+
This workflow keeps your existing default entry in `BootOrder` as the fallback if the trial boot fails, while still letting you verify the UKI end-to-end before committing to it.
270+
236271
## Example Workflow
237272

238273
> [!WARNING]
@@ -245,11 +280,14 @@ sudo rustyuki status
245280
# Step 2 — dry run: see every command that will execute, without running anything
246281
sudo rustyuki generate --dry-run
247282

248-
# Step 3 — build and install the UKI
249-
sudo rustyuki install
283+
# Step 3 — build and schedule a one-time trial boot
284+
sudo rustyuki install --boot-once
285+
286+
# Step 4 — reboot normally and let BootNext test the UKI once
287+
sudo reboot
250288

251-
# Step 4 — use your firmware's one-time boot menu (F12 / F2) to select the UKI entry
252-
# DO NOT change your default boot order until you confirm it boots correctly
289+
# Step 5 — after a successful UKI boot, make it permanent
290+
sudo rustyuki confirm
253291
```
254292

255293
After booting into the UKI, verify it was used:

‎src/app.rs‎

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ use crate::cli::GenerateArgs;
22
use crate::cmd::CommandRunner;
33
use crate::config::AppConfig;
44
use crate::dracut::build_initramfs;
5-
use crate::efi::{register_boot_entry, validate_esp_mount};
5+
use crate::efi::{
6+
promote_current_boot_entry, register_boot_entry, schedule_one_time_boot, validate_esp_mount,
7+
};
68
use crate::kernel::{
79
list_installed_kernels, prune_stale_uki_artifacts, resolve_cmdline, CmdlineSettings,
810
};
@@ -75,7 +77,8 @@ pub fn generate(
7577
runner: &dyn CommandRunner,
7678
cfg: &AppConfig,
7779
settings: &GenerateSettings,
78-
) -> Result<PathBuf> {
80+
boot_once: bool,
81+
) -> Result<(PathBuf, String)> {
7982
validate_esp_mount(&settings.esp_path)?;
8083
ensure_required_paths(settings)?;
8184

@@ -124,19 +127,29 @@ pub fn generate(
124127
let built_uki = build_uki(runner, &params)?;
125128

126129
let label = format!("Linux UKI {}", settings.kernel_version);
127-
register_boot_entry(runner, &settings.esp_path, &built_uki, &label)?;
130+
let boot_num = register_boot_entry(runner, &settings.esp_path, &built_uki, &label)?;
131+
132+
if boot_once {
133+
schedule_one_time_boot(runner, &boot_num)?;
134+
info!("Scheduled BootNext for Boot{boot_num}; run `rustyuki confirm` after a successful trial boot to make it permanent");
135+
}
128136

129-
info!("UKI generation finished: {}", built_uki.display());
130-
Ok(built_uki)
137+
info!(
138+
"UKI generation finished: {} (Boot{})",
139+
built_uki.display(),
140+
boot_num
141+
);
142+
Ok((built_uki, boot_num))
131143
}
132144

133145
/// Performs install flow: generate UKI, then execute bootloader maintenance.
134146
pub fn install(
135147
runner: &dyn CommandRunner,
136148
cfg: &AppConfig,
137149
settings: &GenerateSettings,
150+
boot_once: bool,
138151
) -> Result<PathBuf> {
139-
let path = generate(runner, cfg, settings)?;
152+
let (path, _boot_num) = generate(runner, cfg, settings, boot_once)?;
140153

141154
let installed = list_installed_kernels(runner)?;
142155
let removed = prune_stale_uki_artifacts(&settings.output_dir, &installed)?;
@@ -148,6 +161,7 @@ pub fn install(
148161
runner
149162
.run("bootctl", &["update"])
150163
.context("bootctl update failed")?;
164+
151165
Ok(path)
152166
}
153167

@@ -161,7 +175,7 @@ pub fn reconcile(
161175
for kernel in &kernels {
162176
let mut settings = base.clone();
163177
settings.kernel_version = kernel.clone();
164-
let _ = generate(runner, cfg, &settings)?;
178+
let _ = generate(runner, cfg, &settings, false)?;
165179
}
166180

167181
let removed = prune_stale_uki_artifacts(&base.output_dir, &kernels)?;
@@ -176,6 +190,13 @@ pub fn reconcile(
176190
Ok(())
177191
}
178192

193+
/// Promotes the currently booted EFI entry to the front of BootOrder.
194+
pub fn confirm(runner: &dyn CommandRunner) -> Result<String> {
195+
let boot_num = promote_current_boot_entry(runner)?;
196+
info!("Confirmed Boot{boot_num} as the permanent default boot entry");
197+
Ok(boot_num)
198+
}
199+
179200
/// Reports current status and resolved paths.
180201
pub fn status(runner: &dyn CommandRunner, cfg: &AppConfig) -> Result<String> {
181202
let uname = runner
@@ -229,6 +250,7 @@ mod tests {
229250
cmdline_file: None,
230251
splash: None,
231252
os_release: None,
253+
boot_once: false,
232254
};
233255

234256
let resolved = resolve_generate_settings(&cfg, &args, "6.9.0-test");

‎src/cli.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use clap::{ArgAction, Parser, Subcommand};
1+
use clap::{ArgAction, Args, Parser, Subcommand};
22
use std::path::PathBuf;
33

44
/// RustyUKI command-line interface.
@@ -34,10 +34,12 @@ pub enum Commands {
3434
InstallHook(InstallHookArgs),
3535
/// Show current operational status and resolved settings.
3636
Status,
37+
/// Make the currently booted trial UKI the permanent default boot entry.
38+
Confirm,
3739
}
3840

3941
/// Shared override arguments for generation actions.
40-
#[derive(Debug, Clone, Parser)]
42+
#[derive(Debug, Clone, Args)]
4143
pub struct GenerateArgs {
4244
/// Kernel version override.
4345
#[arg(long)]
@@ -57,6 +59,9 @@ pub struct GenerateArgs {
5759
/// Optional os-release path override.
5860
#[arg(long)]
5961
pub os_release: Option<PathBuf>,
62+
/// Set the new EFI entry as the one-time next boot target instead of immediately changing permanent boot order.
63+
#[arg(long)]
64+
pub boot_once: bool,
6065
}
6166

6267
/// Options for installing the kernel-install hook.

0 commit comments

Comments
 (0)