Compare commits

...

View file

@ -430,6 +430,20 @@ pub async fn rebuild_no_meta(
set_resource_limits(&container)?;
systemd_daemon_reload().await?;
if was_running {
// Pre-build the system toplevel **before** stopping the
// running container so the agent keeps serving its
// previous generation while the eval + fetch + build
// happens out-of-band (#706). `nixos-container update`
// then finds the toplevel cached and skips straight to
// the profile-swap + restart — downtime collapses to that
// window only. Build failures surface here, before we
// touch the container.
//
// When the container is already stopped there's no
// downtime to shave (mara on #721#9007) — let `update`
// do the build inline rather than evaluating the flake
// twice for nothing.
prebuild_toplevel(name, &flake_ref).await?;
run(&["stop", &container]).await?;
}
run(&["update", &container, "--flake", &flake_ref]).await?;
@ -439,8 +453,11 @@ pub async fn rebuild_no_meta(
Ok(())
}
} else {
// First spawn: create the container first (which writes the nspawn
// conf file), then overwrite with our flags and start.
// First spawn: no running container, no downtime to shave.
// `nixos-container create` builds + creates atomically — if
// the build fails, no container record is left around to
// clean up — so a pre-build adds nothing but a duplicate
// eval (mara on #721#9007).
run(&["create", &container, "--flake", &flake_ref]).await?;
set_nspawn_flags(&container, agent_dir, claude_dir, notes_dir)?;
set_resource_limits(&container)?;
@ -449,6 +466,89 @@ pub async fn rebuild_no_meta(
}
}
/// Pre-build the agent's `system.build.toplevel` derivation against
/// `meta#<name>` so the subsequent `nixos-container update` /
/// `create` finds the result already in the store. The container
/// itself is untouched — this is purely a store-warming pass.
///
/// Streams nix's stdout to INFO and stderr to WARN like the
/// `nixos-container` shellouts so progress shows up in journald as
/// it happens. `--no-link` keeps us from littering the working
/// directory with `result` symlinks. Per-derivation cost: pure
/// cache hit when nothing changed (handful of seconds for the
/// eval), expensive only on the rebuild that actually has work.
///
/// Returns the same error shape as the other nixos-container
/// helpers so callers can use `?` without translation.
async fn prebuild_toplevel(name: &str, flake_ref: &str) -> Result<()> {
use tokio::io::{AsyncBufReadExt, BufReader};
let attr = format!("{flake_ref}.config.system.build.toplevel");
let args = vec![
"--extra-experimental-features",
"nix-command flakes",
"build",
"--no-link",
"--print-out-paths",
&attr,
];
let cmdline = format!("nix {}", args.join(" "));
tracing::info!(%name, %cmdline, "prebuild: warming system toplevel");
let mut child = Command::new("nix")
.args(&args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.with_context(|| format!("spawn {cmdline}"))?;
let stdout = child.stdout.take().expect("piped stdout");
let stderr = child.stderr.take().expect("piped stderr");
let stdout_cmdline = cmdline.clone();
let pump_stdout = tokio::spawn(async move {
let mut lines = BufReader::new(stdout).lines();
while let Ok(Some(line)) = lines.next_line().await {
tracing::info!(target: "nix-prebuild", cmdline = %stdout_cmdline, "{line}");
}
});
let stderr_cmdline = cmdline.clone();
let stderr_tail: std::sync::Arc<std::sync::Mutex<std::collections::VecDeque<String>>> =
std::sync::Arc::new(std::sync::Mutex::new(
std::collections::VecDeque::with_capacity(32),
));
let stderr_tail_pump = stderr_tail.clone();
let pump_stderr = tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
while let Ok(Some(line)) = lines.next_line().await {
tracing::warn!(target: "nix-prebuild", cmdline = %stderr_cmdline, "{line}");
let mut tail = stderr_tail_pump.lock().unwrap();
if tail.len() == 32 {
tail.pop_front();
}
tail.push_back(line);
}
});
let status = child
.wait()
.await
.with_context(|| format!("wait {cmdline}"))?;
let _ = pump_stdout.await;
let _ = pump_stderr.await;
if !status.success() {
let tail = stderr_tail
.lock()
.unwrap()
.iter()
.cloned()
.collect::<Vec<_>>()
.join("\n");
bail!("prebuild {cmdline} failed ({status}): {tail}");
}
Ok(())
}
pub async fn list() -> Result<Vec<String>> {
let out = Command::new("nixos-container")
.arg("list")