Skip to content

Commit 1d037d0

Browse files
authored
Merge pull request #36 from GlitchSlayed/codex/update-rustyuki-installation-process
Implement BootNext trial-boot flow with automatic systemd boot-confirm service
2 parents e35a412 + a54b5e6 commit 1d037d0

6 files changed

Lines changed: 115 additions & 17 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -220,18 +220,19 @@ Use `--boot-once` when you want firmware to trial the new UKI exactly once via `
220220

221221
#### `install` — Generate and sync the ESP
222222

223-
Identical to `generate`, followed by `bootctl update` to synchronise the ESP.
223+
`install` now always uses a trial-boot flow:
224+
225+
1. Generates and registers the UKI entry without rewriting permanent `BootOrder`
226+
2. Sets the new entry as `BootNext` (one-time next boot)
227+
3. Installs/enables `rustyuki-boot-confirm.service`
228+
4. Runs `bootctl update` to synchronise the ESP
224229

225230
```bash
226231
sudo rustyuki install
227232
sudo rustyuki install --kernel-version "6.12.0-200.fc41.x86_64"
228233
```
229234

230-
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`.
231-
232-
```bash
233-
sudo rustyuki install --boot-once
234-
```
235+
Use `generate --boot-once` if you want one-time trial semantics without running `bootctl update`.
235236

236237
#### `reconcile` — Rebuild all installed kernel UKIs and prune stale artifacts
237238

@@ -243,7 +244,7 @@ sudo rustyuki reconcile
243244

244245
#### `confirm` — Make a successful trial boot permanent
245246

246-
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.
247+
After booting the one-time UKI successfully, `rustyuki-boot-confirm.service` runs automatically early in userspace and calls `rustyuki confirm`. `confirm` reads `BootCurrent`, moves that entry to the front of `BootOrder`, and clears `BootNext`.
247248

248249
```bash
249250
sudo rustyuki confirm
@@ -265,12 +266,12 @@ For first-time GRUB replacement or any risky UKI change, prefer a one-time boot
265266

266267
```bash
267268
# Build the UKI, refresh the ESP, and schedule it for the next boot only
268-
sudo rustyuki install --boot-once
269+
sudo rustyuki install
269270

270271
# Reboot normally; firmware will use BootNext exactly once
271272
sudo reboot
272273

273-
# If the system came back successfully from the UKI, make it permanent
274+
# Optional manual confirmation (normally done automatically by systemd service)
274275
sudo rustyuki confirm
275276
```
276277

@@ -289,12 +290,12 @@ sudo rustyuki status
289290
sudo rustyuki generate --dry-run
290291

291292
# Step 3 — build and schedule a one-time trial boot
292-
sudo rustyuki install --boot-once
293+
sudo rustyuki install
293294

294295
# Step 4 — reboot normally and let BootNext test the UKI once
295296
sudo reboot
296297

297-
# Step 5 — after a successful UKI boot, make it permanent
298+
# Step 5 — optional manual confirmation (auto-confirm service handles this on success)
298299
sudo rustyuki confirm
299300
```
300301

‎src/app.rs‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::dracut::build_initramfs;
55
use crate::efi::{
66
promote_current_boot_entry, register_boot_entry, schedule_one_time_boot, validate_esp_preflight,
77
};
8+
use crate::hook::install_boot_confirm_service;
89
use crate::kernel::{
910
list_installed_kernels, prune_stale_uki_artifacts, resolve_cmdline, CmdlineSettings,
1011
};
@@ -140,11 +141,23 @@ pub fn install(
140141
runner: &dyn CommandRunner,
141142
cfg: &AppConfig,
142143
settings: &GenerateSettings,
143-
boot_once: bool,
144144
) -> Result<PathBuf> {
145145
validate_esp_preflight(&settings.esp_path, &settings.output_dir)?;
146146

147-
let (path, _boot_num) = generate(runner, cfg, settings, boot_once)?;
147+
let current_exe =
148+
std::env::current_exe().context("failed resolving current executable path")?;
149+
install_boot_confirm_service(
150+
Path::new("/etc/systemd/system/rustyuki-boot-confirm.service"),
151+
&current_exe,
152+
)?;
153+
runner
154+
.run("systemctl", &["daemon-reload"])
155+
.context("failed to reload systemd units after installing rustyuki-boot-confirm.service")?;
156+
runner
157+
.run("systemctl", &["enable", "rustyuki-boot-confirm.service"])
158+
.context("failed enabling rustyuki-boot-confirm.service")?;
159+
160+
let (path, _boot_num) = generate(runner, cfg, settings, true)?;
148161

149162
let installed = list_installed_kernels(runner)?;
150163
let removed = prune_stale_uki_artifacts(&settings.output_dir, &installed)?;

‎src/efi.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ pub fn register_boot_entry(
149149
&[
150150
"--quiet",
151151
"--create",
152+
"--create-only",
152153
"--disk",
153154
&format!("/dev/{disk}"),
154155
"--part",
@@ -202,6 +203,9 @@ pub fn promote_current_boot_entry(runner: &dyn CommandRunner) -> Result<String>
202203
runner
203204
.run("efibootmgr", &["--bootorder", &boot_order_arg])
204205
.with_context(|| format!("failed setting BootOrder to {boot_order_arg}"))?;
206+
runner
207+
.run("efibootmgr", &["--delete-bootnext"])
208+
.context("failed clearing BootNext after confirmation")?;
205209
Ok(current)
206210
}
207211

‎src/hook.rs‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,55 @@ exit 0
7676
))
7777
}
7878

79+
pub fn install_boot_confirm_service(unit_path: &Path, binary: &Path) -> Result<()> {
80+
let parent = unit_path.parent().with_context(|| {
81+
format!(
82+
"systemd unit path has no parent directory: {}",
83+
unit_path.display()
84+
)
85+
})?;
86+
fs::create_dir_all(parent)
87+
.with_context(|| format!("failed creating unit directory {}", parent.display()))?;
88+
89+
let unit = render_boot_confirm_service(binary)?;
90+
fs::write(unit_path, unit)
91+
.with_context(|| format!("failed writing unit {}", unit_path.display()))?;
92+
93+
let mut perms = fs::metadata(unit_path)
94+
.with_context(|| format!("failed stat {}", unit_path.display()))?
95+
.permissions();
96+
perms.set_mode(0o644);
97+
fs::set_permissions(unit_path, perms)
98+
.with_context(|| format!("failed chmod 0644 {}", unit_path.display()))?;
99+
100+
Ok(())
101+
}
102+
103+
pub fn render_boot_confirm_service(binary: &Path) -> Result<String> {
104+
let bin = binary
105+
.to_str()
106+
.with_context(|| format!("binary path is non-UTF8: {}", binary.display()))?;
107+
108+
Ok(format!(
109+
r#"[Unit]
110+
Description=Confirm successful RustyUKI trial boot
111+
DefaultDependencies=no
112+
After=local-fs.target
113+
Before=network.target
114+
115+
[Service]
116+
Type=oneshot
117+
ExecStart={bin} confirm
118+
119+
[Install]
120+
WantedBy=multi-user.target
121+
"#
122+
))
123+
}
124+
79125
#[cfg(test)]
80126
mod tests {
81-
use super::render_kernel_install_plugin;
127+
use super::{render_boot_confirm_service, render_kernel_install_plugin};
82128
use std::path::Path;
83129

84130
#[test]
@@ -145,4 +191,15 @@ mod tests {
145191

146192
assert!(!script.contains("exec "));
147193
}
194+
195+
#[test]
196+
fn boot_confirm_service_has_early_userspace_ordering() {
197+
let unit = render_boot_confirm_service(Path::new("/usr/local/bin/rustyuki"))
198+
.unwrap_or_else(|e| panic!("{e}"));
199+
200+
assert!(unit.contains("Type=oneshot"));
201+
assert!(unit.contains("After=local-fs.target"));
202+
assert!(unit.contains("Before=network.target"));
203+
assert!(unit.contains("ExecStart=/usr/local/bin/rustyuki confirm"));
204+
}
148205
}

‎src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fn run() -> Result<()> {
6060
Commands::Install(args) => {
6161
let uname = current_kernel(&runner)?;
6262
let settings = resolve_generate_settings(&cfg, args, &uname);
63-
let _ = install(&runner, &cfg, &settings, args.boot_once)?;
63+
let _ = install(&runner, &cfg, &settings)?;
6464
}
6565
Commands::Reconcile => {
6666
let uname = current_kernel(&runner)?;

‎tests/integration_cli.rs‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ mod efi;
3636
#[path = "../src/error.rs"]
3737
mod error;
3838
#[allow(dead_code)]
39+
#[path = "../src/hook.rs"]
40+
mod hook;
41+
#[allow(dead_code)]
3942
#[path = "../src/kernel.rs"]
4043
mod kernel;
4144
#[allow(dead_code)]
@@ -507,6 +510,7 @@ Boot0001* Fedora HD(...)
507510
args: vec![
508511
"--quiet".to_string(),
509512
"--create".to_string(),
513+
"--create-only".to_string(),
510514
"--disk".to_string(),
511515
"/dev/nvme0n1".to_string(),
512516
"--part".to_string(),
@@ -562,7 +566,7 @@ Boot0008* Linux UKI 6.11.5-test HD(...)
562566
}
563567

564568
#[test]
565-
fn install_with_boot_once_sets_bootnext_after_bootctl_update() {
569+
fn install_sets_bootnext_after_bootctl_update() {
566570
let temp = TempDir::new().unwrap_or_else(|e| panic!("{e}"));
567571
let esp = test_esp_mountpoint();
568572
let out = esp.join("EFI/Linux");
@@ -585,6 +589,19 @@ fn install_with_boot_once_sets_bootnext_after_bootctl_update() {
585589
let final_out = out.join("linux-6.11.4-test.efi");
586590

587591
let runner = MockRunner::new(vec![
592+
ExpectedCall {
593+
program: "systemctl".to_string(),
594+
args: vec!["daemon-reload".to_string()],
595+
output: Ok(ProcessOutput::default()),
596+
},
597+
ExpectedCall {
598+
program: "systemctl".to_string(),
599+
args: vec![
600+
"enable".to_string(),
601+
"rustyuki-boot-confirm.service".to_string(),
602+
],
603+
output: Ok(ProcessOutput::default()),
604+
},
588605
ExpectedCall {
589606
program: "dracut".to_string(),
590607
args: vec![
@@ -671,6 +688,7 @@ Boot0001* Fedora HD(...)
671688
args: vec![
672689
"--quiet".to_string(),
673690
"--create".to_string(),
691+
"--create-only".to_string(),
674692
"--disk".to_string(),
675693
"/dev/nvme0n1".to_string(),
676694
"--part".to_string(),
@@ -729,7 +747,7 @@ Boot0007* Linux UKI 6.11.4-test HD(...)
729747
os_release,
730748
};
731749

732-
let installed = install(&runner, &cfg, &settings, true).unwrap_or_else(|e| panic!("{e:#}"));
750+
let installed = install(&runner, &cfg, &settings).unwrap_or_else(|e| panic!("{e:#}"));
733751
assert_eq!(installed, final_out);
734752
runner.assert_no_pending();
735753

@@ -761,6 +779,11 @@ Boot0007* Linux UKI 6.11.4-test HD(...)
761779
args: vec!["--bootorder".to_string(), "0007,0001,0003".to_string()],
762780
output: Ok(ProcessOutput::default()),
763781
},
782+
ExpectedCall {
783+
program: "efibootmgr".to_string(),
784+
args: vec!["--delete-bootnext".to_string()],
785+
output: Ok(ProcessOutput::default()),
786+
},
764787
]);
765788

766789
let boot_num = confirm(&runner).unwrap_or_else(|e| panic!("{e:#}"));

0 commit comments

Comments
 (0)