@@ -3,17 +3,21 @@ mod pep639_glob;
3
3
4
4
use crate :: metadata:: { PyProjectToml , ValidationError } ;
5
5
use crate :: pep639_glob:: Pep639GlobError ;
6
+ use flate2:: write:: GzEncoder ;
7
+ use flate2:: Compression ;
6
8
use fs_err:: File ;
7
9
use glob:: { GlobError , PatternError } ;
10
+ use globset:: { Glob , GlobSetBuilder } ;
8
11
use itertools:: Itertools ;
9
12
use sha2:: { Digest , Sha256 } ;
10
13
use std:: fs:: FileType ;
11
- use std:: io:: { BufReader , Read , Write } ;
14
+ use std:: io:: { BufReader , Cursor , Read , Write } ;
12
15
use std:: path:: { Path , PathBuf , StripPrefixError } ;
13
16
use std:: { io, mem} ;
17
+ use tar:: { EntryType , Header } ;
14
18
use thiserror:: Error ;
15
19
use tracing:: { debug, trace} ;
16
- use uv_distribution_filename:: WheelFilename ;
20
+ use uv_distribution_filename:: { SourceDistExtension , SourceDistFilename , WheelFilename } ;
17
21
use uv_fs:: Simplified ;
18
22
use walkdir:: WalkDir ;
19
23
use zip:: { CompressionMethod , ZipWriter } ;
@@ -33,6 +37,9 @@ pub enum Error {
33
37
/// [`GlobError`] is a wrapped io error.
34
38
#[ error( transparent) ]
35
39
Glob ( #[ from] GlobError ) ,
40
+ /// [`globset::Error`] shows the glob that failed to parse.
41
+ #[ error( transparent) ]
42
+ GlobSet ( #[ from] globset:: Error ) ,
36
43
#[ error( "Failed to walk source tree: `{}`" , root. user_display( ) ) ]
37
44
WalkDir {
38
45
root : PathBuf ,
@@ -43,8 +50,8 @@ pub enum Error {
43
50
NotUtf8Path ( PathBuf ) ,
44
51
#[ error( "Failed to walk source tree" ) ]
45
52
StripPrefix ( #[ from] StripPrefixError ) ,
46
- #[ error( "Unsupported file type: {0 :?}" ) ]
47
- UnsupportedFileType ( FileType ) ,
53
+ #[ error( "Unsupported file type {1 :?}: `{}`" , _0 . user_display ( ) ) ]
54
+ UnsupportedFileType ( PathBuf , FileType ) ,
48
55
#[ error( "Failed to write wheel zip archive" ) ]
49
56
Zip ( #[ from] zip:: result:: ZipError ) ,
50
57
#[ error( "Failed to write RECORD file" ) ]
@@ -53,6 +60,8 @@ pub enum Error {
53
60
MissingModule ( PathBuf ) ,
54
61
#[ error( "Inconsistent metadata between prepare and build step: `{0}`" ) ]
55
62
InconsistentSteps ( & ' static str ) ,
63
+ #[ error( "Failed to write to {}" , _0. user_display( ) ) ]
64
+ TarWrite ( PathBuf , #[ source] io:: Error ) ,
56
65
}
57
66
58
67
/// Allow dispatching between writing to a directory, writing to zip and writing to a `.tar.gz`.
@@ -276,7 +285,7 @@ fn write_hashed(
276
285
}
277
286
278
287
/// Build a wheel from the source tree and place it in the output directory.
279
- pub fn build (
288
+ pub fn build_wheel (
280
289
source_tree : & Path ,
281
290
wheel_dir : & Path ,
282
291
metadata_directory : Option < & Path > ,
@@ -323,7 +332,10 @@ pub fn build(
323
332
wheel_writer. write_file ( relative_path_str, entry. path ( ) ) ?;
324
333
} else {
325
334
// TODO(konsti): We may want to support symlinks, there is support for installing them.
326
- return Err ( Error :: UnsupportedFileType ( entry. file_type ( ) ) ) ;
335
+ return Err ( Error :: UnsupportedFileType (
336
+ entry. path ( ) . to_path_buf ( ) ,
337
+ entry. file_type ( ) ,
338
+ ) ) ;
327
339
}
328
340
329
341
entry. path ( ) ;
@@ -342,6 +354,126 @@ pub fn build(
342
354
Ok ( filename)
343
355
}
344
356
357
+ /// Build a source distribution from the source tree and place it in the output directory.
358
+ pub fn build_source_dist (
359
+ source_tree : & Path ,
360
+ source_dist_directory : & Path ,
361
+ uv_version : & str ,
362
+ ) -> Result < SourceDistFilename , Error > {
363
+ let contents = fs_err:: read_to_string ( source_tree. join ( "pyproject.toml" ) ) ?;
364
+ let pyproject_toml = PyProjectToml :: parse ( & contents) ?;
365
+ pyproject_toml. check_build_system ( uv_version) ;
366
+
367
+ let filename = SourceDistFilename {
368
+ name : pyproject_toml. name ( ) . clone ( ) ,
369
+ version : pyproject_toml. version ( ) . clone ( ) ,
370
+ extension : SourceDistExtension :: TarGz ,
371
+ } ;
372
+
373
+ let top_level = format ! ( "{}-{}" , pyproject_toml. name( ) , pyproject_toml. version( ) ) ;
374
+
375
+ let source_dist_path = source_dist_directory. join ( filename. to_string ( ) ) ;
376
+ let tar_gz = File :: create ( & source_dist_path) ?;
377
+ let enc = GzEncoder :: new ( tar_gz, Compression :: default ( ) ) ;
378
+ let mut tar = tar:: Builder :: new ( enc) ;
379
+
380
+ let metadata = pyproject_toml
381
+ . to_metadata ( source_tree) ?
382
+ . core_metadata_format ( ) ;
383
+
384
+ let mut header = Header :: new_gnu ( ) ;
385
+ header. set_size ( metadata. bytes ( ) . len ( ) as u64 ) ;
386
+ header. set_mode ( 0o644 ) ;
387
+ header. set_cksum ( ) ;
388
+ tar. append_data (
389
+ & mut header,
390
+ Path :: new ( & top_level) . join ( "PKG-INFO" ) ,
391
+ Cursor :: new ( metadata) ,
392
+ )
393
+ . map_err ( |err| Error :: TarWrite ( source_dist_path. clone ( ) , err) ) ?;
394
+
395
+ let includes = [ "src/**/*" , "pyproject.toml" ] ;
396
+ let mut include_builder = GlobSetBuilder :: new ( ) ;
397
+ for include in includes {
398
+ include_builder. add ( Glob :: new ( include) ?) ;
399
+ }
400
+ let include_matcher = include_builder. build ( ) ?;
401
+
402
+ let excludes = [ "__pycache__" , "*.pyc" , "*.pyo" ] ;
403
+ let mut exclude_builder = GlobSetBuilder :: new ( ) ;
404
+ for exclude in excludes {
405
+ exclude_builder. add ( Glob :: new ( exclude) ?) ;
406
+ }
407
+ let exclude_matcher = exclude_builder. build ( ) ?;
408
+
409
+ // TODO(konsti): Add files linked by pyproject.toml
410
+
411
+ for file in WalkDir :: new ( source_tree) . into_iter ( ) . filter_entry ( |dir| {
412
+ let relative = dir
413
+ . path ( )
414
+ . strip_prefix ( source_tree)
415
+ . expect ( "walkdir starts with root" ) ;
416
+ // TODO(konsti): Also check that we're matching at least a prefix of an include matcher.
417
+ !exclude_matcher. is_match ( relative)
418
+ } ) {
419
+ let entry = file. map_err ( |err| Error :: WalkDir {
420
+ root : source_tree. to_path_buf ( ) ,
421
+ err,
422
+ } ) ?;
423
+ let relative = entry
424
+ . path ( )
425
+ . strip_prefix ( source_tree)
426
+ . expect ( "walkdir starts with root" ) ;
427
+ if !include_matcher. is_match ( relative) {
428
+ trace ! ( "Excluding {}" , relative. user_display( ) ) ;
429
+ continue ;
430
+ }
431
+ debug ! ( "Including {}" , relative. user_display( ) ) ;
432
+
433
+ let metadata = fs_err:: metadata ( entry. path ( ) ) ?;
434
+ let mut header = Header :: new_gnu ( ) ;
435
+ #[ cfg( unix) ]
436
+ {
437
+ header. set_mode ( std:: os:: unix:: fs:: MetadataExt :: mode ( & metadata) ) ;
438
+ }
439
+ #[ cfg( not( unix) ) ]
440
+ {
441
+ header. set_mode ( 0o644 ) ;
442
+ }
443
+
444
+ if entry. file_type ( ) . is_dir ( ) {
445
+ header. set_entry_type ( EntryType :: Directory ) ;
446
+ header
447
+ . set_path ( Path :: new ( & top_level) . join ( relative) )
448
+ . map_err ( |err| Error :: TarWrite ( source_dist_path. clone ( ) , err) ) ?;
449
+ header. set_size ( 0 ) ;
450
+ header. set_cksum ( ) ;
451
+ tar. append ( & header, io:: empty ( ) )
452
+ . map_err ( |err| Error :: TarWrite ( source_dist_path. clone ( ) , err) ) ?;
453
+ continue ;
454
+ } else if entry. file_type ( ) . is_file ( ) {
455
+ header. set_size ( metadata. len ( ) ) ;
456
+ header. set_cksum ( ) ;
457
+ tar. append_data (
458
+ & mut header,
459
+ Path :: new ( & top_level) . join ( relative) ,
460
+ BufReader :: new ( File :: open ( entry. path ( ) ) ?) ,
461
+ )
462
+ . map_err ( |err| Error :: TarWrite ( source_dist_path. clone ( ) , err) ) ?;
463
+ } else {
464
+ return Err ( Error :: UnsupportedFileType (
465
+ relative. to_path_buf ( ) ,
466
+ entry. file_type ( ) ,
467
+ ) ) ;
468
+ }
469
+ }
470
+
471
+ tar. finish ( )
472
+ . map_err ( |err| Error :: TarWrite ( source_dist_path. clone ( ) , err) ) ?;
473
+
474
+ Ok ( filename)
475
+ }
476
+
345
477
/// Write the dist-info directory to the output directory without building the wheel.
346
478
pub fn metadata (
347
479
source_tree : & Path ,
@@ -350,7 +482,7 @@ pub fn metadata(
350
482
) -> Result < String , Error > {
351
483
let contents = fs_err:: read_to_string ( source_tree. join ( "pyproject.toml" ) ) ?;
352
484
let pyproject_toml = PyProjectToml :: parse ( & contents) ?;
353
- pyproject_toml. check_build_system ( "1.0.0+test" ) ;
485
+ pyproject_toml. check_build_system ( uv_version ) ;
354
486
355
487
let filename = WheelFilename {
356
488
name : pyproject_toml. name ( ) . clone ( ) ,
0 commit comments