@@ -10,6 +10,7 @@ use rustc_hash::FxHashMap;
10
10
use serde:: de:: { value, SeqAccess , Visitor } ;
11
11
use serde:: { de, Deserialize , Deserializer } ;
12
12
use std:: ffi:: OsString ;
13
+ use std:: fmt:: Write ;
13
14
use std:: fmt:: { Display , Formatter } ;
14
15
use std:: io;
15
16
use std:: path:: { Path , PathBuf } ;
@@ -29,7 +30,7 @@ use distribution_types::Resolution;
29
30
use pep440_rs:: Version ;
30
31
use pep508_rs:: PackageName ;
31
32
use pypi_types:: { Requirement , VerbatimParsedUrl } ;
32
- use uv_configuration:: { BuildKind , ConfigSettings } ;
33
+ use uv_configuration:: { BuildKind , BuildOutput , ConfigSettings } ;
33
34
use uv_fs:: { rename_with_retry, PythonExt , Simplified } ;
34
35
use uv_python:: { Interpreter , PythonEnvironment } ;
35
36
use uv_types:: { BuildContext , BuildIsolation , SourceBuildTrait } ;
@@ -93,22 +94,34 @@ pub enum Error {
93
94
#[ error( "Failed to run `{0}`" ) ]
94
95
CommandFailed ( PathBuf , #[ source] io:: Error ) ,
95
96
#[ error( "{message} with {exit_code}\n --- stdout:\n {stdout}\n --- stderr:\n {stderr}\n ---" ) ]
96
- BuildBackend {
97
+ BuildBackendOutput {
97
98
message : String ,
98
99
exit_code : ExitStatus ,
99
100
stdout : String ,
100
101
stderr : String ,
101
102
} ,
102
103
/// Nudge the user towards installing the missing dev library
103
104
#[ error( "{message} with {exit_code}\n --- stdout:\n {stdout}\n --- stderr:\n {stderr}\n ---" ) ]
104
- MissingHeader {
105
+ MissingHeaderOutput {
105
106
message : String ,
106
107
exit_code : ExitStatus ,
107
108
stdout : String ,
108
109
stderr : String ,
109
110
#[ source]
110
111
missing_header_cause : MissingHeaderCause ,
111
112
} ,
113
+ #[ error( "{message} with {exit_code}" ) ]
114
+ BuildBackend {
115
+ message : String ,
116
+ exit_code : ExitStatus ,
117
+ } ,
118
+ #[ error( "{message} with {exit_code}" ) ]
119
+ MissingHeader {
120
+ message : String ,
121
+ exit_code : ExitStatus ,
122
+ #[ source]
123
+ missing_header_cause : MissingHeaderCause ,
124
+ } ,
112
125
#[ error( "Failed to build PATH for build script" ) ]
113
126
BuildScriptPath ( #[ source] env:: JoinPathsError ) ,
114
127
}
@@ -161,6 +174,7 @@ impl Error {
161
174
fn from_command_output (
162
175
message : String ,
163
176
output : & PythonRunnerOutput ,
177
+ level : BuildOutput ,
164
178
version_id : impl Into < String > ,
165
179
) -> Self {
166
180
// In the cases I've seen it was the 5th and 3rd last line (see test case), 10 seems like a reasonable cutoff.
@@ -186,24 +200,71 @@ impl Error {
186
200
} ) ;
187
201
188
202
if let Some ( missing_library) = missing_library {
189
- return Self :: MissingHeader {
203
+ return match level {
204
+ BuildOutput :: Stderr => Self :: MissingHeader {
205
+ message,
206
+ exit_code : output. status ,
207
+ missing_header_cause : MissingHeaderCause {
208
+ missing_library,
209
+ version_id : version_id. into ( ) ,
210
+ } ,
211
+ } ,
212
+ BuildOutput :: Debug => Self :: MissingHeaderOutput {
213
+ message,
214
+ exit_code : output. status ,
215
+ stdout : output. stdout . iter ( ) . join ( "\n " ) ,
216
+ stderr : output. stderr . iter ( ) . join ( "\n " ) ,
217
+ missing_header_cause : MissingHeaderCause {
218
+ missing_library,
219
+ version_id : version_id. into ( ) ,
220
+ } ,
221
+ } ,
222
+ } ;
223
+ }
224
+
225
+ match level {
226
+ BuildOutput :: Stderr => Self :: BuildBackend {
227
+ message,
228
+ exit_code : output. status ,
229
+ } ,
230
+ BuildOutput :: Debug => Self :: BuildBackendOutput {
190
231
message,
191
232
exit_code : output. status ,
192
233
stdout : output. stdout . iter ( ) . join ( "\n " ) ,
193
234
stderr : output. stderr . iter ( ) . join ( "\n " ) ,
194
- missing_header_cause : MissingHeaderCause {
195
- missing_library,
196
- version_id : version_id. into ( ) ,
197
- } ,
198
- } ;
235
+ } ,
236
+ }
237
+ }
238
+ }
239
+
240
+ #[ derive( Debug , Copy , Clone , PartialEq , Eq ) ]
241
+ pub enum Printer {
242
+ /// Send the build backend output to `stderr`.
243
+ Stderr ,
244
+ /// Send the build backend output to `tracing`.
245
+ Debug ,
246
+ }
247
+
248
+ impl From < BuildOutput > for Printer {
249
+ fn from ( output : BuildOutput ) -> Self {
250
+ match output {
251
+ BuildOutput :: Stderr => Self :: Stderr ,
252
+ BuildOutput :: Debug => Self :: Debug ,
199
253
}
254
+ }
255
+ }
200
256
201
- Self :: BuildBackend {
202
- message,
203
- exit_code : output. status ,
204
- stdout : output. stdout . iter ( ) . join ( "\n " ) ,
205
- stderr : output. stderr . iter ( ) . join ( "\n " ) ,
257
+ impl Write for Printer {
258
+ fn write_str ( & mut self , s : & str ) -> std:: fmt:: Result {
259
+ match self {
260
+ Self :: Stderr => {
261
+ anstream:: eprint!( "{s}" ) ;
262
+ }
263
+ Self :: Debug => {
264
+ debug ! ( "{}" , s) ;
265
+ }
206
266
}
267
+ Ok ( ( ) )
207
268
}
208
269
}
209
270
@@ -380,6 +441,8 @@ pub struct SourceBuild {
380
441
version_id : String ,
381
442
/// Whether we do a regular PEP 517 build or an PEP 660 editable build
382
443
build_kind : BuildKind ,
444
+ /// Whether to send build output to `stderr` or `tracing`, etc.
445
+ level : BuildOutput ,
383
446
/// Modified PATH that contains the `venv_bin`, `user_path` and `system_path` variables in that order
384
447
modified_path : OsString ,
385
448
/// Environment variables to be passed in during metadata or wheel building
@@ -405,6 +468,7 @@ impl SourceBuild {
405
468
build_isolation : BuildIsolation < ' _ > ,
406
469
build_kind : BuildKind ,
407
470
mut environment_variables : FxHashMap < OsString , OsString > ,
471
+ level : BuildOutput ,
408
472
concurrent_builds : usize ,
409
473
) -> Result < Self , Error > {
410
474
let temp_dir = build_context. cache ( ) . environment ( ) ?;
@@ -488,7 +552,7 @@ impl SourceBuild {
488
552
489
553
// Create the PEP 517 build environment. If build isolation is disabled, we assume the build
490
554
// environment is already setup.
491
- let runner = PythonRunner :: new ( concurrent_builds) ;
555
+ let runner = PythonRunner :: new ( concurrent_builds, level ) ;
492
556
if build_isolation. is_isolated ( package_name) {
493
557
create_pep517_build_environment (
494
558
& runner,
@@ -498,6 +562,7 @@ impl SourceBuild {
498
562
build_context,
499
563
& version_id,
500
564
build_kind,
565
+ level,
501
566
& config_settings,
502
567
& environment_variables,
503
568
& modified_path,
@@ -513,6 +578,7 @@ impl SourceBuild {
513
578
project,
514
579
venv,
515
580
build_kind,
581
+ level,
516
582
config_settings,
517
583
metadata_directory : None ,
518
584
version_id,
@@ -698,6 +764,7 @@ impl SourceBuild {
698
764
return Err ( Error :: from_command_output (
699
765
format ! ( "Build backend failed to determine metadata through `prepare_metadata_for_build_{}`" , self . build_kind) ,
700
766
& output,
767
+ self . level ,
701
768
& self . version_id ,
702
769
) ) ;
703
770
}
@@ -827,6 +894,7 @@ impl SourceBuild {
827
894
self . build_kind, self . build_kind,
828
895
) ,
829
896
& output,
897
+ self . level ,
830
898
& self . version_id ,
831
899
) ) ;
832
900
}
@@ -839,6 +907,7 @@ impl SourceBuild {
839
907
self . build_kind, self . build_kind,
840
908
) ,
841
909
& output,
910
+ self . level ,
842
911
& self . version_id ,
843
912
) ) ;
844
913
}
@@ -871,6 +940,7 @@ async fn create_pep517_build_environment(
871
940
build_context : & impl BuildContext ,
872
941
version_id : & str ,
873
942
build_kind : BuildKind ,
943
+ level : BuildOutput ,
874
944
config_settings : & ConfigSettings ,
875
945
environment_variables : & FxHashMap < OsString , OsString > ,
876
946
modified_path : & OsString ,
@@ -924,6 +994,7 @@ async fn create_pep517_build_environment(
924
994
return Err ( Error :: from_command_output (
925
995
format ! ( "Build backend failed to determine extra requires with `build_{build_kind}()`" ) ,
926
996
& output,
997
+ level,
927
998
version_id,
928
999
) ) ;
929
1000
}
@@ -935,6 +1006,7 @@ async fn create_pep517_build_environment(
935
1006
"Build backend failed to read extra requires from `get_requires_for_build_{build_kind}`: {err}"
936
1007
) ,
937
1008
& output,
1009
+ level,
938
1010
version_id,
939
1011
)
940
1012
} ) ?;
@@ -946,6 +1018,7 @@ async fn create_pep517_build_environment(
946
1018
"Build backend failed to return extra requires with `get_requires_for_build_{build_kind}`: {err}"
947
1019
) ,
948
1020
& output,
1021
+ level,
949
1022
version_id,
950
1023
)
951
1024
} ) ?;
@@ -985,6 +1058,7 @@ async fn create_pep517_build_environment(
985
1058
#[ derive( Debug ) ]
986
1059
struct PythonRunner {
987
1060
control : Semaphore ,
1061
+ level : BuildOutput ,
988
1062
}
989
1063
990
1064
#[ derive( Debug ) ]
@@ -995,10 +1069,11 @@ struct PythonRunnerOutput {
995
1069
}
996
1070
997
1071
impl PythonRunner {
998
- /// Create a `PythonRunner` with the provided concurrency limit.
999
- fn new ( concurrency : usize ) -> PythonRunner {
1000
- PythonRunner {
1072
+ /// Create a `PythonRunner` with the provided concurrency limit and output level .
1073
+ fn new ( concurrency : usize , level : BuildOutput ) -> Self {
1074
+ Self {
1001
1075
control : Semaphore :: new ( concurrency) ,
1076
+ level,
1002
1077
}
1003
1078
}
1004
1079
@@ -1019,12 +1094,13 @@ impl PythonRunner {
1019
1094
/// Read lines from a reader and store them in a buffer.
1020
1095
async fn read_from (
1021
1096
mut reader : tokio:: io:: Lines < tokio:: io:: BufReader < impl tokio:: io:: AsyncRead + Unpin > > ,
1097
+ mut printer : Printer ,
1022
1098
buffer : & mut Vec < String > ,
1023
1099
) -> io:: Result < ( ) > {
1024
1100
loop {
1025
1101
match reader. next_line ( ) . await ? {
1026
1102
Some ( line) => {
1027
- debug ! ( "{line}" ) ;
1103
+ let _ = writeln ! ( printer , "{line}" ) ;
1028
1104
buffer. push ( line) ;
1029
1105
}
1030
1106
None => return Ok ( ( ) ) ,
@@ -1055,9 +1131,10 @@ impl PythonRunner {
1055
1131
let stderr_reader = tokio:: io:: BufReader :: new ( child. stderr . take ( ) . unwrap ( ) ) . lines ( ) ;
1056
1132
1057
1133
// Asynchronously read from the in-memory pipes.
1134
+ let printer = Printer :: from ( self . level ) ;
1058
1135
let result = tokio:: join!(
1059
- read_from( stdout_reader, & mut stdout_buf) ,
1060
- read_from( stderr_reader, & mut stderr_buf) ,
1136
+ read_from( stdout_reader, printer , & mut stdout_buf) ,
1137
+ read_from( stderr_reader, printer , & mut stderr_buf) ,
1061
1138
) ;
1062
1139
match result {
1063
1140
( Ok ( ( ) ) , Ok ( ( ) ) ) => { }
@@ -1087,9 +1164,9 @@ impl PythonRunner {
1087
1164
mod test {
1088
1165
use std:: process:: ExitStatus ;
1089
1166
1090
- use indoc:: indoc;
1091
-
1092
1167
use crate :: { Error , PythonRunnerOutput } ;
1168
+ use indoc:: indoc;
1169
+ use uv_configuration:: BuildOutput ;
1093
1170
1094
1171
#[ test]
1095
1172
fn missing_header ( ) {
@@ -1120,9 +1197,10 @@ mod test {
1120
1197
let err = Error :: from_command_output (
1121
1198
"Failed building wheel through setup.py" . to_string ( ) ,
1122
1199
& output,
1200
+ BuildOutput :: Debug ,
1123
1201
"pygraphviz-1.11" ,
1124
1202
) ;
1125
- assert ! ( matches!( err, Error :: MissingHeader { .. } ) ) ;
1203
+ assert ! ( matches!( err, Error :: MissingHeaderOutput { .. } ) ) ;
1126
1204
// Unix uses exit status, Windows uses exit code.
1127
1205
let formatted = err. to_string ( ) . replace ( "exit status: " , "exit code: " ) ;
1128
1206
insta:: assert_snapshot!( formatted, @r###"
@@ -1172,9 +1250,10 @@ mod test {
1172
1250
let err = Error :: from_command_output (
1173
1251
"Failed building wheel through setup.py" . to_string ( ) ,
1174
1252
& output,
1253
+ BuildOutput :: Debug ,
1175
1254
"pygraphviz-1.11" ,
1176
1255
) ;
1177
- assert ! ( matches!( err, Error :: MissingHeader { .. } ) ) ;
1256
+ assert ! ( matches!( err, Error :: MissingHeaderOutput { .. } ) ) ;
1178
1257
// Unix uses exit status, Windows uses exit code.
1179
1258
let formatted = err. to_string ( ) . replace ( "exit status: " , "exit code: " ) ;
1180
1259
insta:: assert_snapshot!( formatted, @r###"
@@ -1217,9 +1296,10 @@ mod test {
1217
1296
let err = Error :: from_command_output (
1218
1297
"Failed building wheel through setup.py" . to_string ( ) ,
1219
1298
& output,
1299
+ BuildOutput :: Debug ,
1220
1300
"pygraphviz-1.11" ,
1221
1301
) ;
1222
- assert ! ( matches!( err, Error :: MissingHeader { .. } ) ) ;
1302
+ assert ! ( matches!( err, Error :: MissingHeaderOutput { .. } ) ) ;
1223
1303
// Unix uses exit status, Windows uses exit code.
1224
1304
let formatted = err. to_string ( ) . replace ( "exit status: " , "exit code: " ) ;
1225
1305
insta:: assert_snapshot!( formatted, @r###"
0 commit comments