diff --git a/Cargo.lock b/Cargo.lock index 103d2a031b3..a5acb198508 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "ambient-id" version = "0.0.11" @@ -119,6 +125,19 @@ dependencies = [ "num-traits", ] +[[package]] +name = "archive-trait" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a680a5c5d096c2a657bf01a6f73645d773385d470ca84f96beaa9042b39fc" +dependencies = [ + "cap-std", + "thiserror", + "tokio", + "walkdir", + "windows-sys 0.60.2", +] + [[package]] name = "arcstr" version = "1.2.0" @@ -664,6 +683,36 @@ dependencies = [ "serde_core", ] +[[package]] +name = "cap-primitives" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdadbd7c002d3a484b35243669abdae85a0ebaded5a61117169dc3400f9a7ff0" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes 3.0.1", + "ipnet", + "maybe-owned", + "rustix 1.1.4", + "rustix-linux-procfs", + "windows-sys 0.61.2", + "winx", +] + +[[package]] +name = "cap-std" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7281235d6e96d3544ca18bba9049be92f4190f8d923e3caef1b5f66cfa752608" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes 3.0.1", + "rustix 1.1.4", +] + [[package]] name = "cargo-util" version = "0.2.30" @@ -1717,6 +1766,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes 2.0.4", + "rustix 1.1.4", + "windows-sys 0.52.0", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -2384,6 +2444,28 @@ dependencies = [ "tempfile", ] +[[package]] +name = "io-extras" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" +dependencies = [ + "io-lifetimes 3.0.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + +[[package]] +name = "io-lifetimes" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" + [[package]] name = "ipnet" version = "2.12.0" @@ -2716,6 +2798,12 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "md-5" version = "0.11.0" @@ -4239,6 +4327,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.1.4", +] + [[package]] name = "rustls" version = "0.23.42" @@ -5001,6 +5099,28 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a" +[[package]] +name = "tar-codec" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d380a2c76146049ba9e67b6a8a39a6392b791e328788d9888051b784cbade8" +dependencies = [ + "archive-trait", + "tar-framing", + "thiserror", + "tokio", +] + +[[package]] +name = "tar-framing" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b27712c129a8355a0b63492e6f87c7249fef14a114a7c6fe15424ba3c5065d1c" +dependencies = [ + "thiserror", + "tokio", +] + [[package]] name = "target-lexicon" version = "0.13.5" @@ -5812,6 +5932,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", + "tar-codec", "tempfile", "textwrap", "thiserror", @@ -5965,7 +6086,6 @@ name = "uv-bench" version = "0.0.67" dependencies = [ "anyhow", - "astral-tokio-tar", "astral_async_zip", "clap", "codspeed-criterion-compat", @@ -5974,6 +6094,7 @@ dependencies = [ "futures", "jiff", "sha2 0.11.0", + "tar-codec", "tempfile", "tokio", "tokio-util", @@ -6070,6 +6191,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "spdx", + "tar-codec", "tempfile", "thiserror", "tokio", @@ -6571,18 +6693,22 @@ dependencies = [ "fs-err", "futures", "hex", + "insta", "md-5", "rayon", "regex", "reqwest", "rustc-hash", "sha2 0.11.0", + "tar-codec", + "tempfile", "thiserror", "tokio", "tokio-util", "tracing", "uv-configuration", "uv-distribution-filename", + "uv-preview", "uv-pypi-types", "uv-static", ] @@ -6983,6 +7109,8 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "tar-codec", + "tempfile", "thiserror", "tokio", "tokio-util", @@ -6998,6 +7126,7 @@ dependencies = [ "uv-extract", "uv-fs", "uv-metadata", + "uv-preview", "uv-pypi-types", "uv-redacted", "uv-static", @@ -7365,7 +7494,6 @@ dependencies = [ "anyhow", "assert_cmd", "assert_fs", - "astral-tokio-tar", "astral_async_zip", "base64", "flate2", @@ -7384,6 +7512,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "similar 3.1.1", + "tar-codec", "tempfile", "tokio", "tokio-util", @@ -7935,7 +8064,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", ] [[package]] @@ -7953,14 +8091,31 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -7987,48 +8142,96 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "1.0.3" @@ -8038,6 +8241,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags 2.13.1", + "windows-sys 0.52.0", +] + [[package]] name = "wiremock" version = "0.6.5" diff --git a/Cargo.toml b/Cargo.toml index fe2ae70f495..a84a6e76662 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -254,6 +254,7 @@ sha2 = { version = "0.11.0" } smallvec = { version = "1.13.2" } spdx = { version = "0.13.0" } syn = { version = "3.0.0" } +tar-codec = { version = "0.0.9" } target-lexicon = { version = "0.13.0" } tempfile = { version = "3.14.0" } terminal_size = { version = "0.4.2" } diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index bfd2977d8dc..6b6a9b90351 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -58,7 +58,6 @@ uv-workspace = { workspace = true } anyhow = { workspace = true } async_zip = { workspace = true } -astral-tokio-tar = { workspace = true } clap = { workspace = true } criterion = { version = "5.0.1", default-features = false, package = "codspeed-criterion-compat", features = ["async_tokio"] } flate2 = { workspace = true } @@ -66,6 +65,7 @@ fs-err = { workspace = true } futures = { workspace = true } jiff = { workspace = true } sha2 = { workspace = true } +tar-codec = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index 5cc61d93772..de28b31ffb2 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -4,7 +4,6 @@ extern crate uv_performance_memory_allocator; use std::fmt::Write; use std::hint::black_box; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::str::FromStr; @@ -15,13 +14,14 @@ use flate2::write::GzEncoder; use futures::executor::block_on; use futures::io::AllowStdIo; use sha2::{Digest, Sha256}; -use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; +use tar_codec::{ArchiveBuilder as _, EntryMetadata, TarEncoder}; +use tokio_util::compat::FuturesAsyncWriteCompatExt; use uv_cache::Cache; use uv_client::{BaseClientBuilder, Connectivity, RegistryClientBuilder}; use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::Requirement; use uv_install_wheel::{InstallState, Layout, LinkMode}; -use uv_preview::Preview; +use uv_preview::{MaybePreviewFeature, Preview, PreviewFeature}; use uv_pypi_types::Scheme; use uv_python::PythonEnvironment; use uv_resolver::Manifest; @@ -77,9 +77,8 @@ fn create_many_files_wheel() -> tempfile::NamedTempFile { fn create_many_files_sdist() -> tempfile::NamedTempFile { let archive = tempfile::NamedTempFile::new().expect("Failed to create temporary archive"); - let encoder = GzEncoder::new(archive.as_file(), flate2::Compression::default()); - let mut writer = - tokio_tar::Builder::new_non_terminated(AllowStdIo::new(encoder).compat_write()); + let mut encoder = GzEncoder::new(archive.as_file(), flate2::Compression::default()); + let mut writer = TarEncoder::new(AllowStdIo::new(&mut encoder).compat_write()).builder(); for index in 0..MANY_FILES_SDIST_FILE_COUNT { write_tar_entry( &mut writer, @@ -97,12 +96,8 @@ fn create_many_files_sdist() -> tempfile::NamedTempFile { &format!("{MANY_FILES_SDIST_TOP_LEVEL}/pyproject.toml"), b"[project]\nname = \"manyfiles\"\nversion = \"0.0.0\"\n", ); - let writer = block_on(writer.into_inner()).expect("Failed to finish tar archive"); - writer - .into_inner() - .into_inner() - .finish() - .expect("Failed to finish gzip archive"); + block_on(writer.finish()).expect("Failed to finish tar archive"); + encoder.finish().expect("Failed to finish gzip archive"); archive } @@ -122,6 +117,11 @@ fn unpack_sdist_many_files(c: &mut Criterion) { .build() .expect("Failed to create Tokio runtime"); + uv_preview::set(Preview::from_feature_names(&[MaybePreviewFeature::Known( + PreviewFeature::TarCodec, + )])) + .expect("Failed to configure tar backend preview features"); + c.bench_function("unpack_sdist_many_files", |b| { b.iter_batched( || { @@ -147,6 +147,10 @@ fn unpack_sdist_many_files(c: &mut Criterion) { BatchSize::PerIteration, ); }); + + uv_preview::set(Preview::default()) + .expect("Failed to restore default preview features after tar benchmark"); + uv_preview::finalize().expect("Failed to finalize preview features"); } fn unzip_wheel_many_files(c: &mut Criterion) { @@ -255,22 +259,13 @@ fn write_zip_entry(writer: &mut ZipFileWriter>, path: &str, contents: &[ block_on(writer.write_entry_whole(entry, contents)).expect("Failed to write ZIP entry"); } -fn write_tar_entry( - writer: &mut tokio_tar::Builder, +fn write_tar_entry( + writer: &mut tar_codec::Builder>, path: &str, contents: &[u8], ) { - let mut header = tokio_tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_entry_type(tokio_tar::EntryType::Regular); - header.set_cksum(); - block_on(writer.append_data( - &mut header, - path, - AllowStdIo::new(Cursor::new(contents)).compat(), - )) - .expect("Failed to write tar entry"); + block_on(writer.add_file(path, contents, EntryMetadata::default())) + .expect("Failed to write tar entry"); } fn layout(root: &Path) -> Layout { @@ -331,7 +326,6 @@ fn resolve_warm_airflow(c: &mut Criterion) { fn criterion_with_preview() -> Criterion { uv_preview::set(Preview::default()) .expect("Global preview features should not have been initialized already"); - uv_preview::finalize().expect("Failed to finalize preview features"); Criterion::default() } diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 2fc3243f16b..d9fcf62293b 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -45,6 +45,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } spdx = { workspace = true } +tar-codec = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index d153da15e22..842f187bd08 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -84,8 +84,15 @@ pub enum Error { VenvInSourceTree(PathBuf), #[error("Inconsistent metadata between prepare and build step: {0}")] InconsistentSteps(&'static str), - #[error("Failed to write to {}", _0.user_display())] + #[error("Failed to write tar archive to {}", _0.user_display())] TarWrite(PathBuf, #[source] io::Error), + #[error("Failed to write tar archive to {}", _0.user_display())] + TarCodecWrite( + PathBuf, + #[source] tar_codec::BuildError, + ), + #[error("Failed to finish gzip stream for {}", _0.user_display())] + GzipWrite(PathBuf, #[source] io::Error), } impl uv_errors::Hint for Error { @@ -475,7 +482,7 @@ mod tests { use async_zip::base::read::mem::ZipFileReader; use flate2::bufread::GzDecoder; use fs_err::File; - use futures_lite::{StreamExt, future::block_on}; + use futures_lite::future::block_on; use indoc::indoc; use insta::assert_snapshot; use itertools::Itertools; @@ -483,7 +490,7 @@ mod tests { use sha2::Digest; use std::io::BufReader; use std::iter; - use std::pin::Pin; + use tar_codec::{Archive as _, TarArchive, extract::ExtractPolicy}; use tempfile::TempDir; use uv_distribution_filename::{SourceDistFilename, WheelFilename}; use uv_errors::{ErrorWithHints, Hint}; @@ -603,22 +610,12 @@ mod tests { fn sdist_contents(source_dist_path: &Path) -> Vec { let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap()); - let mut source_dist = - tokio_tar::Archive::new(SyncReader::new(GzDecoder::new(sdist_reader))); + let source_dist = TarArchive::new(SyncReader::new(GzDecoder::new(sdist_reader))); let mut source_dist_contents = block_on(async { - let mut entries = source_dist.entries().unwrap(); - let mut entries = Pin::new(&mut entries); + let mut members = source_dist.members(); let mut contents = Vec::new(); - while let Some(entry) = entries.next().await { - contents.push( - entry - .unwrap() - .path() - .unwrap() - .to_str() - .unwrap() - .replace('\\', "/"), - ); + while let Some(member) = members.next().await.unwrap() { + contents.push(member.metadata().path.replace('\\', "/")); } contents }); @@ -628,12 +625,12 @@ mod tests { fn unpack_sdist(source_dist_path: &Path, target: &Path) -> Result<(), Error> { let sdist_reader = BufReader::new(File::open(source_dist_path)?); - let mut source_dist = - tokio_tar::Archive::new(SyncReader::new(GzDecoder::new(sdist_reader))); + let source_dist = TarArchive::new(SyncReader::new(GzDecoder::new(sdist_reader))); tokio::runtime::Builder::new_current_thread() .enable_all() .build()? - .block_on(source_dist.unpack(target))?; + .block_on(source_dist.extract_in(target, ExtractPolicy::default())) + .map_err(io::Error::other)?; Ok(()) } @@ -716,7 +713,25 @@ mod tests { /// platform-independent deterministic builds. #[test] fn built_by_uv_building() { - let _preview = uv_preview::test::with_features(&[]); + built_by_uv_building_with_backend( + &[], + "1d9ce1ce63195fbee07314c0b595ba9e063670da8d10c252c351b21e94e3f508", + ); + } + + #[test] + fn built_by_uv_building_tar_codec() { + built_by_uv_building_with_backend( + &[PreviewFeature::TarCodec], + "88540014e8884fff1d6479c0c7315fcf497ba2a46f6db5c9f6e04f64a8620dcc", + ); + } + + fn built_by_uv_building_with_backend( + preview_features: &[PreviewFeature], + expected_source_dist_hash: &str, + ) { + let _preview = uv_preview::test::with_features(preview_features); let built_by_uv = Path::new("../../test/packages/built-by-uv"); let src = TempDir::new().unwrap(); for dir in [ @@ -786,9 +801,11 @@ mod tests { "built_by_uv-0.1.0.tar.gz" ); // Check that the source dist is reproducible across platforms. - assert_snapshot!( - hex::encode(sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())), - @"1d9ce1ce63195fbee07314c0b595ba9e063670da8d10c252c351b21e94e3f508" + assert_eq!( + hex::encode(sha2::Sha256::digest( + fs_err::read(&source_dist_path).unwrap() + )), + expected_source_dist_hash ); // Check both the files we report and the actual files assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @" diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 2c708bbca98..9f85d9f7a4c 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -12,15 +12,19 @@ use globset::{Glob, GlobSet}; use rustc_hash::FxHashSet; use std::io; use std::io::{BufReader, Cursor, Read, Write}; +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; use std::path::{Component, Path, PathBuf}; use std::pin::Pin; use std::task::{Context, Poll}; +use tar_codec::{ArchiveBuilder as _, Builder, EntryMetadata, FilePayload, TarEncoder}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio_tar::{EntryType, Header}; use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; use uv_fs::{Simplified, normalize_path}; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; +use uv_preview::PreviewFeature; use uv_warnings::warn_user_once; use walkdir::WalkDir; @@ -44,8 +48,13 @@ pub fn build_source_dist( } let temp_file = uv_fs::tempfile_in(source_dist_directory)?; - let writer = TarGzWriter::new(temp_file.as_file(), &source_dist_path); - write_source_dist(source_tree, writer, uv_version, show_warnings)?; + if uv_preview::is_enabled(PreviewFeature::TarCodec) { + let writer = TarCodecGzWriter::new(temp_file.as_file(), &source_dist_path); + write_source_dist(source_tree, writer, uv_version, show_warnings)?; + } else { + let writer = TokioTarGzWriter::new(temp_file.as_file(), &source_dist_path); + write_source_dist(source_tree, writer, uv_version, show_warnings)?; + } temp_file .persist(&source_dist_path) .map_err(|err| Error::Persist(source_dist_path.clone(), err.error))?; @@ -389,9 +398,8 @@ impl AsyncWrite for SyncWriter { } fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll> { - // `tokio::io::copy` flushes after each copied entry. Forwarding those flushes to the gzip - // encoder changes the deflate stream, even though the tar payload is identical. The - // encoder is finalized by `GzEncoder::finish` when the archive is closed. + // Per-entry flushes change the deflate stream, even though the tar payload is identical. + // The encoder is finalized by `GzEncoder::finish` after the tar archive is closed. Poll::Ready(Ok(())) } @@ -400,16 +408,30 @@ impl AsyncWrite for SyncWriter { } } -struct TarGzWriter { +struct TokioTarGzWriter { path: PathBuf, tar: tokio_tar::Builder>>, } -impl TarGzWriter { +impl TokioTarGzWriter { + fn new(writer: W, path: impl Into) -> Self { + let path = path.into(); + let gzip = GzEncoder::new(writer, Compression::default()); + let tar = tokio_tar::Builder::new_non_terminated(SyncWriter::new(gzip)); + Self { path, tar } + } +} + +struct TarCodecGzWriter { + path: PathBuf, + tar: Builder>>>, +} + +impl TarCodecGzWriter { fn new(writer: W, path: impl Into) -> Self { let path = path.into(); - let enc = GzEncoder::new(writer, Compression::default()); - let tar = tokio_tar::Builder::new_non_terminated(SyncWriter::new(enc)); + let gzip = GzEncoder::new(writer, Compression::default()); + let tar = TarEncoder::new(SyncWriter::new(gzip)).builder(); Self { path, tar } } } @@ -440,7 +462,7 @@ fn normalize_toml10_datetimes(value: &mut toml::Value) { } } -impl DirectoryWriter for TarGzWriter { +impl DirectoryWriter for TokioTarGzWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { let mut header = Header::new_gnu(); // Work around bug in Python's std tar module @@ -448,8 +470,6 @@ impl DirectoryWriter for TarGzWriter { // https://github.com/astral-sh/uv/pull/17043#issuecomment-3636841022 header.set_entry_type(EntryType::Regular); header.set_size(bytes.len() as u64); - // Reasonable default to avoid 0o000 permissions, the user's umask will be applied on - // unpacking. header.set_mode(0o644); block_on( self.tar @@ -466,24 +486,12 @@ impl DirectoryWriter for TarGzWriter { // https://github.com/python/cpython/issues/141707 // https://github.com/astral-sh/uv/pull/17043#issuecomment-3636841022 header.set_entry_type(EntryType::Regular); - // Preserve the executable bit, especially for scripts #[cfg(unix)] - let executable_bit = { - use std::os::unix::fs::PermissionsExt; - file.metadata()?.permissions().mode() & 0o111 != 0 - }; - // Windows has no executable bit + let executable_bit = metadata.permissions().mode() & 0o111 != 0; #[cfg(not(unix))] let executable_bit = false; - // Set reasonable defaults to avoid 0o000 permissions, while avoiding adding the exact - // filesystem permissions to the archive for reproducibility. Where applicable, the - // operating system filters the stored permission by the user's umask when unpacking. - if executable_bit { - header.set_mode(0o755); - } else { - header.set_mode(0o644); - } + header.set_mode(if executable_bit { 0o755 } else { 0o644 }); header.set_size(metadata.len()); let reader = BufReader::new(File::open(file)?); block_on( @@ -496,7 +504,6 @@ impl DirectoryWriter for TarGzWriter { fn write_directory(&mut self, directory: &str) -> Result<(), Error> { let mut header = Header::new_gnu(); - // Directories are always executable, which means they can be listed. header.set_mode(0o755); header.set_entry_type(EntryType::Directory); header.set_size(0); @@ -519,3 +526,49 @@ impl DirectoryWriter for TarGzWriter { Ok(()) } } + +impl DirectoryWriter for TarCodecGzWriter { + fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { + block_on(self.tar.add_file(path, bytes, EntryMetadata::default())) + .map_err(|err| Error::TarCodecWrite(self.path.clone(), err))?; + Ok(()) + } + + fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { + let metadata = fs_err::metadata(file)?; + // Preserve the executable bit, especially for scripts + #[cfg(unix)] + let executable_bit = metadata.permissions().mode() & 0o111 != 0; + // Windows has no executable bit + #[cfg(not(unix))] + let executable_bit = false; + + let reader = BufReader::new(File::open(file)?); + let payload = FilePayload::new(metadata.len(), SyncReader::new(reader)); + block_on(self.tar.add_file( + path, + payload, + EntryMetadata::default().executable(executable_bit), + )) + .map_err(|err| Error::TarCodecWrite(self.path.clone(), err))?; + Ok(()) + } + + fn write_directory(&mut self, directory: &str) -> Result<(), Error> { + block_on(self.tar.add_directory(directory)) + .map_err(|err| Error::TarCodecWrite(self.path.clone(), err))?; + Ok(()) + } + + fn close(self, _dist_info_dir: &str) -> Result<(), Error> { + let path = self.path; + let encoder = block_on(self.tar.finish_into_inner()) + .map_err(|err| Error::TarCodecWrite(path.clone(), err))?; + encoder + .into_inner() + .into_inner() + .finish() + .map_err(|err| Error::GzipWrite(path, err))?; + Ok(()) + } +} diff --git a/crates/uv-dev/src/generate_preview_features_reference.rs b/crates/uv-dev/src/generate_preview_features_reference.rs index d1dcb927b3e..82021e03589 100644 --- a/crates/uv-dev/src/generate_preview_features_reference.rs +++ b/crates/uv-dev/src/generate_preview_features_reference.rs @@ -105,7 +105,7 @@ mod tests { #[test] fn generates_preview_feature_reference() { - assert_snapshot!(generate(), @r" + assert_snapshot!(generate(), @" - `add-bounds`: Allows configuring the [default bounds for `uv add`](../reference/settings.md#add-bounds) invocations. - `adjust-ulimit`: On Unix, raises the process's soft open-file limit at startup, up to the hard limit. - `audit-command`: Allows using `uv audit`. @@ -147,6 +147,7 @@ mod tests { - `s3-endpoint`: Allows signing requests to configured S3-compatible endpoints. - `sbom-export`: Allows using `uv export --format=cyclonedx1.5`. - `special-conda-env-names`: Stops treating Conda environments named `base` or `root` as special. + - `tar-codec`: Uses the new `tar-codec` encoding/decoding backend, instead of `astral-tokio-tar`. - `target-workspace-discovery`: Uses the directory containing a local `uv run` target, rather than the current working directory, as the starting point for project and workspace discovery. This feature takes effect before configuration is loaded. diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index c99abe6f321..1dbdb3c0ceb 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -19,6 +19,7 @@ workspace = true uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-pypi-types = { workspace = true } +uv-preview = { workspace = true } uv-static = { workspace = true } astral-tokio-tar = { workspace = true } @@ -35,6 +36,7 @@ regex = { workspace = true } reqwest = { workspace = true } rustc-hash = { workspace = true } sha2 = { workspace = true } +tar-codec = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } @@ -43,6 +45,12 @@ tracing = { workspace = true } [features] default = [] +[dev-dependencies] +uv-preview = { workspace = true, features = ["testing"] } + +insta = { workspace = true } +tempfile = { workspace = true } + [package.metadata.cargo-shear] # NOTE: `async-compression` owns the actual Deflate codec, but we need to # explicitly declare `flate2` here so the workspace's `zlib-rs` backend stays diff --git a/crates/uv-extract/src/error.rs b/crates/uv-extract/src/error.rs index 88997d10306..20b3a067a76 100644 --- a/crates/uv-extract/src/error.rs +++ b/crates/uv-extract/src/error.rs @@ -9,7 +9,17 @@ pub enum Error { #[error("Invalid zip file structure")] AsyncZip(#[source] async_zip::error::ZipError), #[error("Invalid tar file")] - Tar(#[from] tokio_tar::TarError), + Tar( + #[source] + #[from] + tokio_tar::TarError, + ), + #[error("Invalid tar file")] + TarCodec( + #[source] + #[from] + tar_codec::ExtractError, + ), #[error( "The top-level of the archive must only contain a list directory, but it contains: {0:?}" )] @@ -114,19 +124,14 @@ impl From for Error { } impl Error { - /// When reading from an archive, the error can either be an IO error from the underlying - /// operating system, or an error with the archive. Both get wrapper into an IO error through - /// e.g., `io::copy`. This method extracts zip and tar errors, to distinguish them from invalid - /// archives. - pub(crate) fn io_or_compression(err: std::io::Error) -> Self { + /// When reading from a ZIP archive, the error can either be an I/O error from the underlying + /// operating system, or an error with the archive. Both get wrapped into an I/O error through + /// e.g., `io::copy`. This method extracts ZIP errors to distinguish them from invalid archives. + pub(crate) fn io_or_zip(err: std::io::Error) -> Self { if err.kind() != std::io::ErrorKind::Other { return Self::Io(err); } - let err = match err.downcast::() { - Ok(tar_err) => return Self::Tar(tar_err), - Err(err) => err, - }; let err = match err.downcast::() { Ok(zip_err) => return Self::from(zip_err), Err(err) => err, @@ -134,6 +139,20 @@ impl Error { Self::Io(err) } + /// When reading a tar archive, the error can either be an I/O error from the underlying + /// reader or an error with the archive. Both get wrapped into an I/O error through operations + /// such as `io::copy`. This method extracts tar errors to distinguish them from I/O errors. + pub(crate) fn io_or_tar(err: std::io::Error) -> Self { + if err.kind() != std::io::ErrorKind::Other { + return Self::Io(err); + } + + match err.downcast::() { + Ok(tar_err) => Self::Tar(tar_err), + Err(err) => Self::Io(err), + } + } + /// Returns `true` if the error is due to the server not supporting HTTP streaming. Most /// commonly, this is due to serving ZIP files with features that are incompatible with /// streaming, like data descriptors. @@ -146,12 +165,27 @@ impl Error { /// Returns `true` if the error is due to HTTP streaming request failed. pub fn is_http_streaming_failed(&self) -> bool { - match self { - Self::AsyncZip(async_zip::error::ZipError::UpstreamReadError(_)) => true, - Self::Io(err) if let Some(inner) = err.get_ref() => { - inner.downcast_ref::().is_some() + fn contains_reqwest_error(error: &(dyn std::error::Error + 'static)) -> bool { + if error.downcast_ref::().is_some() { + return true; + } + // `std::io::Error::source` forwards to the source of its custom error and can skip the + // custom error itself, so inspect `get_ref` before following the standard chain. + if let Some(error) = error.downcast_ref::() + && let Some(inner) = error.get_ref() + && contains_reqwest_error(inner) + { + return true; } - _ => false, + error.source().is_some_and(contains_reqwest_error) + } + + if matches!( + self, + Self::AsyncZip(async_zip::error::ZipError::UpstreamReadError(_)) + ) { + return true; } + contains_reqwest_error(self) } } diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 3d332c961c0..911d4f661e5 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -5,10 +5,15 @@ use async_zip::base::read::cd::Entry; use async_zip::error::ZipError; use futures::{AsyncReadExt, StreamExt}; use rustc_hash::{FxHashMap, FxHashSet}; +use tar_codec::extract::{ExtractPolicy, LinkPolicy, SymlinkPolicy}; +use tar_codec::{ + Archive, DecodeError, DecodePolicy, ExtractError, Member, PaxDecodePolicy, TarArchive, +}; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tracing::{debug, warn}; use uv_distribution_filename::{LegacySourceDistExtension, SourceDistExtension}; +use uv_preview::PreviewFeature; use crate::{Error, insecure_no_validate, validate_archive_member_name}; @@ -178,7 +183,7 @@ pub async fn unzip( let mut reader = entry.reader_mut().compat(); let bytes_read = tokio::io::copy(&mut reader, &mut writer) .await - .map_err(Error::io_or_compression)?; + .map_err(Error::io_or_zip)?; let reader = reader.into_inner(); (bytes_read, reader) @@ -198,7 +203,7 @@ pub async fn unzip( let bytes_read = entry_reader .read_to_end(&mut expected_contents) .await - .map_err(Error::io_or_compression)?; + .map_err(Error::io_or_zip)?; // Verify that the existing file contents match the expected contents. if existing_contents != expected_contents { @@ -570,10 +575,83 @@ pub async fn unzip( /// Unpack the given tar archive into the destination directory. /// +/// Returns the list of unpacked files and their sizes. +async fn untar_in_tar_codec( + reader: R, + dst: &Path, +) -> Result, ExtractError> { + // Note: we intentionally allow `VENDOR.name` pax records here, + // and we also allow them to contain non-UTF-8. The latter is technically + // a violation of the pax spec, but is prevalent in the wild thanks + // to both GNU tar and libarchive encoding `SCHILY.xattr` records + // as raw binary. + let decode_policy = DecodePolicy::default().pax_policy( + PaxDecodePolicy::default() + .allow_unknown_pax_vendor_records(true) + .allow_non_utf8_pax_vendor_values(true), + ); + let archive = TarArchive::new_with_policy(reader, decode_policy); + + let mut files = Vec::new(); + RecordingArchive::new(archive, &mut files) + .extract_in(dst, tar_extract_policy()) + .await?; + Ok(files) +} + +/// An archive adapter that records file metadata as members are extracted. +/// +/// Keeping this observation inside the lending archive cursor avoids a second filesystem walk and +/// preserves the paths and declared sizes from the archive itself. +struct RecordingArchive<'files, A> { + archive: A, + files: &'files mut Vec<(PathBuf, u64)>, +} + +impl<'files, A> RecordingArchive<'files, A> { + fn new(archive: A, files: &'files mut Vec<(PathBuf, u64)>) -> Self { + Self { archive, files } + } +} + +impl Archive for RecordingArchive<'_, A> { + type Error = A::Error; + type Payload<'archive> + = A::Payload<'archive> + where + Self: 'archive; + + async fn next_member(&mut self) -> Result>>, Self::Error> { + let Self { archive, files } = self; + let member = archive.next_member().await?; + #[cfg(windows)] + if let Some(Member::SymbolicLink { metadata, .. }) = &member { + warn!("Skipping symlink in tar archive: {}", metadata.path); + } + if let Some(Member::File { metadata, size, .. }) = &member { + files.push((PathBuf::from(&metadata.path), *size)); + } + Ok(member) + } +} + +fn tar_extract_policy() -> ExtractPolicy { + // Keep tar-codec's defaults, including name validation, hardlink rejection, and rejection of + // pre-existing link targets. uv extracts archives into new temporary directories. + if cfg!(windows) { + ExtractPolicy::default() + .link_policy(LinkPolicy::default().symlink_policy(SymlinkPolicy::Skip)) + } else { + ExtractPolicy::default() + } +} + +/// Unpack the given tar archive into the destination directory with `astral-tokio-tar`. +/// /// This is equivalent to `archive.unpack_in(dst)`, but it also preserves the executable bit. /// /// Returns the list of unpacked files and their sizes. -async fn untar_in( +async fn untar_in_tokio_tar( mut archive: tokio_tar::Archive<&'_ mut (dyn tokio::io::AsyncRead + Unpin)>, dst: &Path, ) -> std::io::Result> { @@ -604,7 +682,6 @@ async fn untar_in( let entry_type = file.header().entry_type(); // Unpack the file into the destination directory. - #[cfg_attr(not(unix), allow(unused_variables))] let unpacked_at = file.unpack_in_raw(&dst, &mut memo).await?; // Collect file paths (excluding directories) that were unpacked successfully. @@ -642,6 +719,26 @@ async fn untar_in( Ok(files) } +/// Select the tar implementation and unpack the archive into the destination directory. +async fn untar_in( + mut reader: R, + dst: &Path, +) -> Result, Error> { + if uv_preview::is_enabled(PreviewFeature::TarCodec) { + untar_in_tar_codec(reader, dst).await.map_err(Error::from) + } else { + let archive = + tokio_tar::ArchiveBuilder::new(&mut reader as &mut (dyn tokio::io::AsyncRead + Unpin)) + .set_preserve_mtime(false) + .set_preserve_permissions(false) + .set_allow_external_symlinks(false) + .build(); + untar_in_tokio_tar(archive, dst) + .await + .map_err(Error::io_or_tar) + } +} + /// Unpack a `.tar.gz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. @@ -652,18 +749,8 @@ async fn untar_gz( target: impl AsRef, ) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); - let mut decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); - - let archive = tokio_tar::ArchiveBuilder::new( - &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), - ) - .set_preserve_mtime(false) - .set_preserve_permissions(false) - .set_allow_external_symlinks(false) - .build(); - untar_in(archive, target.as_ref()) - .await - .map_err(Error::io_or_compression) + let decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); + untar_in(decompressed_bytes, target.as_ref()).await } /// Unpack a `.tar.zst` archive into the target directory, without requiring `Seek`. @@ -676,18 +763,8 @@ pub async fn untar_zst( target: impl AsRef, ) -> Result, Error> { let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); - let mut decompressed_bytes = async_compression::tokio::bufread::ZstdDecoder::new(reader); - - let archive = tokio_tar::ArchiveBuilder::new( - &mut decompressed_bytes as &mut (dyn tokio::io::AsyncRead + Unpin), - ) - .set_preserve_mtime(false) - .set_preserve_permissions(false) - .set_allow_external_symlinks(false) - .build(); - untar_in(archive, target.as_ref()) - .await - .map_err(Error::io_or_compression) + let decompressed_bytes = async_compression::tokio::bufread::ZstdDecoder::new(reader); + untar_in(decompressed_bytes, target.as_ref()).await } /// Unpack a `.tar` archive into the target directory, without requiring `Seek`. @@ -699,17 +776,8 @@ async fn untar( reader: R, target: impl AsRef, ) -> Result, Error> { - let mut reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); - - let archive = - tokio_tar::ArchiveBuilder::new(&mut reader as &mut (dyn tokio::io::AsyncRead + Unpin)) - .set_preserve_mtime(false) - .set_preserve_permissions(false) - .set_allow_external_symlinks(false) - .build(); - untar_in(archive, target.as_ref()) - .await - .map_err(Error::io_or_compression) + let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); + untar_in(reader, target.as_ref()).await } /// Unpack a `.zip`, `.tar.gz`, or `.tar.zst` archive into the target directory, @@ -732,3 +800,556 @@ pub async fn archive( SourceDistExtension::Legacy(_) => Err(Error::UnsupportedCompression), } } + +#[cfg(test)] +mod tests { + use std::fmt::Write as _; + use std::io; + use std::path::PathBuf; + use std::pin::Pin; + use std::task::{Context, Poll}; + + use insta::assert_snapshot; + use tar_codec::{DecodeError, ExtractError, ExtractPolicyViolation}; + use tokio::io::{AsyncRead, ReadBuf}; + use uv_preview::PreviewFeature; + + use super::{untar, untar_in, untar_in_tar_codec}; + + const BLOCK_SIZE: usize = 512; + + #[tokio::test] + async fn untar_records_files_and_preserves_executable_intent() { + let mut archive = Vec::new(); + append_entry( + &mut archive, + "pax", + b'x', + &pax_record("path", "pkg/tool"), + "", + 0o644, + ); + append_entry(&mut archive, "ignored", b'0', b"run", "", 0o755); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let files = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect("tar archive should extract"); + + assert_eq!(files, [(PathBuf::from("pkg/tool"), 3)]); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt as _; + + assert_ne!( + fs_err::metadata(temp.path().join("pkg/tool")) + .expect("tool metadata should be readable") + .permissions() + .mode() + & 0o111, + 0 + ); + } + } + + #[tokio::test] + async fn untar_rejects_hardlinks() { + let mut archive = Vec::new(); + append_entry(&mut archive, "pkg/tool", b'0', b"run", "", 0o644); + append_entry(&mut archive, "pkg/tool-copy", b'1', b"", "pkg/tool", 0o644); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err("hardlink should be rejected"); + + assert!(matches!( + error, + ExtractError::PolicyViolation { + violation: ExtractPolicyViolation::HardLink, + .. + } + )); + assert!(!temp.path().join("pkg/tool-copy").exists()); + } + + #[tokio::test] + async fn untar_selects_backend_from_preview_feature() { + let mut archive = Vec::new(); + append_entry(&mut archive, "pkg/tool", b'0', b"run", "", 0o644); + append_entry(&mut archive, "pkg/tool-copy", b'1', b"", "pkg/tool", 0o644); + finish_archive(&mut archive); + + { + let _preview = uv_preview::test::with_features(&[]); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let files = untar_in(archive.as_slice(), temp.path()) + .await + .expect("the default tar backend should allow hardlinks"); + + assert_eq!( + files, + [ + (PathBuf::from("pkg/tool"), 3), + (PathBuf::from("pkg/tool-copy"), 0), + ] + ); + assert_eq!( + fs_err::read(temp.path().join("pkg/tool-copy")) + .expect("hardlink should be readable"), + b"run" + ); + } + + { + let _preview = uv_preview::test::with_features(&[PreviewFeature::TarCodec]); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar_in(archive.as_slice(), temp.path()) + .await + .expect_err("the tar-codec backend should reject hardlinks"); + + assert!(matches!( + error, + crate::Error::TarCodec(ExtractError::PolicyViolation { + violation: ExtractPolicyViolation::HardLink, + .. + }) + )); + } + } + + #[tokio::test] + async fn untar_uses_default_name_validation() { + let mut archive = Vec::new(); + append_entry( + &mut archive, + "pkg/name:stream", + b'0', + b"contents", + "", + 0o644, + ); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err("default name validation should reject colons"); + + assert!(matches!( + error, + ExtractError::PolicyViolation { + violation: ExtractPolicyViolation::NameRejected { + context: "member path", + value, + }, + .. + } if value == "pkg/name:stream" + )); + } + + #[tokio::test] + async fn untar_reads_gnu_archives() { + let mut archive = Vec::new(); + append_entry_with_format( + &mut archive, + TestFormat::Gnu, + "pkg/file", + b'0', + b"contents", + "", + 0o644, + ); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let files = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect("GNU archive should extract"); + + assert_eq!(files, [(PathBuf::from("pkg/file"), 8)]); + assert_eq!( + fs_err::read(temp.path().join("pkg/file")).expect("file should be readable"), + b"contents" + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn untar_preserves_archive_owned_and_missing_symlink_targets() { + let mut archive = Vec::new(); + append_entry( + &mut archive, + "pkg/created-link", + b'2', + b"", + "created", + 0o777, + ); + append_entry(&mut archive, "pkg/created", b'0', b"created", "", 0o644); + append_entry( + &mut archive, + "pkg/missing-link", + b'2', + b"", + "missing", + 0o777, + ); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let files = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect("safe symlinks should extract"); + + assert_eq!(files, [(PathBuf::from("pkg/created"), 7)]); + for (link, target) in [("created-link", "created"), ("missing-link", "missing")] { + assert_eq!( + fs_err::read_link(temp.path().join("pkg").join(link)) + .expect("symlink should be readable"), + PathBuf::from(target) + ); + } + } + + #[cfg(unix)] + #[tokio::test] + async fn untar_rejects_ambient_symlink_targets() { + let mut archive = Vec::new(); + append_entry( + &mut archive, + "pkg/ambient-link", + b'2', + b"", + "ambient", + 0o777, + ); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + fs_err::create_dir_all(temp.path().join("pkg")) + .expect("ambient directory should be created"); + fs_err::write(temp.path().join("pkg/ambient"), b"ambient") + .expect("ambient target should be created"); + + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err("ambient symlink target should be rejected"); + + assert!(matches!( + error, + ExtractError::InvalidLink { + path, + target, + reason: "ambient target is not allowed", + .. + } if path == *"pkg/ambient-link" && target == "ambient" + )); + assert!(!temp.path().join("pkg/ambient-link").exists()); + } + + #[cfg(windows)] + #[tokio::test] + async fn untar_skips_symlinks_on_windows() { + let mut archive = Vec::new(); + append_entry(&mut archive, "target", b'0', b"contents", "", 0o644); + append_entry(&mut archive, "link", b'2', b"", "target", 0o777); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect("archive should extract while skipping its symlink"); + + assert_eq!( + fs_err::read(temp.path().join("target")).expect("target should be readable"), + b"contents" + ); + assert!(matches!( + fs_err::symlink_metadata(temp.path().join("link")), + Err(error) if error.kind() == io::ErrorKind::NotFound + )); + } + + #[tokio::test] + async fn untar_rejects_malformed_archives() { + let temp = tempfile::tempdir().expect("temporary directory should be created"); + let error = untar_in_tar_codec(b"not a tar archive".as_slice(), temp.path()) + .await + .expect_err("malformed archive should be rejected"); + + assert!(matches!( + error, + ExtractError::Archive(DecodeError::Framing(_)) + )); + } + + #[tokio::test] + async fn untar_rejects_member_path_traversal() { + let mut archive = Vec::new(); + append_entry(&mut archive, "../escape", b'0', b"escape", "", 0o644); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + let destination = temp.path().join("out"); + + let error = untar_in_tar_codec(archive.as_slice(), &destination) + .await + .expect_err("parent-directory traversal should be rejected"); + + assert!(matches!( + error, + ExtractError::UnsafePath { + context: "member path", + value, + reason: "contains a parent-directory component", + .. + } if value == "../escape" + )); + assert!(!temp.path().join("escape").exists()); + } + + #[tokio::test] + async fn strict_decoder_rejects_excluded_tar_framing() { + let mut v7 = Vec::new(); + append_entry_with_format(&mut v7, TestFormat::V7, "file", b'0', b"", "", 0o644); + finish_archive(&mut v7); + + let mut mixed = Vec::new(); + append_entry_with_format(&mut mixed, TestFormat::Pax, "pax", b'0', b"", "", 0o644); + append_entry_with_format(&mut mixed, TestFormat::Gnu, "gnu", b'0', b"", "", 0o644); + finish_archive(&mut mixed); + + let mut sparse = Vec::new(); + append_entry_with_format(&mut sparse, TestFormat::Gnu, "sparse", b'S', b"", "", 0o644); + finish_archive(&mut sparse); + + let mut malformed_termination = Vec::new(); + append_entry(&mut malformed_termination, "file", b'0', b"", "", 0o644); + malformed_termination.resize(malformed_termination.len() + BLOCK_SIZE, 0); + + let mut failures = String::new(); + for (case, archive) in [ + ("v7", v7), + ("mixed GNU/pax", mixed), + ("sparse", sparse), + ("malformed termination", malformed_termination), + ] { + let temp = tempfile::tempdir().expect("temporary directory should be created"); + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err(case); + assert!( + matches!(&error, ExtractError::Archive(DecodeError::Framing(_))), + "unexpected error for {case}: {error:?}" + ); + writeln!(failures, "{case}: {error}").expect("writing to a string should succeed"); + } + assert_snapshot!(failures, @r" + v7: at byte 0: invalid tar identity: found [0, 0, 0, 0, 0, 0, 0, 0] + mixed GNU/pax: at byte 512: archive format changed from Pax to Gnu + sparse: at byte 0: unsupported tar typeflag 83 + malformed termination: at byte 1024: missing two-block end-of-archive marker + "); + } + + #[tokio::test] + async fn strict_decoder_rejects_non_utf8_member_names() { + let mut header = test_header(TestFormat::Pax, "file", b'0', 0, "", 0o644); + header[0] = 0xff; + set_checksum(&mut header); + let mut archive = header.to_vec(); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err("non-UTF-8 member path should be rejected"); + + assert!(matches!( + &error, + ExtractError::Archive(DecodeError::InvalidUtf8 { field: "path", .. }) + )); + assert_snapshot!(error, @"at byte 0: path is not valid UTF-8"); + } + + #[tokio::test] + async fn tar_http_errors_are_detected_through_the_source_chain() { + { + let _preview = uv_preview::test::with_features(&[]); + let reqwest_error = reqwest::Client::new() + .get("://") + .build() + .expect_err("invalid URL should be rejected"); + let reader = FailingReader(Some(io::Error::other(reqwest_error))); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar(reader, temp.path()) + .await + .expect_err("reader error should fail astral-tokio-tar extraction"); + + assert!(matches!(error, crate::Error::Io(_))); + assert!(error.is_http_streaming_failed()); + } + + { + let _preview = uv_preview::test::with_features(&[PreviewFeature::TarCodec]); + let reqwest_error = reqwest::Client::new() + .get("://") + .build() + .expect_err("invalid URL should be rejected"); + let reader = FailingReader(Some(io::Error::other(reqwest_error))); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar(reader, temp.path()) + .await + .expect_err("reader error should fail tar-codec extraction"); + + assert!(matches!(error, crate::Error::TarCodec(_))); + assert!(error.is_http_streaming_failed()); + } + } + + #[cfg(unix)] + #[tokio::test] + async fn untar_rejects_external_symlink_targets() { + let mut archive = Vec::new(); + append_entry(&mut archive, "python", b'2', b"", "/bin/python", 0o777); + finish_archive(&mut archive); + let temp = tempfile::tempdir().expect("temporary directory should be created"); + + let error = untar_in_tar_codec(archive.as_slice(), temp.path()) + .await + .expect_err("external symlink target should be rejected"); + + assert!(matches!( + error, + ExtractError::UnsafePath { + context: "symbolic-link target", + value, + reason: "is absolute", + .. + } if value == "/bin/python" + )); + assert!(!temp.path().join("python").exists()); + } + + fn append_entry( + archive: &mut Vec, + path: &str, + typeflag: u8, + payload: &[u8], + link_name: &str, + mode: u32, + ) { + append_entry_with_format( + archive, + TestFormat::Pax, + path, + typeflag, + payload, + link_name, + mode, + ); + } + + fn append_entry_with_format( + archive: &mut Vec, + format: TestFormat, + path: &str, + typeflag: u8, + payload: &[u8], + link_name: &str, + mode: u32, + ) { + let header = test_header( + format, + path, + typeflag, + payload.len() as u64, + link_name, + mode, + ); + archive.extend_from_slice(&header); + archive.extend_from_slice(payload); + archive.resize(archive.len().next_multiple_of(BLOCK_SIZE), 0); + } + + fn test_header( + format: TestFormat, + path: &str, + typeflag: u8, + size: u64, + link_name: &str, + mode: u32, + ) -> [u8; BLOCK_SIZE] { + let mut header = [0; BLOCK_SIZE]; + set_text(&mut header[0..100], path); + header[100..108].copy_from_slice(format!("{mode:07o}\0").as_bytes()); + header[108..116].copy_from_slice(b"0000000\0"); + header[116..124].copy_from_slice(b"0000000\0"); + header[124..136].copy_from_slice(format!("{size:011o}\0").as_bytes()); + header[136..148].copy_from_slice(b"00000000000\0"); + header[156] = typeflag; + set_text(&mut header[157..257], link_name); + match format { + TestFormat::Pax => header[257..265].copy_from_slice(b"ustar\x0000"), + TestFormat::Gnu => header[257..265].copy_from_slice(b"ustar \0"), + TestFormat::V7 => {} + } + set_checksum(&mut header); + header + } + + fn set_checksum(header: &mut [u8; BLOCK_SIZE]) { + header[148..156].fill(b' '); + let checksum: u64 = header.iter().map(|byte| u64::from(*byte)).sum(); + header[148..156].copy_from_slice(format!("{checksum:06o}\0 ").as_bytes()); + } + + fn finish_archive(archive: &mut Vec) { + archive.resize(archive.len() + 2 * BLOCK_SIZE, 0); + } + + fn set_text(field: &mut [u8], value: &str) { + assert!(value.len() < field.len()); + field[..value.len()].copy_from_slice(value.as_bytes()); + } + + fn pax_record(keyword: &str, value: &str) -> Vec { + let mut length = 0; + loop { + let record = format!("{length} {keyword}={value}\n"); + if record.len() == length { + return record.into_bytes(); + } + length = record.len(); + } + } + + #[derive(Clone, Copy)] + enum TestFormat { + Pax, + Gnu, + V7, + } + + struct FailingReader(Option); + + impl AsyncRead for FailingReader { + fn poll_read( + self: Pin<&mut Self>, + _context: &mut Context<'_>, + _buffer: &mut ReadBuf<'_>, + ) -> Poll> { + let error = self + .get_mut() + .0 + .take() + .unwrap_or_else(|| io::Error::other("reader was polled after failing")); + Poll::Ready(Err(error)) + } + } +} diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index 8edb01d4823..3b5addeada4 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -85,10 +85,7 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result, let mut copied = 0; let mut buffer = vec![0; 128 * 1024]; loop { - let read = file - .read(&mut buffer) - .await - .map_err(Error::io_or_compression)?; + let read = file.read(&mut buffer).await.map_err(Error::io_or_zip)?; if read == 0 { break; } diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 0eda60a7962..6ad883a4254 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -327,6 +327,8 @@ pub enum PreviewFeature { LockfileFormatCheck, /// Omit `package.metadata` from `uv.lock`. LockWithoutMetadata, + /// Uses the new `tar-codec` encoding/decoding backend, instead of `astral-tokio-tar`. + TarCodec, } impl Display for PreviewFeature { @@ -517,6 +519,10 @@ mod tests { assert_eq!(PreviewFeature::from_str(alias).unwrap(), feature); } } + + let feature = PreviewFeature::from_str("tar-codec").unwrap(); + assert_eq!(feature, PreviewFeature::TarCodec); + assert_eq!(feature.to_string(), "tar-codec"); } #[test] @@ -525,6 +531,9 @@ mod tests { let preview = Preview::from_str("python-install-default").unwrap(); assert_eq!(preview.flags, PreviewFeature::PythonInstallDefault); + let preview = Preview::from_str("tar-codec").unwrap(); + assert!(preview.is_enabled(PreviewFeature::TarCodec)); + // Test multiple features let preview = Preview::from_str("json-output,pylock").unwrap(); assert!(preview.is_enabled(PreviewFeature::JsonOutput)); @@ -561,6 +570,7 @@ mod tests { // Test enabled (all features) let preview = Preview::all(); assert_eq!(preview.to_string(), "enabled"); + assert!(preview.is_enabled(PreviewFeature::TarCodec)); // Test single feature let preview = Preview::new(&[PreviewFeature::PythonInstallDefault]); diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index dc99a072b6c..826a6d0899d 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -23,6 +23,7 @@ uv-distribution-types = { workspace = true } uv-extract = { workspace = true } uv-fs = { workspace = true } uv-metadata = { workspace = true } +uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-redacted = { workspace = true } uv-static = { workspace = true } @@ -42,6 +43,7 @@ reqwest-retry = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +tar-codec = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["io"] } @@ -49,9 +51,12 @@ tracing = { workspace = true } url = { workspace = true } [dev-dependencies] +uv-preview = { workspace = true, features = ["testing"] } + anstream = { workspace = true } fastrand = { workspace = true } insta = { workspace = true } +tempfile = { workspace = true } wiremock = { workspace = true } [features] diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 95f96c114b3..78b2a390045 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -16,6 +16,7 @@ use reqwest_retry::RetryError; use reqwest_retry::policies::ExponentialBackoff; use rustc_hash::FxHashMap; use serde::Deserialize; +use tar_codec::{Archive as _, Member, MemberPayload as _, TarArchive}; use thiserror::Error; use tokio::io::{AsyncReadExt, BufReader}; use tokio::sync::Semaphore; @@ -35,6 +36,7 @@ use uv_distribution_types::{IndexCapabilities, IndexUrl}; use uv_extract::hash::{HashReader, Hasher}; use uv_fs::{ProgressReader, Simplified}; use uv_metadata::read_metadata_async_seek; +use uv_preview::PreviewFeature; use uv_pypi_types::{HashAlgorithm, HashDigest, Metadata23, MetadataError}; use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError}; use uv_warnings::warn_user; @@ -117,8 +119,14 @@ pub enum PublishPrepareError { MissingPkgInfo, #[error("Multiple PKG-INFO files found: `{0}`")] MultiplePkgInfo(String), + #[error("Failed to decode source distribution")] + Decode(#[source] tar_codec::DecodeError), #[error("Failed to read: `{0}`")] - Read(String, #[source] io::Error), + Read(String, #[source] tar_codec::DecodeError), + #[error(transparent)] + TokioTar(io::Error), + #[error("Failed to read: `{0}`")] + TokioTarRead(String, #[source] io::Error), #[error("Invalid PEP 740 attestation (not JSON): `{0}`")] InvalidAttestation(PathBuf, #[source] serde_json::Error), } @@ -1082,16 +1090,25 @@ async fn hash_file( // Not in `uv-metadata` because we only support tar files here. async fn source_dist_pkg_info(file: &Path) -> Result, PublishPrepareError> { + if uv_preview::is_enabled(PreviewFeature::TarCodec) { + source_dist_pkg_info_tar_codec(file).await + } else { + source_dist_pkg_info_tokio_tar(file).await + } +} + +async fn source_dist_pkg_info_tokio_tar(file: &Path) -> Result, PublishPrepareError> { let reader = BufReader::new(File::open(&file).await?); let decoded = async_compression::tokio::bufread::GzipDecoder::new(reader); let mut archive = tokio_tar::Archive::new(decoded); let mut pkg_infos: Vec<(PathBuf, Vec)> = archive - .entries()? - .map_err(PublishPrepareError::from) + .entries() + .map_err(PublishPrepareError::TokioTar)? + .map_err(PublishPrepareError::TokioTar) .try_filter_map(async |mut entry| { let path = entry .path() - .map_err(PublishPrepareError::from)? + .map_err(PublishPrepareError::TokioTar)? .to_path_buf(); let mut components = path.components(); let Some(_top_level) = components.next() else { @@ -1106,7 +1123,7 @@ async fn source_dist_pkg_info(file: &Path) -> Result, PublishPrepareErro let mut buffer = Vec::new(); // We have to read while iterating or the entry is empty as we're beyond it in the file. entry.read_to_end(&mut buffer).await.map_err(|err| { - PublishPrepareError::Read(path.to_string_lossy().to_string(), err) + PublishPrepareError::TokioTarRead(path.to_string_lossy().to_string(), err) })?; Ok(Some((path, buffer))) }) @@ -1124,6 +1141,52 @@ async fn source_dist_pkg_info(file: &Path) -> Result, PublishPrepareErro } } +async fn source_dist_pkg_info_tar_codec(file: &Path) -> Result, PublishPrepareError> { + let reader = BufReader::new(File::open(&file).await?); + let decoded = async_compression::tokio::bufread::GzipDecoder::new(reader); + // TODO(ww): Consider allowing vendor pax extensions while parsing here? + // We shouldn't encounter those with any normal Python build backend, but users + // can supply arbitrary dists so we may have to loosen here until PyPI itself + // restricts further. + let mut members = TarArchive::new(decoded).members(); + let mut pkg_infos = Vec::new(); + + while let Some(member) = members.next().await.map_err(PublishPrepareError::Decode)? { + let path = member.metadata().path.clone(); + let mut components = Path::new(&path).components(); + let Some(_top_level) = components.next() else { + continue; + }; + let Some(pkg_info) = components.next() else { + continue; + }; + if components.next().is_some() || pkg_info.as_os_str() != "PKG-INFO" { + continue; + } + + let mut buffer = Vec::new(); + if let Member::File { mut payload, .. } | Member::HardLink { mut payload, .. } = member { + let mut chunk = Vec::new(); + while payload + .next_chunk(&mut chunk, 8 * 1024) + .await + .map_err(|err| PublishPrepareError::Read(path.clone(), err))? + { + buffer.extend_from_slice(&chunk); + } + } + pkg_infos.push((path, buffer)); + } + + match pkg_infos.len() { + 0 => Err(PublishPrepareError::MissingPkgInfo), + 1 => Ok(pkg_infos.remove(0).1), + _ => Err(PublishPrepareError::MultiplePkgInfo( + pkg_infos.iter().map(|(path, _buffer)| path).join(", "), + )), + } +} + async fn metadata(file: &Path, filename: &DistFilename) -> Result { let contents = match filename { DistFilename::SourceDistFilename(source_dist) => { @@ -1523,17 +1586,22 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; + use async_compression::tokio::write::GzipEncoder; use insta::{allow_duplicates, assert_debug_snapshot, assert_snapshot}; use itertools::Itertools; + use tar_codec::{ArchiveBuilder as _, EntryMetadata, TarEncoder}; + use tempfile::NamedTempFile; + use tokio::io::AsyncWriteExt as _; use uv_auth::Credentials; use uv_client::{AuthIntegration, BaseClientBuilder, RedirectPolicy}; use uv_distribution_filename::DistFilename; + use uv_preview::PreviewFeature; use uv_pypi_types::{HashDigest, Metadata23}; use uv_redacted::DisplaySafeUrl; use crate::{ - FormMetadata, PublishError, Reporter, UploadDistribution, build_upload_request, - group_files, upload, + FormMetadata, PublishError, PublishPrepareError, Reporter, UploadDistribution, + build_upload_request, group_files, source_dist_pkg_info, upload, }; use tokio::sync::Semaphore; use uv_errors::{ErrorOptions, Hints, write_error_chain_with_options}; @@ -1542,6 +1610,8 @@ mod tests { struct DummyReporter; + const TAR_BACKENDS: &[&[PreviewFeature]] = &[&[], &[PreviewFeature::TarCodec]]; + impl Reporter for DummyReporter { fn on_progress(&self, _name: &str, _id: usize) {} fn on_upload_start(&self, _name: &str, _size: Option) -> usize { @@ -1556,6 +1626,80 @@ mod tests { fn on_hash_complete(&self, _id: usize) {} } + async fn source_dist(entries: &[(&str, &[u8])]) -> NamedTempFile { + let mut bytes = Vec::new(); + { + let mut gzip = GzipEncoder::new(&mut bytes); + { + let mut archive = TarEncoder::new(&mut gzip).builder(); + for (path, contents) in entries { + archive + .add_file(path, *contents, EntryMetadata::default()) + .await + .expect("test archive entry should be added"); + } + archive.finish().await.expect("test archive should finish"); + } + gzip.shutdown().await.expect("gzip stream should finish"); + } + + let file = NamedTempFile::new().expect("temporary file should be created"); + fs_err::write(file.path(), bytes).expect("test archive should be written"); + file + } + + #[tokio::test] + async fn source_dist_pkg_info_reads_single_top_level_file() { + let expected = b"Metadata-Version: 2.3\nName: example\nVersion: 1.0\n"; + let file = source_dist(&[ + ("example-1.0/nested/PKG-INFO", b"ignored"), + ("example-1.0/PKG-INFO", expected), + ("example-1.0/README", b"also ignored"), + ]) + .await; + + for features in TAR_BACKENDS { + let _preview = uv_preview::test::with_features(features); + assert_eq!( + source_dist_pkg_info(file.path()) + .await + .expect("top-level PKG-INFO should be read"), + expected + ); + } + } + + #[tokio::test] + async fn source_dist_pkg_info_requires_a_file() { + let file = source_dist(&[("example-1.0/nested/PKG-INFO", b"ignored")]).await; + + for features in TAR_BACKENDS { + let _preview = uv_preview::test::with_features(features); + assert!(matches!( + source_dist_pkg_info(file.path()).await, + Err(PublishPrepareError::MissingPkgInfo) + )); + } + } + + #[tokio::test] + async fn source_dist_pkg_info_rejects_duplicates() { + let file = source_dist(&[ + ("example-1.0/PKG-INFO", b"first"), + ("other-1.0/PKG-INFO", b"second"), + ]) + .await; + + for features in TAR_BACKENDS { + let _preview = uv_preview::test::with_features(features); + assert!(matches!( + source_dist_pkg_info(file.path()).await, + Err(PublishPrepareError::MultiplePkgInfo(paths)) + if paths == "example-1.0/PKG-INFO, other-1.0/PKG-INFO" + )); + } + } + async fn mock_server_upload(mock_server: &MockServer) -> Result { let raw_filename = "tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl"; let file = PathBuf::from("../../test/links/").join(raw_filename); @@ -1917,6 +2061,7 @@ mod tests { /// Snapshot the data we send for an upload request for a source distribution. #[tokio::test] async fn upload_request_source_dist() { + let _preview = uv_preview::test::with_features(&[]); let group = { let raw_filename = "tqdm-999.0.0.tar.gz"; let file = PathBuf::from("../../test/links/").join(raw_filename); diff --git a/crates/uv-test/Cargo.toml b/crates/uv-test/Cargo.toml index f2b60c2deb7..a1f2bc16e43 100644 --- a/crates/uv-test/Cargo.toml +++ b/crates/uv-test/Cargo.toml @@ -36,7 +36,6 @@ uv-static = { workspace = true } uv-version = { workspace = true } anyhow = { workspace = true } -astral-tokio-tar = { workspace = true } assert_cmd = { workspace = true } assert_fs = { workspace = true } async_zip = { workspace = true } @@ -57,6 +56,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } similar = { workspace = true } +tar-codec = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } diff --git a/crates/uv-test/src/packse/wheel.rs b/crates/uv-test/src/packse/wheel.rs index 9bea22f46e1..07073627819 100644 --- a/crates/uv-test/src/packse/wheel.rs +++ b/crates/uv-test/src/packse/wheel.rs @@ -5,7 +5,6 @@ use std::collections::BTreeMap; use std::fmt::Write as _; -use std::io::Cursor; use async_zip::base::write::ZipFileWriter; use async_zip::{Compression as ZipCompression, ZipEntryBuilder}; @@ -16,7 +15,8 @@ use futures::executor::block_on; use futures::io::AllowStdIo; use indoc::formatdoc; use sha2::{Digest, Sha256}; -use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; +use tar_codec::{ArchiveBuilder as _, EntryMetadata, TarEncoder}; +use tokio_util::compat::FuturesAsyncWriteCompatExt; use uv_normalize::{ExtraName, PackageName}; use uv_pep440::{Version, VersionSpecifiers}; @@ -104,29 +104,27 @@ pub fn generate_sdist( let normalized = name.as_dist_info_name(); let prefix = format!("{normalized}-{version}"); - let buf = Vec::new(); - let encoder = GzEncoder::new(buf, Compression::fast()); - let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(encoder).compat_write()); + let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); + let mut tar = TarEncoder::new(AllowStdIo::new(&mut encoder).compat_write()).builder(); let pyproject = build_pyproject_toml(name, version, requires, extras, requires_python); - append_tar_file( + add_tar_file( &mut tar, &format!("{prefix}/pyproject.toml"), pyproject.as_bytes(), ); let pkg_info = build_metadata(name, version, requires, extras, requires_python); - append_tar_file(&mut tar, &format!("{prefix}/PKG-INFO"), pkg_info.as_bytes()); + add_tar_file(&mut tar, &format!("{prefix}/PKG-INFO"), pkg_info.as_bytes()); let init_py = format!("__version__ = \"{version}\"\n"); - append_tar_file( + add_tar_file( &mut tar, &format!("{prefix}/src/{normalized}/__init__.py"), init_py.as_bytes(), ); - let writer = block_on(tar.into_inner()).expect("failed to finish in-memory source archive"); - let encoder = writer.into_inner().into_inner(); + block_on(tar.finish()).expect("failed to finish in-memory source archive"); let bytes = encoder .finish() .expect("failed to finish in-memory gzip stream"); @@ -232,22 +230,13 @@ fn build_pyproject_toml( } } -/// Append a file entry to a tar archive from a byte slice. -fn append_tar_file(tar: &mut tokio_tar::Builder, path: &str, data: &[u8]) +/// Add a file entry to a tar archive from a byte slice. +fn add_tar_file(tar: &mut tar_codec::Builder>, path: &str, data: &[u8]) where - W: tokio::io::AsyncWrite + Unpin + Send, + W: tokio::io::AsyncWrite + Unpin, { - let mut header = tokio_tar::Header::new_gnu(); - header.set_entry_type(tokio_tar::EntryType::Regular); - header.set_size(data.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - block_on(tar.append_data( - &mut header, - path, - AllowStdIo::new(Cursor::new(data)).compat(), - )) - .expect("failed to append file to in-memory source archive"); + block_on(tar.add_file(path, data, EntryMetadata::default())) + .expect("failed to add file to in-memory source archive"); } /// Compute the SHA-256 hex digest of a byte slice. @@ -259,10 +248,11 @@ pub fn sha256_hex(data: &[u8]) -> String { #[cfg(test)] mod tests { - use std::pin::Pin; + use std::io::Cursor; use std::str::FromStr; - use futures::StreamExt; + use tar_codec::{Archive as _, Member, TarArchive}; + use tokio_util::compat::FuturesAsyncReadCompatExt; use super::*; @@ -344,20 +334,18 @@ mod tests { assert_eq!(filename, "my_package-1.0.0.tar.gz"); let decoder = flate2::read::GzDecoder::new(Cursor::new(bytes)); - let mut archive = tokio_tar::Archive::new(AllowStdIo::new(decoder).compat()); + let archive = TarArchive::new(AllowStdIo::new(decoder).compat()); let names = block_on(async { - let mut entries = archive.entries().expect("sdist archive should be readable"); - let mut entries = Pin::new(&mut entries); + let mut members = archive.members(); let mut names = Vec::new(); - while let Some(entry) = entries.next().await { - let entry = entry.expect("sdist archive entry should be readable"); - names.push( - entry - .path() - .expect("sdist archive entry should have a path") - .to_string_lossy() - .to_string(), - ); + while let Some(member) = members + .next() + .await + .expect("sdist archive member should be readable") + { + if let Member::File { metadata, .. } = member { + names.push(metadata.path); + } } names }); diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 6c9150b92db..68eb99d4e82 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -100,6 +100,7 @@ rkyv = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tar-codec = { workspace = true } tempfile = { workspace = true } textwrap = { workspace = true } thiserror = { workspace = true } @@ -134,6 +135,7 @@ uv-test = { workspace = true } assert_cmd = { workspace = true } assert_fs = { workspace = true } +astral-tokio-tar = { workspace = true } async-compression = { workspace = true, features = ["zstd"] } backon = { workspace = true } byteorder = { workspace = true } @@ -150,7 +152,6 @@ predicates = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, features = ["blocking"], default-features = false } sha2 = { workspace = true } -astral-tokio-tar = { workspace = true } tempfile = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index f0f7c9c7585..eb3ee9d38b6 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -131,6 +131,44 @@ impl Hint for Error { Hints::none() } } + Self::Extract(uv_extract::Error::TarCodec(err)) => { + let is_virtual_environment_python = |path: &Path| { + path.parent().is_some_and(|parent| parent.ends_with("bin")) + && path + .file_name() + .is_some_and(|name| name.to_string_lossy().starts_with("python")) + }; + let involves_virtual_environment_python = match err { + tar_codec::ExtractError::UnsafePath { + context, + value, + reason, + .. + } => { + *context == "symbolic-link target" + && matches!(*reason, "is absolute" | "escapes the destination root") + && is_virtual_environment_python(Path::new(value)) + } + tar_codec::ExtractError::InvalidLink { + path, + target, + reason, + .. + } => { + *reason == "ambient target is not allowed" + && (is_virtual_environment_python(path) + || is_virtual_environment_python(Path::new(target))) + } + _ => false, + }; + if involves_virtual_environment_python { + Hints::from( + "The source distribution includes a virtual environment. Virtual environments must be excluded from source distributions.", + ) + } else { + Hints::none() + } + } _ => Hints::none(), } } diff --git a/crates/uv/tests/build/build.rs b/crates/uv/tests/build/build.rs index 7ecf77468af..eabda80b8e6 100644 --- a/crates/uv/tests/build/build.rs +++ b/crates/uv/tests/build/build.rs @@ -2822,7 +2822,7 @@ fn force_pep517() -> Result<()> { #[cfg(unix)] #[test] fn venv_included_in_sdist() -> Result<()> { - let context = uv_test::test_context!("3.12"); + let context = uv_test::test_context!("3.12").with_filter((r"at byte \d+", "at byte [OFFSET]")); context .init() @@ -2854,7 +2854,7 @@ fn venv_included_in_sdist() -> Result<()> { context.venv().arg("--clear").assert().success(); - // context.filters() + // The default astral-tokio-tar backend recognizes the external virtual-environment link. uv_snapshot!(context.filters(), context.build(), @" exit_code: 2 (failure) ----- stderr ----- @@ -2867,6 +2867,22 @@ fn venv_included_in_sdist() -> Result<()> { hint: The source distribution includes a virtual environment. Virtual environments must be excluded from source distributions. "); + // The preview tar-codec backend reports a structured unsafe-link error and preserves the same + // user-facing hint. + uv_snapshot!(context.filters(), context + .build() + .arg("--preview-features") + .arg("tar-codec"), @r#" + exit_code: 2 (failure) + ----- stderr ----- + Building source distribution... + error: Failed to build `[TEMP_DIR]/` + Caused by: Invalid tar file + Caused by: at byte [OFFSET]: unsafe symbolic-link target "[PYTHON-3.12]": is absolute + + hint: The source distribution includes a virtual environment. Virtual environments must be excluded from source distributions. + "#); + uv_snapshot!(context.filters(), context.build().arg("-q"), @" exit_code: 2 (failure) ----- stderr ----- diff --git a/crates/uv/tests/build/build_backend.rs b/crates/uv/tests/build/build_backend.rs index db67122170b..295ab6e3b28 100644 --- a/crates/uv/tests/build/build_backend.rs +++ b/crates/uv/tests/build/build_backend.rs @@ -9,6 +9,7 @@ use insta::{allow_duplicates, assert_json_snapshot, assert_snapshot}; use std::io::BufReader; use std::path::Path; use std::process::Command; +use tar_codec::{Archive as _, TarArchive, extract::ExtractPolicy}; use tempfile::TempDir; use tokio_util::compat::FuturesAsyncReadCompatExt; use uv_static::EnvVars; @@ -43,12 +44,11 @@ const BUILT_BY_UV_TEST_SCRIPT: &str = indoc! {r#" fn unpack_tar_gz(source_dist_path: &Path, target: &Path) -> Result<()> { let sdist_reader = BufReader::new(File::open(source_dist_path)?); - let mut source_dist = - tokio_tar::Archive::new(AllowStdIo::new(GzDecoder::new(sdist_reader)).compat()); + let source_dist = TarArchive::new(AllowStdIo::new(GzDecoder::new(sdist_reader)).compat()); tokio::runtime::Builder::new_current_thread() .enable_all() .build()? - .block_on(source_dist.unpack(target))?; + .block_on(source_dist.extract_in(target, ExtractPolicy::default()))?; Ok(()) } @@ -224,6 +224,7 @@ fn built_by_uv_editable() -> Result<()> { #[test] fn preserve_executable_bit() -> Result<()> { use std::io::Write; + use std::os::unix::fs::PermissionsExt; let context = uv_test::test_context!("3.12"); @@ -248,12 +249,45 @@ fn preserve_executable_bit() -> Result<()> { )?; fs_err::create_dir(project_dir.join("scripts"))?; + let script = project_dir.join("scripts").join("greet.sh"); fs_err::write( - project_dir.join("scripts").join("greet.sh"), + &script, indoc! {r#" echo "Hi from the shell" "#}, )?; + let mut permissions = fs_err::metadata(&script)?.permissions(); + permissions.set_mode(0o755); + fs_err::set_permissions(&script, permissions)?; + + context + .build_backend() + .arg("--preview-features") + .arg("tar-codec") + .arg("build-sdist") + .arg(context.temp_dir.path()) + .current_dir(&project_dir) + .assert() + .success(); + + uv_snapshot!(context.python_command() + .arg("-c") + .arg(indoc! {r#" + import sys + import tarfile + + with tarfile.open(sys.argv[1], mode="r:gz") as archive: + member = archive.getmember( + "preserve_executable_bit-0.1.0/scripts/greet.sh" + ) + assert "path" in member.pax_headers + print(f"{member.name}: {member.mode:o}") + "#}) + .arg(context.temp_dir.path().join("preserve_executable_bit-0.1.0.tar.gz")), @" + exit_code: 0 (success) + ----- stdout ----- + preserve_executable_bit-0.1.0/scripts/greet.sh: 755 + "); context .build_backend() @@ -511,6 +545,7 @@ fn build_module_name_normalization() -> Result<()> { #[test] fn build_sdist_with_long_path() -> Result<()> { let context = uv_test::test_context!("3.12"); + let default_dir = TempDir::new()?; let temp_dir = TempDir::new()?; context @@ -533,18 +568,92 @@ fn build_sdist_with_long_path() -> Result<()> { let long_path = format!("src/foo/l{}ng/__init__.py", "o".repeat(100)); context .temp_dir - .child(long_path) + .child(&long_path) .write_str(r#"print("Hi from foo")"#)?; + let large_path = "src/foo/large.bin"; + File::create(context.temp_dir.join(large_path))?.set_len(5 * 1024 * 1024 + 1)?; + uv_snapshot!(context .build_backend() .arg("build-sdist") + .arg(default_dir.path()), @" + exit_code: 0 (success) + ----- stdout ----- + foo-1.0.0.tar.gz + "); + + uv_snapshot!(context.python_command() + .arg("-c") + .arg(indoc! {r#" + import gzip + import sys + import tarfile + + with gzip.open(sys.argv[1], mode="rb") as archive: + header = archive.read(512) + assert header[257:265] == b"ustar \x00" + + with tarfile.open(sys.argv[1], mode="r:gz") as archive: + members = archive.getmembers() + assert all(not member.pax_headers for member in members) + print(f"GNU members: {len(members)}") + "#}) + .arg(default_dir.path().join("foo-1.0.0.tar.gz")), @" + exit_code: 0 (success) + ----- stdout ----- + GNU members: 10 + "); + + uv_snapshot!(context + .build_backend() + .env(EnvVars::UV_PREVIEW_FEATURES, "tar-codec") + .arg("build-sdist") .arg(temp_dir.path()), @" exit_code: 0 (success) ----- stdout ----- foo-1.0.0.tar.gz "); + uv_snapshot!(context.python_command() + .arg("-c") + .arg(indoc! {r#" + import sys + import tarfile + + with tarfile.open(sys.argv[1], mode="r:gz") as archive: + members = archive.getmembers() + assert all("path" in member.pax_headers for member in members) + assert all( + member.mode == (0o755 if member.isdir() else 0o644) + for member in members + ) + + long_member = archive.getmember(sys.argv[2]) + assert long_member.isfile() + + large_member = archive.getmember(sys.argv[3]) + with archive.extractfile(large_member) as payload: + streamed = 0 + while chunk := payload.read(64 * 1024): + assert not any(chunk) + streamed += len(chunk) + assert streamed == large_member.size + + print(f"PAX members: {len(members)}") + print(f"Long path bytes: {len(long_member.name.encode())}") + print(f"Streamed bytes: {streamed}") + "#}) + .arg(temp_dir.path().join("foo-1.0.0.tar.gz")) + .arg(format!("foo-1.0.0/{long_path}")) + .arg(format!("foo-1.0.0/{large_path}")), @" + exit_code: 0 (success) + ----- stdout ----- + PAX members: 10 + Long path bytes: 133 + Streamed bytes: 5242881 + "); + Ok(()) } diff --git a/crates/uv/tests/lock/lock.rs b/crates/uv/tests/lock/lock.rs index 722f9011f21..1b5027dc567 100644 --- a/crates/uv/tests/lock/lock.rs +++ b/crates/uv/tests/lock/lock.rs @@ -1397,7 +1397,7 @@ fn lock_sdist_url() -> Result<()> { name = "a" version = "1.0.0" source = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz" } - sdist = { hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097" } + sdist = { hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5" } [[package]] name = "project" @@ -36852,7 +36852,7 @@ fn lock_android() -> Result<()> { name = "deltachat-rpc-server" version = "1.159.5" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/deltachat_rpc_server-1.159.5.tar.gz", hash = "sha256:b923352b2b7253d068cdba7d1d23f36bc82a8dc7eb7f0e3860a1379324c62c32", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/deltachat_rpc_server-1.159.5.tar.gz", hash = "sha256:692864680c65a20745fd19ac0fd459cce7540087e99b39832490ac79d001ae8d", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/deltachat_rpc_server-1.159.5-py3-none-android_21_arm64_v8a.whl", hash = "sha256:a09292118bd3cbe9133c410c8228399a65467f1bcacbfbd35ffa27a2c1e8b3c3", upload-time = "2024-03-24T00:00:00Z" }, { url = "http://[LOCALHOST]/files/deltachat_rpc_server-1.159.5-py3-none-android_21_armeabi_v7a.whl", hash = "sha256:fcedd34dfec4397a5f516266e7d38237b01133e2fbfe29f08140d04cf8ea5b64", upload-time = "2024-03-24T00:00:00Z" }, diff --git a/crates/uv/tests/lock_scenarios/lock_scenarios.rs b/crates/uv/tests/lock_scenarios/lock_scenarios.rs index 48f38fe78f8..16824c4f424 100644 --- a/crates/uv/tests/lock_scenarios/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios/lock_scenarios.rs @@ -125,7 +125,7 @@ fn wrong_backtracking_basic() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -137,7 +137,7 @@ fn wrong_backtracking_basic() -> Result<()> { dependencies = [ { name = "a" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.9.tar.gz", hash = "sha256:f9d404650ef15d09f718b4f99d911962f653516974fa1e01cf8dba7afa695caa", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.9.tar.gz", hash = "sha256:8a0dca91cfb1e865caa23018dc01a32afc0ede285eb653a34e87929401af0152", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.9-py3-none-any.whl", hash = "sha256:fb91402b66338aaf9408407aa3681dcdd0984b9774ecf46632bc3761198399fa", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -288,7 +288,7 @@ fn wrong_backtracking_indirect() -> Result<()> { name = "a" version = "2.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -300,7 +300,7 @@ fn wrong_backtracking_indirect() -> Result<()> { dependencies = [ { name = "b-inner" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:d002926c2038325d9c4287f87bc5c12d7336a32c5bbff9925ac474fa4341149d", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:dc8e65ac8e153f517377e576ac880386219fa74cad98ef9dc8ccc7ffaaebb55e", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:e64d7b65e2bc771f36c53ea70d805ebf643322fa2d1761a0dc45b75d0374e2fb", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -312,7 +312,7 @@ fn wrong_backtracking_indirect() -> Result<()> { dependencies = [ { name = "too-old" }, ] - sdist = { url = "http://[LOCALHOST]/files/b_inner-1.0.0.tar.gz", hash = "sha256:73cb22e45889937a3a26ec58d668e92e488c133a6ecc4515f067532380980631", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b_inner-1.0.0.tar.gz", hash = "sha256:9593374b380761095c60460348d2134a778370ccb51d1bb8a9893c7d51934c8c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b_inner-1.0.0-py3-none-any.whl", hash = "sha256:dc550a3821df74da8f99f92aeff395eae8f050034fe8403a13043d42dca06a95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -336,7 +336,7 @@ fn wrong_backtracking_indirect() -> Result<()> { name = "too-old" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/too_old-1.0.0.tar.gz", hash = "sha256:98267dfd9af634cfcbfa079c8a5cdcbb5169836904a5cb4441b71333467dc682", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/too_old-1.0.0.tar.gz", hash = "sha256:91e0cdc85c04e313e2a5cf8b4ad6459e61594f62d91b04ad658ae44d48b1644a", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/too_old-1.0.0-py3-none-any.whl", hash = "sha256:7efb79d455d0a679335ce5abee7d3bf298cac8c6e0aa19654b7c033d603c93ef", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -432,7 +432,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -539,7 +539,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -644,7 +644,7 @@ fn fork_basic() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -656,7 +656,7 @@ fn fork_basic() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -919,7 +919,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/a-4.3.0.tar.gz", hash = "sha256:ae6dc9fc44095c1d8ec669ea9ce6623fb598874d6436e989ccad9fbb8ecf121f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-4.3.0.tar.gz", hash = "sha256:c827d6b38cef471d2cbb5c4a0ffd2b1b664fcaceb7468285291b09ae720cce85", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-4.3.0-py3-none-any.whl", hash = "sha256:801f46d2474bf22f2eed823a9a9343480a5ea089a45e949dbb1aad91a32cc14f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -931,7 +931,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-4.4.0.tar.gz", hash = "sha256:d531c73543a10f88aa8cb084ab856c7f369d442d09ad3ffdeb4a1771590b1d0f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-4.4.0.tar.gz", hash = "sha256:44ef07c198ff128c43fea8095c6404169d67af81d7a2d5a9d14bf442d46141de", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-4.4.0-py3-none-any.whl", hash = "sha256:3df7c088229de8a0ee9765d9da6f543d5b40155fade904f0c95780fc843c8ed7", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -943,7 +943,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { dependencies = [ { name = "d", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" } }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:e86cc7db081c964c8874d032149c4e84b3f23563c517beb7bedfa875a8f9f4ce", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:0c4fab34d536effcd612484e0340622ee38fc410901455c7f0a1099e1ac09bc3", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:ee44ff0b8963063959a61fdafa6d9742f5e03efec800a3661e4b081bb6343be1", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -955,7 +955,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { dependencies = [ { name = "d", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" } }, ] - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:bc02a5bce314ce38dbd74b2132c25979a09f132c32808726ea3c9a793bd125a9", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:9e5affe114cac181b24f22f6639d9a0dc3f1ec3e39752271f4ccdd23cac7957d", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:c243070ddb01de3029aebe2536aed4b43f324ebaef3a156304c9c4d7ae2f6a63", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -967,7 +967,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:4f363304bad30565286697b70b1b48e348267d318562a3afb36af66a8a8cad1d", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:bbb9d05b6de19e47de8e49fcc69483e76cd868bd556c8173680756d53e6997d4", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/d-1.0.0-py3-none-any.whl", hash = "sha256:362166e5bd895367cc4ba5b7327949b7d417fe30cb3273a76b5db4a280dac05d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -979,7 +979,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/d-2.0.0.tar.gz", hash = "sha256:710cbab9073b67674e70a5f5225f81fe5496739fb4d14e6b9ecec40109290ee9", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/d-2.0.0.tar.gz", hash = "sha256:d5570875cb7c4bb8cba56f4e5aec850f5e6f21cb6ee0316b3a4f90be887edb75", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/d-2.0.0-py3-none-any.whl", hash = "sha256:3cf4357f0fdce5ed2a98b8befcef84e8b8150244737481ebc51d3d9ec000bc3b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1087,7 +1087,7 @@ fn fork_upgrade() -> Result<()> { name = "bar" version = "2.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:fa9a4faf506228722784ed740a362bccd96913f4f98a4e10d45ab79d8abb270a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:29e7bc76f76b7e939dcf1f8fe28b077c4631c11ecea673beb86d297dacda11eb", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/bar-2.0.0-py3-none-any.whl", hash = "sha256:563b1af3238a4ad819f2b95b74f940319a2ef30ed7991a2416fa98aa115da87d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1099,7 +1099,7 @@ fn fork_upgrade() -> Result<()> { dependencies = [ { name = "bar" }, ] - sdist = { url = "http://[LOCALHOST]/files/foo-2.0.0.tar.gz", hash = "sha256:447cd218ce6ee5c4cc8ba7bf5431bd114d5336fa6214a91f28c6fc9ade89bf4c", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/foo-2.0.0.tar.gz", hash = "sha256:06645b1e3c66510cee0c5b852731f1144bbfa98700d36d77a3a1def134c32541", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/foo-2.0.0-py3-none-any.whl", hash = "sha256:36953b42725b8f3b6ebde327b8fab1d1e906ce0902c6272855851ea1964bc37a", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1213,7 +1213,7 @@ fn fork_incomplete_markers() -> Result<()> { resolution-markers = [ "python_full_version < '3.13'", ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1225,7 +1225,7 @@ fn fork_incomplete_markers() -> Result<()> { resolution-markers = [ "python_full_version >= '3.14'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1237,7 +1237,7 @@ fn fork_incomplete_markers() -> Result<()> { dependencies = [ { name = "c", marker = "python_full_version == '3.13.*'" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:6f887656a85fbbf549dddf2da49a17c6b55ca2a31a62ca030e82b8fdfd593177", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:675547bdd1ec8b9552c086605f0ca400a2ef057934366281d0b357127e216384", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:b8c3f2688065abd235cc88b32446d4c807cfa3a1f2d676874bccc7e0f63137bd", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1246,7 +1246,7 @@ fn fork_incomplete_markers() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1359,7 +1359,7 @@ fn fork_marker_accrue() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:8fc0cb224becb328b822daf159f2277eae1be228b916d6caa86484d2d83a5235", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:c6a69af4dc542ea244c21aae42b5639705975100871f1066c58be1245f013c95", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:8ed25f2453465e5f1e91e7b09e21b08234389dc5d6e3f7f27e89e801ba42d807", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1371,7 +1371,7 @@ fn fork_marker_accrue() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'darwin'" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:71e4e9b4e8b7ebf5c34d935f8934c4b3e762ece483247af9ff7913b0d986b96e", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:c855f831e0a345fc9b086051e2a8b02be6f9356bb131f240da96a793bb1d4d1c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:e21416492e59fd38a6e084b303f29fcde08805f201a4f9e4833c60cd18ebfc4f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1380,7 +1380,7 @@ fn fork_marker_accrue() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1570,7 +1570,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { { name = "b", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'pypy'" }, { name = "b", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'cpython'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:7e3d8e97204e1ea461ae625819c340a5a762b70f34421d4f9721bb7b607a0a93", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:23d75e1acf1aaf735e83615f8baba2fa0d0e5f9b885706cfd017b9b72301cdab", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:ebe588eab684413e5969ec398e03f7386a8106c5c88a601dacb2781fe2d0c819", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1582,7 +1582,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1597,7 +1597,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { dependencies = [ { name = "c" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:7bbe7def4bbd23115cf653d246419c773afd3c1b3e6ba94f19acbc1742f51d08", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:af6eb9a200314b36d0f49af106101b43445321bf148054ce024c92d79e93fa31", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:94d6ef21aaf5389c9ec11da5f313697b2f5a3b35f039735d2bcf0cfb7a6f88d1", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1609,7 +1609,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:18fb09ba28eba255186405065e027093a6e952fa71eb565b4c46d619fdb60809", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:256a9af98c362451ef802d6462f06f8d4e26cc52543e8100cba63b584bae9a7c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.0-py3-none-any.whl", hash = "sha256:04cc57f7563029528b6d23283933b244b6f52ba1543fad54687c586d6e639fc4", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1618,7 +1618,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1748,7 +1748,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { { name = "b", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'pypy'" }, { name = "b", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'cpython'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:7e3d8e97204e1ea461ae625819c340a5a762b70f34421d4f9721bb7b607a0a93", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:23d75e1acf1aaf735e83615f8baba2fa0d0e5f9b885706cfd017b9b72301cdab", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:ebe588eab684413e5969ec398e03f7386a8106c5c88a601dacb2781fe2d0c819", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1760,7 +1760,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1772,7 +1772,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:f615a5b13329186c0948d63a275af18758e1346ad512f06366b0534e1c4e3ab3", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:c20e9062f4cf30f8e581130ae2e18959f0f294246eb3bdd9f25053bd72a74267", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:b9dea47846d57e4a52afe31d36ad8fc1e7c01505c768be8855222320a9028f3c", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1784,7 +1784,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:18fb09ba28eba255186405065e027093a6e952fa71eb565b4c46d619fdb60809", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:256a9af98c362451ef802d6462f06f8d4e26cc52543e8100cba63b584bae9a7c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.0-py3-none-any.whl", hash = "sha256:04cc57f7563029528b6d23283933b244b6f52ba1543fad54687c586d6e639fc4", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1915,7 +1915,7 @@ fn fork_marker_inherit_combined() -> Result<()> { { name = "b", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'pypy'" }, { name = "b", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "implementation_name == 'cpython'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:7e3d8e97204e1ea461ae625819c340a5a762b70f34421d4f9721bb7b607a0a93", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:23d75e1acf1aaf735e83615f8baba2fa0d0e5f9b885706cfd017b9b72301cdab", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:ebe588eab684413e5969ec398e03f7386a8106c5c88a601dacb2781fe2d0c819", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1927,7 +1927,7 @@ fn fork_marker_inherit_combined() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1939,7 +1939,7 @@ fn fork_marker_inherit_combined() -> Result<()> { resolution-markers = [ "implementation_name == 'pypy' and sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:675a6c7a1456ba55a2bb89763b3e58b9086a120918d8a9965b616f81f77150fb", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:de35bb11b581875ed4be3c930bcc4d98e7e9ac34d5fe678f74a27c2136b633f9", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:42ee7845ea1960c41a676a74c9add4f48915e78f031b0e0a7d3894a692b9b6dd", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1951,7 +1951,7 @@ fn fork_marker_inherit_combined() -> Result<()> { resolution-markers = [ "implementation_name == 'cpython' and sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:18fb09ba28eba255186405065e027093a6e952fa71eb565b4c46d619fdb60809", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:256a9af98c362451ef802d6462f06f8d4e26cc52543e8100cba63b584bae9a7c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.0-py3-none-any.whl", hash = "sha256:04cc57f7563029528b6d23283933b244b6f52ba1543fad54687c586d6e639fc4", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2067,7 +2067,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:36c9054329425d5b328167c29b8977798e496b738e0c773de19896aeff397ba6", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:bdb790e6d65140f316bfb33a6bc9ab03732245e8b5c6fd8efbcff7744530795d", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:0f04d2c483396a1af08a8baee4a39f08f4e4a1f9775c3b9bce5245852e864eba", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2082,7 +2082,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { dependencies = [ { name = "b" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:1f9238da44c4971ded49abeb4dffd96c319bea753e2b61e4d095cc9110896d13", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:90a1f56c11a242c7437e595d28b6903388568edcb2ab8111657cc36fb7297ece", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:f455166cf613308f098ecf7d0911ca68838ca203b05c62cdb54b3043ad27c2d0", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2091,7 +2091,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { name = "b" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:b532bd9c3ccd69c4d5e915542dc50fb748c91c7a8e204c75387178d68fca113f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:9b42692ac74b4da0eed1ef248b9be6bb0557c49507ba3b38f862b191a06d959c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:a4c65510001153cab97a29ff219ad86e0d4330653ca89d9d4c84187ccf14c621", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2216,7 +2216,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { dependencies = [ { name = "b" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3f8ff2b2832415dfda5a576afabc2f8b0e93e0e7a0ee9064b2f9c0a6488c1320", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:75c52500ad189dbf1bd52b7db63c3a480b381039c554aace83b348c36c39aa25", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:a0f20c0172d171015f1637827325521d0feaa0b85c49058980f660080d96170c", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2228,7 +2228,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2240,7 +2240,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { dependencies = [ { name = "c" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:0e68acfea0cd703f2fa3e0a3b12f71228a0a6f5befc5df7f5f907a4cd153a90e", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:3b8dcd0978bd51a2c96c4580a742545bfcf43ff64c664721b68cc96a71c489d4", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:b1299c4860508b15790e950862174413d29090c6e08d069fe70297a6d4db5ee0", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2249,7 +2249,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:7f4d834ea98e687d4fb313f6b90abcf10a1b574b7273f8157eed433b7371c305", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:97fd5ea7c4a6535adc8b5d3eccdaa599d33edd9e4eccd922d6684b71171c829a", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:a154f7c821c8a9448936b30bcb743e04fb4a187be48bc502e8c465f83a839a0e", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2367,7 +2367,7 @@ fn fork_marker_inherit() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:36c9054329425d5b328167c29b8977798e496b738e0c773de19896aeff397ba6", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:bdb790e6d65140f316bfb33a6bc9ab03732245e8b5c6fd8efbcff7744530795d", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:0f04d2c483396a1af08a8baee4a39f08f4e4a1f9775c3b9bce5245852e864eba", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2379,7 +2379,7 @@ fn fork_marker_inherit() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2503,7 +2503,7 @@ fn fork_marker_limited_inherit() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:c59d625a854e3d8e7cca350ff23a960884bf8a558af994598950e60ecaecf1be", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:4afce24fb7b6b495a2e3521c84d8703e9e1e31faf88f086a6516db3fbc87f7cc", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:83b170dfb388aa657648396e796df2890a54c7125464b7087714da4ee7aaab3b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2515,7 +2515,7 @@ fn fork_marker_limited_inherit() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2527,7 +2527,7 @@ fn fork_marker_limited_inherit() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:675a6c7a1456ba55a2bb89763b3e58b9086a120918d8a9965b616f81f77150fb", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:de35bb11b581875ed4be3c930bcc4d98e7e9ac34d5fe678f74a27c2136b633f9", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:42ee7845ea1960c41a676a74c9add4f48915e78f031b0e0a7d3894a692b9b6dd", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2536,7 +2536,7 @@ fn fork_marker_limited_inherit() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2653,7 +2653,7 @@ fn fork_marker_selection() -> Result<()> { name = "a" version = "0.1.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-0.1.0.tar.gz", hash = "sha256:7500398834f46e3567b86bca779e305a94e6039d344819c8672c589c92ba9629", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-0.1.0.tar.gz", hash = "sha256:758dc8fff4646aa2c7f2ba2f32bdad3004625ce13ea474f163fe60bcb4d1d7d2", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-0.1.0-py3-none-any.whl", hash = "sha256:40f95c8868f537e1a289e86f8d75e208fa22d1f3b46c42834f526e89630f77c0", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2665,7 +2665,7 @@ fn fork_marker_selection() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:b532bd9c3ccd69c4d5e915542dc50fb748c91c7a8e204c75387178d68fca113f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:9b42692ac74b4da0eed1ef248b9be6bb0557c49507ba3b38f862b191a06d959c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:a4c65510001153cab97a29ff219ad86e0d4330653ca89d9d4c84187ccf14c621", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2677,7 +2677,7 @@ fn fork_marker_selection() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:18fb09ba28eba255186405065e027093a6e952fa71eb565b4c46d619fdb60809", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:256a9af98c362451ef802d6462f06f8d4e26cc52543e8100cba63b584bae9a7c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.0-py3-none-any.whl", hash = "sha256:04cc57f7563029528b6d23283933b244b6f52ba1543fad54687c586d6e639fc4", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2808,7 +2808,7 @@ fn fork_marker_track() -> Result<()> { dependencies = [ { name = "c", marker = "implementation_name == 'iron'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.3.1.tar.gz", hash = "sha256:21bb6af59c842bf5ffc008914ca6e817139e07965d4093ff76cff8956c6965ff", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.3.1.tar.gz", hash = "sha256:b46ebdc4ecc8c6670e8da12889df8c1bd286cbc8eb94f69e4626670e17b760c8", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.3.1-py3-none-any.whl", hash = "sha256:7720c67a8765ed540f4bdac4f8653210787d1ae518eb20ac541344ea8002f81c", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2820,7 +2820,7 @@ fn fork_marker_track() -> Result<()> { resolution-markers = [ "sys_platform == 'darwin'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.7.tar.gz", hash = "sha256:30fe41d5a9282b73cd50d58eceb33cec85d57c78af4a91fe3e202335f949949f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.7.tar.gz", hash = "sha256:c3e58feccc8d0cb3b8654491f51b4d53bf75edb0c8c5f3fd039570609acc3957", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.7-py3-none-any.whl", hash = "sha256:973b02bdc3039f4fa7347e091da60de892a0a18b26effd20a1e17fdcf24a6dcd", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2832,7 +2832,7 @@ fn fork_marker_track() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.8.tar.gz", hash = "sha256:673cbbd654751f7880842420431a400e62458486cb428bc7e508cfea4b9c8cd0", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.8.tar.gz", hash = "sha256:1f6eb422782a5730a466c3a9e0c149653751c64f6c4cdfb44a860d8d4eda2d24", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.8-py3-none-any.whl", hash = "sha256:ce8467e32ba82112ebbfbdb23ffd9b537ce7502f09ab70cf65716a1a89eb1683", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2841,7 +2841,7 @@ fn fork_marker_track() -> Result<()> { name = "c" version = "1.10" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.10.tar.gz", hash = "sha256:4824f76781aef886ff01094b7c50133c4a0218f46e3c19ff0bd069f2661eee80", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.10.tar.gz", hash = "sha256:776b6806df1500e84d6b312aaf8d036a9d0d2ed4eb05a5ba52d6420311afd024", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.10-py3-none-any.whl", hash = "sha256:c830c4360164be9ea0027abb265a7f4f7894d8ac454942d98c09464d21a99b96", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2953,7 +2953,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:c2e92ae787edb18204782312a98f7dd9d0116ad5d0e61aa046956bdb2ef2e2e3", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:cd24667d7a4725e13a59e180b76b7e932a074cc7a8a20a18d353db979f4b6707", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:6de66136e60c4e5c1832fdd217af472ad50d9ebd177f4014b9b3f50904f80f5e", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2965,7 +2965,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'darwin'" }, ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:8080895028b838440d4e08b9e6b1cc9c727c625e702c9c208de07fc7e06edfa0", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:d6993f77c784de42e150f111902a2c88a867c555077dacaa6c3a1b71398784a4", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:8cb0c9eaa95e2cf767bf5e6caf4fecfa1b2b9fc09be53c5768372704574ac244", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -2974,7 +2974,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { name = "c" version = "2.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-2.0.0.tar.gz", hash = "sha256:72db9a21521acaa8ff10d0ce3bb4b68bc6b275bcb77bdb3debd95388f5120021", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-2.0.0.tar.gz", hash = "sha256:98b5a57ae857516af05cd6bc5c3f74d31a78cd6559594a51b00b45c4e3891905", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-2.0.0-py3-none-any.whl", hash = "sha256:4a585f74490e3c09faafdb7df1ebb51d5e41c67b82ef08b5b5fd2f4c251b4b23", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3242,7 +3242,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { name = "a" version = "1.2.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.2.0.tar.gz", hash = "sha256:ea1f0436d9d88c51f66e4154f17f7c6778d0dc7674f75cda35dc4b668fb287a7", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.2.0.tar.gz", hash = "sha256:2e50354becbab0cc152f51e0ded5bdf4d7487237d8c1c825a151286117287c62", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.2.0-py3-none-any.whl", hash = "sha256:ac8736ef11e0594522369998752b2780be46e71e034c07d700c7bd0f2cea9863", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3412,7 +3412,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { { name = "reject-cleaver1", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform == 'linux'" }, { name = "reject-cleaver1-proxy" }, ] - sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:049d1cd0cb93f315070151f69682f97eec46ff0a3da64d87d387cc11830ba541", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:e0bb339ac91a41ac2ce20db4866abf934c1be9c1a0b1f83efd701f9cf0a3da3c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/cleaver-1.0.0-py3-none-any.whl", hash = "sha256:c9c97d652936b7293b36e54194d448350852ffa6fbfad051ba0671db46803d7b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3424,7 +3424,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked-2.0.0.tar.gz", hash = "sha256:0e93f72e7bcbdc71a1a3573b1f79a747e82c9c238505a847e74d314143eedc18", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked-2.0.0.tar.gz", hash = "sha256:53bb0ea79f0eb0fc38598b1d6bf4f8e15fc6d61342570db99e6756eb01823223", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/fork_if_not_forked-2.0.0-py3-none-any.whl", hash = "sha256:0604f383a6d7cf8fd69c252c85bdb34967743de531b2b0f4084378f00f5b4ccf", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3436,7 +3436,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked-3.0.0.tar.gz", hash = "sha256:cdbe6609a59da78b2ea6ddb8703131385dc71fd9d13bca82e35ed5d541a7425d", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked-3.0.0.tar.gz", hash = "sha256:15eca5a5864638d4b9a6343b844eb13fec827ad5cd6412df7399e80e60028075", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/fork_if_not_forked-3.0.0-py3-none-any.whl", hash = "sha256:dcfb9267c7ae0868f4f40e29574665b22e336e666c89a542a074355f7bac643e", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3448,7 +3448,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { dependencies = [ { name = "fork-if-not-forked", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" } }, ] - sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked_proxy-1.0.0.tar.gz", hash = "sha256:1da9ee55735388976535cfd2ae143c8b9e21b9dd5f01ce9521a4a095a5e9e258", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/fork_if_not_forked_proxy-1.0.0.tar.gz", hash = "sha256:cc3c846677ae440eb6f6460e7d9d67b68fdacb21950da4123c604d440f83bdc2", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/fork_if_not_forked_proxy-1.0.0-py3-none-any.whl", hash = "sha256:704030c720c21eee6ad9453ccd4d44d983182a73c057ed89325201c5dcead7fe", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3471,7 +3471,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1-1.0.0.tar.gz", hash = "sha256:6da0074d5a0a178cb730df50264fe123ac776ebb72b8624a2e04eefe2c694246", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1-1.0.0.tar.gz", hash = "sha256:d7897b77030f920c3cda8ff08cc1949d73c24cb479f4dfdaf406c4f9ae9b2f44", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/reject_cleaver1-1.0.0-py3-none-any.whl", hash = "sha256:b3f3e0d98b07e6ec99719106244c07fa85658f0be9aa20f28c957aa8efcbeef8", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3483,7 +3483,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1-2.0.0.tar.gz", hash = "sha256:3b6adb7793ff0c4bf6e14d10e16156bc184046e1e5b738d921974ef6b9f9b58a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1-2.0.0.tar.gz", hash = "sha256:4d82244f63049cc6441cbb1e570469e2e997edf28f132e30b1bf38f7ca037581", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/reject_cleaver1-2.0.0-py3-none-any.whl", hash = "sha256:db12d456e29ce239bbfed102b8b613a088adde579403d11ae91b15a08011efd2", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3495,7 +3495,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { dependencies = [ { name = "reject-cleaver1", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1_proxy-1.0.0.tar.gz", hash = "sha256:43034027a9360a2497ff2558c3efc0652ec5a18b30872cf88ab87bd4a3675799", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/reject_cleaver1_proxy-1.0.0.tar.gz", hash = "sha256:3ee85cdcbf1fccefe03ff06787c7b38733e194c769deaf6686cc6190cd7a1cfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/reject_cleaver1_proxy-1.0.0-py3-none-any.whl", hash = "sha256:8032f56f5eaddb524e079a75ab95b25220f916ac692c377a6977b58e61a3508d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3786,7 +3786,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "d" }, { name = "reject-cleaver-1" }, ] - sdist = { url = "http://[LOCALHOST]/files/bar-1.0.0.tar.gz", hash = "sha256:7433981e897b2fdbefc4ccc713282a03663dbe9b7468658a702af1d01d241e27", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/bar-1.0.0.tar.gz", hash = "sha256:4e9b7cce3ef47387b3b7a08bd81e5bb0ac0a19a62e3a0abf64119c18014f917a", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/bar-1.0.0-py3-none-any.whl", hash = "sha256:3e2cba35e1d80892bb436521134f100c183b8f64677be0ec80ff6ee4c710aa43", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3798,7 +3798,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:fa9a4faf506228722784ed740a362bccd96913f4f98a4e10d45ab79d8abb270a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:29e7bc76f76b7e939dcf1f8fe28b077c4631c11ecea673beb86d297dacda11eb", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/bar-2.0.0-py3-none-any.whl", hash = "sha256:563b1af3238a4ad819f2b95b74f940319a2ef30ed7991a2416fa98aa115da87d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3810,7 +3810,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/c-2.0.0.tar.gz", hash = "sha256:72db9a21521acaa8ff10d0ce3bb4b68bc6b275bcb77bdb3debd95388f5120021", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-2.0.0.tar.gz", hash = "sha256:98b5a57ae857516af05cd6bc5c3f74d31a78cd6559594a51b00b45c4e3891905", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-2.0.0-py3-none-any.whl", hash = "sha256:4a585f74490e3c09faafdb7df1ebb51d5e41c67b82ef08b5b5fd2f4c251b4b23", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3822,7 +3822,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/c-3.0.0.tar.gz", hash = "sha256:27d544487f21f6ece9d49d672fc8da1664c48a7ef864d8ff91b756183d77b831", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-3.0.0.tar.gz", hash = "sha256:c01237d1b0816abee804906c0b118675aaa1aa1fdbb6e756c350ecc6ed500ebb", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-3.0.0-py3-none-any.whl", hash = "sha256:1e031928d6a855d65842904337ad7d984de1a64473b1166d548afbfe5a397341", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3835,7 +3835,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "bar", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, { name = "foo", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:9156f0d8df99763e90677ca1b4dab67bee5fa33f59d102626dd91505cf332d2a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:e48e43a500c95e61d1f1e18d830ec1df0ed3065842738493669f850a2c3da9ad", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/cleaver-1.0.0-py3-none-any.whl", hash = "sha256:f49d93330cfe3f7096636c506aa522a497eae44d08b07f6276caf784ec87f65b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3847,7 +3847,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { dependencies = [ { name = "c", version = "3.0.0", source = { registry = "http://[LOCALHOST]/simple/" } }, ] - sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:ef58b0f5f8f9c4d7e7864a36f86ad96ca8cee734095b3e6512ae8c395380ee41", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:41ae55064b4daf92964c6428bb14b433ef5ac82b96ddb2868460ac862b3e80c9", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/d-1.0.0-py3-none-any.whl", hash = "sha256:890292cd440f4d868e46d5e883bd2eee4ced094d39e44a7cd88f7d9f5049f5ef", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3861,7 +3861,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "c", version = "3.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, { name = "reject-cleaver-1" }, ] - sdist = { url = "http://[LOCALHOST]/files/foo-1.0.0.tar.gz", hash = "sha256:b9179a545511f4418bdde59a4907297accb585f0113176790170db5665ca3416", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/foo-1.0.0.tar.gz", hash = "sha256:693cd5b07b84a596dc7595d47d3bedebd7540b5455eedb4fa7ddf4196bbcb205", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/foo-1.0.0-py3-none-any.whl", hash = "sha256:a6a8594d7d818843d31479b216b325e9140b6c5b180a55427b03fd85bbd261ad", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3892,7 +3892,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { { name = "unrelated-dep2", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform == 'linux'" }, { name = "unrelated-dep2", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/reject_cleaver_1-1.0.0.tar.gz", hash = "sha256:a6383fa52eff39c3635fde33f9a4dd03d2fc7cffdd42fd4c148cf997e6fb5246", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/reject_cleaver_1-1.0.0.tar.gz", hash = "sha256:c707cb6622bed86ca850cecb72a1e03e2dfef87b7f19018684f0530d5532cdb2", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/reject_cleaver_1-1.0.0-py3-none-any.whl", hash = "sha256:5b4d622444188c423a9476bb5c92358d7322f121d6e764c4b8d8bdc14e9f4017", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3904,7 +3904,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/unrelated_dep2-1.0.0.tar.gz", hash = "sha256:43649d9e654b0a121308187b8a4f43fa2b498e08565e2432bd1e0e3f1728acb2", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/unrelated_dep2-1.0.0.tar.gz", hash = "sha256:6154dcfd6aa5dc62404702d8d66e908c7482a6107148323259be64e0268a1885", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/unrelated_dep2-1.0.0-py3-none-any.whl", hash = "sha256:a485b74955fe703c7c525c52ef72da556100f2ea4b49ca97e6fd6f183469fb43", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -3916,7 +3916,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/unrelated_dep2-2.0.0.tar.gz", hash = "sha256:292e78906a8e8c74931f9a5b54aba9e6957afe87cca68a2a456363569e7aa652", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/unrelated_dep2-2.0.0.tar.gz", hash = "sha256:b58f99b95e60c3b85f930cb1fae95ef3baba23e85eca0db721f82ad4e8c32cf6", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/unrelated_dep2-2.0.0-py3-none-any.whl", hash = "sha256:f666a4b92a10879856a6962ef582a2ab328edd174b8def895d859b10bfea49b6", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4063,7 +4063,7 @@ fn preferences_dependent_forking() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/bar-1.0.0.tar.gz", hash = "sha256:d373f4858d602855ef53231a7f24ebff4e67e1fe30a2e810ee2b2a29b9d1a50a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/bar-1.0.0.tar.gz", hash = "sha256:bb9cb9098cc77ebe1f2085af0859f2332ab631348e58c687fa344aea81eb4043", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/bar-1.0.0-py3-none-any.whl", hash = "sha256:2fbf0e0a7dd4f48a8b1c2148f73ba0c314699d0bfa0a8ec9b9bcb8105882e9fc", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4075,7 +4075,7 @@ fn preferences_dependent_forking() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:fa9a4faf506228722784ed740a362bccd96913f4f98a4e10d45ab79d8abb270a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/bar-2.0.0.tar.gz", hash = "sha256:29e7bc76f76b7e939dcf1f8fe28b077c4631c11ecea673beb86d297dacda11eb", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/bar-2.0.0-py3-none-any.whl", hash = "sha256:563b1af3238a4ad819f2b95b74f940319a2ef30ed7991a2416fa98aa115da87d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4088,7 +4088,7 @@ fn preferences_dependent_forking() -> Result<()> { { name = "bar", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, { name = "foo", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:9156f0d8df99763e90677ca1b4dab67bee5fa33f59d102626dd91505cf332d2a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/cleaver-1.0.0.tar.gz", hash = "sha256:e48e43a500c95e61d1f1e18d830ec1df0ed3065842738493669f850a2c3da9ad", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/cleaver-1.0.0-py3-none-any.whl", hash = "sha256:f49d93330cfe3f7096636c506aa522a497eae44d08b07f6276caf784ec87f65b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4097,7 +4097,7 @@ fn preferences_dependent_forking() -> Result<()> { name = "foo" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/foo-1.0.0.tar.gz", hash = "sha256:4d4cf959f969e0a39663aca6064428819f1e1c6be60d9a35164801ce17950ed4", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/foo-1.0.0.tar.gz", hash = "sha256:70bd56242b5a5c7f6c04694a8ed2aafdb036726de0fcca1dd1d2f1f467c71ee1", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/foo-1.0.0-py3-none-any.whl", hash = "sha256:df9a39d54a6d71872deb1537d7e331de0ede3b725d630a050fee3efd3fe5145b", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4244,7 +4244,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { { name = "b", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "os_name == 'darwin'" }, { name = "b", version = "2.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "os_name == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:1073194686665f5b1459914275b353a20eafca9d9aa550f4c493aa0aceba119a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:217554d13af0a280cb3f5d95653c460bfdda5ea14c36a48e64fbaf39c7c6e16c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:de627f2f3dc58918c496b5b61036d5a8c19c769fa5c933889bd805366fa2de1d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4256,7 +4256,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { resolution-markers = [ "sys_platform == 'windows'", ] - sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4268,7 +4268,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { resolution-markers = [ "os_name == 'darwin' and sys_platform == 'illumos'", ] - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:b532bd9c3ccd69c4d5e915542dc50fb748c91c7a8e204c75387178d68fca113f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:9b42692ac74b4da0eed1ef248b9be6bb0557c49507ba3b38f862b191a06d959c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:a4c65510001153cab97a29ff219ad86e0d4330653ca89d9d4c84187ccf14c621", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4280,7 +4280,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { resolution-markers = [ "os_name == 'linux' and sys_platform == 'illumos'", ] - sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:18fb09ba28eba255186405065e027093a6e952fa71eb565b4c46d619fdb60809", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-2.0.0.tar.gz", hash = "sha256:256a9af98c362451ef802d6462f06f8d4e26cc52543e8100cba63b584bae9a7c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-2.0.0-py3-none-any.whl", hash = "sha256:04cc57f7563029528b6d23283933b244b6f52ba1543fad54687c586d6e639fc4", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4544,7 +4544,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:0f32d06c5dab1a669df8d282b93b1d1af33e685ed4f52393d0b216436b5f52dc", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:f49e4cc76cd214a2a67efbe254cd317fa72d09cc98cd3d06537083d987284267", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:277ee4f1bb98d9591ba3a3a28364d1f00a54384c08d8f32100afc01ae76491a5", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4723,7 +4723,7 @@ fn prerelease_base_marker_stable_preference_explicit_first() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4826,7 +4826,7 @@ fn prerelease_base_marker_stable_preference_plain_first() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -4925,7 +4925,7 @@ fn prerelease_marker_equivalent_stable_preference() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5043,7 +5043,7 @@ fn prerelease_marker_stable_preference_backtracks() -> Result<()> { dependencies = [ { name = "c", marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:847f294d80290ab0a808ef8b68cb24d1183467244beae37ff7acd36bba5e3402", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3af88a63bb82356a9dc643e557148dd6a50d20b0f74ee15828c7ba066be890cb", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:e5c939c5714630ae301b70dc2ba362c40f3ad6608cbc85035ee2c65be1f6ad70", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5052,7 +5052,7 @@ fn prerelease_marker_stable_preference_backtracks() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5061,7 +5061,7 @@ fn prerelease_marker_stable_preference_backtracks() -> Result<()> { name = "d" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:4f363304bad30565286697b70b1b48e348267d318562a3afb36af66a8a8cad1d", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/d-1.0.0.tar.gz", hash = "sha256:bbb9d05b6de19e47de8e49fcc69483e76cd868bd556c8173680756d53e6997d4", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/d-1.0.0-py3-none-any.whl", hash = "sha256:362166e5bd895367cc4ba5b7327949b7d417fe30cb3273a76b5db4a280dac05d", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5169,7 +5169,7 @@ fn transitive_prerelease_forks() -> Result<()> { { name = "c", version = "1.0.0", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform != 'linux'" }, { name = "c", version = "2.0.0b1", source = { registry = "http://[LOCALHOST]/simple/" }, marker = "sys_platform == 'linux'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:2d3e3f46d2ff4afceeec095a0799195534a719527d94152663853b00f6462bd2", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:70f26f0ea30b0899e8c0e946a6b2c4d57dd4208a028da1a990cde29f4eed5d77", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:392d0dd8be953713399938d2dacc8a361d99eb376285adfa4f0f0e4d448ace2a", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5181,7 +5181,7 @@ fn transitive_prerelease_forks() -> Result<()> { resolution-markers = [ "sys_platform != 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-py3-none-any.whl", hash = "sha256:78c0da7c5681d751d38b2e60c78d1e29d6125d91e68e5aeb22372fa66527ff95", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5193,7 +5193,7 @@ fn transitive_prerelease_forks() -> Result<()> { resolution-markers = [ "sys_platform == 'linux'", ] - sdist = { url = "http://[LOCALHOST]/files/c-2.0.0b1.tar.gz", hash = "sha256:6c4537b683c9ac2640452883415184b7615e6a0675bbbd49c2e019b307df02e7", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-2.0.0b1.tar.gz", hash = "sha256:0d71a5a80e03d71f520e78b06ca94b2f90247332e39c7b90233591fc33797e36", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-2.0.0b1-py3-none-any.whl", hash = "sha256:4afb52babcf2d595eccb483f1b6a641ca25daa2015cd97c519eb9357cc9d69a7", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5282,7 +5282,7 @@ fn requires_python_wheels() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:0f32d06c5dab1a669df8d282b93b1d1af33e685ed4f52393d0b216436b5f52dc", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:f49e4cc76cd214a2a67efbe254cd317fa72d09cc98cd3d06537083d987284267", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-cp310-cp310-any.whl", hash = "sha256:34c6734e2427cc772605ac6710cf4e95c3556fd191b308b65c4d5f056fc95530", upload-time = "2024-03-24T00:00:00Z" }, { url = "http://[LOCALHOST]/files/a-1.0.0-cp311-cp311-any.whl", hash = "sha256:a3aca28e2bd4f75c53a651a42fd54661d8aec43c1f9a4c703b3a301ad9deb5e2", upload-time = "2024-03-24T00:00:00Z" }, @@ -5375,7 +5375,7 @@ fn unreachable_package() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:4e402b215539e074a824ab44c7eaf04b6451bb1ab36d0cb045374828bb8bdcc7", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:9ea08e4e8cc22657585ae7aab665a536fd45dab0f612b3f7cf516aee045058ca", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:9c02f18ae50a1da3421fa31399fe1497172375a7a6c80e8d1485ed12c3e64ee3", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5473,7 +5473,7 @@ fn unreachable_wheels() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:559a9c629536d99c1213064c303dcc6ee8dd2917824c0a0f6549f94d0be02abd", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5482,7 +5482,7 @@ fn unreachable_wheels() -> Result<()> { name = "b" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:b532bd9c3ccd69c4d5e915542dc50fb748c91c7a8e204c75387178d68fca113f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:9b42692ac74b4da0eed1ef248b9be6bb0557c49507ba3b38f862b191a06d959c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d0fc532c5d6e2f11ca522dfd3684b50b132dd1db78fba3ad510023f4d5830b7", upload-time = "2024-03-24T00:00:00Z" }, { url = "http://[LOCALHOST]/files/b-1.0.0-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8f328487f947ee5119d348f2c73ab9119dd29349b840f6f26e0d351245d6084b", upload-time = "2024-03-24T00:00:00Z" }, @@ -5492,7 +5492,7 @@ fn unreachable_wheels() -> Result<()> { name = "c" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:6e14a2e7cc6be61fa5aa41c0e55beff8b708a3aea257fed948306a0741bb5c47", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/c-1.0.0.tar.gz", hash = "sha256:699a07ff61aab66fcba4883a94c6d2b61afb7797fa956ae36f2efdf30d9dfbc7", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/c-1.0.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a25d2ba9ff1417f63313691a2f686f30b617a8696329023e44b7f7fc96573598", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5622,7 +5622,7 @@ fn marker_variants_have_different_extras() -> Result<()> { dependencies = [ { name = "tzdata", marker = "sys_platform == 'win32'" }, ] - sdist = { url = "http://[LOCALHOST]/files/psycopg-1.0.0.tar.gz", hash = "sha256:c513b2021142cd4f00f9c09d92106d6cfe180f1b649f763ea6bd89b84ca9f1fb", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/psycopg-1.0.0.tar.gz", hash = "sha256:33854afc2bc33353fa645560cc0082cf085dc2a0b94afa97ca499b442f3b1a4e", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/psycopg-1.0.0-py3-none-any.whl", hash = "sha256:e91c6b77d7c6b4262b81b606577e444d27fcdaa22d6603742ced03b39492eda3", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5636,7 +5636,7 @@ fn marker_variants_have_different_extras() -> Result<()> { name = "psycopg-binary" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/psycopg_binary-1.0.0.tar.gz", hash = "sha256:64c3ee856436c3f1d564bf46476dfcd049a507b38eb74b14b48de437143d31c8", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/psycopg_binary-1.0.0.tar.gz", hash = "sha256:bc28ec69da2e999fb6475f8f55098375766a57922031775a597998ddc825581a", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/psycopg_binary-1.0.0-py3-none-any.whl", hash = "sha256:35474aba5923a77d2aac9ac1e92bb5e9dc0468b3b2611e120b3947ae956a1482", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5645,7 +5645,7 @@ fn marker_variants_have_different_extras() -> Result<()> { name = "tzdata" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/tzdata-1.0.0.tar.gz", hash = "sha256:f2285c9ed855e3433bcd3aabb0b446753f97bcdf8dffebe4725d70af8a1f2fe5", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/tzdata-1.0.0.tar.gz", hash = "sha256:32da4d714b0879cde291f6a55811e4ddbfdfd93bd17c0514f2f792f6cad38e38", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/tzdata-1.0.0-py3-none-any.whl", hash = "sha256:9dc909bec179ffb4d319a54ab73cb61e769c912be44743ff4411c5191432573f", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5737,7 +5737,7 @@ fn virtual_package_extra_priorities() -> Result<()> { dependencies = [ { name = "b" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:d01ad61c229cf2c4efe61755254b7419b078c7e712907c2e594b0a31ee1ac17e", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:ceae689363b89a7feb87b681fc4f66b85ea8227eb50ae60134f69f6af12f8b3e", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:d3c47a26ec1e08259468942a50736244908a076baf0d5f931f62407814ed96e5", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5746,7 +5746,7 @@ fn virtual_package_extra_priorities() -> Result<()> { name = "b" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:b532bd9c3ccd69c4d5e915542dc50fb748c91c7a8e204c75387178d68fca113f", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/b-1.0.0.tar.gz", hash = "sha256:9b42692ac74b4da0eed1ef248b9be6bb0557c49507ba3b38f862b191a06d959c", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/b-1.0.0-py3-none-any.whl", hash = "sha256:a4c65510001153cab97a29ff219ad86e0d4330653ca89d9d4c84187ccf14c621", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -5951,7 +5951,7 @@ fn specific_architecture() -> Result<()> { { name = "c", marker = "platform_machine == 'aarch64'" }, { name = "d", marker = "platform_machine == 'i686'" }, ] - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:ea7bef4508a5b327b7ce64ab4ffda42d30d0818af6afab6297db8c32300e67d8", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:d128ac9ff5b61e4db85dc86b943210cad23907d060f95a73914c7479acbbf9d1", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:ccd718f14fb28f4e947c39f67a39897f08eaf09034d0641ffb6ce41e580dac1a", upload-time = "2024-03-24T00:00:00Z" }, ] diff --git a/crates/uv/tests/pip/pip_sync.rs b/crates/uv/tests/pip/pip_sync.rs index 8b60ede4397..38053a456d0 100644 --- a/crates/uv/tests/pip/pip_sync.rs +++ b/crates/uv/tests/pip/pip_sync.rs @@ -3453,7 +3453,7 @@ fn require_hashes_source_no_binary() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str( - "a==1.0.0 --hash=sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", + "a==1.0.0 --hash=sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", )?; uv_snapshot!(context.pip_sync() @@ -5722,12 +5722,12 @@ fn pep_751_validates_remote_archive_size() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("--preview") - .arg("pylock.toml"), @r#" + .arg("pylock.toml"), @" exit_code: 1 (failure) ----- stderr ----- × Failed to download and build `a==1.0.0` - ╰─▶ Size mismatch for `a==1.0.0`: expected 1 bytes, but downloaded 453 bytes - "#); + ╰─▶ Size mismatch for `a==1.0.0`: expected 1 bytes, but downloaded 607 bytes + "); Ok(()) } diff --git a/crates/uv/tests/pip_compile/pip_compile.rs b/crates/uv/tests/pip_compile/pip_compile.rs index 7e45ef4ad85..ebfc2b18ebc 100644 --- a/crates/uv/tests/pip_compile/pip_compile.rs +++ b/crates/uv/tests/pip_compile/pip_compile.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use std::env::current_dir; use std::fs; -use std::io::Cursor; use std::process::Command; use std::str::FromStr; @@ -18,7 +17,8 @@ use http::StatusCode; #[cfg(feature = "test-universal")] use indoc::formatdoc; use indoc::indoc; -use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; +use tar_codec::{ArchiveBuilder as _, EntryMetadata, TarEncoder}; +use tokio_util::compat::FuturesAsyncWriteCompatExt; use url::Url; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -34,23 +34,15 @@ use uv_test::packse::scenario::{Package, PackageMetadata, Scenario}; use uv_test::{DEFAULT_PYTHON_VERSION, TestContext, download_to_disk, uv_snapshot}; fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> { - let enc = GzEncoder::new(file, flate2::Compression::default()); - let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(enc).compat_write()); + let mut encoder = GzEncoder::new(file, flate2::Compression::default()); + let mut tar = TarEncoder::new(AllowStdIo::new(&mut encoder).compat_write()).builder(); for (path, contents) in entries { - let mut header = tokio_tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - block_on(tar.append_data( - &mut header, - path, - AllowStdIo::new(Cursor::new(contents)).compat(), - ))?; + block_on(tar.add_file(path, contents.as_bytes(), EntryMetadata::default()))?; } - let writer = block_on(tar.into_inner())?; - writer.into_inner().into_inner().finish()?; + block_on(tar.finish())?; + encoder.finish()?; Ok(()) } @@ -17315,7 +17307,7 @@ fn pep_751_compile_url_sdist() -> Result<()> { [[packages]] name = "a" version = "1.0.0" - archive = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hashes = { sha256 = "3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097" } } + archive = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hashes = { sha256 = "957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5" } } ----- stderr ----- Resolved 1 package in [TIME] diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index cf5636bf00a..fd0a55bbfd0 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -1,5 +1,4 @@ use std::fmt::Write; -use std::io::Cursor; use std::path::{Path, PathBuf}; use std::process::Command; @@ -18,9 +17,10 @@ use futures::io::AllowStdIo; use indoc::{formatdoc, indoc}; use insta::{allow_duplicates, assert_snapshot}; use predicates::prelude::predicate; +use tar_codec::{ArchiveBuilder as _, EntryMetadata, TarEncoder}; #[cfg(unix)] use tokio::io::AsyncWriteExt; -use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; +use tokio_util::compat::FuturesAsyncWriteCompatExt; use url::Url; use walkdir::WalkDir; use wiremock::{ @@ -40,23 +40,15 @@ use uv_test::{ }; fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> { - let enc = GzEncoder::new(file, flate2::Compression::default()); - let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(enc).compat_write()); + let mut encoder = GzEncoder::new(file, flate2::Compression::default()); + let mut tar = TarEncoder::new(AllowStdIo::new(&mut encoder).compat_write()).builder(); for (path, contents) in entries { - let mut header = tokio_tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - block_on(tar.append_data( - &mut header, - path, - AllowStdIo::new(Cursor::new(contents)).compat(), - ))?; + block_on(tar.add_file(path, contents.as_bytes(), EntryMetadata::default()))?; } - let writer = block_on(tar.into_inner())?; - writer.into_inner().into_inner().finish()?; + block_on(tar.finish())?; + encoder.finish()?; Ok(()) } @@ -8023,7 +8015,7 @@ fn require_hashes_build_dependencies() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(indoc::indoc! {r" a==1.0.0 \ - --hash=sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097 + --hash=sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5 "})?; uv_snapshot!(context.pip_install() diff --git a/crates/uv/tests/project/check.rs b/crates/uv/tests/project/check.rs index abac04c6f42..5ab3b51a0eb 100644 --- a/crates/uv/tests/project/check.rs +++ b/crates/uv/tests/project/check.rs @@ -1081,7 +1081,7 @@ fn check_no_sync_creates_lock_without_sync() -> Result<()> { name = "a" version = "1.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, ] @@ -1228,8 +1228,8 @@ fn check_no_sync_updates_stale_lock_without_sync() -> Result<()> { -version = "1.0.0" +version = "2.0.0" source = { registry = "http://[LOCALHOST]/simple/" } - -sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:3d2b4c28a4e112f3a1cef1db4dc5efa33fcbbcc38bc11ccc80321097db86c097", upload-time = "2024-03-24T00:00:00Z" } - +sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:80ec95a66cff82a78a3333e3f5702e4254cf80533f21762933252eec58c9869a", upload-time = "2024-03-24T00:00:00Z" } + -sdist = { url = "http://[LOCALHOST]/files/a-1.0.0.tar.gz", hash = "sha256:957f99ff1d65ce0d7883d50f4e67ed8d4b42e76d2c2b5e62384ff0ba538647b5", upload-time = "2024-03-24T00:00:00Z" } + +sdist = { url = "http://[LOCALHOST]/files/a-2.0.0.tar.gz", hash = "sha256:9610291c2bd57390019f58ca72d0dd4584bb9e7073fa347633ed8bc7267fccfe", upload-time = "2024-03-24T00:00:00Z" } wheels = [ - { url = "http://[LOCALHOST]/files/a-1.0.0-py3-none-any.whl", hash = "sha256:f936eedc194aa91ca01a4c6c9981136ca6c75ce6df47e3951b12522881dce809", upload-time = "2024-03-24T00:00:00Z" }, + { url = "http://[LOCALHOST]/files/a-2.0.0-py3-none-any.whl", hash = "sha256:833374310e0a15880f3be9e6d082f527c9ac70129b2054d733da9b754315361f", upload-time = "2024-03-24T00:00:00Z" }, diff --git a/crates/uv/tests/project/edit.rs b/crates/uv/tests/project/edit.rs index 32474463a19..f327f9ae391 100644 --- a/crates/uv/tests/project/edit.rs +++ b/crates/uv/tests/project/edit.rs @@ -4901,7 +4901,7 @@ fn add_lower_bound_local() -> Result<()> { name = "a" version = "1.2.3+foo" source = { registry = "http://[LOCALHOST]/simple/" } - sdist = { url = "http://[LOCALHOST]/files/a-1.2.3+foo.tar.gz", hash = "sha256:d8c42806a0bf7b47451778ed2a909a2f9d22fe3a4bac673934fc8fb2537a886a", upload-time = "2024-03-24T00:00:00Z" } + sdist = { url = "http://[LOCALHOST]/files/a-1.2.3+foo.tar.gz", hash = "sha256:2fd6e9af06ed622c5b242c080167ffec11fd4c3360dc28d9700aba4c9f3d4a43", upload-time = "2024-03-24T00:00:00Z" } wheels = [ { url = "http://[LOCALHOST]/files/a-1.2.3+foo-py3-none-any.whl", hash = "sha256:2fd8eec176cad72b6c4372682485914aff65d215fb8cd71c85e9ed4511fa8bbe", upload-time = "2024-03-24T00:00:00Z" }, ] diff --git a/crates/uv/tests/python/python_install.rs b/crates/uv/tests/python/python_install.rs index 91de7a3a50c..0e0d2eb5ddc 100644 --- a/crates/uv/tests/python/python_install.rs +++ b/crates/uv/tests/python/python_install.rs @@ -3739,6 +3739,21 @@ fn python_install_armv7() { Installed Python 3.12.12 in [TIME] + cpython-3.12.12-[PLATFORM] (python3.12) "); + + context.python_uninstall().arg("--all").assert().success(); + + // Exercise the tar-codec symlink handling against the armv7 archive that contains duplicate + // terminfo symlinks. + uv_snapshot!(context.filters(), context + .python_install() + .arg("--preview-features") + .arg("tar-codec") + .arg("cpython-3.12.12-linux-armv7-gnueabi"), @" + exit_code: 0 (success) + ----- stderr ----- + Installed Python 3.12.12 in [TIME] + + cpython-3.12.12-[PLATFORM] (python3.12) + "); } #[test] diff --git a/crates/uv/tests/sync/show_settings.rs b/crates/uv/tests/sync/show_settings.rs index 4ab2c86542c..943608004b2 100644 --- a/crates/uv/tests/sync/show_settings.rs +++ b/crates/uv/tests/sync/show_settings.rs @@ -3416,6 +3416,7 @@ fn preview_features() { + IndexHashAlgorithm, + LockfileFormatCheck, + LockWithoutMetadata, + + TarCodec, + ], }, python_preference: Managed, diff --git a/uv.schema.json b/uv.schema.json index a46cb9ed7b2..9c440fa4e1d 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1823,7 +1823,8 @@ "no-distutils-patch", "index-hash-algorithm", "lockfile-format-check", - "lock-without-metadata" + "lock-without-metadata", + "tar-codec" ] }, {