Skip to content

Commit f91037f

Browse files
buildkit: add rawjson progress decoding and streamed build output
1 parent 0db62fb commit f91037f

7 files changed

Lines changed: 708 additions & 8 deletions

File tree

crates/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/vz-cli/src/commands/build.rs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! `vz build` -- Build Dockerfiles into the local vz OCI store.
22
33
use std::collections::BTreeMap;
4+
#[cfg(target_os = "macos")]
5+
use std::io::Write;
46
use std::path::{Path, PathBuf};
57

68
use clap::{Args, Subcommand, ValueEnum};
@@ -99,6 +101,8 @@ pub enum ProgressArg {
99101
Auto,
100102
Plain,
101103
Tty,
104+
#[value(name = "rawjson")]
105+
RawJson,
102106
}
103107

104108
impl From<ProgressArg> for vz_oci_macos::buildkit::BuildProgress {
@@ -107,6 +111,7 @@ impl From<ProgressArg> for vz_oci_macos::buildkit::BuildProgress {
107111
ProgressArg::Auto => Self::Auto,
108112
ProgressArg::Plain => Self::Plain,
109113
ProgressArg::Tty => Self::Tty,
114+
ProgressArg::RawJson => Self::RawJson,
110115
}
111116
}
112117
}
@@ -126,6 +131,7 @@ pub async fn run(args: BuildArgs) -> anyhow::Result<()> {
126131
let build_args = parse_build_args(&args.build_args)?;
127132
let secrets = parse_secrets(&args.secrets)?;
128133
let output = parse_output_mode(args.push, args.output.as_deref())?;
134+
let progress = args.progress;
129135

130136
let request = vz_oci_macos::BuildRequest {
131137
context_dir,
@@ -136,10 +142,15 @@ pub async fn run(args: BuildArgs) -> anyhow::Result<()> {
136142
secrets,
137143
no_cache: args.no_cache,
138144
output,
139-
progress: args.progress.into(),
145+
progress: progress.into(),
140146
};
141147

142-
let result = vz_oci_macos::buildkit::build_image(&config, request).await?;
148+
let mut streamer = BuildEventStreamer::new(progress);
149+
let result = vz_oci_macos::buildkit::build_image_with_events(&config, request, |event| {
150+
streamer.handle(event);
151+
})
152+
.await?;
153+
streamer.finish();
143154
match (&result.image_id, &result.output_path, result.pushed) {
144155
(Some(image_id), _, _) => println!("Built {} as {}", result.tag, image_id.0),
145156
(_, Some(path), _) => {
@@ -163,6 +174,89 @@ pub async fn run(args: BuildArgs) -> anyhow::Result<()> {
163174
}
164175
}
165176

177+
#[cfg(target_os = "macos")]
178+
#[derive(Debug, Clone, Copy, Default)]
179+
struct RawJsonEventCounters {
180+
vertexes: usize,
181+
statuses: usize,
182+
logs: usize,
183+
warnings: usize,
184+
}
185+
186+
#[cfg(target_os = "macos")]
187+
struct BuildEventStreamer {
188+
progress: ProgressArg,
189+
stdout: std::io::Stdout,
190+
stderr: std::io::Stderr,
191+
rawjson: RawJsonEventCounters,
192+
}
193+
194+
#[cfg(target_os = "macos")]
195+
impl BuildEventStreamer {
196+
fn new(progress: ProgressArg) -> Self {
197+
Self {
198+
progress,
199+
stdout: std::io::stdout(),
200+
stderr: std::io::stderr(),
201+
rawjson: RawJsonEventCounters::default(),
202+
}
203+
}
204+
205+
fn handle(&mut self, event: vz_oci_macos::buildkit::BuildEvent) {
206+
use vz_oci_macos::buildkit::{BuildEvent, BuildLogStream};
207+
208+
match event {
209+
BuildEvent::Status { message } => {
210+
let _ = writeln!(self.stderr, "==> {message}");
211+
let _ = self.stderr.flush();
212+
}
213+
BuildEvent::Output { stream, chunk } => match stream {
214+
BuildLogStream::Stdout => {
215+
let _ = self.stdout.write_all(&chunk);
216+
let _ = self.stdout.flush();
217+
}
218+
BuildLogStream::Stderr => {
219+
let _ = self.stderr.write_all(&chunk);
220+
let _ = self.stderr.flush();
221+
}
222+
},
223+
BuildEvent::SolveStatus { status } => {
224+
if matches!(self.progress, ProgressArg::RawJson) {
225+
self.rawjson.vertexes += status.vertexes.len();
226+
self.rawjson.statuses += status.statuses.len();
227+
self.rawjson.logs += status.logs.len();
228+
self.rawjson.warnings += status.warnings.len();
229+
}
230+
}
231+
BuildEvent::RawJsonDecodeError { line, error } => {
232+
if matches!(self.progress, ProgressArg::RawJson) {
233+
let _ = writeln!(
234+
self.stderr,
235+
"warning: failed to parse BuildKit rawjson line ({error}): {line}"
236+
);
237+
let _ = self.stderr.flush();
238+
}
239+
}
240+
}
241+
}
242+
243+
fn finish(&mut self) {
244+
let _ = self.stdout.flush();
245+
let _ = self.stderr.flush();
246+
if matches!(self.progress, ProgressArg::RawJson) {
247+
let _ = writeln!(
248+
self.stderr,
249+
"rawjson summary: vertexes={}, statuses={}, logs={}, warnings={}",
250+
self.rawjson.vertexes,
251+
self.rawjson.statuses,
252+
self.rawjson.logs,
253+
self.rawjson.warnings
254+
);
255+
let _ = self.stderr.flush();
256+
}
257+
}
258+
}
259+
166260
#[cfg(target_os = "macos")]
167261
async fn run_subcommand(
168262
config: vz_oci_macos::RuntimeConfig,
@@ -307,6 +401,7 @@ mod tests {
307401
#![allow(clippy::unwrap_used)]
308402

309403
use super::*;
404+
use clap::ValueEnum;
310405

311406
#[test]
312407
fn parse_build_args_supports_multiple_values() {
@@ -380,4 +475,19 @@ mod tests {
380475
let tag = default_tag(Path::new("/tmp/My App"));
381476
assert_eq!(tag, "my-app:latest");
382477
}
478+
479+
#[test]
480+
fn progress_arg_supports_rawjson_value() {
481+
let parsed = ProgressArg::from_str("rawjson", true).unwrap();
482+
assert!(matches!(parsed, ProgressArg::RawJson));
483+
}
484+
485+
#[test]
486+
fn progress_arg_maps_to_buildkit_progress() {
487+
let mapped: vz_oci_macos::buildkit::BuildProgress = ProgressArg::RawJson.into();
488+
assert!(matches!(
489+
mapped,
490+
vz_oci_macos::buildkit::BuildProgress::RawJson
491+
));
492+
}
383493
}

crates/vz-linux/src/vm.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::time::Duration;
44
use tokio::sync::Mutex;
55
use tokio::time::Instant;
66
use vz::Vm;
7-
use vz::protocol::{ExecOutput, NetworkServiceConfig, OciContainerState, OciExecResult};
7+
use vz::protocol::{ExecEvent, ExecOutput, NetworkServiceConfig, OciContainerState, OciExecResult};
88

99
use crate::grpc_client::{GrpcAgentClient, GrpcPortForwardStream};
1010
use crate::{ExecOptions, LinuxError, LinuxVmConfig, OciExecOptions};
@@ -206,6 +206,86 @@ impl LinuxVm {
206206
})?
207207
}
208208

209+
/// Run a command on the guest and stream output events while buffering final output.
210+
pub async fn exec_capture_streaming<F>(
211+
&self,
212+
command: String,
213+
args: Vec<String>,
214+
timeout: Duration,
215+
on_event: F,
216+
) -> Result<ExecOutput, LinuxError>
217+
where
218+
F: FnMut(&ExecEvent),
219+
{
220+
self.exec_capture_with_options_streaming(
221+
command,
222+
args,
223+
timeout,
224+
ExecOptions::default(),
225+
on_event,
226+
)
227+
.await
228+
}
229+
230+
/// Run a command with explicit execution options and stream output events.
231+
pub async fn exec_capture_with_options_streaming<F>(
232+
&self,
233+
command: String,
234+
args: Vec<String>,
235+
timeout: Duration,
236+
options: ExecOptions,
237+
mut on_event: F,
238+
) -> Result<ExecOutput, LinuxError>
239+
where
240+
F: FnMut(&ExecEvent),
241+
{
242+
self.ensure_grpc().await?;
243+
let mut grpc = self.grpc.lock().await;
244+
let client = grpc
245+
.as_mut()
246+
.ok_or_else(|| LinuxError::Protocol("gRPC client not connected".to_string()))?;
247+
248+
tokio::time::timeout(timeout, async move {
249+
let mut stream = client.exec_stream(command, args, options).await?;
250+
let mut stdout_bytes = Vec::new();
251+
let mut stderr_bytes = Vec::new();
252+
let mut saw_exit = false;
253+
let mut exit_code = -1;
254+
255+
while let Some(event) = stream.next().await {
256+
on_event(&event);
257+
match event {
258+
ExecEvent::Stdout(data) => stdout_bytes.extend_from_slice(&data),
259+
ExecEvent::Stderr(data) => stderr_bytes.extend_from_slice(&data),
260+
ExecEvent::Exit(code) => {
261+
saw_exit = true;
262+
exit_code = code;
263+
break;
264+
}
265+
}
266+
}
267+
268+
if !saw_exit {
269+
return Err(LinuxError::Protocol(
270+
"exec stream ended without exit code".to_string(),
271+
));
272+
}
273+
274+
Ok(ExecOutput {
275+
exit_code,
276+
stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(),
277+
stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(),
278+
})
279+
})
280+
.await
281+
.map_err(|_| {
282+
LinuxError::Protocol(format!(
283+
"exec timed out after {:.3}s",
284+
timeout.as_secs_f64()
285+
))
286+
})?
287+
}
288+
209289
/// Open a dedicated port-forward stream to a guest-local target port.
210290
pub async fn open_port_forward_stream(
211291
&self,

crates/vz-oci-macos/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ shell-words = { workspace = true }
2424
reqwest = { workspace = true }
2525
sha2 = { workspace = true }
2626
oci-distribution = { workspace = true }
27+
base64 = { workspace = true }
2728

2829
[dev-dependencies]
2930
tempfile = { workspace = true }

0 commit comments

Comments
 (0)