Skip to content

Commit d5b0c31

Browse files
committed
Add kernel-update reconcile hook and lifecycle commands
1 parent 7ed377b commit d5b0c31

6 files changed

Lines changed: 181 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,22 @@ sudo rustyuki install
212212
sudo rustyuki install --kernel-version "6.12.0-200.fc41.x86_64"
213213
```
214214

215+
#### `reconcile` — Rebuild all installed kernel UKIs and prune stale artifacts
216+
217+
Rebuilds UKIs for every kernel reported by `rpm -q kernel`, prunes stale `linux-*.efi` entries in the output directory, and runs `bootctl update`.
218+
219+
```bash
220+
sudo rustyuki reconcile
221+
```
222+
223+
#### `install-hook` — Run reconcile automatically on kernel updates
224+
225+
Installs a `kernel-install` plugin so `rustyuki reconcile` runs on kernel add/remove events.
226+
227+
```bash
228+
sudo rustyuki install-hook
229+
```
230+
215231
---
216232

217233
## Example Workflow

‎src/app.rs‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,31 @@ pub fn install(
151151
Ok(path)
152152
}
153153

154+
/// Rebuilds UKIs for all installed kernels and prunes stale artifacts.
155+
pub fn reconcile(
156+
runner: &dyn CommandRunner,
157+
cfg: &AppConfig,
158+
base: &GenerateSettings,
159+
) -> Result<()> {
160+
let kernels = list_installed_kernels(runner)?;
161+
for kernel in &kernels {
162+
let mut settings = base.clone();
163+
settings.kernel_version = kernel.clone();
164+
let _ = generate(runner, cfg, &settings)?;
165+
}
166+
167+
let removed = prune_stale_uki_artifacts(&base.output_dir, &kernels)?;
168+
if !removed.is_empty() {
169+
info!("Pruned {} stale UKI artifact(s)", removed.len());
170+
}
171+
172+
runner
173+
.run("bootctl", &["update"])
174+
.context("bootctl update failed")?;
175+
176+
Ok(())
177+
}
178+
154179
/// Reports current status and resolved paths.
155180
pub fn status(runner: &dyn CommandRunner, cfg: &AppConfig) -> Result<String> {
156181
let uname = runner

‎src/cli.rs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ pub enum Commands {
2828
Generate(GenerateArgs),
2929
/// Generate UKI and run bootloader update/install step.
3030
Install(GenerateArgs),
31+
/// Reconcile UKIs for all installed kernels and prune stale artifacts.
32+
Reconcile,
33+
/// Install kernel-install hook so reconcile runs on kernel updates.
34+
InstallHook(InstallHookArgs),
3135
/// Show current operational status and resolved settings.
3236
Status,
3337
}
@@ -54,3 +58,15 @@ pub struct GenerateArgs {
5458
#[arg(long)]
5559
pub os_release: Option<PathBuf>,
5660
}
61+
62+
/// Options for installing the kernel-install hook.
63+
#[derive(Debug, Clone, Parser)]
64+
pub struct InstallHookArgs {
65+
/// Destination path for the kernel-install plugin script.
66+
#[arg(long, default_value = "/usr/lib/kernel/install.d/90-rustyuki.install")]
67+
pub plugin_path: PathBuf,
68+
69+
/// Optional explicit path to the rustyuki binary to invoke from the hook.
70+
#[arg(long)]
71+
pub binary_path: Option<PathBuf>,
72+
}

‎src/config.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ extra_args = []
136136
extra_args = []
137137
"#;
138138
let cfg: AppConfig = toml::from_str(content).unwrap_or_else(|e| panic!("{e}"));
139-
assert_eq!(cfg.uki.cmdline_state_dir, std::path::PathBuf::from("/var/lib/uki-build"));
139+
assert_eq!(
140+
cfg.uki.cmdline_state_dir,
141+
std::path::PathBuf::from("/var/lib/uki-build")
142+
);
140143
assert_eq!(cfg.uki.cmdline_min_tokens, 3);
141144
}
142145
}

‎src/hook.rs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use anyhow::{Context, Result};
2+
use std::fs;
3+
use std::os::unix::fs::PermissionsExt;
4+
use std::path::Path;
5+
6+
pub fn install_kernel_update_hook(binary: &Path, config: &Path, plugin_path: &Path) -> Result<()> {
7+
let parent = plugin_path.parent().with_context(|| {
8+
format!(
9+
"kernel-install plugin path has no parent: {}",
10+
plugin_path.display()
11+
)
12+
})?;
13+
fs::create_dir_all(parent)
14+
.with_context(|| format!("failed creating plugin directory {}", parent.display()))?;
15+
16+
let script = render_kernel_install_plugin(binary, config)?;
17+
fs::write(plugin_path, script)
18+
.with_context(|| format!("failed writing plugin {}", plugin_path.display()))?;
19+
20+
let mut perms = fs::metadata(plugin_path)
21+
.with_context(|| format!("failed stat {}", plugin_path.display()))?
22+
.permissions();
23+
perms.set_mode(0o755);
24+
fs::set_permissions(plugin_path, perms)
25+
.with_context(|| format!("failed chmod +x {}", plugin_path.display()))?;
26+
27+
Ok(())
28+
}
29+
30+
pub fn render_kernel_install_plugin(binary: &Path, config: &Path) -> Result<String> {
31+
let bin = binary
32+
.to_str()
33+
.with_context(|| format!("binary path is non-UTF8: {}", binary.display()))?;
34+
let cfg = config
35+
.to_str()
36+
.with_context(|| format!("config path is non-UTF8: {}", config.display()))?;
37+
38+
Ok(format!(
39+
r#"#!/usr/bin/env bash
40+
set -euo pipefail
41+
42+
COMMAND="${{1:-}}"
43+
KERNEL_VER="${{2:-unknown}}"
44+
RUSTYUKI_BIN="{bin}"
45+
RUSTYUKI_CONFIG="{cfg}"
46+
47+
log() {{
48+
echo "[rustyuki-hook] $*" >&2
49+
}}
50+
51+
if [[ ! -x "$RUSTYUKI_BIN" ]]; then
52+
log "RustyUKI binary not executable: $RUSTYUKI_BIN"
53+
exit 1
54+
fi
55+
56+
case "$COMMAND" in
57+
add)
58+
log "kernel add: $KERNEL_VER; reconciling all installed kernels"
59+
;;
60+
remove)
61+
log "kernel remove: $KERNEL_VER; reconciling all installed kernels"
62+
;;
63+
*)
64+
log "kernel command '$COMMAND'; running reconcile"
65+
;;
66+
esac
67+
68+
exec "$RUSTYUKI_BIN" --config "$RUSTYUKI_CONFIG" reconcile
69+
"#
70+
))
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::render_kernel_install_plugin;
76+
use std::path::Path;
77+
78+
#[test]
79+
fn plugin_contains_reconcile_and_config() {
80+
let script = render_kernel_install_plugin(
81+
Path::new("/usr/local/bin/rustyuki"),
82+
Path::new("/etc/uki/uki.conf"),
83+
)
84+
.unwrap_or_else(|e| panic!("{e}"));
85+
86+
assert!(script.contains("reconcile"));
87+
assert!(script.contains("--config \"$RUSTYUKI_CONFIG\""));
88+
assert!(script.contains("KERNEL_VER"));
89+
}
90+
}

‎src/main.rs‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ mod config;
77
mod dracut;
88
mod efi;
99
mod error;
10+
mod hook;
1011
mod kernel;
1112
mod privilege;
1213
mod ukify;
1314

1415
use anyhow::{Context, Result};
15-
use app::{generate, install, resolve_generate_settings, status};
16+
use app::{generate, install, reconcile, resolve_generate_settings, status};
1617
use clap::Parser;
1718
use cli::{Cli, Commands};
1819
use cmd::{CommandRunner, RealCommandRunner};
1920
use config::AppConfig;
2021
use env_logger::Env;
22+
use hook::install_kernel_update_hook;
2123
use log::LevelFilter;
2224
use privilege::require_root;
2325

@@ -60,6 +62,33 @@ fn run() -> Result<()> {
6062
let settings = resolve_generate_settings(&cfg, args, &uname);
6163
let _ = install(&runner, &cfg, &settings)?;
6264
}
65+
Commands::Reconcile => {
66+
let uname = current_kernel(&runner)?;
67+
let settings = resolve_generate_settings(
68+
&cfg,
69+
&cli::GenerateArgs {
70+
kernel_version: Some(uname),
71+
esp_path: None,
72+
output_dir: None,
73+
cmdline_file: None,
74+
splash: None,
75+
os_release: None,
76+
},
77+
"",
78+
);
79+
reconcile(&runner, &cfg, &settings)?;
80+
}
81+
Commands::InstallHook(args) => {
82+
let binary = args.binary_path.clone().unwrap_or_else(|| {
83+
std::env::current_exe()
84+
.unwrap_or_else(|_| std::path::PathBuf::from("/usr/local/bin/rustyuki"))
85+
});
86+
install_kernel_update_hook(&binary, &cli.config, &args.plugin_path)?;
87+
println!(
88+
"installed kernel-install hook at {}",
89+
args.plugin_path.display()
90+
);
91+
}
6392
Commands::Status => {
6493
let text = status(&runner, &cfg)?;
6594
println!("{text}");

0 commit comments

Comments
 (0)