@@ -281,8 +281,124 @@ mod tests {
281281 use sha2:: Digest ;
282282 use std:: io:: { BufReader , Read } ;
283283 use tempfile:: TempDir ;
284+ use uv_distribution_filename:: { SourceDistFilename , WheelFilename } ;
284285 use uv_fs:: { copy_dir_all, relative_to} ;
285286
287+ /// File listings, generated archives and archive contents for both a build with
288+ /// source tree -> wheel
289+ /// and a build with
290+ /// source tree -> source dist -> wheel.
291+ struct BuildResults {
292+ source_dist_list_files : FileList ,
293+ source_dist_filename : SourceDistFilename ,
294+ source_dist_contents : Vec < String > ,
295+ wheel_list_files : FileList ,
296+ wheel_filename : WheelFilename ,
297+ wheel_contents : Vec < String > ,
298+ }
299+
300+ /// Run both a direct wheel build and an indirect wheel build through a source distribution,
301+ /// while checking that directly built wheel and indirectly built wheel are the same.
302+ fn build ( source_root : & Path , dist : & Path ) -> BuildResults {
303+ // Build a direct wheel, capture all its properties to compare it with the indirect wheel
304+ // latest and remove it since it has the same filename as the indirect wheel.
305+ let ( _name, direct_wheel_list_files) = list_wheel ( source_root, "1.0.0+test" ) . unwrap ( ) ;
306+ let direct_wheel_filename = build_wheel ( source_root, dist, None , "1.0.0+test" ) . unwrap ( ) ;
307+ let direct_wheel_path = dist. join ( direct_wheel_filename. to_string ( ) ) ;
308+ let direct_wheel_contents = wheel_contents ( & direct_wheel_path) ;
309+ let direct_wheel_hash = sha2:: Sha256 :: digest ( fs_err:: read ( & direct_wheel_path) . unwrap ( ) ) ;
310+ fs_err:: remove_file ( & direct_wheel_path) . unwrap ( ) ;
311+
312+ // Build a source distribution.
313+ let ( _name, source_dist_list_files) = list_source_dist ( source_root, "1.0.0+test" ) . unwrap ( ) ;
314+ // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to
315+ // normalize the path.
316+ let ( _name, wheel_list_files) = list_wheel ( source_root, "1.0.0+test" ) . unwrap ( ) ;
317+ let source_dist_filename = build_source_dist ( source_root, dist, "1.0.0+test" ) . unwrap ( ) ;
318+ let source_dist_path = dist. join ( source_dist_filename. to_string ( ) ) ;
319+ let source_dist_contents = sdist_contents ( & source_dist_path) ;
320+
321+ // Unpack the source distribution and build a wheel from it.
322+ let sdist_tree = TempDir :: new ( ) . unwrap ( ) ;
323+ let sdist_reader = BufReader :: new ( File :: open ( & source_dist_path) . unwrap ( ) ) ;
324+ let mut source_dist = tar:: Archive :: new ( GzDecoder :: new ( sdist_reader) ) ;
325+ source_dist. unpack ( sdist_tree. path ( ) ) . unwrap ( ) ;
326+ let sdist_top_level_directory = sdist_tree. path ( ) . join ( format ! (
327+ "{}-{}" ,
328+ source_dist_filename. name. as_dist_info_name( ) ,
329+ source_dist_filename. version
330+ ) ) ;
331+ let wheel_filename =
332+ build_wheel ( & sdist_top_level_directory, dist, None , "1.0.0+test" ) . unwrap ( ) ;
333+ let wheel_contents = wheel_contents ( & dist. join ( wheel_filename. to_string ( ) ) ) ;
334+
335+ // Check that direct and indirect wheels are identical.
336+ assert_eq ! ( direct_wheel_filename, wheel_filename) ;
337+ assert_eq ! ( direct_wheel_contents, wheel_contents) ;
338+ assert_eq ! ( direct_wheel_list_files, wheel_list_files) ;
339+ assert_eq ! (
340+ direct_wheel_hash,
341+ sha2:: Sha256 :: digest( fs_err:: read( dist. join( wheel_filename. to_string( ) ) ) . unwrap( ) )
342+ ) ;
343+
344+ BuildResults {
345+ source_dist_list_files,
346+ source_dist_filename,
347+ source_dist_contents,
348+ wheel_list_files,
349+ wheel_filename,
350+ wheel_contents,
351+ }
352+ }
353+
354+ fn sdist_contents ( source_dist_path : & Path ) -> Vec < String > {
355+ let sdist_reader = BufReader :: new ( File :: open ( source_dist_path) . unwrap ( ) ) ;
356+ let mut source_dist = tar:: Archive :: new ( GzDecoder :: new ( sdist_reader) ) ;
357+ let mut source_dist_contents: Vec < _ > = source_dist
358+ . entries ( )
359+ . unwrap ( )
360+ . map ( |entry| {
361+ entry
362+ . unwrap ( )
363+ . path ( )
364+ . unwrap ( )
365+ . to_str ( )
366+ . unwrap ( )
367+ . replace ( '\\' , "/" )
368+ } )
369+ . collect ( ) ;
370+ source_dist_contents. sort ( ) ;
371+ source_dist_contents
372+ }
373+
374+ fn wheel_contents ( direct_output_dir : & Path ) -> Vec < String > {
375+ let wheel = zip:: ZipArchive :: new ( File :: open ( direct_output_dir) . unwrap ( ) ) . unwrap ( ) ;
376+ let mut wheel_contents: Vec < _ > = wheel
377+ . file_names ( )
378+ . map ( |path| path. replace ( '\\' , "/" ) )
379+ . collect ( ) ;
380+ wheel_contents. sort_unstable ( ) ;
381+ wheel_contents
382+ }
383+
384+ fn format_file_list ( file_list : FileList , src : & Path ) -> String {
385+ file_list
386+ . into_iter ( )
387+ . map ( |( path, source) | {
388+ let path = path. replace ( '\\' , "/" ) ;
389+ if let Some ( source) = source {
390+ let source = relative_to ( source, src)
391+ . unwrap ( )
392+ . portable_display ( )
393+ . to_string ( ) ;
394+ format ! ( "{path} ({source})" )
395+ } else {
396+ format ! ( "{path} (generated)" )
397+ }
398+ } )
399+ . join ( "\n " )
400+ }
401+
286402 /// Tests that builds are stable and include the right files and.
287403 ///
288404 /// Tests that both source tree -> source dist -> wheel and source tree -> wheel include the
@@ -335,91 +451,39 @@ mod tests {
335451 File :: create ( module_root. join ( "__pycache__" ) . join ( "compiled.pyc" ) ) . unwrap ( ) ;
336452 File :: create ( module_root. join ( "arithmetic" ) . join ( "circle.pyc" ) ) . unwrap ( ) ;
337453
338- // Build a wheel from the source tree
339- let direct_output_dir = TempDir :: new ( ) . unwrap ( ) ;
340- let ( _name, wheel_list_files) = list_wheel ( src. path ( ) , "1.0.0+test" ) . unwrap ( ) ;
341- build_wheel ( src. path ( ) , direct_output_dir. path ( ) , None , "1.0.0+test" ) . unwrap ( ) ;
454+ // Perform both the direct and the indirect build.
455+ let dist = TempDir :: new ( ) . unwrap ( ) ;
456+ let build = build ( src. path ( ) , dist. path ( ) ) ;
342457
343- let wheel = zip:: ZipArchive :: new (
344- File :: open (
345- direct_output_dir
346- . path ( )
347- . join ( "built_by_uv-0.1.0-py3-none-any.whl" ) ,
348- )
349- . unwrap ( ) ,
350- )
351- . unwrap ( ) ;
352- let mut direct_wheel_contents: Vec < _ > = wheel. file_names ( ) . collect ( ) ;
353- direct_wheel_contents. sort_unstable ( ) ;
354-
355- // List file and build a source dist from the source tree
356- let source_dist_dir = TempDir :: new ( ) . unwrap ( ) ;
357- let ( _name, source_dist_list_files) = list_source_dist ( src. path ( ) , "1.0.0+test" ) . unwrap ( ) ;
358- build_source_dist ( src. path ( ) , source_dist_dir. path ( ) , "1.0.0+test" ) . unwrap ( ) ;
359- let source_dist_path = source_dist_dir. path ( ) . join ( "built_by_uv-0.1.0.tar.gz" ) ;
458+ let source_dist_path = dist. path ( ) . join ( build. source_dist_filename . to_string ( ) ) ;
459+ assert_eq ! (
460+ build. source_dist_filename. to_string( ) ,
461+ "built_by_uv-0.1.0.tar.gz"
462+ ) ;
360463 // Check that the source dist is reproducible across platforms.
361464 assert_snapshot ! (
362465 format!( "{:x}" , sha2:: Sha256 :: digest( fs_err:: read( & source_dist_path) . unwrap( ) ) ) ,
363466 @"dab46bcc4d66960a11cfdc19604512a8e1a3241a67536f7e962166760e9c575c"
364467 ) ;
365-
366- // Build a wheel from the source dist
367- let sdist_tree = TempDir :: new ( ) . unwrap ( ) ;
368- let sdist_reader = BufReader :: new ( File :: open ( & source_dist_path) . unwrap ( ) ) ;
369- let mut source_dist = tar:: Archive :: new ( GzDecoder :: new ( sdist_reader) ) ;
370- let mut source_dist_contents: Vec < _ > = source_dist
371- . entries ( )
372- . unwrap ( )
373- . map ( |entry| entry. unwrap ( ) . path ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) . to_string ( ) )
374- . collect ( ) ;
375- source_dist_contents. sort ( ) ;
376- // Reset the reader and unpack
377- let sdist_reader = BufReader :: new ( File :: open ( & source_dist_path) . unwrap ( ) ) ;
378- let mut source_dist = tar:: Archive :: new ( GzDecoder :: new ( sdist_reader) ) ;
379- source_dist. unpack ( sdist_tree. path ( ) ) . unwrap ( ) ;
380- drop ( source_dist_dir) ;
381-
382- let indirect_output_dir = TempDir :: new ( ) . unwrap ( ) ;
383- build_wheel (
384- & sdist_tree. path ( ) . join ( "built_by_uv-0.1.0" ) ,
385- indirect_output_dir. path ( ) ,
386- None ,
387- "1.0.0+test" ,
388- )
389- . unwrap ( ) ;
390- let wheel = zip:: ZipArchive :: new (
391- File :: open (
392- indirect_output_dir
393- . path ( )
394- . join ( "built_by_uv-0.1.0-py3-none-any.whl" ) ,
395- )
396- . unwrap ( ) ,
397- )
398- . unwrap ( ) ;
399- let mut indirect_wheel_contents: Vec < _ > = wheel. file_names ( ) . collect ( ) ;
400- indirect_wheel_contents. sort_unstable ( ) ;
401- assert_eq ! ( indirect_wheel_contents, direct_wheel_contents) ;
402-
403- let format_file_list = |file_list : FileList | {
404- file_list
405- . into_iter ( )
406- . map ( |( path, source) | {
407- let path = path. replace ( '\\' , "/" ) ;
408- if let Some ( source) = source {
409- let source = relative_to ( source, src. path ( ) )
410- . unwrap ( )
411- . portable_display ( )
412- . to_string ( ) ;
413- format ! ( "{path} ({source})" )
414- } else {
415- format ! ( "{path} (generated)" )
416- }
417- } )
418- . join ( "\n " )
419- } ;
420-
421- // Check the contained files and directories
422- assert_snapshot ! ( source_dist_contents. iter( ) . map( |path| path. replace( '\\' , "/" ) ) . join( "\n " ) , @r###"
468+ // Check both the files we report and the actual files
469+ assert_snapshot ! ( format_file_list( build. source_dist_list_files, src. path( ) ) , @r"
470+ built_by_uv-0.1.0/PKG-INFO (generated)
471+ built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
472+ built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
473+ built_by_uv-0.1.0/README.md (README.md)
474+ built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
475+ built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
476+ built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
477+ built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
478+ built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
479+ built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
480+ built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
481+ built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
482+ built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
483+ built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
484+ built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
485+ " ) ;
486+ assert_snapshot ! ( build. source_dist_contents. iter( ) . join( "\n " ) , @r"
423487 built_by_uv-0.1.0/
424488 built_by_uv-0.1.0/LICENSE-APACHE
425489 built_by_uv-0.1.0/LICENSE-MIT
@@ -443,26 +507,19 @@ mod tests {
443507 built_by_uv-0.1.0/src/built_by_uv/cli.py
444508 built_by_uv-0.1.0/third-party-licenses
445509 built_by_uv-0.1.0/third-party-licenses/PEP-401.txt
446- "### ) ;
447- assert_snapshot ! ( format_file_list( source_dist_list_files) , @r"
448- built_by_uv-0.1.0/PKG-INFO (generated)
449- built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE)
450- built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT)
451- built_by_uv-0.1.0/README.md (README.md)
452- built_by_uv-0.1.0/assets/data.csv (assets/data.csv)
453- built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h)
454- built_by_uv-0.1.0/pyproject.toml (pyproject.toml)
455- built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh)
456- built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py)
457- built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
458- built_by_uv-0.1.0/src/built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
459- built_by_uv-0.1.0/src/built_by_uv/arithmetic/pi.txt (src/built_by_uv/arithmetic/pi.txt)
460- built_by_uv-0.1.0/src/built_by_uv/build-only.h (src/built_by_uv/build-only.h)
461- built_by_uv-0.1.0/src/built_by_uv/cli.py (src/built_by_uv/cli.py)
462- built_by_uv-0.1.0/third-party-licenses/PEP-401.txt (third-party-licenses/PEP-401.txt)
463510 " ) ;
464511
465- assert_snapshot ! ( indirect_wheel_contents. iter( ) . map( |path| path. replace( '\\' , "/" ) ) . join( "\n " ) , @r###"
512+ let wheel_path = dist. path ( ) . join ( build. wheel_filename . to_string ( ) ) ;
513+ assert_eq ! (
514+ build. wheel_filename. to_string( ) ,
515+ "built_by_uv-0.1.0-py3-none-any.whl"
516+ ) ;
517+ // Check that the wheel is reproducible across platforms.
518+ assert_snapshot ! (
519+ format!( "{:x}" , sha2:: Sha256 :: digest( fs_err:: read( & wheel_path) . unwrap( ) ) ) ,
520+ @"ac3f68ac448023bca26de689d80401bff57f764396ae802bf4666234740ffbe3"
521+ ) ;
522+ assert_snapshot ! ( build. wheel_contents. join( "\n " ) , @r"
466523 built_by_uv-0.1.0.data/data/
467524 built_by_uv-0.1.0.data/data/data.csv
468525 built_by_uv-0.1.0.data/headers/
@@ -486,9 +543,8 @@ mod tests {
486543 built_by_uv/arithmetic/circle.py
487544 built_by_uv/arithmetic/pi.txt
488545 built_by_uv/cli.py
489- "### ) ;
490-
491- assert_snapshot ! ( format_file_list( wheel_list_files) , @r"
546+ " ) ;
547+ assert_snapshot ! ( format_file_list( build. wheel_list_files, src. path( ) ) , @r"
492548 built_by_uv/__init__.py (src/built_by_uv/__init__.py)
493549 built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py)
494550 built_by_uv/arithmetic/circle.py (src/built_by_uv/arithmetic/circle.py)
@@ -504,19 +560,6 @@ mod tests {
504560 built_by_uv-0.1.0.dist-info/entry_points.txt (generated)
505561 built_by_uv-0.1.0.dist-info/METADATA (generated)
506562 " ) ;
507-
508- // Check that the wheel is the same for both build paths and reproducible across platforms.
509- let wheel_filename = "built_by_uv-0.1.0-py3-none-any.whl" ;
510- let index_wheel_contents =
511- fs_err:: read ( indirect_output_dir. path ( ) . join ( wheel_filename) ) . unwrap ( ) ;
512- assert_eq ! (
513- fs_err:: read( direct_output_dir. path( ) . join( wheel_filename) ) . unwrap( ) ,
514- index_wheel_contents
515- ) ;
516- assert_snapshot ! (
517- format!( "{:x}" , sha2:: Sha256 :: digest( & index_wheel_contents) ) ,
518- @"ac3f68ac448023bca26de689d80401bff57f764396ae802bf4666234740ffbe3"
519- ) ;
520563 }
521564
522565 /// Test that `license = { file = "LICENSE" }` is supported.
0 commit comments