deb: print raw build output again with --verbose

The record-log wrapper provides a sink for every command, which removed
the runner's subprocess-inherit fallback -- and with it the raw output
of --verbose runs (and of piped stdout, where the live view disables
itself): Quiet's None sink got wrapped inside, every line went to
build.log, and the terminal showed nothing but pkh's own logs, breaking
what --help still promises. When the caller's view has no sink of its
own, a raw terminal sink (stdout/stderr writers, one per stream) now
takes its place inside the record-log wrapper: the terminal shows the
raw lines again while build.log keeps the full tee. A view that
consumes lines itself (the live view on a tty) is wrapped exactly as
before.
This commit is contained in:
2026-09-29 15:19:17 +02:00
parent 562626e878
commit 2de191cf6d
2 changed files with 168 additions and 5 deletions
+70 -5
View File
@@ -364,11 +364,23 @@ async fn build_binary_package_impl(
// Tee every captured build line into the record's log: the wrapper // Tee every captured build line into the record's log: the wrapper
// view hands the combined sink to everything downstream (the chroot // view hands the combined sink to everything downstream (the chroot
// bootstrap, the build phases), so the log is a full tee whatever // bootstrap, the build phases), so the log is a full tee whatever
// renders the output. A record-less build (non-local base context) // renders the output. A view that consumes lines itself (the live
// keeps the plain view. // view on a terminal) is wrapped as-is; a view with NO sink (Quiet,
let record_log: Option<Result<Arc<session::RecordLogSink>, std::io::Error>> = session // i.e. --verbose or a piped stdout) would have let the subprocess
.as_ref() // inherit the terminal — the wrapper removes that fallback, so the
.map(|s| session::RecordLogSink::new(s, opts.view.sink()).map(Arc::new)); // raw lines are written by a terminal sink instead, and --verbose
// keeps showing raw output. A record-less build (non-local base
// context) keeps the plain view.
let record_log: Option<Result<Arc<session::RecordLogSink>, std::io::Error>> =
session.as_ref().map(|s| {
session::RecordLogSink::new(
s,
opts.view
.sink()
.or_else(|| Some(Arc::new(session::RawWriterSink::terminal()))),
)
.map(Arc::new)
});
let view: &dyn BuildView = match record_log { let view: &dyn BuildView = match record_log {
Some(Ok(log)) => &RecordLogView { Some(Ok(log)) => &RecordLogView {
inner: opts.view, inner: opts.view,
@@ -1726,4 +1738,57 @@ mod tests {
"wrong error: {err}" "wrong error: {err}"
); );
} }
/// The record-log view keeps the terminal alive for sink-less
/// views: when the caller's view has no sink of its own (Quiet —
/// `--verbose` or a piped stdout), the raw terminal sink takes its
/// place, and every captured line lands BOTH in the record's
/// build.log and on the writers (the terminal, in production).
#[test]
fn record_log_view_shows_raw_output_when_the_view_has_no_sink() {
use crate::context::{LineSink, Stream};
use crate::deb::session::record_fixtures::SharedWriter;
let (root, _tarball) = sessions_fixture();
let identity = test_identity();
let host_tree = root.path().join("tree");
let record = session::new_record_in(root.path(), &host_tree, &identity, "1.0").unwrap();
// The production wiring, with the terminal sink replaced by
// injectable capture buffers: the Quiet view has no sink, so
// the raw writer sink fills the fallback.
let stdout = SharedWriter::new();
let stderr = SharedWriter::new();
let quiet = crate::report::Quiet;
let fallback: Option<Arc<dyn LineSink>> = quiet.sink().or_else(|| {
Some(Arc::new(session::RawWriterSink::new(
Box::new(stdout.clone()),
Box::new(stderr.clone()),
)))
});
let log = Arc::new(session::RecordLogSink::new(&record, fallback).unwrap());
let view = RecordLogView { inner: &quiet, log };
let sink = view
.sink()
.expect("the record-log view always provides one");
sink.line(Stream::Stdout, "raw stdout line");
sink.line(Stream::Stderr, "raw stderr line");
assert_eq!(
stdout.contents(),
"raw stdout line\n",
"the fallback writer received the raw line"
);
assert_eq!(
stderr.contents(),
"raw stderr line\n",
"stderr lines reach the stderr writer"
);
assert_eq!(
std::fs::read_to_string(record.log_file()).unwrap(),
"raw stdout line\nraw stderr line\n",
"build.log keeps the full tee"
);
}
} }
+98
View File
@@ -1524,6 +1524,49 @@ impl LineSink for RecordLogSink {
} }
} }
/// A sink writing captured lines to two raw writers, one per stream:
/// stdout lines to the first, stderr lines to the second.
///
/// This is the record-log view's stand-in for the subprocess-inherit
/// fallback: with `--verbose` (or a piped stdout, where the live view
/// disables itself) the caller's view is [`crate::report::Quiet`], whose
/// sink is `None` — the runner would let the subprocess inherit the
/// terminal — but the record-log wrapper always provides a sink, so
/// the fallback is never reached anymore. Wrapping this sink in its
/// place keeps the raw lines on the terminal while the record's
/// build.log keeps the full tee.
pub(crate) struct RawWriterSink {
stdout: Mutex<Box<dyn Write + Send>>,
stderr: Mutex<Box<dyn Write + Send>>,
}
impl RawWriterSink {
/// A sink over explicit writer pairs (tests capture into memory).
pub(crate) fn new(stdout: Box<dyn Write + Send>, stderr: Box<dyn Write + Send>) -> Self {
Self {
stdout: Mutex::new(stdout),
stderr: Mutex::new(stderr),
}
}
/// A sink over the real terminal: the process's stdout and stderr.
pub(crate) fn terminal() -> Self {
Self::new(Box::new(io::stdout()), Box::new(io::stderr()))
}
}
impl LineSink for RawWriterSink {
fn line(&self, stream: Stream, line: &str) {
let writer = match stream {
Stream::Stdout => &self.stdout,
Stream::Stderr => &self.stderr,
};
if let Ok(mut out) = writer.lock() {
let _ = writeln!(out, "{line}");
}
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Host-tree snapshot and overlay upperdir sync // Host-tree snapshot and overlay upperdir sync
// //
@@ -1854,6 +1897,34 @@ pub(crate) fn sync_upper_for_resume(session: &Session, host_parent: &Path) -> io
pub(crate) mod record_fixtures { pub(crate) mod record_fixtures {
use super::*; use super::*;
/// A writer copying everything it receives into a shared buffer:
/// lets tests observe what a `Box<dyn Write + Send>` sink wrote.
#[derive(Clone)]
pub(crate) struct SharedWriter(pub Arc<parking_lot::Mutex<Vec<u8>>>);
impl SharedWriter {
/// A fresh empty capture buffer.
pub(crate) fn new() -> Self {
Self(Arc::new(parking_lot::Mutex::new(Vec::new())))
}
/// What has been written so far.
pub(crate) fn contents(&self) -> String {
String::from_utf8_lossy(&self.0.lock()).into_owned()
}
}
impl std::io::Write for SharedWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.lock().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub fn test_identity() -> SessionIdentity { pub fn test_identity() -> SessionIdentity {
SessionIdentity { SessionIdentity {
package: "pkh".to_string(), package: "pkh".to_string(),
@@ -2739,6 +2810,33 @@ mod tests {
assert_eq!(out.len(), 0); assert_eq!(out.len(), 0);
} }
/// The raw writer sink routes each stream to its own writer,
/// newline-terminated: this is what keeps --verbose (and piped
/// runs) showing raw output under the record-log wrapper.
#[test]
fn raw_writer_sink_routes_streams_to_their_writers() {
use record_fixtures::SharedWriter;
let stdout = SharedWriter::new();
let stderr = SharedWriter::new();
let sink = RawWriterSink::new(Box::new(stdout.clone()), Box::new(stderr.clone()));
sink.line(Stream::Stdout, "from stdout");
sink.line(Stream::Stderr, "from stderr");
sink.line(Stream::Stdout, "second line");
assert_eq!(
stdout.contents(),
"from stdout\nsecond line\n",
"stdout lines land in order, newline-terminated"
);
assert_eq!(
stderr.contents(),
"from stderr\n",
"stderr lines land on their own writer only"
);
}
/// Ages render in the coarse units the list promises. /// Ages render in the coarse units the list promises.
#[test] #[test]
fn age_rendering() { fn age_rendering() {