@@ -27,7 +27,7 @@ use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceError};
27
27
/// Build source distributions and wheels.
28
28
#[ allow( clippy:: fn_params_excessive_bools) ]
29
29
pub ( crate ) async fn build (
30
- src_dir : Option < PathBuf > ,
30
+ src : Option < PathBuf > ,
31
31
output_dir : Option < PathBuf > ,
32
32
sdist : bool ,
33
33
wheel : bool ,
@@ -43,7 +43,7 @@ pub(crate) async fn build(
43
43
printer : Printer ,
44
44
) -> Result < ExitStatus > {
45
45
let assets = build_impl (
46
- src_dir . as_deref ( ) ,
46
+ src . as_deref ( ) ,
47
47
output_dir. as_deref ( ) ,
48
48
sdist,
49
49
wheel,
@@ -81,7 +81,7 @@ pub(crate) async fn build(
81
81
82
82
#[ allow( clippy:: fn_params_excessive_bools) ]
83
83
async fn build_impl (
84
- src_dir : Option < & Path > ,
84
+ src : Option < & Path > ,
85
85
output_dir : Option < & Path > ,
86
86
sdist : bool ,
87
87
wheel : bool ,
@@ -118,41 +118,63 @@ async fn build_impl(
118
118
. connectivity ( connectivity)
119
119
. native_tls ( native_tls) ;
120
120
121
- let src_dir = if let Some ( src_dir) = src_dir {
122
- Cow :: Owned ( std:: path:: absolute ( src_dir) ?)
121
+ let src = if let Some ( src) = src {
122
+ let src = std:: path:: absolute ( src) ?;
123
+ let metadata = match fs_err:: tokio:: metadata ( & src) . await {
124
+ Ok ( metadata) => metadata,
125
+ Err ( err) if err. kind ( ) == std:: io:: ErrorKind :: NotFound => {
126
+ return Err ( anyhow:: anyhow!(
127
+ "Source `{}` does not exist" ,
128
+ src. user_display( )
129
+ ) ) ;
130
+ }
131
+ Err ( err) => return Err ( err. into ( ) ) ,
132
+ } ;
133
+ if metadata. is_file ( ) {
134
+ Source :: File ( Cow :: Owned ( src) )
135
+ } else {
136
+ Source :: Directory ( Cow :: Owned ( src) )
137
+ }
123
138
} else {
124
- Cow :: Borrowed ( & * CWD )
139
+ Source :: Directory ( Cow :: Borrowed ( & * CWD ) )
140
+ } ;
141
+
142
+ let src_dir = match src {
143
+ Source :: Directory ( ref src) => src,
144
+ Source :: File ( ref src) => src. parent ( ) . unwrap ( ) ,
125
145
} ;
126
146
127
147
let output_dir = if let Some ( output_dir) = output_dir {
128
- std:: path:: absolute ( output_dir) ?
148
+ Cow :: Owned ( std:: path:: absolute ( output_dir) ?)
129
149
} else {
130
- src_dir. join ( "dist" )
150
+ match src {
151
+ Source :: Directory ( ref src) => Cow :: Owned ( src. join ( "dist" ) ) ,
152
+ Source :: File ( ref src) => Cow :: Borrowed ( src. parent ( ) . unwrap ( ) ) ,
153
+ }
131
154
} ;
132
155
133
156
// (1) Explicit request from user
134
157
let mut interpreter_request = python_request. map ( PythonRequest :: parse) ;
135
158
136
159
// (2) Request from `.python-version`
137
160
if interpreter_request. is_none ( ) {
138
- interpreter_request = PythonVersionFile :: discover ( src_dir. as_ref ( ) , no_config, false )
161
+ interpreter_request = PythonVersionFile :: discover ( & src_dir, no_config, false )
139
162
. await ?
140
163
. and_then ( PythonVersionFile :: into_version) ;
141
164
}
142
165
143
166
// (3) `Requires-Python` in `pyproject.toml`
144
167
if interpreter_request. is_none ( ) {
145
- let project =
146
- match VirtualProject :: discover ( src_dir. as_ref ( ) , & DiscoveryOptions :: default ( ) ) . await {
147
- Ok ( project) => Some ( project) ,
148
- Err ( WorkspaceError :: MissingProject ( _) ) => None ,
149
- Err ( WorkspaceError :: MissingPyprojectToml ) => None ,
150
- Err ( WorkspaceError :: NonWorkspace ( _) ) => None ,
151
- Err ( err) => {
152
- warn_user_once ! ( "{err}" ) ;
153
- None
154
- }
155
- } ;
168
+ let project = match VirtualProject :: discover ( src_dir, & DiscoveryOptions :: default ( ) ) . await {
169
+ Ok ( project) => Some ( project) ,
170
+ Err ( WorkspaceError :: MissingProject ( _) ) => None ,
171
+ Err ( WorkspaceError :: MissingPyprojectToml ) => None ,
172
+ Err ( WorkspaceError :: NonWorkspace ( _) ) => None ,
173
+ Err ( err) => {
174
+ warn_user_once ! ( "{err}" ) ;
175
+ None
176
+ }
177
+ } ;
156
178
157
179
if let Some ( project) = project {
158
180
interpreter_request = find_requires_python ( project. workspace ( ) ) ?
@@ -242,27 +264,49 @@ async fn build_impl(
242
264
concurrency,
243
265
) ;
244
266
267
+ // Create the output directory.
245
268
fs_err:: tokio:: create_dir_all ( & output_dir) . await ?;
246
269
247
- // Determine the build plan from the command-line arguments.
248
- let plan = match ( sdist, wheel) {
249
- ( false , false ) => BuildPlan :: SdistToWheel ,
250
- ( true , false ) => BuildPlan :: Sdist ,
251
- ( false , true ) => BuildPlan :: Wheel ,
252
- ( true , true ) => BuildPlan :: SdistAndWheel ,
270
+ // Determine the build plan.
271
+ let plan = match & src {
272
+ Source :: File ( _) => {
273
+ // We're building from a file, which must be a source distribution.
274
+ match ( sdist, wheel) {
275
+ ( false , true ) => BuildPlan :: WheelFromSdist ,
276
+ ( false , false ) => {
277
+ return Err ( anyhow:: anyhow!(
278
+ "Pass `--wheel` explicitly to build a wheel from a source distribution"
279
+ ) ) ;
280
+ }
281
+ ( true , _) => {
282
+ return Err ( anyhow:: anyhow!(
283
+ "Building an `--sdist` from a source distribution is not supported"
284
+ ) ) ;
285
+ }
286
+ }
287
+ }
288
+ Source :: Directory ( _) => {
289
+ // We're building from a directory.
290
+ match ( sdist, wheel) {
291
+ ( false , false ) => BuildPlan :: SdistToWheel ,
292
+ ( false , true ) => BuildPlan :: Wheel ,
293
+ ( true , false ) => BuildPlan :: Sdist ,
294
+ ( true , true ) => BuildPlan :: SdistAndWheel ,
295
+ }
296
+ }
253
297
} ;
254
298
255
299
// Prepare some common arguments for the build.
256
300
let subdirectory = None ;
257
- let version_id = src_dir . file_name ( ) . unwrap ( ) . to_string_lossy ( ) ;
301
+ let version_id = src . path ( ) . file_name ( ) . unwrap ( ) . to_string_lossy ( ) ;
258
302
let dist = None ;
259
303
260
304
let assets = match plan {
261
305
BuildPlan :: SdistToWheel => {
262
306
// Build the sdist.
263
307
let builder = build_dispatch
264
308
. setup_build (
265
- src_dir . as_ref ( ) ,
309
+ src . path ( ) ,
266
310
subdirectory,
267
311
& version_id,
268
312
dist,
@@ -274,7 +318,9 @@ async fn build_impl(
274
318
// Extract the source distribution into a temporary directory.
275
319
let path = output_dir. join ( & sdist) ;
276
320
let reader = fs_err:: tokio:: File :: open ( & path) . await ?;
277
- let ext = SourceDistExtension :: from_path ( & path) ?;
321
+ let ext = SourceDistExtension :: from_path ( path. as_path ( ) ) . map_err ( |err| {
322
+ anyhow:: anyhow!( "`{}` is not a valid source distribution, as it ends with an unsupported extension. Expected one of: {err}." , path. user_display( ) )
323
+ } ) ?;
278
324
let temp_dir = tempfile:: tempdir_in ( & output_dir) ?;
279
325
uv_extract:: stream:: archive ( reader, ext, temp_dir. path ( ) ) . await ?;
280
326
@@ -302,7 +348,7 @@ async fn build_impl(
302
348
BuildPlan :: Sdist => {
303
349
let builder = build_dispatch
304
350
. setup_build (
305
- src_dir . as_ref ( ) ,
351
+ src . path ( ) ,
306
352
subdirectory,
307
353
& version_id,
308
354
dist,
@@ -316,7 +362,7 @@ async fn build_impl(
316
362
BuildPlan :: Wheel => {
317
363
let builder = build_dispatch
318
364
. setup_build (
319
- src_dir . as_ref ( ) ,
365
+ src . path ( ) ,
320
366
subdirectory,
321
367
& version_id,
322
368
dist,
@@ -330,7 +376,7 @@ async fn build_impl(
330
376
BuildPlan :: SdistAndWheel => {
331
377
let builder = build_dispatch
332
378
. setup_build (
333
- src_dir . as_ref ( ) ,
379
+ src . path ( ) ,
334
380
subdirectory,
335
381
& version_id,
336
382
dist,
@@ -341,7 +387,7 @@ async fn build_impl(
341
387
342
388
let builder = build_dispatch
343
389
. setup_build (
344
- src_dir . as_ref ( ) ,
390
+ src . path ( ) ,
345
391
subdirectory,
346
392
& version_id,
347
393
dist,
@@ -352,12 +398,59 @@ async fn build_impl(
352
398
353
399
BuiltDistributions :: Both ( output_dir. join ( & sdist) , output_dir. join ( & wheel) )
354
400
}
401
+ BuildPlan :: WheelFromSdist => {
402
+ // Extract the source distribution into a temporary directory.
403
+ let reader = fs_err:: tokio:: File :: open ( src. path ( ) ) . await ?;
404
+ let ext = SourceDistExtension :: from_path ( src. path ( ) ) . map_err ( |err| {
405
+ anyhow:: anyhow!( "`{}` is not a valid build source. Expected to receive a source directory, or a source distribution ending in one of: {err}." , src. path( ) . user_display( ) )
406
+ } ) ?;
407
+ let temp_dir = tempfile:: tempdir_in ( & output_dir) ?;
408
+ uv_extract:: stream:: archive ( reader, ext, temp_dir. path ( ) ) . await ?;
409
+
410
+ // Extract the top-level directory from the archive.
411
+ let extracted = match uv_extract:: strip_component ( temp_dir. path ( ) ) {
412
+ Ok ( top_level) => top_level,
413
+ Err ( uv_extract:: Error :: NonSingularArchive ( _) ) => temp_dir. path ( ) . to_path_buf ( ) ,
414
+ Err ( err) => return Err ( err. into ( ) ) ,
415
+ } ;
416
+
417
+ // Build a wheel from the source distribution.
418
+ let builder = build_dispatch
419
+ . setup_build (
420
+ & extracted,
421
+ subdirectory,
422
+ & version_id,
423
+ dist,
424
+ BuildKind :: Wheel ,
425
+ )
426
+ . await ?;
427
+ let wheel = builder. build ( & output_dir) . await ?;
428
+
429
+ BuiltDistributions :: Wheel ( output_dir. join ( wheel) )
430
+ }
355
431
} ;
356
432
357
433
Ok ( assets)
358
434
}
359
435
360
- #[ derive( Debug , Clone , PartialEq ) ]
436
+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
437
+ enum Source < ' a > {
438
+ /// The input source is a file (i.e., a source distribution in a `.tar.gz` or `.zip` file).
439
+ File ( Cow < ' a , Path > ) ,
440
+ /// The input source is a directory.
441
+ Directory ( Cow < ' a , Path > ) ,
442
+ }
443
+
444
+ impl < ' a > Source < ' a > {
445
+ fn path ( & self ) -> & Path {
446
+ match self {
447
+ Source :: File ( path) => path. as_ref ( ) ,
448
+ Source :: Directory ( path) => path. as_ref ( ) ,
449
+ }
450
+ }
451
+ }
452
+
453
+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
361
454
enum BuiltDistributions {
362
455
/// A built wheel.
363
456
Wheel ( PathBuf ) ,
@@ -380,4 +473,7 @@ enum BuildPlan {
380
473
381
474
/// Build a source distribution and a wheel from source.
382
475
SdistAndWheel ,
476
+
477
+ /// Build a wheel from a source distribution.
478
+ WheelFromSdist ,
383
479
}
0 commit comments