@@ -2,6 +2,7 @@ use std::{
22 collections:: HashMap ,
33 convert:: Infallible ,
44 ffi:: { OsStr , OsString } ,
5+ io:: { Read , Write } ,
56 path:: PathBuf ,
67 str:: FromStr ,
78 sync:: { Mutex , OnceLock } ,
@@ -19,6 +20,118 @@ use which::which;
1920
2021use crate :: { args:: NixBuildPassthroughArgs , installable:: Installable } ;
2122
23+ /// Execute a command, streaming output to stdout/stderr while optionally
24+ /// capturing it for error reporting.
25+ ///
26+ /// # Arguments
27+ ///
28+ /// * `capture_output` - When `true`, stdout and stderr are accumulated and
29+ /// returned as strings. When `false`, output is streamed but not captured.
30+ ///
31+ /// # Returns
32+ ///
33+ /// Returns the exit status and captured stdout/stderr (or empty strings if
34+ /// `capture_output` is `false`), or an error.
35+ ///
36+ /// # Errors
37+ ///
38+ /// Returns an error if:
39+ ///
40+ /// - The command fails to start
41+ /// - stdout or stderr cannot be captured
42+ /// - The command fails to complete
43+ /// - Either output thread panics
44+ pub fn exec_with_streaming (
45+ cmd : Exec ,
46+ capture_output : bool ,
47+ ) -> Result < ( subprocess:: ExitStatus , String , String ) > {
48+ let mut job = cmd
49+ . stdout ( Redirection :: Pipe )
50+ . start ( )
51+ . wrap_err ( "Failed to start command" ) ?;
52+
53+ let stdout_pipe = job
54+ . stdout
55+ . take ( )
56+ . ok_or_else ( || eyre:: eyre!( "Failed to capture stdout" ) ) ?;
57+
58+ let stdout_thread = std:: thread:: spawn ( move || {
59+ let mut stdout_reader = std:: io:: BufReader :: new ( stdout_pipe) ;
60+ let mut stdout_bytes = Vec :: new ( ) ;
61+ let mut stdout_buf = [ 0u8 ; 4096 ] ;
62+
63+ loop {
64+ match stdout_reader. read ( & mut stdout_buf) {
65+ Ok ( 0 ) => break ,
66+ Ok ( n) => {
67+ let _ = std:: io:: stdout ( ) . write_all ( & stdout_buf[ ..n] ) ;
68+ let _ = std:: io:: stdout ( ) . flush ( ) ;
69+ if capture_output {
70+ stdout_bytes. extend_from_slice ( & stdout_buf[ ..n] ) ;
71+ }
72+ } ,
73+ Err ( e) => {
74+ debug ! ( "stdout read error: {e}" ) ;
75+ break ;
76+ } ,
77+ }
78+ }
79+
80+ if capture_output {
81+ String :: from_utf8_lossy ( & stdout_bytes) . into_owned ( )
82+ } else {
83+ String :: new ( )
84+ }
85+ } ) ;
86+
87+ let stderr_thread = if let Some ( stderr_pipe) = job. stderr . take ( ) {
88+ Some ( std:: thread:: spawn ( move || {
89+ let mut stderr_reader = std:: io:: BufReader :: new ( stderr_pipe) ;
90+ let mut stderr_bytes = Vec :: new ( ) ;
91+ let mut stderr_buf = [ 0u8 ; 4096 ] ;
92+
93+ loop {
94+ match stderr_reader. read ( & mut stderr_buf) {
95+ Ok ( 0 ) => break ,
96+ Ok ( n) => {
97+ let _ = std:: io:: stderr ( ) . write_all ( & stderr_buf[ ..n] ) ;
98+ let _ = std:: io:: stderr ( ) . flush ( ) ;
99+ if capture_output {
100+ stderr_bytes. extend_from_slice ( & stderr_buf[ ..n] ) ;
101+ }
102+ } ,
103+ Err ( e) => {
104+ debug ! ( "stderr read error: {e}" ) ;
105+ break ;
106+ } ,
107+ }
108+ }
109+
110+ if capture_output {
111+ String :: from_utf8_lossy ( & stderr_bytes) . into_owned ( )
112+ } else {
113+ String :: new ( )
114+ }
115+ } ) )
116+ } else {
117+ None
118+ } ;
119+
120+ let exit_status = job
121+ . wait ( )
122+ . wrap_err ( "Failed to wait for command completion" ) ?;
123+
124+ let stdout_output = stdout_thread
125+ . join ( )
126+ . map_err ( |_| eyre:: eyre!( "Stdout thread panicked" ) ) ?;
127+ let stderr_output = stderr_thread
128+ . map ( |t| t. join ( ) . map_err ( |_| eyre:: eyre!( "Stderr thread panicked" ) ) )
129+ . transpose ( ) ?
130+ . unwrap_or_default ( ) ;
131+
132+ Ok ( ( exit_status, stdout_output, stderr_output) )
133+ }
134+
22135static PASSWORD_CACHE : OnceLock < Mutex < HashMap < String , SecretString > > > =
23136 OnceLock :: new ( ) ;
24137
@@ -729,26 +842,38 @@ impl Command {
729842 . message
730843 . clone ( )
731844 . unwrap_or_else ( || "Command failed" . to_string ( ) ) ;
732- let res = cmd. capture ( ) ;
733- match res {
734- Ok ( capture) => {
735- let status = & capture. exit_status ;
736- if !status. success ( ) {
737- let stderr = capture. stderr_str ( ) ;
738- if stderr. trim ( ) . is_empty ( ) {
845+
846+ if self . show_output {
847+ let exit_status = cmd. join ( ) . wrap_err ( msg. clone ( ) ) ?;
848+ if !exit_status. success ( ) {
849+ return Err ( eyre:: eyre!( format!(
850+ "{} (exit status {:?})" ,
851+ msg, exit_status
852+ ) ) ) ;
853+ }
854+ Ok ( ( ) )
855+ } else {
856+ let res = cmd. capture ( ) ;
857+ match res {
858+ Ok ( capture) => {
859+ let status = & capture. exit_status ;
860+ if !status. success ( ) {
861+ let stderr = capture. stderr_str ( ) ;
862+ if stderr. trim ( ) . is_empty ( ) {
863+ return Err ( eyre:: eyre!( format!(
864+ "{} (exit status {:?})" ,
865+ msg, status
866+ ) ) ) ;
867+ }
739868 return Err ( eyre:: eyre!( format!(
740- "{} (exit status {:?})" ,
741- msg, status
869+ "{} (exit status {:?})\n stderr: \n {} " ,
870+ msg, status, stderr
742871 ) ) ) ;
743872 }
744- return Err ( eyre:: eyre!( format!(
745- "{} (exit status {:?})\n stderr:\n {}" ,
746- msg, status, stderr
747- ) ) ) ;
748- }
749- Ok ( ( ) )
750- } ,
751- Err ( e) => Err ( e) . wrap_err ( msg) ,
873+ Ok ( ( ) )
874+ } ,
875+ Err ( e) => Err ( e) . wrap_err ( msg) ,
876+ }
752877 }
753878 }
754879
0 commit comments