diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index a31b1cf8..7ab4d2ba 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -772,7 +772,7 @@ async fn container_flake_action( let toplevel = nix_build_toplevel(name, stream.then_some(&mut *writer)).await?; let system_name = container_system_name(name); if verb == "update" { - return swap_container_profile(&system_name, &toplevel).await; + return swap_container_profile(&system_name, &toplevel, stream, writer).await; } let args = [verb, &system_name, "--system-path", &toplevel]; if stream { @@ -790,10 +790,16 @@ async fn container_flake_action( /// it's currently running — the container's own next start already reads /// from the profile, so a stopped container needs nothing further. /// -/// Deliberately not stream-forwarded to `writer` — both operations here -/// are near-instant (a profile symlink swap, a `systemctl reload`), unlike -/// the multi-minute build `container_flake_action` already streams. -async fn swap_container_profile(system_name: &str, toplevel: &str) -> Result<(String, String)> { +/// Both steps are near-instant in the happy case, but still stream- +/// forwarded like the rest of this operation: a failure here (e.g. a +/// wedged `systemctl reload`) is exactly when the caller most wants the +/// live line in the build log, not just a summary error afterward. +async fn swap_container_profile( + system_name: &str, + toplevel: &str, + stream: bool, + writer: &mut OwnedWriteHalf, +) -> Result<(String, String)> { let profile = format!("/nix/var/nix/profiles/per-container/{system_name}/system"); let set_out = Command::new("nix-env") .args(["-p", &profile, "--set", toplevel]) @@ -802,12 +808,7 @@ async fn swap_container_profile(system_name: &str, toplevel: &str) -> Result<(St .context("invoke nix-env --set")?; let mut stdout = String::from_utf8_lossy(&set_out.stdout).into_owned(); let mut stderr = String::from_utf8_lossy(&set_out.stderr).into_owned(); - for line in stdout.lines() { - tracing::info!(target: "nixos-container", "{line}"); - } - for line in stderr.lines() { - tracing::warn!(target: "nixos-container", "{line}"); - } + log_and_forward(&stdout, &stderr, stream, writer).await; if !set_out.status.success() { bail!( "nix-env -p {profile} --set failed ({}): {}", @@ -831,12 +832,7 @@ async fn swap_container_profile(system_name: &str, toplevel: &str) -> Result<(St .context("invoke systemctl reload")?; let r_stdout = String::from_utf8_lossy(&reload_out.stdout).into_owned(); let r_stderr = String::from_utf8_lossy(&reload_out.stderr).into_owned(); - for line in r_stdout.lines() { - tracing::info!(target: "nixos-container", "{line}"); - } - for line in r_stderr.lines() { - tracing::warn!(target: "nixos-container", "{line}"); - } + log_and_forward(&r_stdout, &r_stderr, stream, writer).await; if !reload_out.status.success() { bail!( "systemctl reload {unit} failed ({}): {}", @@ -850,6 +846,26 @@ async fn swap_container_profile(system_name: &str, toplevel: &str) -> Result<(St Ok((stdout, stderr)) } +/// Log a command's captured stdout/stderr the same way every other +/// `nixos-container`-adjacent shellout in this file does, and — when +/// `stream` is set — forward each line to the client as a live +/// [`PrivEvent::Line`] too, so a caller watching the build log sees these +/// lines exactly like any other step's, not just a summary on failure. +async fn log_and_forward(stdout: &str, stderr: &str, stream: bool, writer: &mut OwnedWriteHalf) { + for line in stdout.lines() { + tracing::info!(target: "nixos-container", "{line}"); + if stream { + write_line_event(writer, PrivStream::Stdout, line).await; + } + } + for line in stderr.lines() { + tracing::warn!(target: "nixos-container", "{line}"); + if stream { + write_line_event(writer, PrivStream::Stderr, line).await; + } + } +} + /// The explicit `nixosConfigurations..config.system.build.toplevel` /// flake attr path — same construction `hive-c0re`'s own /// `lifecycle::prebuild_toplevel` uses, kept here as a pure function so