diff --git a/Cargo.lock b/Cargo.lock index 68a2831b48e61..3dd2de623ed15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,14 +363,12 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ - "bzip2", "flate2", "futures-core", "futures-io", "memchr", "pin-project-lite", "tokio", - "xz2", "zstd", "zstd-safe", ] @@ -657,25 +655,6 @@ version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" -[[package]] -name = "bzip2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" -dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "camino" version = "1.2.2" @@ -2697,17 +2676,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "mailparse" version = "0.16.1" @@ -6489,7 +6457,6 @@ dependencies = [ "uv-python", "uv-redacted", "uv-types", - "uv-warnings", "uv-workspace", "walkdir", ] @@ -6592,8 +6559,6 @@ dependencies = [ "uv-distribution-filename", "uv-pypi-types", "uv-static", - "uv-warnings", - "xz2", ] [[package]] @@ -7415,8 +7380,6 @@ name = "uv-toml" version = "0.0.66" dependencies = [ "serde", - "toml_datetime", - "toml_parser", ] [[package]] @@ -7543,7 +7506,6 @@ dependencies = [ "uv-python", "uv-shell", "uv-version", - "uv-warnings", ] [[package]] @@ -8122,15 +8084,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index eae00a84c77ee..0ad49d12ce6be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,19 +103,14 @@ arrayvec = { version = "0.7.6" } astral-tokio-tar = { version = "0.6.3" } async-channel = { version = "2.3.1" } async-compression = { version = "0.4.12", features = [ - "bzip2", "gzip", - "xz", "zstd", ] } async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.11.0", package = "astral_async_http_range_reader" } async_zip = { version = "0.0.20", package = "astral_async_zip", features = [ - "bzip2", "deflate", - "lzma", "tokio", - "xz", "zstd", ] } axoupdater = { version = "0.10.0", default-features = false } @@ -278,7 +273,6 @@ tokio = { version = "1.40.0", features = [ tokio-stream = { version = "0.1.16" } tokio-util = { version = "0.7.12", features = ["compat", "io"] } toml = { version = "1.1.0", features = ["fast_hash"] } -toml_datetime = { version = "1.1.0" } toml_edit = { version = "0.25.8", features = ["serde"] } toml_parser = { version = "1.1.0", features = ["simd"] } toml_writer = { version = "1.1.0" } @@ -317,7 +311,6 @@ windows = { version = "0.61.0", features = [ windows-registry = { version = "0.5.0" } windows-version = { version = "0.1.6" } wiremock = { version = "0.6.4" } -xz2 = { version = "0.1.7", features = ["static"] } zeroize = { version = "1.8.1" } # dev-dependencies diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index a1331696fbd31..e7c1ff30ece95 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -69,6 +69,3 @@ tempfile = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } toml = { workspace = true } - -[features] -static = ["uv-extract/static"] diff --git a/crates/uv-bench/benches/uv.rs b/crates/uv-bench/benches/uv.rs index cb371739797bd..d37339178c6fb 100644 --- a/crates/uv-bench/benches/uv.rs +++ b/crates/uv-bench/benches/uv.rs @@ -27,7 +27,6 @@ use uv_resolver::Manifest; const MANY_FILES_WHEEL_FILENAME: &str = "manyfiles-0.0.0-py3-none-any.whl"; const MANY_FILES_WHEEL_FILE_COUNT: usize = 10_000; -const MANY_FILES_SDIST_FILENAME: &str = "manyfiles-0.0.0.tar.gz"; const MANY_FILES_SDIST_TOP_LEVEL: &str = "manyfiles-0.0.0"; const MANY_FILES_SDIST_FILE_COUNT: usize = 10_000; @@ -126,7 +125,6 @@ fn unpack_sdist_many_files(c: &mut Criterion) { |(archive, extracted_sdist)| { let files = runtime .block_on(uv_extract::stream::archive( - MANY_FILES_SDIST_FILENAME, archive, SourceDistExtension::TarGz, extracted_sdist.path(), diff --git a/crates/uv-bin-install/src/lib.rs b/crates/uv-bin-install/src/lib.rs index 1766ed9f95e7a..0bd35ed8d19e4 100644 --- a/crates/uv-bin-install/src/lib.rs +++ b/crates/uv-bin-install/src/lib.rs @@ -21,6 +21,7 @@ use tokio::io::{AsyncRead, ReadBuf}; use tokio_util::compat::FuturesAsyncReadCompatExt; use url::Url; use uv_client::retryable_on_request_failure; +use uv_distribution_filename::LegacySourceDistExtension; use uv_distribution_filename::SourceDistExtension; use uv_static::{astral_mirror_base_url, astral_mirror_url_from_env, custom_astral_mirror_url}; @@ -230,7 +231,7 @@ impl ArchiveFormat { impl From for SourceDistExtension { fn from(val: ArchiveFormat) -> Self { match val { - ArchiveFormat::Zip => Self::Zip, + ArchiveFormat::Zip => Self::Legacy(LegacySourceDistExtension::Zip), ArchiveFormat::TarGz => Self::TarGz, } } @@ -841,14 +842,9 @@ async fn download_and_unpack( let id = reporter.on_download_start(binary.name(), version, size); let mut progress_reader = ProgressReader::new(reader, id, reporter); - stream::archive( - &download_url, - &mut progress_reader, - format.into(), - temp_dir.path(), - ) - .await - .map_err(|e| Error::Extract { source: e })?; + stream::archive(&mut progress_reader, format.into(), temp_dir.path()) + .await + .map_err(|e| Error::Extract { source: e })?; reporter.on_download_complete(id); // Find the binary in the extracted files diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 54210510896d1..d760c8eba2108 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -788,17 +788,18 @@ mod tests { // Check that the source dist is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&source_dist_path).unwrap())), - @"8bed1f7a8059064bcbeedb61a867cca7f63a474306011d0114280de631ac705e" + @"1d9ce1ce63195fbee07314c0b595ba9e063670da8d10c252c351b21e94e3f508" ); // Check both the files we report and the actual files assert_snapshot!(format_file_list(build.source_dist_list_files, src.path()), @" built_by_uv-0.1.0/PKG-INFO (generated) + built_by_uv-0.1.0/pyproject.toml (generated) + built_by_uv-0.1.0/pyproject.toml.orig (pyproject.toml) built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE) built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT) built_by_uv-0.1.0/README.md (README.md) built_by_uv-0.1.0/assets/data.csv (assets/data.csv) built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h) - built_by_uv-0.1.0/pyproject.toml (pyproject.toml) built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh) built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py) @@ -819,6 +820,7 @@ mod tests { built_by_uv-0.1.0/header built_by_uv-0.1.0/header/built_by_uv.h built_by_uv-0.1.0/pyproject.toml + built_by_uv-0.1.0/pyproject.toml.orig built_by_uv-0.1.0/scripts built_by_uv-0.1.0/scripts/whoami.sh built_by_uv-0.1.0/src @@ -1060,6 +1062,7 @@ mod tests { two_step_build-1.0.0/ two_step_build-1.0.0/PKG-INFO two_step_build-1.0.0/pyproject.toml + two_step_build-1.0.0/pyproject.toml.orig two_step_build-1.0.0/two_step_build two_step_build-1.0.0/two_step_build/__init__.py "); @@ -1462,6 +1465,7 @@ mod tests { simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO simple_namespace_part-1.0.0/pyproject.toml + simple_namespace_part-1.0.0/pyproject.toml.orig simple_namespace_part-1.0.0/src simple_namespace_part-1.0.0/src/simple_namespace simple_namespace_part-1.0.0/src/simple_namespace/part @@ -1724,6 +1728,7 @@ mod tests { simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO simple_namespace_part-1.0.0/pyproject.toml + simple_namespace_part-1.0.0/pyproject.toml.orig simple_namespace_part-1.0.0/src simple_namespace_part-1.0.0/src/foo simple_namespace_part-1.0.0/src/foo/__init__.py @@ -1839,6 +1844,7 @@ mod tests { duplicate-1.0.0/ duplicate-1.0.0/PKG-INFO duplicate-1.0.0/pyproject.toml + duplicate-1.0.0/pyproject.toml.orig duplicate-1.0.0/src duplicate-1.0.0/src/bar duplicate-1.0.0/src/bar/baz @@ -1906,8 +1912,7 @@ mod tests { /// not accidentally skipped by the root-level TOML rewriting logic. #[test] fn nested_pyproject_toml_preserved() { - let _preview = - uv_preview::test::with_features(&[PreviewFeature::TomlBackwardsCompatibility]); + let _preview = uv_preview::test::with_features(&[]); let tmp_dir = TempDir::new().unwrap(); fs_err::write( @@ -1966,8 +1971,7 @@ mod tests { /// compatibility with older tools. The original file is preserved as pyproject.toml.orig. #[test] fn toml_1_1_backward_compatibility() { - let _preview = - uv_preview::test::with_features(&[PreviewFeature::TomlBackwardsCompatibility]); + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); // A `pyproject.toml` with a TOML 1.1 feature, trailing commas in inline tables. @@ -2054,8 +2058,7 @@ mod tests { "#); } - /// Test that TOML 1.1 features in pyproject.toml trigger auto-detection and rewrite to TOML - /// 1.0, even without explicitly enabling `PreviewFeature::TomlBackwardsCompatibility`. + /// Test that TOML 1.1 features in pyproject.toml are rewritten by default. #[test] fn toml_1_1_backward_compatibility_auto_detection() { let _preview = uv_preview::test::with_features(&[]); diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 56d09c255c153..2c708bbca9807 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -21,8 +21,6 @@ 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_toml::has_toml11_features; use uv_warnings::warn_user_once; use walkdir::WalkDir; @@ -242,47 +240,28 @@ fn write_source_dist( // // To work around this, we do a best-effort rewrite of `pyproject.toml` to TOML 1.0. We also // add the original `pyproject.toml` as `pyproject.toml.orig` for reference. - // - // The feature is enabled either explicitly via the preview flag, or automatically when the - // `pyproject.toml` is detected to contain TOML 1.1-only syntax. let pyproject_path = source_tree.join("pyproject.toml"); let pyproject_contents = fs_err::read_to_string(&pyproject_path)?; - let toml_backwards_compatibility = - if uv_preview::is_enabled(PreviewFeature::TomlBackwardsCompatibility) { - true - } else if has_toml11_features(&pyproject_contents) { - warn_user_once!( - "`pyproject.toml` uses TOML 1.1 features; rewriting to TOML 1.0 for \ - compatibility with older build tools. Use `--preview-feature \ - {feature}` to suppress this warning.", - feature = PreviewFeature::TomlBackwardsCompatibility - ); - true - } else { - false - }; - if toml_backwards_compatibility { - let mut pyproject_value: toml::Value = toml::from_str(&pyproject_contents) - .map_err(|err| Error::Toml(pyproject_path.clone(), err))?; - // See https://github.com/toml-rs/toml/issues/1088 for `to_string_pretty`. - normalize_toml10_datetimes(&mut pyproject_value); - let pyproject_rewritten = - toml::to_string_pretty(&pyproject_value).map_err(Error::TomlSerialize)?; - writer.write_bytes( - &Path::new(&top_level) - .join("pyproject.toml") - .portable_display() - .to_string(), - pyproject_rewritten.as_bytes(), - )?; - writer.write_file( - &Path::new(&top_level) - .join("pyproject.toml.orig") - .portable_display() - .to_string(), - &pyproject_path, - )?; - } + let mut pyproject_value: toml::Value = toml::from_str(&pyproject_contents) + .map_err(|err| Error::Toml(pyproject_path.clone(), err))?; + // See https://github.com/toml-rs/toml/issues/1088 for `to_string_pretty`. + normalize_toml10_datetimes(&mut pyproject_value); + let pyproject_rewritten = + toml::to_string_pretty(&pyproject_value).map_err(Error::TomlSerialize)?; + writer.write_bytes( + &Path::new(&top_level) + .join("pyproject.toml") + .portable_display() + .to_string(), + pyproject_rewritten.as_bytes(), + )?; + writer.write_file( + &Path::new(&top_level) + .join("pyproject.toml.orig") + .portable_display() + .to_string(), + &pyproject_path, + )?; let (include_matcher, exclude_matcher) = source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?; @@ -333,15 +312,13 @@ fn write_source_dist( continue; } - if toml_backwards_compatibility { - // `pyproject.toml` is handled separately. - if relative == "pyproject.toml" { - continue; - } - if relative == "pyproject.toml.orig" { - debug!("Ignoring existing `pyproject.toml.orig`"); - continue; - } + // `pyproject.toml` is handled separately. + if relative == "pyproject.toml" { + continue; + } + if relative == "pyproject.toml.orig" { + debug!("Ignoring existing `pyproject.toml.orig`"); + continue; } error_on_venv(entry.file_name(), entry.path())?; diff --git a/crates/uv-cli/src/compat.rs b/crates/uv-cli/src/compat.rs index b2d4e9b073087..cedd2a8f41a30 100644 --- a/crates/uv-cli/src/compat.rs +++ b/crates/uv-cli/src/compat.rs @@ -32,9 +32,6 @@ pub struct PipCompileCompatArgs { #[clap(long, hide = true)] max_rounds: Option, - #[clap(long, hide = true)] - cert: Option, - #[clap(long, hide = true)] client_cert: Option, @@ -192,9 +189,6 @@ pub struct PipSyncCompatArgs { #[clap(long, hide = true)] user: bool, - #[clap(long, hide = true)] - cert: Option, - #[clap(long, hide = true)] client_cert: Option, diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index ae12a7bbc6a5e..8a66c7a2b7c8c 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -970,6 +970,12 @@ pub struct SizeArgs { pub struct PipNamespace { #[command(subcommand)] pub command: PipCommand, + + /// Path to a PEM-encoded CA certificate bundle. + /// + /// If provided, this overrides the default certificate source. + #[arg(global = true, long, value_name = "FILE", value_hint = ValueHint::FilePath)] + pub cert: Option, } #[derive(Subcommand)] @@ -1063,9 +1069,8 @@ pub enum ProjectCommand { /// or a parent directory, the command will be run in that environment. Otherwise, the command /// will be run in the environment of the discovered interpreter. /// - /// By default, the project or workspace is discovered from the current working directory. - /// However, when using `--preview-features target-workspace-discovery`, the project or - /// workspace is instead discovered from the target script's directory. + /// When running a script, the project or workspace is discovered from the script's directory. + /// Otherwise, the project or workspace is discovered from the current working directory. /// /// Arguments following the command (or script) are not interpreted as arguments to uv. All /// options to uv must be provided before the command, e.g., `uv run --verbose foo`. A `--` can @@ -3255,32 +3260,23 @@ pub struct InitArgs { /// /// Defines a `[build-system]` for the project. /// - /// This is the default behavior when using `--lib` or `--build-backend`, or when the - /// `packaged-init` preview feature is enabled. It will become the default unconditionally in - /// the future. - /// - /// When using `--app`, this will include a `[project.scripts]` entrypoint and use a `src/` - /// project structure. + /// This is the default behavior. #[arg(long, overrides_with = "no_package")] pub r#package: bool, /// Do not set up the project to be built as a Python package. /// - /// Does not include a `[build-system]` for the project. - /// - /// This is the default behavior when using `--app`. + /// This option creates the project structure as a flat directory that is not importable as a + /// module and has no `[build-system]` entry. It can be used for applications that are not + /// expected to be distributed as a package. #[arg(long, overrides_with = "package", conflicts_with_all = ["lib", "build_backend"])] pub r#no_package: bool, /// Create a project for an application. /// - /// This is the default behavior if `--lib` is not requested. - /// /// This project kind is for web servers, scripts, and command-line interfaces. /// - /// By default, an application is not intended to be built and distributed as a Python package. - /// The `--package` option can be used to create an application that is distributable, e.g., if - /// you want to distribute a command-line interface via PyPI. + /// Applications are packaged by default. Use `--no-package` to create an unpackaged application. #[arg(long, alias = "application", conflicts_with_all = ["lib", "script"])] pub r#app: bool, @@ -6434,6 +6430,8 @@ pub struct PythonInstallArgs { /// Reinstall the requested Python version, if it's already installed. /// + /// If a minor version is requested, all matching installed patch versions are reinstalled. + /// /// By default, uv will exit successfully if the version is already /// installed. #[arg(long, short)] @@ -7010,9 +7008,9 @@ pub struct VersionSelectionArgs { /// The strategy to use when considering pre-release versions. /// - /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, along - /// with first-party requirements that contain an explicit pre-release marker in the declared - /// specifiers (`if-necessary-or-explicit`). + /// By default, uv will prefer stable candidates, falling back to pre-releases only after every + /// stable candidate that satisfies the active constraints is rejected + /// (`if-necessary`). #[arg( long, value_enum, diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index d4421abd3d07a..1c0d07567d1fc 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -1,3 +1,4 @@ +use std::env; use std::error::Error; use std::fmt; @@ -233,7 +234,7 @@ impl TryFrom for Refresh { } /// Extract the `--index` and `--default-index` values from [`IndexArgs`]. -pub fn indexes_from_args( +fn indexes_from_args( default_index: Option<&Maybe>, index: Option<&[Vec>]>, ) -> Option> { @@ -336,7 +337,7 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } @@ -406,7 +407,7 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } @@ -508,13 +509,15 @@ impl TryFrom for PipOptions { } else { Some(no_sources_package) }, - ..Self::from(index_args) + ..Self::try_from(index_args)? }) } } -impl From for PipOptions { - fn from(args: FetchArgs) -> Self { +impl TryFrom for PipOptions { + type Error = anyhow::Error; + + fn try_from(args: FetchArgs) -> anyhow::Result { let FetchArgs { index_args, registry_client: @@ -529,18 +532,20 @@ impl From for PipOptions { }, } = args; - Self { + Ok(Self { index_strategy, keyring_provider, exclude_newer, exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter), - ..Self::from(index_args) - } + ..Self::try_from(index_args)? + }) } } -impl From for PipOptions { - fn from(args: IndexArgs) -> Self { +impl TryFrom for PipOptions { + type Error = anyhow::Error; + + fn try_from(args: IndexArgs) -> anyhow::Result { let IndexArgs { default_index, index, @@ -568,6 +573,8 @@ impl From for PipOptions { }), ..Self::default() } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } } @@ -626,7 +633,7 @@ pub fn resolver_options( no_binary_package, } = build_args; - Ok(ResolverOptions { + ResolverOptions { index: indexes_from_args( index_args.default_index.as_ref(), index_args.index.as_deref(), @@ -699,26 +706,15 @@ pub fn resolver_options( } else { Some(no_sources_package) }, - }) + } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } /// Construct the [`ResolverInstallerOptions`] from the [`ResolverInstallerArgs`] and [`BuildOptionsArgs`]. pub fn resolver_installer_options( resolver_installer_args: ResolverInstallerArgs, build_args: BuildOptionsArgs, -) -> anyhow::Result { - let index = indexes_from_args( - resolver_installer_args.index_args.default_index.as_ref(), - resolver_installer_args.index_args.index.as_deref(), - ); - resolver_installer_options_with_indexes(resolver_installer_args, build_args, index) -} - -/// Construct the [`ResolverInstallerOptions`] with a precomputed list of indexes. -pub fn resolver_installer_options_with_indexes( - resolver_installer_args: ResolverInstallerArgs, - build_args: BuildOptionsArgs, - index: Option>, ) -> anyhow::Result { let ResolverInstallerArgs { index_args, @@ -781,8 +777,11 @@ pub fn resolver_installer_options_with_indexes( no_binary_package, } = build_args; - Ok(ResolverInstallerOptions { - index, + ResolverInstallerOptions { + index: indexes_from_args( + index_args.default_index.as_ref(), + index_args.index.as_deref(), + ), index_url: index_args.index_url.and_then(Maybe::into_option), extra_index_url: index_args.extra_index_url.map(|extra_index_url| { extra_index_url @@ -856,5 +855,7 @@ pub fn resolver_installer_options_with_indexes( Some(no_sources_package) }, torch_backend: None, - }) + } + .relative_to(&env::current_dir()?) + .map_err(Into::into) } diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 260c0f2fbb016..aaeee20fb24f1 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -93,6 +93,7 @@ pub struct BaseClientBuilder<'a> { preview: Preview, allow_insecure_host: Vec, system_certs: bool, + custom_certificates: Option, retries: u32, pub connectivity: Connectivity, markers: Option<&'a MarkerEnvironment>, @@ -203,6 +204,7 @@ impl Default for BaseClientBuilder<'_> { preview: Preview::default(), allow_insecure_host: vec![], system_certs: false, + custom_certificates: None, connectivity: Connectivity::Online, retries: DEFAULT_RETRIES, markers: None, @@ -304,6 +306,13 @@ impl<'a> BaseClientBuilder<'a> { self } + /// Use custom certificate authorities for TLS verification. + #[must_use] + pub fn custom_certificates(mut self, certificates: Certificates) -> Self { + self.custom_certificates = Some(certificates); + self + } + #[must_use] pub(crate) fn markers(mut self, markers: &'a MarkerEnvironment) -> Self { self.markers = Some(markers); @@ -524,8 +533,10 @@ impl<'a> BaseClientBuilder<'a> { let _ = write!(user_agent_string, " {output}"); } - // Load custom CA certificates from `SSL_CERT_FILE` and `SSL_CERT_DIR`. - let custom_certs = Certificates::from_env().map(|certs| certs.to_reqwest_certs()); + let custom_certs = self + .custom_certificates + .as_ref() + .map(Certificates::to_reqwest_certs); let certificate_source = if custom_certs.is_some() { CertificateSource::Custom } else if self.system_certs { @@ -585,8 +596,8 @@ impl<'a> BaseClientBuilder<'a> { // Configure the certificate source. // - // `SSL_CERT_FILE` and `SSL_CERT_DIR` override the default certificate source when they - // contain valid certificates. + // Non-empty `SSL_CERT_FILE` and `SSL_CERT_DIR` values override the default certificate + // source, even when no valid certificates can be loaded from their configured paths. let client_builder = if let Some(custom_certs) = custom_certs { client_builder.tls_certs_only(custom_certs) } else if self.system_certs { @@ -759,7 +770,7 @@ pub(crate) enum CertificateSource { System, /// The bundled `WebPKI` certificate roots. WebPki, - /// Custom roots loaded from `SSL_CERT_FILE` or `SSL_CERT_DIR`. + /// Custom certificate roots. Custom, /// An externally constructed client whose certificate roots are unknown. Unknown, diff --git a/crates/uv-client/src/lib.rs b/crates/uv-client/src/lib.rs index dd43e7ebddced..2d04b5e99d378 100644 --- a/crates/uv-client/src/lib.rs +++ b/crates/uv-client/src/lib.rs @@ -14,6 +14,7 @@ pub use registry_client::{ pub(crate) use retry::UvRetryableStrategy; pub use retry::{RetriableError, RetryState, retryable_on_request_failure}; pub use rkyvutil::OwnedArchive; +pub use tls::{CertificateFileError, Certificates}; mod base_client; mod cached_client; diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index a9cb021a9622d..55ce9cbb5f758 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -1089,6 +1089,7 @@ impl RegistryClient { filename, file, index, + .. } = wheel; // If the metadata file is available at its own url (PEP 658), download it from there. diff --git a/crates/uv-client/src/tls.rs b/crates/uv-client/src/tls.rs index 9ade0f9e3c8e5..53504ce42e3a8 100644 --- a/crates/uv-client/src/tls.rs +++ b/crates/uv-client/src/tls.rs @@ -17,13 +17,15 @@ use uv_warnings::warn_user_once; #[derive(Debug, Clone)] enum CertificateSource { + CertFileArg(PathBuf), SslCertFile(PathBuf), SslCertDir(PathBuf), } impl CertificateSource { - const fn env_var(&self) -> &'static str { + const fn description(&self) -> &'static str { match self { + Self::CertFileArg(_) => "--cert", Self::SslCertFile(_) => EnvVars::SSL_CERT_FILE, Self::SslCertDir(_) => EnvVars::SSL_CERT_DIR, } @@ -31,7 +33,7 @@ impl CertificateSource { fn path(&self) -> &Path { match self { - Self::SslCertFile(path) | Self::SslCertDir(path) => path, + Self::CertFileArg(path) | Self::SslCertFile(path) | Self::SslCertDir(path) => path, } } } @@ -133,7 +135,7 @@ impl Display for InvalidCertificateWarning { f, "certificate in `{}` (from `{}`) ", self.source.path().simplified_display(), - self.source.env_var() + self.source.description() )?; match &self.reason { InvalidCertificateReason::UnsupportedCriticalExtension => { @@ -192,7 +194,7 @@ impl Display for InvalidCertificateWarning { /// A collection of TLS certificates in DER form. #[derive(Debug, Clone, Default)] -pub(crate) struct Certificates(Vec>); +pub struct Certificates(Vec>); impl Certificates { /// Load the bundled Mozilla root certificates. @@ -210,27 +212,61 @@ impl Certificates { Self(webpki_root_certs::TLS_SERVER_ROOT_CERTS.to_vec()) } + /// Load a custom CA certificate bundle from an explicit path. + /// + /// Unlike [`Self::from_ssl_cert_file`], an invalid path or a bundle without any valid + /// certificates returns an error instead of being ignored with a warning. + pub fn from_file(file: &Path) -> Result { + let metadata = file + .metadata() + .map_err(|err| CertificateFileError::Io(file.to_path_buf(), err))?; + if !metadata.is_file() { + return Err(CertificateFileError::NotFile(file.to_path_buf())); + } + + let result = Self::from_paths(Some(file), None); + for err in &result.errors { + warn!( + "Failed to load certificate file ({}): {err}", + file.simplified_display() + ); + } + let certs = + Self::from(result).filter_invalid(&CertificateSource::CertFileArg(file.to_path_buf())); + if certs.0.is_empty() { + return Err(CertificateFileError::NoValidCertificates( + file.to_path_buf(), + )); + } + Ok(certs) + } + /// Load custom CA certificates from `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables. /// - /// Returns `None` if neither variable is set, if the referenced files or directories are - /// missing or inaccessible, or if no valid certificates are found (with a warning in each - /// case). Delegates path loading to [`rustls_native_certs::load_certs_from_paths`]. - pub(crate) fn from_env() -> Option { + /// Returns `None` if neither variable is set to a non-empty value. An explicitly configured + /// file or directory always replaces the default certificate roots, even when it is missing, + /// inaccessible, or contains no valid certificates. Delegates path loading to + /// [`rustls_native_certs::load_certs_from_paths`]. + pub fn from_env() -> Option { let mut certs = Self::default(); let mut has_source = false; if let Some(ssl_cert_file) = env::var_os(EnvVars::SSL_CERT_FILE) - && let Some(file_certs) = Self::from_ssl_cert_file(&ssl_cert_file) + && !ssl_cert_file.is_empty() { has_source = true; - certs.merge(file_certs); + if let Some(file_certs) = Self::from_ssl_cert_file(&ssl_cert_file) { + certs.merge(file_certs); + } } if let Some(ssl_cert_dir) = env::var_os(EnvVars::SSL_CERT_DIR) - && let Some(dir_certs) = Self::from_ssl_cert_dir(&ssl_cert_dir) + && !ssl_cert_dir.is_empty() { has_source = true; - certs.merge(dir_certs); + if let Some(dir_certs) = Self::from_ssl_cert_dir(&ssl_cert_dir) { + certs.merge(dir_certs); + } } if has_source { Some(certs) } else { None } @@ -259,7 +295,7 @@ impl Certificates { .filter_invalid(&CertificateSource::SslCertFile(file.clone())); if certs.0.is_empty() { warn_user_once!( - "Ignoring `SSL_CERT_FILE`. No valid certificates found in: {}.", + "No valid certificates found in `SSL_CERT_FILE`: {}. No default certificates will be trusted.", file.simplified_display().cyan() ); return None; @@ -268,21 +304,21 @@ impl Certificates { } Ok(_) => { warn_user_once!( - "Ignoring invalid `SSL_CERT_FILE`. Path is not a file: {}.", + "Invalid `SSL_CERT_FILE`. Path is not a file: {}. No default certificates will be trusted.", file.simplified_display().cyan() ); None } Err(err) if err.kind() == io::ErrorKind::NotFound => { warn_user_once!( - "Ignoring invalid `SSL_CERT_FILE`. Path does not exist: {}.", + "Invalid `SSL_CERT_FILE`. Path does not exist: {}. No default certificates will be trusted.", file.simplified_display().cyan() ); None } Err(err) => { warn_user_once!( - "Ignoring invalid `SSL_CERT_FILE`. Path is not accessible: {} ({err}).", + "Invalid `SSL_CERT_FILE`. Path is not accessible: {} ({err}). No default certificates will be trusted.", file.simplified_display().cyan() ); None @@ -307,12 +343,12 @@ impl Certificates { if existing.is_empty() { let end_note = if missing.len() == 1 { - "The directory does not exist." + "The directory does not exist" } else { - "The entries do not exist." + "The entries do not exist" }; warn_user_once!( - "Ignoring invalid `SSL_CERT_DIR`. {end_note}: {}.", + "Invalid `SSL_CERT_DIR`. {end_note}: {}. No default certificates will be trusted.", missing .iter() .map(Simplified::simplified_display) @@ -358,7 +394,7 @@ impl Certificates { // Unlike `SSL_CERT_FILE`, it's plausible for this to be intentionally set to an // empty directory that a user _could_ put certificates in. warn!( - "Ignoring `SSL_CERT_DIR`. No valid certificates found in: {}.", + "No valid certificates found in `SSL_CERT_DIR`: {}. No default certificates will be trusted.", existing .iter() .map(Simplified::simplified_display) @@ -441,6 +477,16 @@ pub(crate) enum CertificateError { Reqwest(reqwest::Error), } +#[derive(thiserror::Error, Debug)] +pub enum CertificateFileError { + #[error("Failed to read certificate file `{}`", .0.simplified_display())] + Io(PathBuf, #[source] io::Error), + #[error("Certificate path is not a file: `{}`", .0.simplified_display())] + NotFile(PathBuf), + #[error("No valid certificates found in: `{}`", .0.simplified_display())] + NoValidCertificates(PathBuf), +} + /// Return the `Identity` from the provided file. pub(crate) fn read_identity( ssl_client_cert: &std::ffi::OsStr, @@ -473,6 +519,23 @@ mod tests { assert!(certs.is_none()); } + #[test] + fn test_from_env_missing_ssl_cert_file_returns_empty_roots() { + let dir = tempfile::tempdir().unwrap(); + let missing_file = dir.path().join("missing.pem"); + + temp_env::with_vars( + [ + (EnvVars::SSL_CERT_FILE, Some(missing_file.as_os_str())), + (EnvVars::SSL_CERT_DIR, None), + ], + || { + let certs = Certificates::from_env().expect("explicit file should override roots"); + assert_eq!(certs.iter().count(), 0); + }, + ); + } + #[test] fn test_from_ssl_cert_file_empty_value_returns_none() { let certs = Certificates::from_ssl_cert_file(OsString::new().as_os_str()); @@ -514,6 +577,23 @@ mod tests { assert!(certs.is_none()); } + #[test] + fn test_from_env_empty_ssl_cert_dir_returns_empty_roots() { + let dir = tempfile::tempdir().unwrap(); + + temp_env::with_vars( + [ + (EnvVars::SSL_CERT_FILE, None), + (EnvVars::SSL_CERT_DIR, Some(dir.path().as_os_str())), + ], + || { + let certs = + Certificates::from_env().expect("explicit directory should override roots"); + assert_eq!(certs.iter().count(), 0); + }, + ); + } + #[test] fn test_merge_deduplicates() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/uv-client/tests/it/remote_metadata.rs b/crates/uv-client/tests/it/remote_metadata.rs index bf06962e30c19..643c510dab10e 100644 --- a/crates/uv-client/tests/it/remote_metadata.rs +++ b/crates/uv-client/tests/it/remote_metadata.rs @@ -1,6 +1,9 @@ +use std::path::Path; use std::str::FromStr; use anyhow::Result; +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; @@ -12,25 +15,36 @@ use uv_redacted::DisplaySafeUrl; #[tokio::test] async fn remote_metadata_with_and_without_cache() -> Result<()> { + let server = MockServer::start().await; + let wheel = fs_err::read( + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../test/links/ok-1.0.0-py3-none-any.whl"), + )?; + Mock::given(method("GET")) + .and(path("/ok-1.0.0-py3-none-any.whl")) + .respond_with(ResponseTemplate::new(200).set_body_raw(wheel, "application/octet-stream")) + .mount(&server) + .await; + let cache = Cache::temp()?.init().await?; let client = RegistryClientBuilder::new(BaseClientBuilder::default(), cache).build()?; // The first run is without cache (the tempdir is empty), the second has the cache from the // first run. for _ in 0..2 { - let url = "https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl"; - let filename = WheelFilename::from_str(url.rsplit_once('/').unwrap().1)?; + let url = format!("{}/ok-1.0.0-py3-none-any.whl", server.uri()); + let filename = WheelFilename::from_str("ok-1.0.0-py3-none-any.whl")?; let dist = BuiltDist::DirectUrl(DirectUrlBuiltDist { filename, - location: Box::new(DisplaySafeUrl::parse(url)?), - url: VerbatimUrl::from_str(url)?, + location: Box::new(DisplaySafeUrl::parse(&url)?), + url: VerbatimUrl::from_str(&url)?, + size: None, }); let resolver = GitResolver::default(); let capabilities = IndexCapabilities::default(); let metadata = client .wheel_metadata(&dist, &resolver, &capabilities, None) .await?; - assert_eq!(metadata.version.to_string(), "4.66.1"); + assert_eq!(metadata.version.to_string(), "1.0.0"); } Ok(()) diff --git a/crates/uv-client/tests/it/ssl_certs.rs b/crates/uv-client/tests/it/ssl_certs.rs index 43914ca9d1c6e..6189b24625915 100644 --- a/crates/uv-client/tests/it/ssl_certs.rs +++ b/crates/uv-client/tests/it/ssl_certs.rs @@ -12,8 +12,7 @@ use tokio::net::{TcpListener, TcpStream}; use url::Url; use uv_cache::Cache; -use uv_client::BaseClientBuilder; -use uv_client::RegistryClientBuilder; +use uv_client::{BaseClientBuilder, Certificates, RegistryClientBuilder}; use uv_distribution_types::IndexUrl; use uv_errors::{ErrorOptions, Hint, write_error_chain_with_options}; use uv_redacted::DisplaySafeUrl; @@ -141,6 +140,7 @@ struct TestClient { overrides: Vec<(&'static str, String)>, system_certs: bool, custom_client: bool, + cert: Option, } /// Create a [`TestClient`] with no environment overrides. @@ -149,10 +149,17 @@ fn client() -> TestClient { overrides: Vec::new(), system_certs: false, custom_client: false, + cert: None, } } impl TestClient { + /// Set the explicit certificate file used to construct [`Certificates`]. + fn cert(mut self, path: &Path) -> Self { + self.cert = Some(path.to_path_buf()); + self + } + /// Enable or disable system certificate loading. fn system_certs(mut self, enabled: bool) -> Self { self.system_certs = enabled; @@ -229,13 +236,16 @@ impl TestClient { async_with_vars(vars, async { let (server_task, addr) = start_https_user_agent_server(&cert.server).await.unwrap(); let cache = Cache::temp().unwrap().init().await.unwrap(); - let client = RegistryClientBuilder::new( - BaseClientBuilder::default() - .retries(0) - .no_retry_delay(true) - .with_system_certs(system_certs), - cache, - ); + let base = BaseClientBuilder::default() + .retries(0) + .no_retry_delay(true) + .with_system_certs(system_certs); + let base = if let Some(certificates) = Certificates::from_env() { + base.custom_certificates(certificates) + } else { + base + }; + let client = RegistryClientBuilder::new(base, cache); let client = if custom_client { client.with_reqwest_client(reqwest::Client::new()) } else { @@ -381,7 +391,7 @@ impl TestClient { let system_certs = self.system_certs; async_with_vars(vars, async { let (server_task, addr) = start_https_user_agent_server(&cert.server).await.unwrap(); - let response = send_request(addr, system_certs).await; + let response = send_request(addr, system_certs, self.cert.as_deref()).await; check(response, server_task).await; }) .await; @@ -403,7 +413,7 @@ impl TestClient { let (server_task, addr) = start_https_mtls_user_agent_server(&cert.ca, &cert.server) .await .unwrap(); - let response = send_request(addr, system_certs).await; + let response = send_request(addr, system_certs, self.cert.as_deref()).await; check(response, server_task).await; }) .await; @@ -414,20 +424,40 @@ impl TestClient { async fn send_request( addr: SocketAddr, system_certs: bool, + cert: Option<&Path>, ) -> Result { let url = DisplaySafeUrl::from_str(&format!("https://{addr}")).unwrap(); - send_request_to(&url, system_certs).await + send_request_to_with_cert(&url, system_certs, cert).await } /// Send a GET request to an arbitrary URL using a fresh registry client. +#[cfg(feature = "test-pypi")] async fn send_request_to( url: &DisplaySafeUrl, system_certs: bool, +) -> Result { + send_request_to_with_cert(url, system_certs, None).await +} + +async fn send_request_to_with_cert( + url: &DisplaySafeUrl, + system_certs: bool, + cert: Option<&Path>, ) -> Result { let cache = Cache::temp().unwrap().init().await.unwrap(); + let custom_certificates = if let Some(cert) = cert { + Some(Certificates::from_file(cert).expect("failed to load certificate file")) + } else { + Certificates::from_env() + }; let base = BaseClientBuilder::default() .no_retry_delay(true) .with_system_certs(system_certs); + let base = if let Some(certificates) = custom_certificates { + base.custom_certificates(certificates) + } else { + base + }; let client = RegistryClientBuilder::new(base, cache) .build() .expect("failed to build registry client"); @@ -502,7 +532,7 @@ async fn test_certificate_tls_alerts_are_not_retried() -> Result<()> { Ok::<_, anyhow::Error>(()) }); - let response = send_request(addr, false).await; + let response = send_request(addr, false, None).await; assert_fatal_reqwest_error(&response); server_task.await??; Ok(()) @@ -533,7 +563,7 @@ async fn test_non_certificate_tls_errors_are_retried() -> Result<()> { Ok::<_, anyhow::Error>(()) }); - let response = send_request(addr, false).await; + let response = send_request(addr, false, None).await; let Err(reqwest_middleware::Error::Middleware(middleware_error)) = response else { panic!("expected middleware error, got: {response:?}"); }; @@ -581,10 +611,9 @@ async fn test_ssl_cert_file_wrong_cert_rejected() -> Result<()> { Ok(()) } -/// A nonexistent `SSL_CERT_FILE` is ignored; the client falls back to webpki -/// roots which don't include our test CA. +/// A nonexistent `SSL_CERT_FILE` replaces the default roots with an empty trust store. #[tokio::test] -async fn test_ssl_cert_file_nonexistent_falls_back() -> Result<()> { +async fn test_ssl_cert_file_nonexistent_overrides_default_roots() -> Result<()> { let cert = TestCertificate::new()?; let dir = TempDir::new()?; let missing = dir.path().join("missing.pem"); @@ -592,13 +621,16 @@ async fn test_ssl_cert_file_nonexistent_falls_back() -> Result<()> { .ssl_cert_file(&missing) .expect_https_connect_fails(&cert) .await; + client() + .ssl_cert_file(&missing) + .expect_index_fetch_system_certs_hint(&cert, false) + .await; Ok(()) } -/// A nonexistent `SSL_CERT_DIR` is ignored; the client falls back to webpki -/// roots which don't include our test CA. +/// A nonexistent `SSL_CERT_DIR` replaces the default roots with an empty trust store. #[tokio::test] -async fn test_ssl_cert_dir_nonexistent_falls_back() -> Result<()> { +async fn test_ssl_cert_dir_nonexistent_overrides_default_roots() -> Result<()> { let cert = TestCertificate::new()?; let dir = TempDir::new()?; let missing = dir.path().join("missing-certs"); @@ -606,6 +638,10 @@ async fn test_ssl_cert_dir_nonexistent_falls_back() -> Result<()> { .ssl_cert_dir(&missing) .expect_https_connect_fails(&cert) .await; + client() + .ssl_cert_dir(&missing) + .expect_index_fetch_system_certs_hint(&cert, false) + .await; Ok(()) } @@ -620,6 +656,30 @@ async fn test_ssl_cert_file_valid() -> Result<()> { Ok(()) } +/// An explicit certificate file pointing to the server's CA cert is trusted. +#[tokio::test] +async fn test_cli_cert_valid() -> Result<()> { + let cert = TestCertificate::new()?; + client() + .cert(&cert.trust_path) + .expect_https_connect_succeeds(&cert) + .await; + Ok(()) +} + +/// An explicit certificate file overrides the environment certificate sources. +#[tokio::test] +async fn test_cli_cert_overrides_environment() -> Result<()> { + let cert = TestCertificate::new()?; + let wrong_cert = TestCertificate::new()?; + client() + .ssl_cert_file(&wrong_cert.trust_path) + .cert(&cert.trust_path) + .expect_https_connect_succeeds(&cert) + .await; + Ok(()) +} + /// If `SSL_CERT_FILE` contains only an invalid trust anchor, the invalid /// certificate is ignored and the client falls back to webpki roots. #[tokio::test] diff --git a/crates/uv-configuration/src/hash.rs b/crates/uv-configuration/src/hash.rs index cadebbd1510a0..473a9588facbd 100644 --- a/crates/uv-configuration/src/hash.rs +++ b/crates/uv-configuration/src/hash.rs @@ -32,6 +32,15 @@ impl HashCheckingMode { } } + /// Apply the `--require-hashes` setting from a requirements file. + pub fn from_requirements_txt(mode: Option, require_hashes: bool) -> Option { + if require_hashes { + Some(Self::Require) + } else { + mode + } + } + /// Returns `true` if the hash checking mode is `Require`. pub fn is_require(&self) -> bool { matches!(self, Self::Require) diff --git a/crates/uv-configuration/src/package_options.rs b/crates/uv-configuration/src/package_options.rs index 7546d820bb69e..ed0bbb266c193 100644 --- a/crates/uv-configuration/src/package_options.rs +++ b/crates/uv-configuration/src/package_options.rs @@ -134,7 +134,10 @@ pub enum UpgradeStrategy { None, /// Allow package upgrades for all packages, ignoring the existing lockfile. - All, + /// + /// Group names are retained for validation purposes; they do not limit which packages are + /// upgraded. + All(FxHashSet), /// Allow package upgrades, but only for the specified packages and/or dependency groups. Some(FxHashSet, FxHashSet), @@ -168,7 +171,7 @@ impl Upgrade { let groups: FxHashSet = upgrade_group.into_iter().collect(); let strategy = match upgrade { - Some(true) => UpgradeStrategy::All, + Some(true) => UpgradeStrategy::All(groups), Some(false) => { if upgrade_package.is_empty() && groups.is_empty() { return Some(Self::none()); @@ -224,7 +227,7 @@ impl Upgrade { /// Returns `true` if all packages should be upgraded. pub fn is_all(&self) -> bool { - matches!(self.strategy, UpgradeStrategy::All) + matches!(self.strategy, UpgradeStrategy::All(_)) } /// Returns an iterator over the constraints. @@ -244,10 +247,14 @@ impl Upgrade { } } - /// Returns the set of dependency groups whose packages should be upgraded. + /// Returns the set of dependency groups explicitly requested for upgrade. pub fn groups(&self) -> Option<&FxHashSet> { match &self.strategy { - UpgradeStrategy::Some(_, groups) if !groups.is_empty() => Some(groups), + UpgradeStrategy::All(groups) | UpgradeStrategy::Some(_, groups) + if !groups.is_empty() => + { + Some(groups) + } _ => None, } } @@ -258,9 +265,20 @@ impl Upgrade { // For `strategy`: an explicit `All` or `None` in `self` takes precedence; otherwise, // merge. let strategy = match (self.strategy, other.strategy) { - (UpgradeStrategy::All, _) => UpgradeStrategy::All, + (UpgradeStrategy::All(mut groups), UpgradeStrategy::All(other_groups)) => { + groups.extend(other_groups); + UpgradeStrategy::All(groups) + } + (UpgradeStrategy::All(mut groups), UpgradeStrategy::Some(_, other_groups)) => { + groups.extend(other_groups); + UpgradeStrategy::All(groups) + } + (UpgradeStrategy::All(groups), UpgradeStrategy::None) => UpgradeStrategy::All(groups), (UpgradeStrategy::None, _) => UpgradeStrategy::None, - (UpgradeStrategy::Some(_, _), UpgradeStrategy::All) => UpgradeStrategy::All, + (UpgradeStrategy::Some(_, groups), UpgradeStrategy::All(mut other_groups)) => { + other_groups.extend(groups); + UpgradeStrategy::All(other_groups) + } (UpgradeStrategy::Some(packages, groups), UpgradeStrategy::None) => { UpgradeStrategy::Some(packages, groups) } @@ -295,7 +313,7 @@ impl From for Refresh { fn from(value: Upgrade) -> Self { match value.strategy { UpgradeStrategy::None => Self::None(Timestamp::now()), - UpgradeStrategy::All => Self::All(Timestamp::now()), + UpgradeStrategy::All(_) => Self::All(Timestamp::now()), UpgradeStrategy::Some(packages, _) => Self::Packages( packages.into_iter().collect::>(), Vec::new(), diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index a951aa98c0dec..280bc808380a6 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -79,7 +79,7 @@ name = "uv-dev" required-features = ["dev"] [features] -default = ["performance", "uv-extract/static"] +default = ["performance"] # Actually build the dev CLI. dev = [] performance = ["performance-memory-allocator"] diff --git a/crates/uv-dev/src/validate_zip.rs b/crates/uv-dev/src/validate_zip.rs index e7a44835db7d0..1614254274872 100644 --- a/crates/uv-dev/src/validate_zip.rs +++ b/crates/uv-dev/src/validate_zip.rs @@ -47,7 +47,7 @@ pub(crate) async fn validate_zip( let target = tempfile::TempDir::new()?; - uv_extract::stream::unzip(args.url.into_url(), reader.compat(), target.path()).await?; + uv_extract::stream::unzip(reader.compat(), target.path()).await?; Ok(()) } diff --git a/crates/uv-dev/src/wheel_metadata.rs b/crates/uv-dev/src/wheel_metadata.rs index 1cc5ec0afcc4a..c5364c4f5f3c9 100644 --- a/crates/uv-dev/src/wheel_metadata.rs +++ b/crates/uv-dev/src/wheel_metadata.rs @@ -47,6 +47,7 @@ pub(crate) async fn wheel_metadata( filename, location: Box::new(archive.url), url: args.url, + size: None, }), &resolver, &capabilities, diff --git a/crates/uv-distribution-filename/src/extension.rs b/crates/uv-distribution-filename/src/extension.rs index 5fb4f033caf6e..4c75caee7b8fa 100644 --- a/crates/uv-distribution-filename/src/extension.rs +++ b/crates/uv-distribution-filename/src/extension.rs @@ -27,9 +27,29 @@ pub enum DistExtension { )] #[rkyv(derive(Debug))] pub enum SourceDistExtension { + TarGz, + Legacy(LegacySourceDistExtension), +} + +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Serialize, + Deserialize, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] +#[rkyv(derive(Debug))] +pub enum LegacySourceDistExtension { Tar, TarBz2, - TarGz, TarLz, TarLzma, TarXz, @@ -83,20 +103,17 @@ impl SourceDistExtension { match extension { "gz" if is_tar(path.as_ref()) => Ok(Self::TarGz), - // TODO: Remove these other extensions in the future. - // NOTE: These get parsed from network sources like PyPI, so we don't - // necessarily want to hard-fail, just skip in the future. - "zip" => Ok(Self::Zip), - "tar" => Ok(Self::Tar), - "tgz" => Ok(Self::Tgz), - "tbz" => Ok(Self::Tbz), - "txz" => Ok(Self::Txz), - "tlz" => Ok(Self::Tlz), - "bz2" if is_tar(path.as_ref()) => Ok(Self::TarBz2), - "xz" if is_tar(path.as_ref()) => Ok(Self::TarXz), - "lz" if is_tar(path.as_ref()) => Ok(Self::TarLz), - "lzma" if is_tar(path.as_ref()) => Ok(Self::TarLzma), - "zst" if is_tar(path.as_ref()) => Ok(Self::TarZst), + "zip" => Ok(Self::Legacy(LegacySourceDistExtension::Zip)), + "tar" => Ok(Self::Legacy(LegacySourceDistExtension::Tar)), + "tgz" => Ok(Self::Legacy(LegacySourceDistExtension::Tgz)), + "tbz" => Ok(Self::Legacy(LegacySourceDistExtension::Tbz)), + "txz" => Ok(Self::Legacy(LegacySourceDistExtension::Txz)), + "tlz" => Ok(Self::Legacy(LegacySourceDistExtension::Tlz)), + "bz2" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarBz2)), + "xz" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarXz)), + "lz" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarLz)), + "lzma" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarLzma)), + "zst" if is_tar(path.as_ref()) => Ok(Self::Legacy(LegacySourceDistExtension::TarZst)), _ => Err(ExtensionError::SourceDist), } } @@ -104,20 +121,31 @@ impl SourceDistExtension { /// Return the name for the extension. pub(crate) fn name(self) -> &'static str { match self { - Self::Tar => "tar", - Self::TarBz2 => "tar.bz2", Self::TarGz => "tar.gz", - Self::TarLz => "tar.lz", - Self::TarLzma => "tar.lzma", - Self::TarXz => "tar.xz", - Self::TarZst => "tar.zst", - Self::Tbz => "tbz", - Self::Tgz => "tgz", - Self::Tlz => "tlz", - Self::Txz => "txz", - Self::Zip => "zip", + Self::Legacy(LegacySourceDistExtension::Tar) => "tar", + Self::Legacy(LegacySourceDistExtension::TarBz2) => "tar.bz2", + Self::Legacy(LegacySourceDistExtension::TarLz) => "tar.lz", + Self::Legacy(LegacySourceDistExtension::TarLzma) => "tar.lzma", + Self::Legacy(LegacySourceDistExtension::TarXz) => "tar.xz", + Self::Legacy(LegacySourceDistExtension::TarZst) => "tar.zst", + Self::Legacy(LegacySourceDistExtension::Tbz) => "tbz", + Self::Legacy(LegacySourceDistExtension::Tgz) => "tgz", + Self::Legacy(LegacySourceDistExtension::Tlz) => "tlz", + Self::Legacy(LegacySourceDistExtension::Txz) => "txz", + Self::Legacy(LegacySourceDistExtension::Zip) => "zip", } } + + /// Returns `true` if the extension conforms to [PEP 625](https://peps.python.org/pep-0625/)'s + /// naming requirements. + /// + /// PEP 625 mandates `.tar.gz`; `.zip` is also accepted for backwards compatibility. + pub fn is_pep625_compliant(&self) -> bool { + matches!( + self, + Self::TarGz | Self::Legacy(LegacySourceDistExtension::Zip) + ) + } } impl Display for SourceDistExtension { diff --git a/crates/uv-distribution-filename/src/lib.rs b/crates/uv-distribution-filename/src/lib.rs index bb83a50aa3bce..8fa91b804e85d 100644 --- a/crates/uv-distribution-filename/src/lib.rs +++ b/crates/uv-distribution-filename/src/lib.rs @@ -8,7 +8,9 @@ use uv_pep440::Version; pub use build_tag::{BuildTag, BuildTagError}; pub use egg::{EggInfoFilename, EggInfoFilenameError}; pub use expanded_tags::{ExpandedTagError, ExpandedTags}; -pub use extension::{DistExtension, ExtensionError, SourceDistExtension}; +pub use extension::{ + DistExtension, ExtensionError, LegacySourceDistExtension, SourceDistExtension, +}; pub use source_dist::{SourceDistFilename, SourceDistFilenameError}; pub use wheel::{WheelFilename, WheelFilenameError}; diff --git a/crates/uv-distribution-filename/src/source_dist.rs b/crates/uv-distribution-filename/src/source_dist.rs index e58228bf88b48..69252fd6ae568 100644 --- a/crates/uv-distribution-filename/src/source_dist.rs +++ b/crates/uv-distribution-filename/src/source_dist.rs @@ -188,7 +188,7 @@ mod tests { use uv_normalize::PackageName; - use crate::{SourceDistExtension, SourceDistFilename}; + use crate::{SourceDistExtension, SourceDistFilename, extension::LegacySourceDistExtension}; /// Only test already normalized names since the parsing is lossy /// @@ -239,8 +239,14 @@ mod tests { fn malformed_non_ascii() { let package_name = PackageName::from_str("a").unwrap(); for (filename, extension) in [ - ("é-1.2.3.zip", SourceDistExtension::Zip), - ("aé1.2.3.zip", SourceDistExtension::Zip), + ( + "é-1.2.3.zip", + SourceDistExtension::Legacy(LegacySourceDistExtension::Zip), + ), + ( + "aé1.2.3.zip", + SourceDistExtension::Legacy(LegacySourceDistExtension::Zip), + ), ("é-1.zip", SourceDistExtension::TarGz), ] { assert!(SourceDistFilename::parse(filename, extension, &package_name).is_err()); @@ -257,9 +263,13 @@ mod tests { "Foo.Bar-1.2.3.zip", ] { assert_eq!( - SourceDistFilename::parse(filename, SourceDistExtension::Zip, &package_name) - .unwrap() - .name, + SourceDistFilename::parse( + filename, + SourceDistExtension::Legacy(LegacySourceDistExtension::Zip), + &package_name + ) + .unwrap() + .name, package_name ); } @@ -270,7 +280,7 @@ mod tests { assert!( SourceDistFilename::parse( "foo.zip", - SourceDistExtension::Zip, + SourceDistExtension::Legacy(LegacySourceDistExtension::Zip), &PackageName::from_str("foo-lib").unwrap() ) .is_err() diff --git a/crates/uv-distribution-types/src/error.rs b/crates/uv-distribution-types/src/error.rs index f78ec47386037..b18d27361a178 100644 --- a/crates/uv-distribution-types/src/error.rs +++ b/crates/uv-distribution-types/src/error.rs @@ -24,4 +24,9 @@ pub enum Error { #[error("Requested package name `{0}` does not match `{1}` in the distribution filename: {2}")] PackageNameMismatch(PackageName, PackageName, String), + + #[error( + "Source distribution `{0}` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted" + )] + NotPep625Filename(String), } diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs index 0a749d65c8a53..3f3e2888957e4 100644 --- a/crates/uv-distribution-types/src/lib.rs +++ b/crates/uv-distribution-types/src/lib.rs @@ -233,6 +233,8 @@ pub struct RegistryBuiltWheel { pub filename: WheelFilename, pub file: Box, pub index: IndexUrl, + /// Whether the recorded size must be validated when the wheel is downloaded. + pub size_is_authoritative: bool, } /// A built distribution (wheel) that exists in a registry, like `PyPI`. @@ -275,6 +277,8 @@ pub struct DirectUrlBuiltDist { pub location: Box, /// The URL as it was provided by the user. pub url: VerbatimUrl, + /// The expected size of the archive, if provided by a lockfile. + pub size: Option, } /// A built distribution (wheel) that exists in a local directory. @@ -316,6 +320,8 @@ pub struct RegistrySourceDist { /// skip emitting wheels to the lockfile just because the host generating /// the lockfile didn't have any compatible wheels available. pub wheels: Vec, + /// Whether the recorded size must be validated when the source distribution is downloaded. + pub size_is_authoritative: bool, } /// A source distribution that exists at an arbitrary URL. @@ -332,6 +338,8 @@ pub struct DirectUrlSourceDist { pub ext: SourceDistExtension, /// The URL as it was provided by the user, including the subdirectory fragment. pub url: VerbatimUrl, + /// The expected size of the archive, if provided by a lockfile. + pub size: Option, } /// A source distribution that exists at the root or in a subdirectory of a Git repository. @@ -414,15 +422,20 @@ impl Dist { filename, location: Box::new(location), url, + size: None, }))) } DistExtension::Source(ext) => { + if !ext.is_pep625_compliant() { + return Err(Error::NotPep625Filename(url.verbatim().to_string())); + } Ok(Self::Source(SourceDist::DirectUrl(DirectUrlSourceDist { name, location: Box::new(location), subdirectory, ext, url, + size: None, }))) } } @@ -469,6 +482,10 @@ impl Dist { }))) } DistExtension::Source(ext) => { + if !ext.is_pep625_compliant() { + return Err(Error::NotPep625Filename(url.verbatim().to_string())); + } + // If there is a version in the filename, record it. let version = url .filename() @@ -712,17 +729,6 @@ impl BuiltDist { } impl SourceDist { - /// Returns the [`SourceDistExtension`] of the distribution, if it has one. - pub fn extension(&self) -> Option { - match self { - Self::Registry(source_dist) => Some(source_dist.ext), - Self::DirectUrl(source_dist) => Some(source_dist.ext), - Self::GitPath(source_dist) => Some(source_dist.ext), - Self::Path(source_dist) => Some(source_dist.ext), - Self::GitDirectory(_) | Self::Directory(_) => None, - } - } - /// Returns the [`IndexUrl`], if the distribution is from a registry. fn index(&self) -> Option<&IndexUrl> { match self { @@ -1260,7 +1266,7 @@ impl RemoteSource for DirectUrlBuiltDist { } fn size(&self) -> Option { - self.url.size() + self.size } } @@ -1270,7 +1276,7 @@ impl RemoteSource for DirectUrlSourceDist { } fn size(&self) -> Option { - self.url.size() + self.size } } diff --git a/crates/uv-distribution-types/src/prioritized_distribution.rs b/crates/uv-distribution-types/src/prioritized_distribution.rs index ac9fcdf10439b..1ff67e49529ff 100644 --- a/crates/uv-distribution-types/src/prioritized_distribution.rs +++ b/crates/uv-distribution-types/src/prioritized_distribution.rs @@ -144,6 +144,7 @@ impl IncompatibleDist { IncompatibleSource::RequiresPython(..) => { format!("requires {self}") } + IncompatibleSource::NotPep625Filename => format!("has {self}"), }, Self::Unavailable => format!("has {self}"), } @@ -172,6 +173,7 @@ impl IncompatibleDist { IncompatibleSource::RequiresPython(..) => { format!("require {self}") } + IncompatibleSource::NotPep625Filename => format!("have {self}"), }, Self::Unavailable => format!("have {self}"), } @@ -277,6 +279,9 @@ impl Display for IncompatibleDist { IncompatibleSource::RequiresPython(python, _) => { write!(f, "Python {python}") } + IncompatibleSource::NotPep625Filename => { + f.write_str("a non-PEP 625-compliant source distribution filename") + } }, Self::Unavailable => f.write_str("no available distributions"), } @@ -327,6 +332,8 @@ pub enum IncompatibleSource { RequiresPython(VersionSpecifiers, PythonRequirementKind), Yanked(Yanked), NoBuild, + /// The source distribution's filename does not confirm to PEP 625. + NotPep625Filename, } #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -737,21 +744,26 @@ impl IncompatibleSource { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self, - Self::NoBuild | Self::RequiresPython(_, _) | Self::Yanked(_) => true, + Self::NoBuild + | Self::RequiresPython(_, _) + | Self::Yanked(_) + | Self::NotPep625Filename => true, }, Self::RequiresPython(_, _) => match other { Self::ExcludeNewer(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_, _) => false, - Self::NoBuild | Self::Yanked(_) => true, + Self::NoBuild | Self::Yanked(_) | Self::NotPep625Filename => true, }, Self::Yanked(_) => match other { - Self::ExcludeNewer(_) | Self::RequiresPython(_, _) => false, + Self::ExcludeNewer(_) | Self::RequiresPython(_, _) | Self::NotPep625Filename => { + false + } // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), Self::NoBuild => true, }, - Self::NoBuild => false, + Self::NoBuild | Self::NotPep625Filename => false, } } } diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index a857103e60fe9..83e677163c339 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -40,7 +40,6 @@ uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-redacted = { workspace = true } uv-types = { workspace = true } -uv-warnings = { workspace = true } uv-workspace = { workspace = true } anyhow = { workspace = true } @@ -68,4 +67,3 @@ insta = { workspace = true } [features] default = [] -static = ["uv-extract/static"] diff --git a/crates/uv-distribution/src/archive.rs b/crates/uv-distribution/src/archive.rs index 5e2034087fbb2..799044f9a5051 100644 --- a/crates/uv-distribution/src/archive.rs +++ b/crates/uv-distribution/src/archive.rs @@ -14,16 +14,25 @@ pub struct Archive { pub filename: WheelFilename, /// The version of the archive bucket. pub version: u8, + /// The size of the downloaded archive. + #[serde(default)] + pub size: Option, } impl Archive { /// Create a new [`Archive`] with the given ID and hashes. - pub(crate) fn new(id: ArchiveId, hashes: HashDigests, filename: WheelFilename) -> Self { + pub(crate) fn new( + id: ArchiveId, + hashes: HashDigests, + filename: WheelFilename, + size: Option, + ) -> Self { Self { id, hashes, filename, version: ARCHIVE_VERSION, + size, } } @@ -38,3 +47,33 @@ impl Hashed for Archive { self.hashes.as_slice() } } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn deserialize_legacy_archive() { + #[derive(serde::Serialize)] + struct LegacyArchive { + id: ArchiveId, + hashes: HashDigests, + filename: WheelFilename, + version: u8, + } + + let legacy = LegacyArchive { + id: ArchiveId::default(), + hashes: HashDigests::empty(), + filename: WheelFilename::from_str("iniconfig-2.0.0-py3-none-any.whl") + .expect("valid wheel filename"), + version: ARCHIVE_VERSION, + }; + let bytes = rmp_serde::to_vec(&legacy).expect("serialize legacy archive"); + let archive: Archive = rmp_serde::from_slice(&bytes).expect("deserialize legacy archive"); + + assert_eq!(archive.size, None); + } +} diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 96fe70c780b50..937061acb4d17 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -17,7 +17,7 @@ use uv_cache_info::{CacheInfo, Timestamp}; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; -use uv_distribution_filename::{SourceDistExtension, WheelFilename}; +use uv_distribution_filename::WheelFilename; use uv_distribution_types::{ BuildInfo, BuildableSource, BuiltDist, Dist, DistRef, File, HashPolicy, Hashed, IndexUrl, InstalledDist, Name, SourceDist, ToUrlError, @@ -31,7 +31,6 @@ use uv_pypi_types::{HashDigest, HashDigests, PyProjectToml}; use uv_python::PythonVariant; use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildStack}; -use uv_warnings::warn_user_once; use crate::archive::Archive; use crate::error::PythonVersion; @@ -299,7 +298,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { None, &wheel.filename, WheelExtension::Whl, - None, + wheel.size, &wheel_entry, dist, hashes, @@ -337,7 +336,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { None, &wheel.filename, WheelExtension::Whl, - None, + wheel.size, &wheel_entry, dist, hashes, @@ -437,34 +436,6 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { - // Warn if the source distribution isn't PEP 625 compliant. - // We do this here instead of in `SourceDistExtension::from_path` to minimize log volume: - // a non-compliant distribution isn't a huge problem if it's not actually being - // materialized into a wheel. Observe that we also allow no extension, since we expect that - // for directory and Git installs. - // NOTE: Observe that we also allow `.zip` sdists here, which are not PEP 625 compliant. - // This is because they were allowed on PyPI until relatively recently (2020). - if let Some(extension) = dist.extension() - && !matches!( - extension, - SourceDistExtension::TarGz | SourceDistExtension::Zip - ) - { - if matches!(dist, SourceDist::Registry(_)) { - // Observe that we display a slightly different warning when the sdist comes - // from a registry, since that suggests that the user has inadvertently - // (rather than explicitly) depended on a non-compliant sdist. - warn_user_once!( - "{dist} uses a legacy source distribution format ('.{extension}') that is not compliant with PEP 625. A future version of uv will reject this source distribution. Consider upgrading to a newer version of {package}", - package = dist.name(), - ); - } else { - warn_user_once!( - "{dist} is not a standards-compliant source distribution: expected '.tar.gz' but found '.{extension}'. A future version of uv will reject source distributions that do not meet the requirements specified in PEP 625", - ); - } - } - let built_wheel = self .builder .download_and_build(&BuildableSource::Dist(dist), tags, hashes, &self.client) @@ -704,6 +675,12 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { + let expected_size = match dist { + BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => size, + BuiltDist::DirectUrl(_) => size, + _ => None, + }; + // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { @@ -714,16 +691,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.cache_key())); - let query_url = &url.clone(); - let download = |response: reqwest::Response| { async { - let size = size.or_else(|| content_length(&response)); + let progress_size = size.or_else(|| content_length(&response)); - let progress = self - .reporter - .as_ref() - .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); + let progress = self.reporter.as_ref().map(|reporter| { + ( + reporter, + reporter.on_download_start(dist.name(), progress_size), + ) + }); let reader = response .bytes_stream() @@ -744,7 +721,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { let mut reader = ProgressReader::new(&mut hasher, progress, &**reporter); match extension { WheelExtension::Whl => { - uv_extract::stream::unzip(query_url, &mut reader, temp_dir.path()) + uv_extract::stream::unzip(&mut reader, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))? } @@ -757,7 +734,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } None => match extension { WheelExtension::Whl => { - uv_extract::stream::unzip(query_url, &mut hasher, temp_dir.path()) + uv_extract::stream::unzip(&mut hasher, temp_dir.path()) .await .map_err(|err| Error::Extract(filename.to_string(), err))? } @@ -770,6 +747,16 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { }; // Exhaust the reader to compute the hashes. hasher.finish().await.map_err(Error::HashExhaustion)?; + let actual_size = hasher.bytes_read(); + if let Some(expected) = expected_size + && actual_size != expected + { + return Err(Error::MismatchedSize { + distribution: dist.to_string(), + expected, + actual: actual_size, + }); + } // Before we make the wheel accessible by persisting it, ensure that the RECORD is // valid. @@ -792,6 +779,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { id, hashers.into_iter().map(HashDigest::from).collect(), filename.clone(), + Some(actual_size), )) } .instrument(info_span!("wheel", wheel = %dist)) @@ -836,10 +824,21 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { CachedClientError::Client(err) => Error::Client(err), })?; - // If the archive is missing the required hashes, or has since been removed, force a refresh. + if let (Some(expected), Some(actual)) = (expected_size, archive.size) + && expected != actual + { + return Err(Error::MismatchedSize { + distribution: dist.to_string(), + expected, + actual, + }); + } + + // If the archive is missing the required hashes or size, or has since been removed, force a refresh. let archive = Some(archive) .filter(|archive| archive.has_digests(hashes)) - .filter(|archive| archive.exists(self.build_context.cache())); + .filter(|archive| archive.exists(self.build_context.cache())) + .filter(|archive| expected_size.is_none() || archive.size.is_some()); let archive = if let Some(archive) = archive { archive @@ -878,6 +877,12 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { + let expected_size = match dist { + BuiltDist::Registry(dist) if dist.best_wheel().size_is_authoritative => size, + BuiltDist::DirectUrl(_) => size, + _ => None, + }; + // Acquire an advisory lock, to guard against concurrent writes. #[cfg(windows)] let _lock = { @@ -890,12 +895,14 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { let download = |response: reqwest::Response| { async { - let size = size.or_else(|| content_length(&response)); + let progress_size = size.or_else(|| content_length(&response)); - let progress = self - .reporter - .as_ref() - .map(|reporter| (reporter, reporter.on_download_start(dist.name(), size))); + let progress = self.reporter.as_ref().map(|reporter| { + ( + reporter, + reporter.on_download_start(dist.name(), progress_size), + ) + }); let reader = response .bytes_stream() @@ -930,6 +937,18 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { } } + if let Some(expected) = expected_size + && hasher.bytes_read() != expected + { + return Err(Error::MismatchedSize { + distribution: dist.to_string(), + expected, + actual: hasher.bytes_read(), + }); + } + + let actual_size = hasher.bytes_read(); + // Unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; @@ -967,7 +986,12 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { reporter.on_download_complete(dist.name(), progress); } - Ok(Archive::new(id, hashes, filename.clone())) + Ok(Archive::new( + id, + hashes, + filename.clone(), + Some(actual_size), + )) } .instrument(info_span!("wheel", wheel = %dist)) }; @@ -1011,10 +1035,21 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { CachedClientError::Client(err) => Error::Client(err), })?; - // If the archive is missing the required hashes, or has since been removed, force a refresh. + if let (Some(expected), Some(actual)) = (expected_size, archive.size) + && expected != actual + { + return Err(Error::MismatchedSize { + distribution: dist.to_string(), + expected, + actual, + }); + } + + // If the archive is missing the required hashes or size, or has since been removed, force a refresh. let archive = Some(archive) .filter(|archive| archive.has_digests(hashes)) - .filter(|archive| archive.exists(self.build_context.cache())); + .filter(|archive| archive.exists(self.build_context.cache())) + .filter(|archive| expected_size.is_none() || archive.size.is_some()); let archive = if let Some(archive) = archive { archive @@ -1091,6 +1126,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .await?, HashDigests::empty(), filename.clone(), + None, ); // Write the archive pointer to the cache. @@ -1127,11 +1163,9 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { // Unzip the wheel to a temporary directory. let files = match extension { - WheelExtension::Whl => { - uv_extract::stream::unzip(path.display(), &mut hasher, temp_dir.path()) - .await - .map_err(|err| Error::Extract(filename.to_string(), err))? - } + WheelExtension::Whl => uv_extract::stream::unzip(&mut hasher, temp_dir.path()) + .await + .map_err(|err| Error::Extract(filename.to_string(), err))?, WheelExtension::WhlZst => { uv_extract::stream::untar_zst(&mut hasher, temp_dir.path()) .await @@ -1158,7 +1192,7 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { .map_err(Error::CacheWrite)?; // Create an archive. - let archive = Archive::new(id, hashes, filename.clone()); + let archive = Archive::new(id, hashes, filename.clone(), None); // Write the archive pointer to the cache. let pointer = PathArchivePointer { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index da5aae7bb7032..cc4008f5368dc 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -173,6 +173,15 @@ pub enum Error { actual: String, }, + #[error( + "Size mismatch for `{distribution}`: expected {expected} bytes, but downloaded {actual} bytes" + )] + MismatchedSize { + distribution: String, + expected: u64, + actual: u64, + }, + #[error( "Hash-checking is enabled, but no hashes were provided or computed for: `{distribution}`" )] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 59e78a7c3e52f..ae65795d8579e 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -33,7 +33,7 @@ use uv_distribution_filename::{SourceDistExtension, WheelFilename}; use uv_distribution_types::{ BuildInfo, BuildVariables, BuildableSource, ConfigSettings, DirectorySourceUrl, ExtraBuildRequirement, GitDirectorySourceUrl, GitPathSourceUrl, HashPolicy, Hashed, IndexUrl, - PathSourceUrl, RequirementSource, RequiresPython, SourceDist, SourceUrl, + PathSourceUrl, RemoteSource, RequirementSource, RequiresPython, SourceDist, SourceUrl, }; use uv_extract::hash::Hasher; use uv_fs::{Simplified, rename_with_retry, write_atomic}; @@ -974,8 +974,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; let download = |response| { - let query_url = url.clone(); - async { // At this point, we're seeing a new or updated source distribution. Initialize a // new revision, to collect the source and built artifacts. @@ -985,11 +983,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Downloading source distribution: {source}"); let entry = cache_shard.shard(revision.id()).entry(SOURCE); let algorithms = http_hash_algorithms(hashes); - let hashes = self - .download_archive(query_url, response, source, ext, entry.path(), &algorithms) + let (hashes, size) = self + .download_archive(response, source, ext, entry.path(), &algorithms) .await?; - Ok(revision.with_hashes(HashDigests::from(hashes))) + Ok(revision + .with_hashes(HashDigests::from(hashes)) + .with_size(size)) } .boxed_local() .instrument(info_span!("download", source_dist = %source)) @@ -1010,8 +1010,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { CachedClientError::Client(err) => Error::Client(err), })?; - // If the archive is missing the required hashes, force a refresh. - if revision.has_digests(hashes) { + let expected_size = match source { + BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => { + dist.size() + } + BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(), + _ => None, + }; + if let (Some(expected), Some(actual)) = (expected_size, revision.size()) + && expected != actual + { + return Err(Error::MismatchedSize { + distribution: source.to_string(), + expected, + actual, + }); + } + + // If the archive is missing the required hashes or size, force a refresh. + if revision.has_digests(hashes) && (expected_size.is_none() || revision.size().is_some()) { Ok(revision) } else { client @@ -2749,8 +2766,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; let download = |response| { - let query_url = url.clone(); - async { // Take the union of the requested and existing hash algorithms. let algorithms = { @@ -2763,15 +2778,18 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { algorithms }; - let hashes = self - .download_archive(query_url, response, source, ext, entry.path(), &algorithms) + let (hashes, size) = self + .download_archive(response, source, ext, entry.path(), &algorithms) .await?; for existing in revision.hashes() { if !hashes.contains(existing) { return Err(Error::CacheHeal(source.to_string(), existing.algorithm())); } } - Ok(revision.clone().with_hashes(HashDigests::from(hashes))) + Ok(revision + .clone() + .with_hashes(HashDigests::from(hashes)) + .with_size(size)) } .boxed_local() .instrument(info_span!("download", source_dist = %source)) @@ -2798,13 +2816,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { /// Download and unzip a source distribution into the cache from an HTTP response. async fn download_archive( &self, - query_url: DisplaySafeUrl, response: Response, source: &BuildableSource<'_>, ext: SourceDistExtension, target: &Path, algorithms: &[HashAlgorithm], - ) -> Result, Error> { + ) -> Result<(Vec, u64), Error> { let temp_dir = tempfile::tempdir_in( self.build_context .cache() @@ -2827,16 +2844,34 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Download and unzip the source distribution into a temporary directory. let span = info_span!("download_source_dist", source_dist = %source); - uv_extract::stream::archive(query_url, &mut hasher, ext, temp_dir.path()) + uv_extract::stream::archive(&mut hasher, ext, temp_dir.path()) .await .map_err(|err| Error::Extract(source.to_string(), err))?; drop(span); - // If necessary, exhaust the reader to compute the hash. - if !algorithms.is_empty() { + let expected_size = match source { + BuildableSource::Dist(SourceDist::Registry(dist)) if dist.size_is_authoritative => { + dist.size() + } + BuildableSource::Dist(SourceDist::DirectUrl(dist)) => dist.size(), + _ => None, + }; + + // If necessary, exhaust the reader to compute the hash or validate the archive size. + if !algorithms.is_empty() || expected_size.is_some() { hasher.finish().await.map_err(Error::HashExhaustion)?; } + if let Some(expected) = expected_size + && hasher.bytes_read() != expected + { + return Err(Error::MismatchedSize { + distribution: source.to_string(), + expected, + actual: hasher.bytes_read(), + }); + } + let size = hasher.bytes_read(); let hashes = hashers.into_iter().map(HashDigest::from).collect(); // Extract the top-level directory. @@ -2864,7 +2899,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } - Ok(hashes) + Ok((hashes, size)) } /// Extract a local archive, and store it at the given [`CacheEntry`]. @@ -2896,7 +2931,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers); // Unzip the archive into a temporary directory. - uv_extract::stream::archive(path.display(), &mut hasher, ext, &temp_dir.path()) + uv_extract::stream::archive(&mut hasher, ext, &temp_dir.path()) .await .map_err(|err| Error::Extract(temp_dir.path().to_string_lossy().into_owned(), err))?; diff --git a/crates/uv-distribution/src/source/revision.rs b/crates/uv-distribution/src/source/revision.rs index 34958ed4c9a04..8bc257e353dcf 100644 --- a/crates/uv-distribution/src/source/revision.rs +++ b/crates/uv-distribution/src/source/revision.rs @@ -14,6 +14,8 @@ use uv_pypi_types::{HashDigest, HashDigests}; pub(crate) struct Revision { id: RevisionId, hashes: HashDigests, + #[serde(default)] + size: Option, } impl Revision { @@ -22,6 +24,7 @@ impl Revision { Self { id: RevisionId::new(), hashes: HashDigests::empty(), + size: None, } } @@ -40,12 +43,24 @@ impl Revision { self.hashes } + /// Return the size of the downloaded archive. + pub(crate) fn size(&self) -> Option { + self.size + } + /// Set the computed hashes of the archive. #[must_use] pub(crate) fn with_hashes(mut self, hashes: HashDigests) -> Self { self.hashes = hashes; self } + + /// Set the size of the downloaded archive. + #[must_use] + pub(crate) fn with_size(mut self, size: u64) -> Self { + self.size = Some(size); + self + } } impl Hashed for Revision { @@ -92,9 +107,15 @@ mod tests { /// Regression test for . #[test] fn deserialize_legacy_nanoid_revision() { + #[derive(Serialize)] + struct LegacyRevision { + id: RevisionId, + hashes: HashDigests, + } + // A representative 21-character nanoid ID, drawn from the same alphabet // used by both the old `nanoid` crate and `uv_fastid`. - let legacy = Revision { + let legacy = LegacyRevision { id: RevisionId("HM0NxJml5hc7UjbfTWT1r".to_string()), hashes: HashDigests::empty(), }; diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index 1d781bb2c7733..c736be9a8f35f 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -20,10 +20,9 @@ uv-configuration = { workspace = true } uv-distribution-filename = { workspace = true } uv-pypi-types = { workspace = true } uv-static = { workspace = true } -uv-warnings = { workspace = true } astral-tokio-tar = { workspace = true } -async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] } +async-compression = { workspace = true, features = ["gzip", "zstd"] } async_zip = { workspace = true } blake2 = { workspace = true } flate2 = { workspace = true } @@ -39,15 +38,12 @@ thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } -xz2 = { workspace = true } [features] default = [] -# Avoid a liblzma.so dependency -static = ["xz2/static"] [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 # selected after removing the synchronous `zip` dependency. -ignored = ["flate2", "xz2"] +ignored = ["flate2"] diff --git a/crates/uv-extract/src/error.rs b/crates/uv-extract/src/error.rs index dc1f1d504cb50..88997d103060a 100644 --- a/crates/uv-extract/src/error.rs +++ b/crates/uv-extract/src/error.rs @@ -93,17 +93,23 @@ pub enum Error { EmptyFilename, #[error("Archive contains unacceptable filename: {filename}")] UnacceptableFilename { filename: String }, + #[error( + "Archive contains a file with an unsupported compression method; files must be compressed with 'stored', 'DEFLATE', or 'zstd'" + )] + UnsupportedCompression, } impl From for Error { fn from(err: async_zip::error::ZipError) -> Self { - let async_zip::error::ZipError::FileNameContainsNul { filename } = err else { - return Self::AsyncZip(err); - }; - - let filename = String::from_utf8_lossy(&filename); - validate_archive_member_name(&filename) - .expect_err("a filename containing an embedded NUL must be rejected") + match err { + async_zip::error::ZipError::CompressionNotSupported(_) => Self::UnsupportedCompression, + async_zip::error::ZipError::FileNameContainsNul { filename } => { + let filename = String::from_utf8_lossy(&filename); + validate_archive_member_name(&filename) + .expect_err("a filename containing an embedded NUL must be rejected") + } + error => Self::AsyncZip(error), + } } } @@ -122,7 +128,7 @@ impl Error { Err(err) => err, }; let err = match err.downcast::() { - Ok(zip_err) => return Self::AsyncZip(zip_err), + Ok(zip_err) => return Self::from(zip_err), Err(err) => err, }; Self::Io(err) diff --git a/crates/uv-extract/src/hash.rs b/crates/uv-extract/src/hash.rs index 6e0162ee6944e..01d3015b49c7a 100644 --- a/crates/uv-extract/src/hash.rs +++ b/crates/uv-extract/src/hash.rs @@ -69,6 +69,7 @@ impl From for HashDigest { pub struct HashReader<'a, R> { reader: R, hashers: &'a mut [Hasher], + bytes_read: u64, } impl<'a, R> HashReader<'a, R> @@ -76,7 +77,16 @@ where R: tokio::io::AsyncRead + Unpin, { pub fn new(reader: R, hashers: &'a mut [Hasher]) -> Self { - HashReader { reader, hashers } + HashReader { + reader, + hashers, + bytes_read: 0, + } + } + + /// Return the number of bytes read from the underlying reader. + pub fn bytes_read(&self) -> u64 { + self.bytes_read } /// Exhaust the underlying reader. @@ -97,10 +107,13 @@ where buf: &mut ReadBuf<'_>, ) -> Poll> { let reader = Pin::new(&mut self.reader); + let filled = buf.filled().len(); match reader.poll_read(cx, buf) { Poll::Ready(Ok(())) => { + let bytes = &buf.filled()[filled..]; + self.bytes_read += bytes.len() as u64; for hasher in self.hashers.iter_mut() { - hasher.update(buf.filled()); + hasher.update(bytes); } Poll::Ready(Ok(())) } diff --git a/crates/uv-extract/src/lib.rs b/crates/uv-extract/src/lib.rs index 6fd5deacdf887..c6a20b3014015 100644 --- a/crates/uv-extract/src/lib.rs +++ b/crates/uv-extract/src/lib.rs @@ -1,5 +1,3 @@ -use std::fmt::Display; - pub use error::Error; use regex::regex; pub use sync::*; @@ -13,50 +11,6 @@ mod vendor; static REPLACEMENT_CHARACTER: &str = "\u{FFFD}"; -/// Compression methods that we consider supported. -/// -/// Our underlying ZIP dependencies may support more. -pub(crate) enum CompressionMethod { - Stored, - Deflated, - Zstd, - // NOTE: This will become `Unsupported(...)` in the future. - Deprecated(&'static str), -} - -impl CompressionMethod { - /// Returns `true` if this is a well-known compression method that we - /// expect other ZIP implementations to support. - fn is_well_known(&self) -> bool { - matches!(self, Self::Stored | Self::Deflated | Self::Zstd) - } -} - -impl Display for CompressionMethod { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Stored => write!(f, "stored"), - Self::Deflated => write!(f, "DEFLATE"), - Self::Zstd => write!(f, "zstd"), - Self::Deprecated(name) => write!(f, "{name}"), - } - } -} - -impl From for CompressionMethod { - fn from(value: async_zip::Compression) -> Self { - match value { - async_zip::Compression::Stored => Self::Stored, - async_zip::Compression::Deflate => Self::Deflated, - async_zip::Compression::Zstd => Self::Zstd, - async_zip::Compression::Bz => Self::Deprecated("bzip2"), - async_zip::Compression::Lzma => Self::Deprecated("lzma"), - async_zip::Compression::Xz => Self::Deprecated("xz"), - _ => Self::Deprecated("unknown"), - } - } -} - /// Validate that a given filename (e.g. reported by a ZIP archive's /// local file entries or central directory entries) is "safe" to use. /// diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 368ae07c8b3cb..3d332c961c0aa 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -1,4 +1,3 @@ -use std::fmt::Display; use std::path::{Component, Path, PathBuf}; use std::pin::Pin; @@ -9,10 +8,9 @@ use rustc_hash::{FxHashMap, FxHashSet}; use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tracing::{debug, warn}; -use uv_distribution_filename::SourceDistExtension; -use uv_warnings::warn_user_once; +use uv_distribution_filename::{LegacySourceDistExtension, SourceDistExtension}; -use crate::{CompressionMethod, Error, insecure_no_validate, validate_archive_member_name}; +use crate::{Error, insecure_no_validate, validate_archive_member_name}; const DEFAULT_BUF_SIZE: usize = 128 * 1024; @@ -66,12 +64,8 @@ struct ComputedEntry { /// is already fully on disk, consider using `unzip_archive`, which can use multiple /// threads to work faster in that case. /// -/// `source_hint` is used for warning messages, to identify the source of the ZIP archive -/// beneath the reader. It might be a URL, a file path, or something else. -/// /// Returns the list of unpacked files and their sizes. -pub async fn unzip( - source_hint: D, +pub async fn unzip( reader: R, target: impl AsRef, ) -> Result, Error> { @@ -90,18 +84,6 @@ pub async fn unzip( while let Some(mut entry) = zip.next_with_entry().await? { let zip_entry = entry.reader().entry(); - // Check for unexpected compression methods. - // A future version of uv will reject instead of warning about these. - let compression = CompressionMethod::from(zip_entry.compression()); - if !compression.is_well_known() { - warn_user_once!( - "One or more file entries in '{source_hint}' use the '{compression}' compression method, which is not widely supported. A future version of uv will reject ZIP archives containing entries compressed with this method. Entries must be compressed with the '{stored}', '{deflate}', or '{zstd}' compression methods.", - stored = CompressionMethod::Stored, - deflate = CompressionMethod::Deflated, - zstd = CompressionMethod::Zstd, - ); - } - // Construct the (expected) path to the file on-disk. let path = match zip_entry.filename().as_str() { Ok(path) => path, @@ -684,30 +666,6 @@ async fn untar_gz( .map_err(Error::io_or_compression) } -/// Unpack a `.tar.bz2` archive into the target directory, without requiring `Seek`. -/// -/// This is useful for unpacking files as they're being downloaded. -/// -/// Returns the list of unpacked files and their sizes. -async fn untar_bz2( - reader: R, - target: impl AsRef, -) -> Result, Error> { - let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); - let mut decompressed_bytes = async_compression::tokio::bufread::BzDecoder::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) -} - /// Unpack a `.tar.zst` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. @@ -732,30 +690,6 @@ pub async fn untar_zst( .map_err(Error::io_or_compression) } -/// Unpack a `.tar.xz` archive into the target directory, without requiring `Seek`. -/// -/// This is useful for unpacking files as they're being downloaded. -/// -/// Returns the list of unpacked files and their sizes. -async fn untar_xz( - reader: R, - target: impl AsRef, -) -> Result, Error> { - let reader = tokio::io::BufReader::with_capacity(DEFAULT_BUF_SIZE, reader); - let mut decompressed_bytes = async_compression::tokio::bufread::XzDecoder::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) -} - /// Unpack a `.tar` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. @@ -778,29 +712,23 @@ async fn untar( .map_err(Error::io_or_compression) } -/// Unpack a `.zip`, `.tar.gz`, `.tar.bz2`, `.tar.zst`, or `.tar.xz` archive into the target directory, +/// Unpack a `.zip`, `.tar.gz`, or `.tar.zst` archive into the target directory, /// without requiring `Seek`. /// -/// `source_hint` is used for warning messages, to identify the source of the archive -/// beneath the reader. It might be a URL, a file path, or something else. -/// /// Returns the list of unpacked files and their sizes. -pub async fn archive( - source_hint: D, +pub async fn archive( reader: R, ext: SourceDistExtension, target: impl AsRef, ) -> Result, Error> { match ext { - SourceDistExtension::Zip => unzip(source_hint, reader, target).await, - SourceDistExtension::Tar => untar(reader, target).await, - SourceDistExtension::Tgz | SourceDistExtension::TarGz => untar_gz(reader, target).await, - SourceDistExtension::Tbz | SourceDistExtension::TarBz2 => untar_bz2(reader, target).await, - SourceDistExtension::Txz - | SourceDistExtension::TarXz - | SourceDistExtension::Tlz - | SourceDistExtension::TarLz - | SourceDistExtension::TarLzma => untar_xz(reader, target).await, - SourceDistExtension::TarZst => untar_zst(reader, target).await, + SourceDistExtension::Legacy(LegacySourceDistExtension::Zip) => unzip(reader, target).await, + SourceDistExtension::Legacy(LegacySourceDistExtension::Tar) => untar(reader, target).await, + SourceDistExtension::Legacy(LegacySourceDistExtension::Tgz) + | SourceDistExtension::TarGz => untar_gz(reader, target).await, + SourceDistExtension::Legacy(LegacySourceDistExtension::TarZst) => { + untar_zst(reader, target).await + } + SourceDistExtension::Legacy(_) => Err(Error::UnsupportedCompression), } } diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index 795db51a828d8..8edb01d482340 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; use crate::vendor::CloneableSeekableReader; -use crate::{CompressionMethod, Error, insecure_no_validate, validate_archive_member_name}; +use crate::{Error, insecure_no_validate, validate_archive_member_name}; use async_zip::base::read::seek::ZipFileReader; use async_zip::error::ZipError; use futures::executor::block_on; @@ -11,13 +11,12 @@ use rayon::prelude::*; use rustc_hash::FxHashSet; use tracing::warn; use uv_configuration::initialize_rayon_once; -use uv_warnings::warn_user_once; /// Unzip a `.zip` archive into the target directory. /// /// Returns the list of unpacked files and their sizes. pub fn unzip(reader: fs_err::File, target: &Path) -> Result, Error> { - let (reader, filename) = reader.into_parts(); + let (reader, _) = reader.into_parts(); // Parse the central directory once, then clone the archive reader per Rayon worker so // extraction stays parallel for already-downloaded wheels. @@ -43,17 +42,6 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result, Err(err) => return Err(err.into()), }; - let compression = CompressionMethod::from(entry.compression()); - if !compression.is_well_known() { - warn_user_once!( - "One or more file entries in '{filename}' use the '{compression}' compression method, which is not widely supported. A future version of uv will reject ZIP archives containing entries compressed with this method. Entries must be compressed with the '{stored}', '{deflate}', or '{zstd}' compression methods.", - filename = filename.display(), - stored = CompressionMethod::Stored, - deflate = CompressionMethod::Deflated, - zstd = CompressionMethod::Zstd, - ); - } - if let Err(e) = validate_archive_member_name(file_name) { if !skip_validation { return Err(e); diff --git a/crates/uv-metadata/src/lib.rs b/crates/uv-metadata/src/lib.rs index a94765322c0e3..0c651e1690ea6 100644 --- a/crates/uv-metadata/src/lib.rs +++ b/crates/uv-metadata/src/lib.rs @@ -40,12 +40,25 @@ pub enum Error { expected: u32, }, #[error("Failed to read from zip file")] - AsyncZip(#[from] async_zip::error::ZipError), + AsyncZip(#[source] async_zip::error::ZipError), + #[error( + "Archive contains a file with an unsupported compression method; files must be compressed with 'stored', 'DEFLATE', or 'zstd'" + )] + UnsupportedCompression, // No `#[from]` to enforce manual review of `io::Error` sources. #[error(transparent)] Io(io::Error), } +impl From for Error { + fn from(err: async_zip::error::ZipError) -> Self { + match err { + async_zip::error::ZipError::CompressionNotSupported(_) => Self::UnsupportedCompression, + o => Self::AsyncZip(o), + } + } +} + /// Find the `.dist-info` directory in a zipped wheel. /// /// Returns the dist info dir prefix without the `.dist-info` extension. diff --git a/crates/uv-pep508/src/lib.rs b/crates/uv-pep508/src/lib.rs index 17a536912adb5..a3b1ba5200ba2 100644 --- a/crates/uv-pep508/src/lib.rs +++ b/crates/uv-pep508/src/lib.rs @@ -545,7 +545,9 @@ fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool { /// Returns `true` if a file looks like an archive. /// /// See -/// for the list of supported archive extensions. +/// for the original list of supported archive extensions. +/// +/// Observe that we're currently much stricter here, as we no longer accept bz2 or lzma variants. fn looks_like_archive(file: impl AsRef) -> bool { let file = file.as_ref(); @@ -561,8 +563,7 @@ fn looks_like_archive(file: impl AsRef) -> bool { matches!( (pre_extension, extension), - (_, "whl" | "tbz" | "txz" | "tlz" | "zip" | "tgz" | "tar") - | (Some("tar"), "bz2" | "xz" | "lz" | "lzma" | "gz") + (_, "whl" | "zip" | "tgz" | "tar") | (Some("tar"), "gz") ) } diff --git a/crates/uv-pep508/src/unnamed.rs b/crates/uv-pep508/src/unnamed.rs index 5b894e2392f27..cba933815ef56 100644 --- a/crates/uv-pep508/src/unnamed.rs +++ b/crates/uv-pep508/src/unnamed.rs @@ -8,6 +8,7 @@ use uv_normalize::ExtraName; use crate::marker::parse; use crate::verbatim_url::strip_host; +use crate::verbatim_url::were_vars_expanded; use crate::{ Cursor, MarkerEnvironment, MarkerTree, Pep508Error, Pep508ErrorSource, Pep508Url, Reporter, RequirementOrigin, Scheme, TracingReporter, VerbatimUrl, VerbatimUrlError, expand_env_vars, @@ -32,6 +33,12 @@ pub trait UnnamedRequirementUrl: Pep508Url { #[must_use] fn with_given(self, given: impl AsRef) -> Self; + /// Set the "given value contained variables which were expanded" flag. + #[must_use] + fn with_expanded(self, _expanded: bool) -> Self { + self + } + /// Return the original string as given by the user, if available. fn given(&self) -> Option<&str>; } @@ -56,6 +63,10 @@ impl UnnamedRequirementUrl for VerbatimUrl { self.with_given(given) } + fn with_expanded(self, expanded: bool) -> Self { + self.with_expanded(expanded) + } + fn given(&self) -> Option<&str> { self.given() } @@ -234,6 +245,7 @@ fn preprocess_unnamed_url( // Expand environment variables in the URL. let expanded = expand_env_vars(url); + let vars_expanded = were_vars_expanded(url, expanded.as_ref()); if let Some((scheme, path)) = split_scheme(&expanded) { match Scheme::parse(scheme) { @@ -254,7 +266,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); return Ok((url, extras)); } @@ -265,7 +278,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); Ok((url, extras)) } // Ex) `https://download.pytorch.org/whl/torch_stable.html` @@ -278,7 +292,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); Ok((url, extras)) } @@ -292,7 +307,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); return Ok((url, extras)); } @@ -303,7 +319,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); Ok((url, extras)) } } @@ -317,7 +334,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); return Ok((url, extras)); } @@ -328,7 +346,8 @@ fn preprocess_unnamed_url( len, input: cursor.to_string(), })? - .with_given(url); + .with_given(url) + .with_expanded(vars_expanded); Ok((url, extras)) } } diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs index ada68d3ee1441..01710bbf9b4ef 100644 --- a/crates/uv-pep508/src/verbatim_url.rs +++ b/crates/uv-pep508/src/verbatim_url.rs @@ -285,9 +285,9 @@ impl VerbatimUrl { /// Set the "given value contained variables which were expanded" flag. /// - /// Intended to only be used by the [`Pep508Url`] impl. + /// Intended to only be used by the URL parser implementations. #[must_use] - fn with_expanded(self, expanded: bool) -> Self { + pub(crate) fn with_expanded(self, expanded: bool) -> Self { Self { expanded, ..self } } @@ -399,15 +399,7 @@ impl Pep508Url for VerbatimUrl { // Expand environment variables in the URL. let expanded = expand_env_vars(url); - // Since `expand_env_vars` can return `Cow::Owned` even when variables were not expanded, - // the check needs to fall back to comparison for that case. - // - // Note: If a variable named `FOO` expands to `${FOO}` then this will produce a false - // negative. This seems like too much of a corner case to justify trying to fix it. - let vars_expanded = match &expanded { - Cow::Owned(owned) => owned != url, - Cow::Borrowed(_) => false, - }; + let vars_expanded = were_vars_expanded(url, expanded.as_ref()); if let Some((scheme, path)) = split_scheme(&expanded) { match Scheme::parse(scheme) { @@ -545,6 +537,14 @@ pub fn expand_env_vars(s: &str) -> Cow<'_, str> { }) } +/// Returns `true` if [`expand_env_vars`] changed the given value. +/// +/// Note: If a variable named `FOO` expands to `${FOO}` then this will produce a false negative. +/// This seems like too much of a corner case to justify trying to fix it. +pub(crate) fn were_vars_expanded(given: &str, expanded: &str) -> bool { + expanded != given +} + /// Like [`Url::parse`], but only splits the scheme. Derived from the `url` crate. pub fn split_scheme(s: &str) -> Option<(&str, &str)> { /// diff --git a/crates/uv-pypi-types/src/parsed_url.rs b/crates/uv-pypi-types/src/parsed_url.rs index 88814a9b7bb16..f8b5b7e757eb2 100644 --- a/crates/uv-pypi-types/src/parsed_url.rs +++ b/crates/uv-pypi-types/src/parsed_url.rs @@ -158,6 +158,16 @@ impl UnnamedRequirementUrl for VerbatimParsedUrl { } } + fn with_expanded(self, expanded: bool) -> Self { + Self { + verbatim: ::with_expanded( + self.verbatim, + expanded, + ), + ..self + } + } + fn given(&self) -> Option<&str> { self.verbatim.given() } diff --git a/crates/uv-python/download-metadata.json b/crates/uv-python/download-metadata.json index b6770754d6881..b09f11303c2ab 100644 --- a/crates/uv-python/download-metadata.json +++ b/crates/uv-python/download-metadata.json @@ -87702,210 +87702,6 @@ "variant": null, "build": "7.3.23" }, - "pypy-3.11.13-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_arm64.tar.bz2", - "sha256": "84a48e09c97f57df62cc9f01b7a6d8c3e306b6270671d871aa8ab8c06945940d", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.13-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-macos_x86_64.tar.bz2", - "sha256": "bb3ae80cf5fca5044af2e42933e7692c7c5e76a828ce0eb6404a5d5da83b313c", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.13-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-aarch64.tar.bz2", - "sha256": "9347fe691a07fd9df17a1b186554fb9d9e6210178ffef19520a579ce1f9eb741", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.13-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-linux32.tar.bz2", - "sha256": "d08ce15dd61e9ace5e010b047104f0137110a258184e448ea8239472f10cf99b", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.13-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-linux64.tar.bz2", - "sha256": "1410db3a7ae47603e2b7cbfd7ff6390b891b2e041c9eb4f1599f333677bccb3e", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.13-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.20-win64.zip", - "sha256": "a8d36f6ceb1d9be6cf24a73b0ba103e7567e396b2f7a33426b05e4a06330755b", - "variant": null, - "build": "7.3.20" - }, - "pypy-3.11.11-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_arm64.tar.bz2", - "sha256": "7704e0d5302e53920d32dcfe9afeeb10436d4c94233e8830cf603aa955a861c1", - "variant": null, - "build": "7.3.19" - }, - "pypy-3.11.11-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-macos_x86_64.tar.bz2", - "sha256": "a2439f9d30dfdae96a5e9101c7dc54a8a68b56c9d7314ea399b0a25d3e87ebb2", - "variant": null, - "build": "7.3.19" - }, - "pypy-3.11.11-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-aarch64.tar.bz2", - "sha256": "13207dbf81ce24e96da760b1b863627b77bb20b1fb4c95191e02a0b72383df74", - "variant": null, - "build": "7.3.19" - }, - "pypy-3.11.11-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-linux32.tar.bz2", - "sha256": "5c6cdafd0a0abd14ca59926ed1b6aeb13b228c18b4b46de655aae48734c731ad", - "variant": null, - "build": "7.3.19" - }, - "pypy-3.11.11-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-linux64.tar.bz2", - "sha256": "9177d9e0bb91b05f921c642cb0ff71a0f3653b5d29a42d40d6a078c15b75720f", - "variant": null, - "build": "7.3.19" - }, - "pypy-3.11.11-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 11, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.11-v7.3.19-win64.zip", - "sha256": "b61c7c1dbf879eda6f779c374bfbbeecd3f618ada08404705a1a19d39df48dbd", - "variant": null, - "build": "7.3.19" - }, "pypy-3.10.16-darwin-aarch64-none": { "name": "pypy", "arch": { @@ -88008,7 +87804,7 @@ "variant": null, "build": "7.3.19" }, - "pypy-3.10.14-darwin-aarch64-none": { + "pypy-3.9.19-darwin-aarch64-none": { "name": "pypy", "arch": { "family": "aarch64", @@ -88017,15 +87813,15 @@ "os": "darwin", "libc": "none", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_arm64.tar.bz2", - "sha256": "a050e25e8d686853dd5afc363e55625165825dacfb55f8753d8225ebe417cfd2", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.gz", + "sha256": "b09ce3a865c1c0f4e58c0ee4f70511f1b27dfc1f7148f4b6060cfdfd581c9e82", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.14-darwin-x86_64-none": { + "pypy-3.9.19-darwin-x86_64-none": { "name": "pypy", "arch": { "family": "x86_64", @@ -88034,15 +87830,15 @@ "os": "darwin", "libc": "none", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-macos_x86_64.tar.bz2", - "sha256": "6c2c5f2300d7564e711421b4968abd63243cb96f76e363975dd648ebf4a362ee", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.gz", + "sha256": "7ea762a1b0af7545a74cd4260bb3fd2812813deffef2ef8d2fddd5e03fa63ef1", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.14-linux-aarch64-gnu": { + "pypy-3.9.19-linux-aarch64-gnu": { "name": "pypy", "arch": { "family": "aarch64", @@ -88051,15 +87847,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-aarch64.tar.bz2", - "sha256": "53b6e5907df869c49e4eae7aca09fba16d150741097efb245892c1477d2395f2", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-aarch64.tar.gz", + "sha256": "2ccee5d2bdea7814f7f1f65e5c2193fbe16c790534efbf267b423013c039199d", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.14-linux-i686-gnu": { + "pypy-3.9.19-linux-i686-gnu": { "name": "pypy", "arch": { "family": "i686", @@ -88068,15 +87864,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-linux32.tar.bz2", - "sha256": "e534110e1047da37c1d586c392f74de3424f871d906a2083de6d41f2a8cc9164", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux32.tar.gz", + "sha256": "fa8a65326a3a8bef9c85efd424d58cc957b6e12069c1c70b178f9ccd12414e01", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.14-linux-s390x-gnu": { + "pypy-3.9.19-linux-s390x-gnu": { "name": "pypy", "arch": { "family": "s390x", @@ -88085,15 +87881,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.16-s390x.tar.bz2", - "sha256": "af97efe498a209ba18c7bc7d084164a9907fb3736588b6864955177e19d5216a", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-s390x.tar.gz", + "sha256": "1e4ace733829b2cdab3fe24a1fdf0fb34cc4d5411736cdbbc6281e5ddbe227cd", "variant": null, "build": "7.3.16" }, - "pypy-3.10.14-linux-x86_64-gnu": { + "pypy-3.9.19-linux-x86_64-gnu": { "name": "pypy", "arch": { "family": "x86_64", @@ -88102,15 +87898,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-linux64.tar.bz2", - "sha256": "fdcdb9b24f1a7726003586503fdeb264fd68fc37fbfcea022dcfe825a7fee18b", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux64.tar.gz", + "sha256": "95885b864c5b1560d6ae3bc1fd00b3732cac6db5328beb9cd425bf0cdee0e5b0", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.14-windows-x86_64-none": { + "pypy-3.9.19-windows-x86_64-none": { "name": "pypy", "arch": { "family": "x86_64", @@ -88119,15 +87915,15 @@ "os": "windows", "libc": "none", "major": 3, - "minor": 10, - "patch": 14, + "minor": 9, + "patch": 19, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.17-win64.zip", - "sha256": "cab794a03ddda26238c72942ea6f225612e0dc17c76cac6652da83a95024e6e8", + "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip", + "sha256": "06ec12a5e964dc0ad33e6f380185a4d295178dce6d6df512f508e7aee00a1323", "variant": null, - "build": "7.3.17" + "build": "7.3.16" }, - "pypy-3.10.13-darwin-aarch64-none": { + "pypy-3.8.16-darwin-aarch64-none": { "name": "pypy", "arch": { "family": "aarch64", @@ -88136,15 +87932,15 @@ "os": "darwin", "libc": "none", "major": 3, - "minor": 10, - "patch": 13, + "minor": 8, + "patch": 16, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_arm64.tar.bz2", - "sha256": "d927c5105ea7880f7596fe459183e35cc17c853ef5105678b2ad62a8d000a548", + "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.gz", + "sha256": "110dea6402ec1e23d03ed08b832fed7d9e95aee1d83f3a20907a66682867795c", "variant": null, - "build": "7.3.15" + "build": "7.3.11" }, - "pypy-3.10.13-darwin-x86_64-none": { + "pypy-3.8.16-darwin-x86_64-none": { "name": "pypy", "arch": { "family": "x86_64", @@ -88153,15 +87949,15 @@ "os": "darwin", "libc": "none", "major": 3, - "minor": 10, - "patch": 13, + "minor": 8, + "patch": 16, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-macos_x86_64.tar.bz2", - "sha256": "559b61ba7e7c5a5c23cef5370f1fab47ccdb939ac5d2b42b4bef091abe3f6964", + "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.gz", + "sha256": "0e83febf891abc489eeb3ae2b26b466f37db4e1aec933d287378a66a04e0d057", "variant": null, - "build": "7.3.15" + "build": "7.3.11" }, - "pypy-3.10.13-linux-aarch64-gnu": { + "pypy-3.8.16-linux-aarch64-gnu": { "name": "pypy", "arch": { "family": "aarch64", @@ -88170,15 +87966,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 13, + "minor": 8, + "patch": 16, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-aarch64.tar.bz2", - "sha256": "52146fccaf64e87e71d178dda8de63c01577ec3923073dc69e1519622bcacb74", + "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.gz", + "sha256": "a2bd02adf8dda887123bf558104377483acbcdf48eb7f679af48d7e6d066591e", "variant": null, - "build": "7.3.15" + "build": "7.3.11" }, - "pypy-3.10.13-linux-i686-gnu": { + "pypy-3.8.16-linux-i686-gnu": { "name": "pypy", "arch": { "family": "i686", @@ -88187,15 +87983,15 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 13, + "minor": 8, + "patch": 16, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux32.tar.bz2", - "sha256": "75dd58c9abd8b9d78220373148355bc3119febcf27a2c781d64ad85e7232c4aa", + "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux32.tar.gz", + "sha256": "880c9654bca1671315b10de485f574e436cffdbfc7916226f6a3663e8a01ecb6", "variant": null, - "build": "7.3.15" + "build": "7.3.11" }, - "pypy-3.10.13-linux-s390x-gnu": { + "pypy-3.8.16-linux-s390x-gnu": { "name": "pypy", "arch": { "family": "s390x", @@ -88204,1048 +88000,11 @@ "os": "linux", "libc": "gnu", "major": 3, - "minor": 10, - "patch": 13, + "minor": 8, + "patch": 16, "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-s390x.tar.bz2", - "sha256": "209e57596381e13c9914d1332f359dc4b78de06576739747eb797bdbf85062b8", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.10.13-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-linux64.tar.bz2", - "sha256": "33c584e9a70a71afd0cb7dd8ba9996720b911b3b8ed0156aea298d4487ad22c3", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.10.13-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.15-win64.zip", - "sha256": "b378b3ab1c3719aee0c3e5519e7bff93ff67b2d8aa987fe4f088b54382db676c", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.10.12-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_arm64.tar.bz2", - "sha256": "45671b1e9437f95ccd790af10dbeb57733cca1ed9661463b727d3c4f5caa7ba0", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-macos_x86_64.tar.bz2", - "sha256": "dbc15d8570560d5f79366883c24bc42231a92855ac19a0f28cb0adeb11242666", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-aarch64.tar.bz2", - "sha256": "26208b5a134d9860a08f74cce60960005758e82dc5f0e3566a48ed863a1f16a1", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux32.tar.bz2", - "sha256": "811667825ae58ada4b7c3d8bc1b5055b9f9d6a377e51aedfbe0727966603f60e", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-s390x.tar.bz2", - "sha256": "043c13a585479428b463ab69575a088db74aadc16798d6e677d97f563585fee3", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-linux64.tar.bz2", - "sha256": "6c577993160b6f5ee8cab73cd1a807affcefafe2f7441c87bd926c10505e8731", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.10.12-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.10-v7.3.12-win64.zip", - "sha256": "8c3b1d34fb99100e230e94560410a38d450dc844effbee9ea183518e4aff595c", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.19-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_arm64.tar.gz", - "sha256": "b09ce3a865c1c0f4e58c0ee4f70511f1b27dfc1f7148f4b6060cfdfd581c9e82", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-macos_x86_64.tar.gz", - "sha256": "7ea762a1b0af7545a74cd4260bb3fd2812813deffef2ef8d2fddd5e03fa63ef1", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-aarch64.tar.gz", - "sha256": "2ccee5d2bdea7814f7f1f65e5c2193fbe16c790534efbf267b423013c039199d", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux32.tar.gz", - "sha256": "fa8a65326a3a8bef9c85efd424d58cc957b6e12069c1c70b178f9ccd12414e01", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-s390x.tar.gz", - "sha256": "1e4ace733829b2cdab3fe24a1fdf0fb34cc4d5411736cdbbc6281e5ddbe227cd", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-linux64.tar.gz", - "sha256": "95885b864c5b1560d6ae3bc1fd00b3732cac6db5328beb9cd425bf0cdee0e5b0", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.19-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 19, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.16-win64.zip", - "sha256": "06ec12a5e964dc0ad33e6f380185a4d295178dce6d6df512f508e7aee00a1323", - "variant": null, - "build": "7.3.16" - }, - "pypy-3.9.18-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_arm64.tar.bz2", - "sha256": "300541c32125767a91b182b03d9cc4257f04971af32d747ecd4d62549d72acfd", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-macos_x86_64.tar.bz2", - "sha256": "18ad7c9cb91c5e8ef9d40442b2fd1f6392ae113794c5b6b7d3a45e04f19edec6", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-aarch64.tar.bz2", - "sha256": "03e35fcba290454bb0ccf7ee57fb42d1e63108d10d593776a382c0a2fe355de0", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux32.tar.bz2", - "sha256": "c6209380977066c9e8b96e8258821c70f996004ce1bc8659ae83d4fd5a89ff5c", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-s390x.tar.bz2", - "sha256": "deeb5e54c36a0fd9cfefd16e63a0d5bed4f4a43e6bbc01c23f0ed8f7f1c0aaf3", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-linux64.tar.bz2", - "sha256": "f062be307200bde434817e1620cebc13f563d6ab25309442c5f4d0f0d68f0912", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.18-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 18, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.15-win64.zip", - "sha256": "a156dad8b58570597eaaabe05663f00f80c60bc11df4a9c46d0953b6c5eb9209", - "variant": null, - "build": "7.3.15" - }, - "pypy-3.9.17-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_arm64.tar.bz2", - "sha256": "0e8a1a3468b9790c734ac698f5b00cc03fc16899ccc6ce876465fac0b83980e3", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-macos_x86_64.tar.bz2", - "sha256": "64f008ffa070c407e5ef46c8256b2e014de7196ea5d858385861254e7959f4eb", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-aarch64.tar.bz2", - "sha256": "e9327fb9edaf2ad91935d5b8563ec5ff24193bddb175c1acaaf772c025af1824", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux32.tar.bz2", - "sha256": "aa04370d38f451683ccc817d76c2b3e0f471dbb879e0bd618d9affbdc9cd37a4", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-s390x.tar.bz2", - "sha256": "20d84658a6899bdd2ca35b00ead33a2f56cff2c40dce1af630466d27952f6d4f", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-linux64.tar.bz2", - "sha256": "84c89b966fab2b58f451a482ee30ca7fec3350435bd0b9614615c61dc6da2390", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.17-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 17, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.12-win64.zip", - "sha256": "0996054207b401aeacace1aa11bad82cfcb463838a1603c5f263626c47bbe0e6", - "variant": null, - "build": "7.3.12" - }, - "pypy-3.9.16-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_arm64.tar.bz2", - "sha256": "91ad7500f1a39531dbefa0b345a3dcff927ff9971654e8d2e9ef7c5ae311f57e", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-macos_x86_64.tar.bz2", - "sha256": "d33f40b207099872585afd71873575ca6ea638a27d823bc621238c5ae82542ed", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-aarch64.tar.bz2", - "sha256": "09175dc652ed895d98e9ad63d216812bf3ee7e398d900a9bf9eb2906ba8302b9", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux32.tar.bz2", - "sha256": "0099d72c2897b229057bff7e2c343624aeabdc60d6fb43ca882bff082f1ffa48", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-s390x.tar.bz2", - "sha256": "e1f30f2ddbe3f446ddacd79677b958d56c07463b20171fb2abf8f9a3178b79fc", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-linux64.tar.bz2", - "sha256": "d506172ca11071274175d74e9c581c3166432d0179b036470e3b9e8d20eae581", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.16-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.11-win64.zip", - "sha256": "57faad132d42d3e7a6406fcffafffe0b4f390cf0e2966abb8090d073c6edf405", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.9.15-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_arm64.tar.bz2", - "sha256": "e2a6bec7408e6497c7de8165aa4a1b15e2416aec4a72f2578f793fb06859ccba", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-macos_x86_64.tar.bz2", - "sha256": "f90c8619b41e68ec9ffd7d5e913fe02e60843da43d3735b1c1bc75bcfe638d97", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-aarch64.tar.bz2", - "sha256": "657a04fd9a5a992a2f116a9e7e9132ea0c578721f59139c9fb2083775f71e514", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux32.tar.bz2", - "sha256": "b6db59613b9a1c0c1ab87bc103f52ee95193423882dc8a848b68850b8ba59cc5", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-s390x.tar.bz2", - "sha256": "ca6525a540cf0c682d1592ae35d3fbc97559a97260e4b789255cc76dde7a14f0", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-linux64.tar.bz2", - "sha256": "95cf99406179460d63ddbfe1ec870f889d05f7767ce81cef14b88a3a9e127266", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.15-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.10-win64.zip", - "sha256": "07e18b7b24c74af9730dfaab16e24b22ef94ea9a4b64cbb2c0d80610a381192a", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.9.12-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-osx64.tar.bz2", - "sha256": "59c8852168b2b1ba1f0211ff043c678760380d2f9faf2f95042a8878554dbc25", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.12-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-aarch64.tar.bz2", - "sha256": "2e1ae193d98bc51439642a7618d521ea019f45b8fb226940f7e334c548d2b4b9", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.12-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux32.tar.bz2", - "sha256": "0de4b9501cf28524cdedcff5052deee9ea4630176a512bdc408edfa30914bae7", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.12-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-s390x.tar.bz2", - "sha256": "774dca83bcb4403fb99b3d155e7bd572ef8c52b9fe87a657109f64e75ad71732", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.12-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-linux64.tar.bz2", - "sha256": "46818cb3d74b96b34787548343d266e2562b531ddbaf330383ba930ff1930ed5", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.12-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.9-win64.zip", - "sha256": "be48ab42f95c402543a7042c999c9433b17e55477c847612c8733a583ca6dff5", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.9.10-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-osx64.tar.bz2", - "sha256": "95bd88ac8d6372cd5b7b5393de7b7d5c615a0c6e42fdb1eb67f2d2d510965aee", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.9.10-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-aarch64-portable.tar.bz2", - "sha256": "b7282bc4484bceae5bc4cc04e05ee4faf51cb624c8fc7a69d92e5fdf0d0c96aa", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.9.10-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux32.tar.bz2", - "sha256": "a0d18e4e73cc655eb02354759178b8fb161d3e53b64297d05e2fff91f7cf862d", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.9.10-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-s390x.tar.bz2", - "sha256": "37b596bfe76707ead38ffb565629697e9b6fa24e722acc3c632b41ec624f5d95", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.9.10-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-linux64.tar.bz2", - "sha256": "129a055032bba700cd1d0acacab3659cf6b7180e25b1b2f730e792f06d5b3010", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.9.10-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 10, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.9-v7.3.8-win64.zip", - "sha256": "c1b2e4cde2dcd1208d41ef7b7df8e5c90564a521e7a5db431673da335a1ba697", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.16-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_arm64.tar.gz", - "sha256": "110dea6402ec1e23d03ed08b832fed7d9e95aee1d83f3a20907a66682867795c", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.8.16-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-macos_x86_64.tar.gz", - "sha256": "0e83febf891abc489eeb3ae2b26b466f37db4e1aec933d287378a66a04e0d057", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.8.16-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-aarch64.tar.gz", - "sha256": "a2bd02adf8dda887123bf558104377483acbcdf48eb7f679af48d7e6d066591e", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.8.16-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-linux32.tar.gz", - "sha256": "880c9654bca1671315b10de485f574e436cffdbfc7916226f6a3663e8a01ecb6", - "variant": null, - "build": "7.3.11" - }, - "pypy-3.8.16-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 16, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-s390x.tar.gz", - "sha256": "178bd0a509e63fc6047e450d5bdc199d9ac5151c2173808e4b72055098c9995a", + "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.11-s390x.tar.gz", + "sha256": "178bd0a509e63fc6047e450d5bdc199d9ac5151c2173808e4b72055098c9995a", "variant": null, "build": "7.3.11" }, @@ -89283,329 +88042,6 @@ "variant": null, "build": "7.3.11" }, - "pypy-3.8.15-darwin-aarch64-none": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_arm64.tar.bz2", - "sha256": "6cb1429371e4854b718148a509d80143f801e3abfc72fef58d88aeeee1e98f9e", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-macos_x86_64.tar.bz2", - "sha256": "399eb1ce4c65f62f6a096b7c273536601b7695e3c0dc0457393a659b95b7615b", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-aarch64.tar.bz2", - "sha256": "e4caa1a545f22cfee87d5b9aa6f8852347f223643ad7d2562e0b2a2f4663ad98", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux32.tar.bz2", - "sha256": "b70ed7fdc73a74ebdc04f07439f7bad1a849aaca95e26b4a74049d0e483f071c", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-s390x.tar.bz2", - "sha256": "c294f8e815158388628fe77ac5b8ad6cd93c8db1359091fa02d41cf6da4d61a1", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-linux64.tar.bz2", - "sha256": "ceef6496fd4ab1c99e3ec22ce657b8f10f8bb77a32427fadfb5e1dd943806011", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.15-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 15, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.10-win64.zip", - "sha256": "362dd624d95bd64743190ea2539b97452ecb3d53ea92ceb2fbe9f48dc60e6b8f", - "variant": null, - "build": "7.3.10" - }, - "pypy-3.8.13-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-osx64.tar.bz2", - "sha256": "91a5c2c1facd5a4931a8682b7d792f7cf4f2ba25cd2e7e44e982139a6d5e4840", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.13-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-aarch64.tar.bz2", - "sha256": "5e124455e207425e80731dff317f0432fa0aba1f025845ffca813770e2447e32", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.13-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux32.tar.bz2", - "sha256": "4b261516c6c59078ab0c8bd7207327a1b97057b4ec1714ed5e79a026f9efd492", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.13-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-s390x.tar.bz2", - "sha256": "c6177a0016c9145c7b99fddb5d74cc2e518ccdb216a6deb51ef6a377510cc930", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.13-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-linux64.tar.bz2", - "sha256": "08be25ec82fc5d23b78563eda144923517daba481a90af0ace7a047c9c9a3c34", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.13-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 13, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.9-win64.zip", - "sha256": "05022baaa55db2b60880f2422312d9e4025e1267303ac57f33e8253559d0be88", - "variant": null, - "build": "7.3.9" - }, - "pypy-3.8.12-darwin-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-osx64.tar.bz2", - "sha256": "de1b283ff112d76395c0162a1cf11528e192bdc230ee3f1b237f7694c7518dee", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.12-linux-aarch64-gnu": { - "name": "pypy", - "arch": { - "family": "aarch64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-aarch64-portable.tar.bz2", - "sha256": "0210536e9f1841ba283c13b04783394050837bb3e6f4091c9f1bd9c7f2b94b55", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.12-linux-i686-gnu": { - "name": "pypy", - "arch": { - "family": "i686", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux32.tar.bz2", - "sha256": "bea4b275decd492af6462157d293dd6fcf08a949859f8aec0959537b40afd032", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.12-linux-s390x-gnu": { - "name": "pypy", - "arch": { - "family": "s390x", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-s390x.tar.bz2", - "sha256": "ad53d373d6e275a41ca64da7d88afb6a17e48e7bfb2a6fff92daafdc06da6b90", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.12-linux-x86_64-gnu": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-linux64.tar.bz2", - "sha256": "089f8e3e357d6130815964ddd3507c13bd53e4976ccf0a89b5c36a9a6775a188", - "variant": null, - "build": "7.3.8" - }, - "pypy-3.8.12-windows-x86_64-none": { - "name": "pypy", - "arch": { - "family": "x86_64", - "variant": null - }, - "os": "windows", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 12, - "prerelease": "", - "url": "https://downloads.python.org/pypy/pypy3.8-v7.3.8-win64.zip", - "sha256": "0894c468e7de758c509a602a28ef0ba4fbf197ccdf946c7853a7283d9bb2a345", - "variant": null, - "build": "7.3.8" - }, "graalpy-3.12.0-darwin-aarch64-none": { "name": "graalpy", "arch": { diff --git a/crates/uv-python/fetch-download-metadata.py b/crates/uv-python/fetch-download-metadata.py index 00c88d655b356..8cf22e65e134f 100755 --- a/crates/uv-python/fetch-download-metadata.py +++ b/crates/uv-python/fetch-download-metadata.py @@ -443,6 +443,7 @@ async def _fetch_downloads(self) -> list[PythonDownload]: versions = resp.json() results = {} + incomplete_versions = set() for version in versions: if not version["stable"]: continue @@ -451,6 +452,20 @@ async def _fetch_downloads(self) -> list[PythonDownload]: continue pypy_version = version["pypy_version"] for file in version["files"]: + # Only a small number of older pypy builds are bz2; we filter + # them because uv 0.12+ won't support extracting them. + if file["filename"].endswith(".tar.bz2"): + incomplete_versions.add(version["python_version"]) + continue + + # If we've filtered out a bz2 distribution above, we want to + # make sure we filter out its other variants as well. Otherwise + # we'd have a partial state where some patch versions of PyPy + # are available on Windows but not other platforms (since Windows + # uses zip, not bz2). + if version["python_version"] in incomplete_versions: + continue + arch = self._normalize_arch(file["arch"]) platform = self._normalize_os(file["platform"]) libc = "gnu" if platform == "linux" else "none" diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index fe938f6c622c9..3306015dbb8f7 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -1485,12 +1485,12 @@ impl ManagedPythonDownload { if let Some(reporter) = reporter { let progress_key = reporter.on_request_start(direction, &self.key, size); let mut reader = ProgressReader::new(&mut hasher, progress_key, reporter); - uv_extract::stream::archive(filename, &mut reader, ext, target) + uv_extract::stream::archive(&mut reader, ext, target) .await .map_err(|err| Error::ExtractError(filename.to_owned(), err))?; reporter.on_request_complete(direction, progress_key); } else { - uv_extract::stream::archive(filename, &mut hasher, ext, target) + uv_extract::stream::archive(&mut hasher, ext, target) .await .map_err(|err| Error::ExtractError(filename.to_owned(), err))?; } diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index f83f55c906623..8bf7714bb5569 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -206,6 +206,9 @@ mod tests { use std::{ env, ffi::{OsStr, OsString}, + fs::Permissions, + io, + os::unix::fs::PermissionsExt, path::{Path, PathBuf}, str::FromStr, }; @@ -1693,7 +1696,8 @@ mod tests { "We should find the conda environment when name matches" ); - // When CONDA_DEFAULT_ENV is "base", it should always be treated as base environment + // A special Conda environment name only identifies the base environment when its path + // does not match the environment name. let result = context.run_with_vars_and_preview( &[ (EnvVars::CONDA_PREFIX, Some(condaenv.as_os_str())), @@ -1715,35 +1719,35 @@ mod tests { "We should not allow the base environment when looking for virtual environments" ); - // With the `special-conda-env-names` preview feature, "base" is not special-cased - // and uses path-based heuristics instead. When the directory name matches the env name, - // it should be treated as a child environment. - let base_dir = context.tempdir.child("base"); - TestContext::mock_conda_prefix(&base_dir, "3.12.6")?; - let python = context - .run_with_vars_and_preview( - &[ - (EnvVars::CONDA_PREFIX, Some(base_dir.as_os_str())), - (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from("base"))), - (EnvVars::CONDA_ROOT, None), - ], - &[PreviewFeature::SpecialCondaEnvNames], - || { - find_python_installation( - &PythonRequest::Default, - EnvironmentPreference::OnlyVirtual, - PythonPreference::OnlySystem, - &context.cache, - ) - }, - )? - .unwrap(); + // When the directory name matches a special Conda environment name, it should be treated + // as a child environment. + for (name, version) in [("base", "3.12.6"), ("root", "3.12.7")] { + let environment = context.tempdir.child(name); + TestContext::mock_conda_prefix(&environment, version)?; + let python = context + .run_with_vars( + &[ + (EnvVars::CONDA_PREFIX, Some(environment.as_os_str())), + (EnvVars::CONDA_DEFAULT_ENV, Some(&OsString::from(name))), + (EnvVars::CONDA_ROOT, None), + ], + || { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + }, + )? + .unwrap(); - assert_eq!( - python.interpreter().python_full_version().to_string(), - "3.12.6", - "With special-conda-env-names preview, 'base' named env in matching dir should be treated as child" - ); + assert_eq!( + python.interpreter().python_full_version().to_string(), + version, + "We should find the child Conda environment named {name}" + ); + } // When environment name matches directory name, it should be treated as a child environment let myenv_dir = context.tempdir.child("myenv"); @@ -2254,6 +2258,65 @@ mod tests { Ok(()) } + #[cfg(unix)] + #[test] + fn find_python_does_not_traverse_broken_child_virtualenv() -> Result<()> { + let context = TestContext::new()?; + + let parent_venv = context.tempdir.child(".venv"); + TestContext::mock_venv(&parent_venv, "3.12.0")?; + let child_venv = context.workdir.child(".venv"); + fs_err::os::unix::fs::symlink(context.workdir.child("missing"), &child_venv)?; + + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + }); + assert!( + matches!( + &result, + Err(discovery::Error::VirtualEnv( + crate::virtualenv::Error::MissingPyVenvCfg(path) + )) if path == child_venv.path() + ), + "A broken symlink at `.venv` should be eagerly rejected; got {result:?}" + ); + + Ok(()) + } + + #[test] + fn find_python_propagates_virtualenv_metadata_errors() -> Result<()> { + let context = TestContext::new()?; + + let permissions = fs_err::metadata(&context.workdir)?.permissions(); + fs_err::set_permissions(&context.workdir, Permissions::from_mode(0o000))?; + let result = context.run(|| { + find_python_installation( + &PythonRequest::Default, + EnvironmentPreference::OnlyVirtual, + PythonPreference::OnlySystem, + &context.cache, + ) + }); + fs_err::set_permissions(&context.workdir, permissions)?; + + assert!( + matches!( + &result, + Err(discovery::Error::VirtualEnv(crate::virtualenv::Error::Io(error))) + if error.kind() == io::ErrorKind::PermissionDenied + ), + "A virtual environment metadata error should not be ignored; got {result:?}" + ); + + Ok(()) + } + #[test] fn find_python_allows_name_in_working_directory() -> Result<()> { let context = TestContext::new()?; diff --git a/crates/uv-python/src/virtualenv.rs b/crates/uv-python/src/virtualenv.rs index a1bb0eec30442..59b7d5fe39b10 100644 --- a/crates/uv-python/src/virtualenv.rs +++ b/crates/uv-python/src/virtualenv.rs @@ -8,7 +8,6 @@ use std::{ use fs_err as fs; use thiserror::Error; -use uv_preview::PreviewFeature; use uv_pypi_types::Scheme; use uv_static::EnvVars; @@ -113,17 +112,7 @@ impl CondaEnvironmentKind { return Self::Child; } - // If the environment name is "base" or "root", treat it as a base environment - // - // These are the expected names for the base environment; and is retained for backwards - // compatibility, but can be removed with the `special-conda-env-names` preview feature. - if !uv_preview::is_enabled(PreviewFeature::SpecialCondaEnvNames) - && (current_env == "base" || current_env == "root") - { - return Self::Base; - } - - // For other environment names, use the path-based logic + // Use path-based logic for environment names, including `base` and `root`. let Some(name) = path.file_name() else { return Self::Child; }; @@ -160,9 +149,9 @@ pub(crate) fn conda_environment_from_env(kind: CondaEnvironmentKind) -> Option

Result, Error> { let current_dir = crate::current_dir()?; @@ -174,7 +163,12 @@ pub(crate) fn virtualenv_from_working_dir() -> Result, Error> { // Otherwise, search for a `.venv` directory. let dot_venv = dir.join(".venv"); - if dot_venv.is_dir() { + let metadata = match fs::symlink_metadata(&dot_venv) { + Ok(metadata) => metadata, + Err(err) if err.kind() == io::ErrorKind::NotFound => continue, + Err(err) => return Err(err.into()), + }; + if metadata.is_dir() || metadata.file_type().is_symlink() { if !uv_fs::is_virtualenv_base(&dot_venv) { return Err(Error::MissingPyVenvCfg(dot_venv)); } diff --git a/crates/uv-requirements-txt/src/lib.rs b/crates/uv-requirements-txt/src/lib.rs index 6a32e6f721ab8..f6475b66e14ae 100644 --- a/crates/uv-requirements-txt/src/lib.rs +++ b/crates/uv-requirements-txt/src/lib.rs @@ -95,6 +95,8 @@ enum RequirementsTxtStatement { FindLinks(VerbatimUrl), /// `--no-index` NoIndex, + /// `--require-hashes` + RequireHashes, /// `--no-binary` NoBinary(NoBinary), /// `--only-binary` @@ -158,6 +160,8 @@ pub struct RequirementsTxt { pub find_links: Vec, /// Whether to ignore the index, specified with `--no-index`. pub no_index: bool, + /// Whether all requirements must be hashed, specified with `--require-hashes`. + pub require_hashes: bool, /// Whether to disallow wheels, specified with `--no-binary`. pub no_binary: NoBinary, /// Whether to allow only wheels, specified with `--only-binary`. @@ -542,6 +546,9 @@ impl RequirementsTxt { RequirementsTxtStatement::NoIndex => { data.no_index = true; } + RequirementsTxtStatement::RequireHashes => { + data.require_hashes = true; + } RequirementsTxtStatement::NoBinary(no_binary) => { data.no_binary.extend(no_binary); } @@ -592,6 +599,7 @@ impl RequirementsTxt { extra_index_urls, find_links, no_index, + require_hashes, no_binary, only_binary, } = other; @@ -604,6 +612,7 @@ impl RequirementsTxt { self.extra_index_urls.extend(extra_index_urls); self.find_links.extend(find_links); self.no_index = self.no_index || no_index; + self.require_hashes = self.require_hashes || require_hashes; self.no_binary.extend(no_binary); self.only_binary.extend(only_binary); } @@ -615,7 +624,6 @@ impl RequirementsTxt { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum UnsupportedOption { PreferBinary, - RequireHashes, Pre, TrustedHost, UseFeature, @@ -626,7 +634,6 @@ impl UnsupportedOption { fn name(self) -> &'static str { match self { Self::PreferBinary => "--prefer-binary", - Self::RequireHashes => "--require-hashes", Self::Pre => "--pre", Self::TrustedHost => "--trusted-host", Self::UseFeature => "--use-feature", @@ -637,7 +644,6 @@ impl UnsupportedOption { fn cli(self) -> bool { match self { Self::PreferBinary => false, - Self::RequireHashes => true, Self::Pre => true, Self::TrustedHost => true, Self::UseFeature => false, @@ -648,7 +654,6 @@ impl UnsupportedOption { fn iter() -> impl Iterator { [ Self::PreferBinary, - Self::RequireHashes, Self::Pre, Self::TrustedHost, Self::UseFeature, @@ -811,6 +816,8 @@ fn parse_entry( RequirementsTxtStatement::ExtraIndexUrl(url.with_given(given)) } else if s.eat_if("--no-index") { RequirementsTxtStatement::NoIndex + } else if s.eat_if("--require-hashes") { + RequirementsTxtStatement::RequireHashes } else if s.eat_if("--find-links") || s.eat_if("-f") { let given = parse_value("--find-links", content, s, |c: char| !is_terminal(c))?; let given = unquote(given) @@ -2087,6 +2094,7 @@ mod test { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } @@ -2147,6 +2155,7 @@ mod test { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: Packages( [ PackageName( @@ -2253,6 +2262,7 @@ mod test { extra_index_urls: [], find_links: [], no_index: true, + require_hashes: false, no_binary: None, only_binary: None, } @@ -2503,6 +2513,7 @@ mod test { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: All, only_binary: None, } @@ -2863,6 +2874,7 @@ mod test { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-basic.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-basic.txt.snap index 0856eedc5a4cc..69051aa439b9b 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-basic.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-basic.txt.snap @@ -185,6 +185,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-a.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-a.txt.snap index a86e9ce1e4534..4477acc6d015c 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-a.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-a.txt.snap @@ -89,6 +89,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-b.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-b.txt.snap index d90bf92f17392..e21a5a19a07c5 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-b.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-constraints-b.txt.snap @@ -69,6 +69,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-empty.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-empty.txt.snap index 3f2d8676972cf..53c9e2458cfb6 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-empty.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-empty.txt.snap @@ -10,6 +10,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-for-poetry.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-for-poetry.txt.snap index 5c683b6506302..d61f0c4f03138 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-for-poetry.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-for-poetry.txt.snap @@ -124,6 +124,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-a.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-a.txt.snap index bb190b738b7a5..052af1189a293 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-a.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-a.txt.snap @@ -58,6 +58,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-b.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-b.txt.snap index 19477047a5691..2c0f887eddf27 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-b.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-include-b.txt.snap @@ -29,6 +29,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap index bddbc6f3feafe..df4a56872424b 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-poetry-with-hashes.txt.snap @@ -170,6 +170,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-small.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-small.txt.snap index c60e7d36f804e..cafec2bf5d6b3 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-small.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-small.txt.snap @@ -69,6 +69,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap index 5084362ed87bb..8b0a443292a6d 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__line-endings-whitespace.txt.snap @@ -1,6 +1,5 @@ --- source: crates/uv-requirements-txt/src/lib.rs -assertion_line: 1639 expression: actual --- RequirementsTxt { @@ -121,6 +120,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-basic.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-basic.txt.snap index 0856eedc5a4cc..69051aa439b9b 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-basic.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-basic.txt.snap @@ -185,6 +185,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-a.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-a.txt.snap index a86e9ce1e4534..4477acc6d015c 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-a.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-a.txt.snap @@ -89,6 +89,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-b.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-b.txt.snap index d90bf92f17392..e21a5a19a07c5 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-b.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-constraints-b.txt.snap @@ -69,6 +69,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-empty.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-empty.txt.snap index 3f2d8676972cf..53c9e2458cfb6 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-empty.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-empty.txt.snap @@ -10,6 +10,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-for-poetry.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-for-poetry.txt.snap index 5c683b6506302..d61f0c4f03138 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-for-poetry.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-for-poetry.txt.snap @@ -124,6 +124,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-a.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-a.txt.snap index bb190b738b7a5..052af1189a293 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-a.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-a.txt.snap @@ -58,6 +58,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-b.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-b.txt.snap index 19477047a5691..2c0f887eddf27 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-b.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-include-b.txt.snap @@ -29,6 +29,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap index bddbc6f3feafe..df4a56872424b 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-poetry-with-hashes.txt.snap @@ -170,6 +170,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-small.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-small.txt.snap index c60e7d36f804e..cafec2bf5d6b3 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-small.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-small.txt.snap @@ -69,6 +69,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap index 0091e225e5331..4ebe4a45714bc 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-bare-url.txt.snap @@ -321,6 +321,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap index 7dc49dc0bdc57..6139226365e45 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-unix-editable.txt.snap @@ -463,6 +463,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap index 510c64213ea8d..8b0a443292a6d 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-whitespace.txt.snap @@ -1,6 +1,5 @@ --- source: crates/uv-requirements-txt/src/lib.rs -assertion_line: 1591 expression: actual --- RequirementsTxt { @@ -121,6 +120,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap index 80b8d4a4a0339..cbc8b22554917 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-bare-url.txt.snap @@ -321,6 +321,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap index bab5c4af99db1..983485df7b99a 100644 --- a/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap +++ b/crates/uv-requirements-txt/src/snapshots/uv_requirements_txt__test__parse-windows-editable.txt.snap @@ -463,6 +463,7 @@ RequirementsTxt { extra_index_urls: [], find_links: [], no_index: false, + require_hashes: false, no_binary: None, only_binary: None, } diff --git a/crates/uv-requirements/src/sources.rs b/crates/uv-requirements/src/sources.rs index c219a96cbacff..cf6218326b96d 100644 --- a/crates/uv-requirements/src/sources.rs +++ b/crates/uv-requirements/src/sources.rs @@ -59,7 +59,7 @@ impl RequirementsSource { .is_some_and(|ext| ext.eq_ignore_ascii_case("toml")) { Err(anyhow::anyhow!( - "`{}` is not a valid PEP 751 filename: expected TOML file to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`)", + "`{}` is not a valid PEP 751 filename: expected `pylock.toml` or `pylock..toml`, where `` is non-empty and contains no dots", path.user_display(), )) } else if path @@ -329,7 +329,13 @@ impl std::fmt::Display for RequirementsSource { } /// Returns `true` if a file name matches the `pylock.toml` pattern defined in PEP 751. -#[expect(clippy::case_sensitive_file_extension_comparisons)] pub fn is_pylock_toml(file_name: &str) -> bool { - file_name.starts_with("pylock.") && file_name.ends_with(".toml") + if file_name == "pylock.toml" { + return true; + } + + file_name + .strip_prefix("pylock.") + .and_then(|name| name.strip_suffix(".toml")) + .is_some_and(|name| !name.is_empty() && !name.contains('.')) } diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index be383017224d0..538f94c181318 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -83,6 +83,8 @@ pub struct RequirementsSpecification { pub extra_index_urls: Vec, /// Whether to disallow index usage. pub no_index: bool, + /// Whether all requirements must be hashed. + pub require_hashes: bool, /// The `--find-links` locations to use for fetching packages. pub find_links: Vec, /// The `--no-binary` flags to enforce when selecting distributions. @@ -240,6 +242,7 @@ impl RequirementsSpecification { .collect(), no_binary: requirements_txt.no_binary, no_build: requirements_txt.only_binary, + require_hashes: requirements_txt.require_hashes, ..Self::default() } } @@ -584,6 +587,7 @@ impl RequirementsSpecification { spec.find_links.extend(source.find_links); spec.no_binary.extend(source.no_binary); spec.no_build.extend(source.no_build); + spec.require_hashes |= source.require_hashes; } // Read all constraints, treating both requirements _and_ constraints as constraints. @@ -623,6 +627,7 @@ impl RequirementsSpecification { spec.find_links.extend(source.find_links); spec.no_binary.extend(source.no_binary); spec.no_build.extend(source.no_build); + spec.require_hashes |= source.require_hashes; } // Read all overrides, treating both requirements _and_ overrides as overrides. @@ -650,6 +655,7 @@ impl RequirementsSpecification { spec.find_links.extend(source.find_links); spec.no_binary.extend(source.no_binary); spec.no_build.extend(source.no_build); + spec.require_hashes |= source.require_hashes; } // Collect excludes. diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index e14a888bbf0a6..534291bd4d3aa 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -15,7 +15,7 @@ use uv_platform_tags::Tags; use uv_types::InstalledPackagesProvider; use crate::preferences::{Entry, PreferenceSource, Preferences}; -use crate::prerelease::{AllowPrerelease, PrereleaseStrategy}; +use crate::prerelease::{PrereleaseSelection, PrereleaseStrategy}; use crate::pubgrub::Range; use crate::resolution_mode::ResolutionStrategy; use crate::version_map::{VersionMap, VersionMapDistHandle}; @@ -90,6 +90,7 @@ impl CandidateSelector { ) -> Option> { let reinstall = exclusions.reinstall(package_name); let upgrade = exclusions.upgrade(package_name); + let prerelease_selection = self.prerelease_strategy.selection(package_name, env); // If we have a preference (e.g., from a lockfile), search for a version matching that // preference. @@ -108,6 +109,7 @@ impl CandidateSelector { installed_packages, reinstall, index, + prerelease_selection, env, tags, ) { @@ -133,7 +135,13 @@ impl CandidateSelector { } // Otherwise, find the best candidate from the version maps. - let compatible = self.select_no_preference(package_name, range, version_maps, env); + let compatible = self.select_no_preference_with( + package_name, + range, + version_maps, + prerelease_selection, + env, + ); // Cross-reference against the already-installed distribution. // @@ -177,6 +185,7 @@ impl CandidateSelector { installed_packages: &'a InstalledPackages, reinstall: bool, index: Option<&'a IndexUrl>, + prerelease_selection: PrereleaseSelection, env: &ResolverEnvironment, tags: Option<&'a Tags>, ) -> Option> { @@ -226,28 +235,27 @@ impl CandidateSelector { } }; - self.get_preferred_from_iter( + Self::get_preferred_from_iter( preferences, package_name, range, version_maps, installed_packages, reinstall, - env, + prerelease_selection, tags, ) } /// Return the first preference that satisfies the current range and is allowed. fn get_preferred_from_iter<'a, InstalledPackages: InstalledPackagesProvider>( - &'a self, preferences: impl Iterator, package_name: &'a PackageName, range: &Range, version_maps: &'a [VersionMap], installed_packages: &'a InstalledPackages, reinstall: bool, - env: &ResolverEnvironment, + prerelease_selection: PrereleaseSelection, tags: Option<&Tags>, ) -> Option> { for (version, source) in preferences { @@ -301,12 +309,12 @@ impl CandidateSelector { // Respect the pre-release strategy for this fork. if version.any_prerelease() { - let allow = match self.prerelease_strategy.allows(package_name, env) { - AllowPrerelease::Yes => true, - AllowPrerelease::No => false, + let allow = match prerelease_selection { + PrereleaseSelection::Allow => true, + PrereleaseSelection::Disallow => false, // If the pre-release was provided via an existing file, rather than from the // current solve, accept it unless pre-releases are completely banned. - AllowPrerelease::IfNecessary => match source { + PrereleaseSelection::PreferStable => match source { PreferenceSource::Resolver => false, PreferenceSource::Lock | PreferenceSource::Environment @@ -417,6 +425,89 @@ impl CandidateSelector { range: &Range, version_maps: &'a [VersionMap], env: &ResolverEnvironment, + ) -> Option> { + self.select_no_preference_with( + package_name, + range, + version_maps, + self.prerelease_strategy.selection(package_name, env), + env, + ) + } + + fn select_no_preference_with<'a>( + &'a self, + package_name: &'a PackageName, + range: &Range, + version_maps: &'a [VersionMap], + prerelease_selection: PrereleaseSelection, + env: &ResolverEnvironment, + ) -> Option> { + match prerelease_selection { + PrereleaseSelection::Allow => self.select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::All, + env, + ), + PrereleaseSelection::Disallow => self.select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::Stable, + env, + ), + PrereleaseSelection::PreferStable + if self.index_strategy == IndexStrategy::UnsafeFirstMatch => + { + version_maps.iter().find_map(|version_map| { + let version_maps = std::slice::from_ref(version_map); + self.select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::Stable, + env, + ) + .or_else(|| { + self.select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::Prerelease, + env, + ) + }) + }) + } + PrereleaseSelection::PreferStable => self + .select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::Stable, + env, + ) + .or_else(|| { + self.select_no_preference_from( + package_name, + range, + version_maps, + PrereleaseCandidates::Prerelease, + env, + ) + }), + } + } + + fn select_no_preference_from<'a>( + &'a self, + package_name: &'a PackageName, + range: &Range, + version_maps: &'a [VersionMap], + prerelease_candidates: PrereleaseCandidates, + env: &ResolverEnvironment, ) -> Option> { trace!( "Selecting candidate for {package_name} with range {range} with {} remote versions", @@ -424,13 +515,6 @@ impl CandidateSelector { ); let highest = self.use_highest_version(package_name, env); - let allow_prerelease = match self.prerelease_strategy.allows(package_name, env) { - AllowPrerelease::Yes => true, - AllowPrerelease::No => false, - // Allow pre-releases if there are no stable versions available. - AllowPrerelease::IfNecessary => !version_maps.iter().any(VersionMap::stable), - }; - if self.index_strategy == IndexStrategy::UnsafeBestMatch { if highest { Self::select_candidate( @@ -455,7 +539,7 @@ impl CandidateSelector { .map(|(_, item)| item), package_name, range, - allow_prerelease, + prerelease_candidates, highest, ) } else { @@ -480,7 +564,7 @@ impl CandidateSelector { .map(|(_, item)| item), package_name, range, - allow_prerelease, + prerelease_candidates, highest, ) } @@ -491,7 +575,7 @@ impl CandidateSelector { version_map.iter_included(range).rev(), package_name, range, - allow_prerelease, + prerelease_candidates, highest, ) }) @@ -501,7 +585,7 @@ impl CandidateSelector { version_map.iter_included(range), package_name, range, - allow_prerelease, + prerelease_candidates, highest, ) }) @@ -538,7 +622,7 @@ impl CandidateSelector { versions: impl Iterator)>, package_name: &'a PackageName, range: &Range, - allow_prerelease: bool, + prerelease_candidates: PrereleaseCandidates, highest: bool, ) -> Option> { let segments = range.iter(); @@ -568,7 +652,11 @@ impl CandidateSelector { } let candidate = { - if version.any_prerelease() && !allow_prerelease { + if match prerelease_candidates { + PrereleaseCandidates::All => false, + PrereleaseCandidates::Stable => version.any_prerelease(), + PrereleaseCandidates::Prerelease => !version.any_prerelease(), + } { continue; } if !cursor.contains(version) { @@ -762,6 +850,21 @@ mod tests { } } +/// Controls which release classes are visible during one candidate-selection pass. +/// +/// Stable-first selection uses separate [`Self::Stable`] and [`Self::Prerelease`] passes so that +/// an incompatible stable candidate does not prevent falling back to a pre-release. For +/// [`IndexStrategy::UnsafeFirstMatch`], these passes are applied to each index in turn. +#[derive(Debug, Clone, Copy)] +enum PrereleaseCandidates { + /// Consider versions with or without a pre-release component. + All, + /// Consider only versions without a pre-release component. + Stable, + /// Consider only versions with a pre-release component. + Prerelease, +} + #[derive(Debug, Clone)] pub(crate) enum CandidateDist<'a> { Compatible(CompatibleDist<'a>), diff --git a/crates/uv-resolver/src/dependency_provider.rs b/crates/uv-resolver/src/dependency_provider.rs index ab52996434609..752bbb4fbb2da 100644 --- a/crates/uv-resolver/src/dependency_provider.rs +++ b/crates/uv-resolver/src/dependency_provider.rs @@ -53,6 +53,7 @@ mod tests { #[test] fn priority_size() { + assert_eq!(size_of::(), 4); assert_eq!( size_of::<::Priority>(), 24 diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index a60e90f5b6ccb..30ab3bc7be92b 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -25,7 +25,7 @@ use crate::candidate_selector::CandidateSelector; use crate::dependency_provider::UvDependencyProvider; use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; -use crate::prerelease::AllowPrerelease; +use crate::prerelease::PrereleaseSelection; use crate::pubgrub::{ PubGrubHint, PubGrubPackage, PubGrubPackageInner, PubGrubReportFormatter, Range, report_derivation_tree, @@ -1556,8 +1556,8 @@ fn simplify_range( // Check if pre-releases are allowed let prereleases_not_allowed = candidate_selector .prerelease_strategy() - .allows(name, resolver_environment) - != AllowPrerelease::Yes; + .selection(name, resolver_environment) + == PrereleaseSelection::Disallow; let any_prerelease = range.iter().any(|(start, end)| { let is_pre1 = match start { diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index 2115487db3a7a..2f2c5ec496a98 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -116,6 +116,7 @@ impl FlatDistributions { filename, file: Box::new(file), index, + size_is_authoritative: false, }; match self.0.entry(version) { Entry::Occupied(mut entry) => { @@ -140,6 +141,7 @@ impl FlatDistributions { file: Box::new(file), index, wheels: vec![], + size_is_authoritative: false, }; match self.0.entry(filename.version) { Entry::Occupied(mut entry) => { @@ -164,6 +166,14 @@ impl FlatDistributions { return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild); } + // Check if the filename is PEP 625-compliant. + // TODO: Strengthen this check more; right now we allow `.zip` + // (which is not compliant) and we don't strictly + // enforce the formatting rules for the name or version. + if !filename.extension.is_pep625_compliant() { + return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename); + } + // Check if hashes line up let hash_policy = hasher.get_package(&filename.name, &filename.version); let hash = if hash_policy.requires_validation() { diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index 90441617c03e6..3a8cd6ccba94c 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -61,6 +61,15 @@ fn each_element_on_its_line_array(elements: impl Iterator) -> Result<(), PylockTomlErrorKind> { + let Some(expected) = expected else { + return Ok(()); + }; + let actual = fs_err::metadata(path)?.len(); + if actual != expected { + return Err(PylockTomlErrorKind::ArchiveSizeMismatch { + path: path.to_path_buf(), + expected, + actual, + }); + } + Ok(()) +} + impl From for PylockTomlError where PylockTomlErrorKind: From, @@ -223,7 +247,6 @@ pub struct PylockToml { pub dependency_groups: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub default_groups: Vec, - #[serde(skip_serializing_if = "Vec::is_empty", default)] pub packages: Vec, #[serde(skip_serializing_if = "Vec::is_empty", default)] attestation_identities: Vec, @@ -1452,6 +1475,7 @@ impl PylockTomlWheel { let file_url = if let Some(path) = self.path.as_ref() { let path = install_path.join(path); + validate_path_size(&path, self.size)?; let url = DisplaySafeUrl::from_file_path(path) .map_err(|()| PylockTomlErrorKind::PathToUrl)?; UrlString::from(url) @@ -1492,6 +1516,7 @@ impl PylockTomlWheel { filename, file, index, + size_is_authoritative: true, }) } } @@ -1613,6 +1638,7 @@ impl PylockTomlSdist { let file_url = if let Some(path) = self.path.as_ref() { let path = install_path.join(path); + validate_path_size(&path, self.size)?; let url = DisplaySafeUrl::from_file_path(path) .map_err(|()| PylockTomlErrorKind::PathToUrl)?; UrlString::from(url) @@ -1656,6 +1682,7 @@ impl PylockTomlSdist { ext, index, wheels: vec![], + size_is_authoritative: true, }) } } @@ -1681,6 +1708,7 @@ impl PylockTomlArchive { DistExtension::Wheel => { let filename = WheelFilename::from_str(filename)?; let install_path = install_path.join(path); + validate_path_size(&install_path, self.size)?; let url = VerbatimUrl::from_absolute_path(&install_path) .map_err(|_| PylockTomlErrorKind::PathToUrl)?; Ok(Dist::Built(BuiltDist::Path(PathBuiltDist { @@ -1691,6 +1719,7 @@ impl PylockTomlArchive { } DistExtension::Source(ext) => { let install_path = install_path.join(path); + validate_path_size(&install_path, self.size)?; let url = VerbatimUrl::from_absolute_path(&install_path) .map_err(|_| PylockTomlErrorKind::PathToUrl)?; Ok(Dist::Source(SourceDist::Path(PathSourceDist { @@ -1715,6 +1744,7 @@ impl PylockTomlArchive { filename, location: Box::new(url.clone()), url: VerbatimUrl::from_url(url.clone()), + size: self.size, }))) } DistExtension::Source(ext) => { @@ -1724,6 +1754,7 @@ impl PylockTomlArchive { subdirectory: self.subdirectory.clone().map(Box::::from), ext, url: VerbatimUrl::from_url(url.clone()), + size: self.size, }))) } } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 67b44d065eab8..67be7d01cd1cd 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -3702,6 +3702,7 @@ impl Package { filename, location: Box::new(url.clone()), url: VerbatimUrl::from_url(url), + size: None, }; let built_dist = BuiltDist::DirectUrl(direct_dist); Dist::Built(built_dist) @@ -3865,6 +3866,12 @@ impl Package { else { return Ok(None); }; + if !ext.is_pep625_compliant() { + return Err(LockErrorKind::NotPep625Filename { + id: self.id.clone(), + } + .into()); + } let install_path = absolute_path(workspace_root, path)?; let given = path.to_str().expect("lock file paths must be UTF-8"); let path_dist = PathSourceDist { @@ -3980,6 +3987,12 @@ impl Package { else { return Ok(None); }; + if !ext.is_pep625_compliant() { + return Err(LockErrorKind::NotPep625Filename { + id: self.id.clone(), + } + .into()); + } let location = url.to_url().map_err(LockErrorKind::InvalidUrl)?; let url = DisplaySafeUrl::from(ParsedArchiveUrl { url: location.clone(), @@ -3992,6 +4005,7 @@ impl Package { subdirectory: direct.subdirectory.clone(), ext, url: VerbatimUrl::from_url(url), + size: None, }; uv_distribution_types::SourceDist::DirectUrl(direct_dist) } @@ -4047,6 +4061,7 @@ impl Package { ext, index, wheels: vec![], + size_is_authoritative: false, }; uv_distribution_types::SourceDist::Registry(reg_dist) } @@ -4123,6 +4138,7 @@ impl Package { ext, index, wheels: vec![], + size_is_authoritative: false, }; uv_distribution_types::SourceDist::Registry(reg_dist) } @@ -5884,6 +5900,7 @@ impl Wheel { filename, file, index, + size_is_authoritative: false, }) } RegistrySource::Path(index_path) => { @@ -5935,6 +5952,7 @@ impl Wheel { filename, file, index, + size_is_authoritative: false, }) } } @@ -6564,6 +6582,12 @@ impl LockError { LockErrorKind::NoBuild { .. } | LockErrorKind::NoBinaryNoBuild { .. } ) } + + /// Returns true if the [`LockError`] indicates that the lockfile references a + /// non-PEP 625-compliant source distribution. + pub fn is_not_pep625(&self) -> bool { + matches!(&*self.kind, LockErrorKind::NotPep625Filename { .. }) + } } impl From for LockError @@ -6986,6 +7010,16 @@ enum LockErrorKind { /// The list of valid extensions that were expected. err: ExtensionError, }, + /// An error that occurs when a locked source distribution has a + /// non-PEP 625-compliant filename (e.g., `.tar.bz2`). + #[error( + "Source distribution for `{id}` has a non-PEP 625-compliant filename; only `.tar.gz` and `.zip` archives are accepted", + id = id.cyan() + )] + NotPep625Filename { + /// The ID of the package whose source distribution has a non-PEP 625-compliant filename. + id: PackageId, + }, /// Failed to parse a Git source URL. #[error("Failed to parse Git URL")] InvalidGitSourceUrl( diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index 8ee8fbda3c548..0d434b93bff47 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index 3b242e68d24bd..a20a50e50d394 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 07fccc488a539..8ee4bc89e3840 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index 259fff07b7fec..e227db2bfac03 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index 259fff07b7fec..e227db2bfac03 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap index d060457149752..9dcc6ba5388a8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_dynamic.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index 259fff07b7fec..e227db2bfac03 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 265bf990e64c6..c45d12e1035c3 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index 21ec255956805..60c46e7309670 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index e55db2db67ede..12d34d3f37593 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index 528fe289840dd..6d505cc702a1e 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -34,7 +34,7 @@ Ok( }, options: ResolverOptions { resolution_mode: Highest, - prerelease_mode: IfNecessaryOrExplicit, + prerelease_mode: IfNecessary, fork_strategy: RequiresPython, exclude_newer: ExcludeNewer { global: None, diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index 0143b8b305d02..3e8e0bdf2fcc4 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -123,8 +123,8 @@ impl Manifest { /// Return all requirements that affect manifest-wide candidate selection policy. /// /// Scoped overrides are included even when their scope is not selected. Whether a scoped - /// override applies is only known during resolution, after pre-release and yanked-version - /// policy has already been initialized. + /// override applies is only known during resolution, after yanked-version policy has already + /// been initialized. pub(crate) fn candidate_selection_requirements<'a>( &'a self, env: &'a ResolverEnvironment, diff --git a/crates/uv-resolver/src/prerelease.rs b/crates/uv-resolver/src/prerelease.rs index edcde0b2c77d7..ddb929b7a98e9 100644 --- a/crates/uv-resolver/src/prerelease.rs +++ b/crates/uv-resolver/src/prerelease.rs @@ -1,6 +1,8 @@ -use uv_distribution_types::RequirementSource; +use std::borrow::Cow; + +use uv_distribution_types::{Requirement, RequirementSource}; use uv_normalize::PackageName; -use uv_pep440::Operator; +use uv_pep440::{Operator, VersionSpecifiers}; use crate::resolver::ForkSet; use crate::{DependencyMode, Manifest, ResolverEnvironment}; @@ -16,19 +18,21 @@ pub enum PrereleaseMode { /// Allow all pre-release versions. Allow, - /// Allow pre-release versions if all versions of a package are pre-release. + /// Prefer stable versions, falling back to pre-release versions when necessary. + #[default] IfNecessary, - /// Allow pre-release versions for first-party packages with explicit pre-release markers in - /// their version requirements. + /// Prefer stable versions for first-party packages with explicit pre-release specifiers, + /// falling back to pre-release versions when necessary. Disallow pre-release versions for all + /// other packages. Explicit, - /// Allow pre-release versions if all versions of a package are pre-release, or if the package - /// has an explicit pre-release marker in its version requirements. - #[default] + /// Deprecated alias for `if-necessary`. + #[deprecated(note = "use `if-necessary` instead")] IfNecessaryOrExplicit, } +#[allow(deprecated)] impl std::fmt::Display for PrereleaseMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -51,94 +55,98 @@ pub(crate) enum PrereleaseStrategy { /// Allow all pre-release versions. Allow, - /// Allow pre-release versions if all versions of a package are pre-release. + /// Prefer stable versions, falling back to pre-release versions when necessary. IfNecessary, - /// Allow pre-release versions for first-party packages with explicit pre-release markers in - /// their version requirements. + /// Prefer stable versions for first-party packages with explicit pre-release specifiers, + /// falling back to pre-release versions when necessary. Disallow pre-release versions for all + /// other packages. Explicit(ForkSet), - - /// Allow pre-release versions if all versions of a package are pre-release, or if the package - /// has an explicit pre-release marker in its version requirements. - IfNecessaryOrExplicit(ForkSet), } impl PrereleaseStrategy { + #[allow(deprecated)] pub(crate) fn from_mode( mode: PrereleaseMode, manifest: &Manifest, env: &ResolverEnvironment, dependencies: DependencyMode, ) -> Self { - let mut packages = ForkSet::default(); - match mode { PrereleaseMode::Disallow => Self::Disallow, PrereleaseMode::Allow => Self::Allow, - PrereleaseMode::IfNecessary => Self::IfNecessary, - _ => { - for requirement in manifest.candidate_selection_requirements(env, dependencies) { - let RequirementSource::Registry { specifier, .. } = &requirement.source else { - continue; - }; - - if specifier - .iter() - .filter(|spec| { - !matches!(spec.operator(), Operator::NotEqual | Operator::NotEqualStar) - }) - .any(uv_pep440::VersionSpecifier::any_prerelease) - { - packages.add(&requirement, ()); - } - } + PrereleaseMode::IfNecessary | PrereleaseMode::IfNecessaryOrExplicit => { + Self::IfNecessary + } + PrereleaseMode::Explicit => Self::Explicit(Self::explicit_packages( + manifest.candidate_selection_requirements(env, dependencies), + )), + } + } - match mode { - PrereleaseMode::Explicit => Self::Explicit(packages), - PrereleaseMode::IfNecessaryOrExplicit => Self::IfNecessaryOrExplicit(packages), - _ => unreachable!(), - } + fn explicit_packages<'a>(requirements: impl Iterator>) -> ForkSet { + let mut packages = ForkSet::default(); + for requirement in requirements { + let RequirementSource::Registry { specifier, .. } = &requirement.source else { + continue; + }; + + if contains_prerelease(specifier) { + packages.add(&requirement, ()); } } + packages } - /// Returns `true` if a [`PackageName`] is allowed to have pre-release versions. - pub(crate) fn allows( + /// Returns the pre-release candidate selection policy for a package. + /// + /// Pre-releases remain in the candidate universe but, unless they are globally allowed, are + /// considered only after stable candidates. Keeping the candidate universe fixed is required + /// for PubGrub's learned incompatibilities to remain valid. + pub(crate) fn selection( &self, package_name: &PackageName, env: &ResolverEnvironment, - ) -> AllowPrerelease { + ) -> PrereleaseSelection { match self { - Self::Disallow => AllowPrerelease::No, - Self::Allow => AllowPrerelease::Yes, - Self::IfNecessary => AllowPrerelease::IfNecessary, + Self::Disallow => PrereleaseSelection::Disallow, + Self::Allow => PrereleaseSelection::Allow, + Self::IfNecessary => PrereleaseSelection::PreferStable, Self::Explicit(packages) => { if packages.contains(package_name, env) { - AllowPrerelease::Yes - } else { - AllowPrerelease::No - } - } - Self::IfNecessaryOrExplicit(packages) => { - if packages.contains(package_name, env) { - AllowPrerelease::Yes + PrereleaseSelection::PreferStable } else { - AllowPrerelease::IfNecessary + PrereleaseSelection::Disallow } } } } } -/// The pre-release strategy for a given package. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum AllowPrerelease { - /// Allow all pre-release versions. - Yes, - - /// Disallow all pre-release versions. - No, +/// Returns `true` if the specifiers explicitly mention a pre-release version. +/// +/// Exclusions do not opt a package into pre-releases. For example, `!=1.0a1` should not change +/// which candidate kinds are considered. +fn contains_prerelease(specifiers: &VersionSpecifiers) -> bool { + specifiers + .iter() + .filter(|specifier| { + !matches!( + specifier.operator(), + Operator::NotEqual | Operator::NotEqualStar + ) + }) + .any(uv_pep440::VersionSpecifier::any_prerelease) +} - /// Allow pre-release versions if all versions of this package are pre-release. - IfNecessary, +/// How pre-release candidates participate in version selection. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum PrereleaseSelection { + /// Do not consider pre-release candidates. + Disallow, + /// Consider stable and pre-release candidates in normal version order. + Allow, + /// Prefer stable candidates, falling back to pre-releases only after stable candidates are + /// exhausted. + PreferStable, } diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index f0380c27d49b6..2659a3cbbbdda 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -27,7 +27,7 @@ use crate::error::{ErrorTree, PrefixMatch}; use crate::exclude_newer::EffectiveExcludeNewerSource; use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; -use crate::prerelease::AllowPrerelease; +use crate::prerelease::PrereleaseSelection; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner, PubGrubPython, Range}; use crate::python_requirement::{PythonRequirement, PythonRequirementSource}; use crate::resolver::{ @@ -1324,7 +1324,7 @@ impl PubGrubReportFormatter<'_> { options: &Options, hints: &mut IndexSet, ) { - if selector.prerelease_strategy().allows(name, env) == AllowPrerelease::Yes { + if selector.prerelease_strategy().selection(name, env) != PrereleaseSelection::Disallow { return; } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 9957e9c4bc9c7..7f708729731a8 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -515,12 +515,10 @@ impl ResolverState ResolverState Self { + self.selected_versions.clear(); self.env = env; // If the fork contains a narrowed Python requirement, apply it. if let Some(req) = self.env.narrow_python_requirement(&self.python_requirement) { diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 7bf663e5e9a0b..8bd5d40174ce2 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -9,7 +9,7 @@ use tracing::{instrument, trace}; use uv_client::{FlatIndexEntry, OwnedArchive, SimpleDetailMetadata, VersionFiles}; use uv_configuration::BuildOptions; -use uv_distribution_filename::{DistFilename, WheelFilename}; +use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use uv_distribution_types::{ HashComparison, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, RegistryBuiltWheel, RegistrySourceDist, RequiresPython, SourceDistCompatibility, @@ -56,7 +56,6 @@ impl VersionMap { flat_index: Option, build_options: &BuildOptions, ) -> Self { - let mut stable = false; let mut local = false; let mut entries = Vec::with_capacity(simple_metadata.iter().size_hint().0); // Create stubs for each entry in simple metadata. The full conversion @@ -66,7 +65,6 @@ impl VersionMap { let version = rkyv::deserialize::(&datum.version) .expect("archived version always deserializes"); - stable |= version.is_stable(); local |= version.is_local(); debug_assert!( entries @@ -89,14 +87,12 @@ impl VersionMap { // If a set of flat distributions have been given, linearly merge the // already sorted flat entries with the archive-ordered simple vector. if let Some(flat_index) = flat_index { - stable |= flat_index.iter().any(|(version, _)| version.is_stable()); map = map.merge_flat(flat_index); } Self { inner: VersionMapInner::Lazy(VersionMapLazy { package_name: package_name.clone(), map, - stable, local, simple_metadata, no_binary: build_options.no_binary_package(package_name), @@ -119,20 +115,18 @@ impl VersionMap { hasher: &HashStrategy, build_options: &BuildOptions, ) -> Self { - let mut stable = false; let mut local = false; let mut map = BTreeMap::new(); for (version, prioritized_dist) in FlatDistributions::from_entries(flat_metadata, tags, hasher, build_options) { - stable |= version.is_stable(); local |= version.is_local(); map.insert(version, prioritized_dist); } Self { - inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }), + inner: VersionMapInner::Eager(VersionMapEager { map, local }), } } @@ -287,14 +281,6 @@ impl VersionMap { } } - /// Returns `true` if the map contains at least one stable (non-pre-release) version. - pub(crate) fn stable(&self) -> bool { - match self.inner { - VersionMapInner::Eager(ref map) => map.stable, - VersionMapInner::Lazy(ref map) => map.stable, - } - } - /// Returns `true` if the map contains at least one local version (e.g., `2.6.0+cpu`). pub(crate) fn local(&self) -> bool { match self.inner { @@ -306,11 +292,10 @@ impl VersionMap { impl From for VersionMap { fn from(flat_index: FlatDistributions) -> Self { - let stable = flat_index.iter().any(|(version, _)| version.is_stable()); let local = flat_index.iter().any(|(version, _)| version.is_local()); let map = flat_index.into(); Self { - inner: VersionMapInner::Eager(VersionMapEager { map, stable, local }), + inner: VersionMapInner::Eager(VersionMapEager { map, local }), } } } @@ -383,8 +368,6 @@ enum VersionMapInner { struct VersionMapEager { /// A map from version to distribution. map: BTreeMap, - /// Whether the version map contains at least one stable (non-pre-release) version. - stable: bool, /// Whether the version map contains at least one local version. local: bool, } @@ -495,8 +478,6 @@ struct VersionMapLazy { package_name: PackageName, /// An immutable archive-order index from version to possibly-initialized distribution. map: VersionMapLazyIndex, - /// Whether the version map contains at least one stable (non-pre-release) version. - stable: bool, /// Whether the version map contains at least one local version. local: bool, /// The raw simple metadata from which `PrioritizedDist`s should @@ -706,13 +687,13 @@ impl VersionMapLazy { filename, file: Box::new(file), index: self.index.clone(), + size_is_authoritative: false, }; priority_dist.insert_built(dist, hashes, compatibility); } DistFilename::SourceDistFilename(filename) => { let compatibility = self.source_dist_compatibility( - &filename.name, - &filename.version, + &filename, hashes.as_slice(), yanked, excluded, @@ -725,6 +706,7 @@ impl VersionMapLazy { file: Box::new(file), index: self.index.clone(), wheels: vec![], + size_is_authoritative: false, }; priority_dist.insert_source(dist, hashes, compatibility); } @@ -741,8 +723,7 @@ impl VersionMapLazy { fn source_dist_compatibility( &self, - name: &PackageName, - version: &Version, + filename: &SourceDistFilename, hashes: &[HashDigest], yanked: Option<&Yanked>, excluded: bool, @@ -762,15 +743,27 @@ impl VersionMapLazy { // Check if yanked if let Some(yanked) = yanked { - if yanked.is_yanked() && !self.allowed_yanks.contains(name, version) { + if yanked.is_yanked() + && !self + .allowed_yanks + .contains(&filename.name, &filename.version) + { return SourceDistCompatibility::Incompatible(IncompatibleSource::Yanked( yanked.clone(), )); } } + // Check if the filename is PEP 625-compliant. + // TODO: Strengthen this check more; right now we allow `.zip` + // (which is not compliant) and we don't strictly + // enforce the formatting rules for the name or version. + if !filename.extension.is_pep625_compliant() { + return SourceDistCompatibility::Incompatible(IncompatibleSource::NotPep625Filename); + } + // Check if hashes line up. If hashes aren't required, they're considered matching. - let hash_policy = self.hasher.get_package(name, version); + let hash_policy = self.hasher.get_package(&filename.name, &filename.version); let required_hashes = hash_policy.digests(); let hash = if required_hashes.is_empty() { HashComparison::Matched diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 03852f7650f11..bd00fc00d5cef 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -510,6 +510,49 @@ impl TryFrom for GlobalOptions { } } +/// Resolve registry indexes and find-links relative to the given root directory. +fn rebase_indexes( + root_dir: &Path, + indexes: &mut Option>, + index_url: &mut Option, + extra_index_urls: &mut Option>, + find_links: &mut Option>, +) -> Result<(), IndexUrlError> { + *indexes = indexes + .take() + .map(|indexes| { + indexes + .into_iter() + .map(|index| index.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + *index_url = index_url + .take() + .map(|index| index.relative_to(root_dir)) + .transpose()?; + *extra_index_urls = extra_index_urls + .take() + .map(|indexes| { + indexes + .into_iter() + .map(|index| index.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + *find_links = find_links + .take() + .map(|find_links| { + find_links + .into_iter() + .map(|find_link| find_link.relative_to(root_dir)) + .collect::, _>>() + }) + .transpose()?; + + Ok(()) +} + /// Settings relevant to all installer operations. #[derive(Debug, Clone, Default, CombineOptions)] pub struct InstallerOptions { @@ -566,6 +609,21 @@ pub struct ResolverOptions { pub no_sources_package: Option>, } +impl ResolverOptions { + /// Resolve the [`ResolverOptions`] relative to the given root directory. + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) + } +} + /// Shared settings, relevant to all operations that must resolve and install dependencies. The /// union of [`InstallerOptions`] and [`ResolverOptions`]. #[derive(Debug, Clone, Default, CombineOptions)] @@ -601,6 +659,21 @@ pub struct ResolverInstallerOptions { pub no_binary_package: Option>, } +impl ResolverInstallerOptions { + /// Resolve the [`ResolverInstallerOptions`] relative to the given root directory. + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) + } +} + impl From for ResolverInstallerOptions { fn from(value: ResolverInstallerSchema) -> Self { let ResolverInstallerSchema { @@ -684,41 +757,16 @@ impl From for ResolverInstallerOptions { impl ResolverInstallerSchema { /// Resolve the [`ResolverInstallerSchema`] relative to the given root directory. - fn relative_to(self, root_dir: &Path) -> Result { - Ok(Self { - index: self - .index - .map(|index| { - index - .into_iter() - .map(|index| index.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - index_url: self - .index_url - .map(|index_url| index_url.relative_to(root_dir)) - .transpose()?, - extra_index_url: self - .extra_index_url - .map(|extra_index_url| { - extra_index_url - .into_iter() - .map(|extra_index_url| extra_index_url.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - find_links: self - .find_links - .map(|find_links| { - find_links - .into_iter() - .map(|find_link| find_link.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - ..self - }) + fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) } } @@ -870,11 +918,11 @@ pub struct ResolverInstallerSchema { pub resolution: Option, /// The strategy to use when considering pre-release versions. /// - /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, - /// along with first-party requirements that contain an explicit pre-release marker in the - /// declared specifiers (`if-necessary-or-explicit`). + /// By default, uv will prefer stable candidates, falling back to pre-releases only after every + /// stable candidate that satisfies the active constraints is rejected + /// (`if-necessary`). #[option( - default = "\"if-necessary-or-explicit\"", + default = "\"if-necessary\"", value_type = "str", example = r#" prerelease = "allow" @@ -1619,11 +1667,11 @@ pub struct PipOptions { pub resolution: Option, /// The strategy to use when considering pre-release versions. /// - /// By default, uv will accept pre-releases for packages that _only_ publish pre-releases, - /// along with first-party requirements that contain an explicit pre-release marker in the - /// declared specifiers (`if-necessary-or-explicit`). + /// By default, uv will prefer stable candidates, falling back to pre-releases only after every + /// stable candidate that satisfies the active constraints is rejected + /// (`if-necessary`). #[option( - default = "\"if-necessary-or-explicit\"", + default = "\"if-necessary\"", value_type = "str", example = r#" prerelease = "allow" @@ -2079,41 +2127,16 @@ pub struct PipOptions { impl PipOptions { /// Resolve the [`PipOptions`] relative to the given root directory. - fn relative_to(self, root_dir: &Path) -> Result { - Ok(Self { - index: self - .index - .map(|index| { - index - .into_iter() - .map(|index| index.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - index_url: self - .index_url - .map(|index_url| index_url.relative_to(root_dir)) - .transpose()?, - extra_index_url: self - .extra_index_url - .map(|extra_index_url| { - extra_index_url - .into_iter() - .map(|extra_index_url| extra_index_url.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - find_links: self - .find_links - .map(|find_links| { - find_links - .into_iter() - .map(|find_link| find_link.relative_to(root_dir)) - .collect::, _>>() - }) - .transpose()?, - ..self - }) + pub fn relative_to(mut self, root_dir: &Path) -> Result { + rebase_indexes( + root_dir, + &mut self.index, + &mut self.index_url, + &mut self.extra_index_url, + &mut self.find_links, + )?; + + Ok(self) } } diff --git a/crates/uv-test/src/lib.rs b/crates/uv-test/src/lib.rs index 6c55d6964d08d..680698b36bde4 100755 --- a/crates/uv-test/src/lib.rs +++ b/crates/uv-test/src/lib.rs @@ -114,6 +114,11 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[ ), // Trim end-of-line whitespaces, to allow removing them on save. (r"([^\s])[ \t]+(\r?\n)", "$1$2"), + // Certificate overrides and their contents depend on the host environment. + ( + r"(?ms)^([ \t]*custom_certificates: )(?:None|Some\(\n.*?^[ \t]*\),\n[ \t]*\)),", + "${1}[CERTIFICATES],", + ), // Filter SSL certificate loading debug messages (environment-dependent) (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""), ]; diff --git a/crates/uv-toml/Cargo.toml b/crates/uv-toml/Cargo.toml index 3872304d089fd..1b1aaa0ec7431 100644 --- a/crates/uv-toml/Cargo.toml +++ b/crates/uv-toml/Cargo.toml @@ -17,5 +17,3 @@ workspace = true [dependencies] serde = { workspace = true } -toml_datetime = { workspace = true } -toml_parser = { workspace = true } diff --git a/crates/uv-toml/src/lib.rs b/crates/uv-toml/src/lib.rs index 2e096ea349023..e0022c3051d29 100644 --- a/crates/uv-toml/src/lib.rs +++ b/crates/uv-toml/src/lib.rs @@ -1,11 +1,6 @@ use std::collections::BTreeMap; use serde::{Deserialize, Deserializer}; -use toml_datetime::Datetime; -use toml_parser::decoder::Encoding; -use toml_parser::lexer::Token; -use toml_parser::parser::{EventReceiver, parse_document}; -use toml_parser::{ErrorSink, Source, Span}; /// Deserialize a map while ensuring all keys are unique. pub fn deserialize_unique_map<'de, D, K, V, F>( @@ -55,310 +50,3 @@ where deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData)) } - -/// Detect TOML 1.1 specific features in a TOML document. -/// -/// Note: This function does _not_ perform any validation. -pub fn has_toml11_features(source: &str) -> bool { - let tokens: Box<[Token]> = Source::new(source).lex().collect(); - let mut checker = DetectToml11::new(source); - let mut errors = None; - parse_document(&tokens, &mut checker, &mut errors); - checker.is_11() -} - -/// Structure state in a TOML document -#[derive(Debug, Copy, Clone)] -enum State { - /// Regular table (e.g. `[foo]`) - StdTable, - /// Array table (e.g. `[[foo]]`) - ArrayTable, - /// Inline table (e.g. `{ k = "v" }` - InlineTable { trailing_sep: bool }, - /// Array (e.g. `[1, 2, 3]`) - Array, -} - -/// Detect TOML 1.1 specific features. -pub struct DetectToml11<'s> { - /// The underlying TOML source - source: &'s str, - /// Current nesting state - state: Vec, - /// Set to true when a TOML 1.1 specific feature is seen - toml11: bool, -} - -impl<'s> DetectToml11<'s> { - fn new(source: &'s str) -> Self { - Self { - source, - state: Vec::new(), - toml11: false, - } - } - - fn raw_at(&self, span: Span) -> &'s str { - &self.source[span.start()..span.end()] - } - - fn flag_11(&mut self) { - self.toml11 = true; - } - - fn set_sep(&mut self, sep: bool) { - if let Some(State::InlineTable { trailing_sep }) = self.state.last_mut() { - *trailing_sep = sep; - } - } - - fn is_11(&self) -> bool { - self.toml11 - } -} - -impl EventReceiver for DetectToml11<'_> { - fn std_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.state.push(State::StdTable); - } - - fn std_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.state.pop(); - } - - fn array_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.state.push(State::ArrayTable); - } - - fn array_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.state.pop(); - } - - fn inline_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool { - self.state.push(State::InlineTable { - trailing_sep: false, - }); - true - } - - fn inline_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - if matches!( - self.state.last(), - Some(State::InlineTable { trailing_sep: true }) - ) { - // TOML 1.1 introduces trailing commas in inline tables - self.flag_11(); - } - self.state.pop(); - } - - fn array_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool { - self.state.push(State::Array); - true - } - - fn array_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.state.pop(); - } - - fn simple_key(&mut self, span: Span, kind: Option, _error: &mut dyn ErrorSink) { - self.set_sep(false); - - if matches!(kind, Some(Encoding::BasicString | Encoding::MlBasicString)) - && has_toml11_escapes(self.raw_at(span)) - { - // TOML 1.1 introduces new escape sequences - self.flag_11(); - } - } - - fn scalar(&mut self, span: Span, kind: Option, _error: &mut dyn ErrorSink) { - self.set_sep(false); - - if matches!(kind, Some(Encoding::BasicString | Encoding::MlBasicString)) { - if has_toml11_escapes(self.raw_at(span)) { - // TOML 1.1 introduces new escape sequences - self.flag_11(); - } - } else if has_toml11_optional_second_time(self.raw_at(span)) { - // TOML 1.1 makes seconds optional in times and datetimes. - self.flag_11(); - } - } - - fn value_sep(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - self.set_sep(true); - } - - fn newline(&mut self, _span: Span, _error: &mut dyn ErrorSink) { - if matches!(self.state.last(), Some(State::InlineTable { .. })) { - // TOML 1.1 introduces newlines in inline tables - self.flag_11(); - } - } -} - -/// Scan the characters of a snippet of TOML representing a basic string for the TOML 1.1 exclusive -/// escape sequences: `\xHH` and `\e` -fn has_toml11_escapes(raw: &str) -> bool { - let mut chars = raw.chars(); - while let Some(c) = chars.next() { - if c == '\\' - && let Some(c) = chars.next() - && matches!(c, 'x' | 'e') - { - return true; - } - } - false -} - -/// Scan for the TOML 1.1 optional-second time syntax, such as `12:34` and `1969-06-20T20:17Z`. -fn has_toml11_optional_second_time(raw: &str) -> bool { - // Non-datetime scalars, such as booleans and integers, fail to parse as date and/or time. - let Ok(datetime) = raw.parse::() else { - return false; - }; - - datetime - .time - .as_ref() - .is_some_and(|time| time.second.is_none()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn escapes_plain_string() { - assert!(!has_toml11_escapes(r#""hello world""#)); - } - - #[test] - fn escapes_toml10_escape_n() { - assert!(!has_toml11_escapes(r#""hello\nworld""#)); - } - - #[test] - fn escapes_toml10_escape_u() { - assert!(!has_toml11_escapes(r#""r\u00E9sum\u00E9""#)); - } - - #[test] - fn escapes_toml11_hex() { - assert!(has_toml11_escapes(r#""val \x41""#)); - } - - #[test] - fn escapes_toml11_esc() { - assert!(has_toml11_escapes(r#""val \e""#)); - } - - #[test] - fn escapes_double_backslash_e() { - assert!(!has_toml11_escapes(r#""\\e""#)); - } - - #[test] - fn escapes_double_backslash_x() { - assert!(!has_toml11_escapes(r#""\\x41""#)); - } - - #[test] - fn features_plain_toml10() { - assert!(!has_toml11_features("x = 1\ny = \"hello\"\nz = true\n")); - } - - #[test] - fn features_std_table() { - assert!(!has_toml11_features( - "[server]\nhost = \"localhost\"\nport = 8080\n" - )); - } - - #[test] - fn features_array_of_tables() { - assert!(!has_toml11_features( - "[[items]]\nname = \"a\"\n[[items]]\nname = \"b\"\n" - )); - } - - #[test] - fn features_inline_table_no_trailing_comma() { - assert!(!has_toml11_features("x = {a = 1, b = 2}\n")); - } - - #[test] - fn features_trailing_comma_in_inline_table() { - assert!(has_toml11_features("x = {a = 1, b = 2,}\n")); - } - - #[test] - fn features_multiline_inline_table() { - assert!(has_toml11_features("x = {\n a = 1\n}\n")); - } - - #[test] - fn features_multiline_inline_table_with_trailing_comma() { - assert!(has_toml11_features("x = {\n a = 1,\n}\n")); - } - - #[test] - fn features_hex_escape() { - assert!(has_toml11_features("x = \"val \\x41\"\n")); - } - - #[test] - fn features_hex_escape_in_quoted_key() { - assert!(has_toml11_features("\"\\x62ar\" = \"baz\"\n")); - } - - #[test] - fn features_hex_escape_in_dotted_quoted_key() { - assert!(has_toml11_features("foo.\"\\x62ar\" = \"baz\"\n")); - } - - #[test] - fn features_esc_escape() { - assert!(has_toml11_features("x = \"val \\e\"\n")); - } - - #[test] - fn features_double_backslash_not_escape() { - assert!(!has_toml11_features("x = \"\\\\e\"\n")); - } - - #[test] - fn features_toml10_escape_in_value() { - assert!(!has_toml11_features("x = \"tab\\there\"\n")); - } - - #[test] - fn features_escape_in_nested_structure() { - assert!(has_toml11_features("[t]\na = {b = \"\\x20\",}\n")); - } - - #[test] - fn features_trailing_comma_in_array_is_not_11() { - assert!(!has_toml11_features("x = [1, 2, 3,]\n")); - } - - #[test] - fn features_optional_second_time_values() { - assert!(has_toml11_features("x = 20:17\n")); - assert!(has_toml11_features("x = 1969-06-20T20:17\n")); - assert!(has_toml11_features("x = 1969-06-20 20:17\n")); - assert!(has_toml11_features("x = 1969-06-20T20:17Z\n")); - assert!(has_toml11_features("x = 1969-06-20T20:17z\n")); - assert!(has_toml11_features("x = 1969-06-20T20:17-07:00\n")); - } - - #[test] - fn features_toml10_time_values_are_not_11() { - assert!(!has_toml11_features("x = 20:17:00\n")); - assert!(!has_toml11_features("x = 1969-06-20T20:17:00Z\n")); - assert!(!has_toml11_features("x = 1969-06-20\n")); - } -} diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 7f8a34d713de7..1369e59ac2e2c 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -28,7 +28,6 @@ uv-pypi-types = { workspace = true } uv-python = { workspace = true } uv-shell = { workspace = true } uv-version = { workspace = true } -uv-warnings = { workspace = true } console = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 922d620785bb3..d187504a7a544 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -25,7 +25,6 @@ use uv_python::managed::{ use uv_python::{Interpreter, VirtualEnvironment}; use uv_shell::escape_posix_for_single_quotes; use uv_version::version; -use uv_warnings::warn_user_once; /// Activation scripts for the environment, with dependent paths templated out. const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[ @@ -149,16 +148,6 @@ pub(crate) fn create( { match clear_non_virtualenv { ClearNonVirtualenv::Allow => {} - ClearNonVirtualenv::Warn => { - warn_user_once!( - "The `--clear` option will remove the existing directory at `{}` \ - even though it is not a virtual environment. \ - This will become an error in a future release. \ - Use `--force` to suppress this warning, or \ - `--preview-features venv-safe-clear` to error on this now.", - location.user_display() - ); - } ClearNonVirtualenv::Error => { return Err(Error::ClearNonVirtualenv { path: location.to_path_buf(), @@ -644,8 +633,6 @@ fn confirm_clear(location: &Path, name: &'static str) -> Result, io pub enum ClearNonVirtualenv { /// Allow clearing a non-virtual environment directory. Allow, - /// Warn before clearing a non-virtual environment directory. - Warn, /// Refuse to clear a non-virtual environment directory. Error, } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 3af5e246f7a85..cfa3b3b18c3e7 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -22,7 +22,7 @@ use tracing::instrument; use uv_build_backend::BuildBackendSettings; use uv_configuration::{ExcludeDependency, GitLfsSetting, Override}; use uv_distribution_types::{Index, IndexName, RequirementSource}; -use uv_fs::{PortablePathBuf, relative_to}; +use uv_fs::{PortablePathBuf, try_relative_to_if}; use uv_git_types::GitReference; use uv_macros::OptionsMetadata; use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName}; @@ -1772,12 +1772,13 @@ impl Source { } } RequirementSource::Registry { index: None, .. } => return Ok(None), - RequirementSource::Path { install_path, .. } => Self::Path { + RequirementSource::Path { + install_path, url, .. + } => Self::Path { editable: None, package: None, path: PortablePathBuf::from( - relative_to(&install_path, root) - .or_else(|_| std::path::absolute(&install_path)) + try_relative_to_if(&install_path, root, !url.was_given_absolute()) .map_err(SourceError::Absolute)? .into_boxed_path(), ), @@ -1788,13 +1789,13 @@ impl Source { RequirementSource::Directory { install_path, editable: is_editable, + url, .. } => Self::Path { editable: editable.or(is_editable), package: None, path: PortablePathBuf::from( - relative_to(&install_path, root) - .or_else(|_| std::path::absolute(&install_path)) + try_relative_to_if(&install_path, root, !url.was_given_absolute()) .map_err(SourceError::Absolute)? .into_boxed_path(), ), diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index a619625d2ab50..964a188d58331 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -160,7 +160,7 @@ nix = { workspace = true } uv-unix = { workspace = true } [features] -default = ["performance", "uv-distribution/static", "test-defaults"] +default = ["performance", "test-defaults"] native-auth = [] # Use better memory allocators, etc. performance = ["performance-memory-allocator"] diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 433e7412a1e71..f0f7c9c758535 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -711,6 +711,13 @@ async fn build_package( } }; + if matches!(build_action, BuildAction::DirectBuild | BuildAction::List) { + debug!( + "Using bundled `uv_build` backend for `{}`", + source.path().user_display() + ); + } + // Prepare some common arguments for the build. let dist = None; let subdirectory = None; @@ -773,7 +780,7 @@ async fn build_package( let ext = SourceDistExtension::from_path(path.as_path()) .map_err(|err| Error::InvalidSourceDistExt(path.user_display().to_string(), err))?; let temp_dir = tempfile::tempdir_in(cache.bucket(CacheBucket::SourceDistributions))?; - uv_extract::stream::archive(path.display(), reader, ext, temp_dir.path()).await?; + uv_extract::stream::archive(reader, ext, temp_dir.path()).await?; // Extract the top-level directory from the archive. let extracted = match uv_extract::strip_component(temp_dir.path()) { @@ -880,8 +887,7 @@ async fn build_package( Error::InvalidSourceDistExt(source.path().user_display().to_string(), err) })?; let temp_dir = tempfile::tempdir_in(&output_dir)?; - uv_extract::stream::archive(source.path().display(), reader, ext, temp_dir.path()) - .await?; + uv_extract::stream::archive(reader, ext, temp_dir.path()).await?; // If the source distribution has a normalized filename, check its identity. let source_dist = source @@ -985,7 +991,7 @@ async fn build_sdist( printer.stderr(), "{}", format!( - "{}Building {} (uv build backend)...", + "{}Building {}...", source.message_prefix(), build_kind_message ) @@ -1094,7 +1100,7 @@ async fn build_wheel( printer.stderr(), "{}", format!( - "{}Building {} (uv build backend)...", + "{}Building {}...", source.message_prefix(), build_kind_message ) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 6579cfc7664d6..5bfb0290b7103 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -159,7 +159,7 @@ pub(crate) async fn pip_compile( { if !is_pylock_toml(file_name) { return Err(anyhow!( - "Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `{file_name}` won't be recognized as a `pylock.toml` file in subsequent commands", + "Expected the output filename to be `pylock.toml` or `pylock..toml`, where `` is non-empty and contains no dots; found `{file_name}`", )); } } @@ -213,6 +213,7 @@ pub(crate) async fn pip_compile( index_url, extra_index_urls, no_index, + require_hashes: _, find_links, no_binary, no_build, @@ -775,7 +776,7 @@ pub(crate) async fn pip_compile( .into_iter() .filter(|name| resolution.contains(name)) .collect::>(); - if !excluded.is_empty() { + if include_annotations && !excluded.is_empty() { writeln!(writer)?; writeln!( writer, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index f771fe1f36b12..77b3a257b05c7 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -151,6 +151,7 @@ pub(crate) async fn pip_install( index_url, extra_index_urls, no_index, + require_hashes, find_links, no_binary, no_build, @@ -168,6 +169,8 @@ pub(crate) async fn pip_install( override_dependencies.extend(overrides_from_workspace); + let hash_checking = HashCheckingMode::from_requirements_txt(hash_checking, require_hashes); + if pylock.is_some() { if !preview.is_enabled(PreviewFeature::Pylock) { warn_user!( diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 2c677a2b32e5f..19f1c8c7232f4 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -122,6 +122,7 @@ pub(crate) async fn pip_sync( index_url, extra_index_urls, no_index, + require_hashes, find_links, no_binary, no_build, @@ -137,6 +138,8 @@ pub(crate) async fn pip_sync( ) .await?; + let hash_checking = HashCheckingMode::from_requirements_txt(hash_checking, require_hashes); + if pylock.is_some() { if !preview.is_enabled(PreviewFeature::Pylock) { warn_user!( diff --git a/crates/uv/src/commands/project/export.rs b/crates/uv/src/commands/project/export.rs index c3cd9c06d652b..156ea24ca55d7 100644 --- a/crates/uv/src/commands/project/export.rs +++ b/crates/uv/src/commands/project/export.rs @@ -360,7 +360,7 @@ pub(crate) async fn export( { if !is_pylock_toml(file_name) { return Err(anyhow!( - "Expected the output filename to start with `pylock.` and end with `.toml` (e.g., `pylock.toml`, `pylock.dev.toml`); `{file_name}` won't be recognized as a `pylock.toml` file in subsequent commands", + "Expected the output filename to be `pylock.toml` or `pylock..toml`, where `` is non-empty and contains no dots; found `{file_name}`", )); } } diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 4cef932c0cf05..c9dc1368f3c1f 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use std::process::Stdio; use std::str::FromStr; -use anyhow::{Context, Result, anyhow, bail}; +use anyhow::{Context, Result, bail}; use owo_colors::OwoColorize; use toml_edit::{InlineTable, Value}; use tracing::{debug, trace, warn}; @@ -45,7 +45,6 @@ pub(crate) async fn init( project_dir: &Path, explicit_path: Option, name: Option, - package: bool, init_kind: InitKind, bare: bool, description: Option, @@ -85,7 +84,6 @@ pub(crate) async fn init( no_readme, author_from, pin_python, - package, config_discovery, ) .await?; @@ -144,7 +142,6 @@ pub(crate) async fn init( Box::pin(init_project( &path, &name, - package, project_kind, bare, description, @@ -212,7 +209,6 @@ async fn init_script( no_readme: bool, author_from: Option, pin_python: bool, - package: bool, config_discovery: ConfigDiscovery, ) -> Result<()> { if no_workspace { @@ -224,10 +220,6 @@ async fn init_script( if author_from.is_some() { warn_user_once!("`--author-from` is a no-op for Python scripts, which are standalone"); } - if package { - warn_user_once!("`--package` is a no-op for Python scripts, which are standalone"); - } - let reporter = PythonDownloadReporter::single(printer); // If the file already exists, read its content. @@ -283,8 +275,6 @@ async fn init_script( async fn init_project( path: &Path, name: &PackageName, - // TODO(konsti): Remove when stabilizing. - package: bool, project_kind: InitProjectKind, bare: bool, description: Option, @@ -416,7 +406,6 @@ async fn init_project( build_backend, author_from, no_readme, - package, )?; if let Some(workspace) = workspace { @@ -742,203 +731,10 @@ pub(crate) enum InitProjectKind { /// Initialize only a `pyproject.toml` with `[build-system]` table (but without associated /// source files). BareWithBuildSystem, - // TODO(konsti): Remove when stabilizing. - /// Initialize a Python application. - ApplicationOld, - // TODO(konsti): Remove when stabilizing. - /// Initialize a Python library. - LibraryOld, } impl InitProjectKind { /// Initialize this project kind at the target path. - // TODO(konsti): Remove when stabilizing packaged-init. - #[expect(clippy::fn_params_excessive_bools)] - fn init_old( - self, - name: &PackageName, - path: &Path, - requires_python: &RequiresPython, - description: Option<&str>, - no_description: bool, - bare: bool, - vcs: Option, - build_backend: Option, - author_from: Option, - no_readme: bool, - package: bool, - ) -> Result<()> { - match self { - Self::ApplicationOld => Self::init_application_old( - name, - path, - requires_python, - description, - no_description, - bare, - vcs, - build_backend, - author_from, - no_readme, - package, - ), - Self::LibraryOld => Self::init_library_old( - name, - path, - requires_python, - description, - no_description, - bare, - vcs, - build_backend, - author_from, - no_readme, - package, - ), - _ => unreachable!(), - } - } - - /// Initialize a Python application at the target path. - // TODO(konsti): Remove when stabilizing packaged-init. - #[expect(clippy::fn_params_excessive_bools)] - fn init_application_old( - name: &PackageName, - path: &Path, - requires_python: &RequiresPython, - description: Option<&str>, - no_description: bool, - bare: bool, - vcs: Option, - build_backend: Option, - author_from: Option, - no_readme: bool, - package: bool, - ) -> Result<()> { - fs_err::create_dir_all(path)?; - - // Initialize the version control system first so that Git configuration can properly - // read conditional includes that depend on the repository path. - init_vcs(path, vcs)?; - - // Do not fill in `authors` for non-packaged applications unless explicitly requested. - let author_from = author_from.unwrap_or_else(|| { - if package { - AuthorFrom::default() - } else { - AuthorFrom::None - } - }); - let author = get_author_info(path, author_from); - - // Create the `pyproject.toml` - let mut pyproject = pyproject_project( - name, - requires_python, - author.as_ref(), - description, - no_description, - no_readme || bare, - ); - - // Include additional project configuration for packaged applications - if package { - // Since it'll be packaged, we can add a `[project.scripts]` entry - if !bare { - pyproject.push('\n'); - pyproject.push_str(&pyproject_project_scripts(name, name.as_str(), "main")); - } - - // Add a build system - let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv); - pyproject.push('\n'); - pyproject.push_str(&pyproject_build_system(name, build_backend)); - pyproject_build_backend_prerequisites(name, path, build_backend)?; - - if !bare { - // Generate `src` files - generate_package_scripts(name, path, build_backend, false)?; - } - } else { - // Create `main.py` if it doesn't exist - // (This isn't intended to be a particularly special or magical filename, just nice) - // TODO(zanieb): Only create `main.py` if there are no other Python files? - let main_py = path.join("main.py"); - if !main_py.try_exists()? && !bare { - fs_err::write( - path.join("main.py"), - indoc::formatdoc! {r#" - def main(): - print("Hello from {name}!") - - - if __name__ == "__main__": - main() - "#}, - )?; - } - } - fs_err::write(path.join("pyproject.toml"), pyproject)?; - - Ok(()) - } - - /// Initialize a library project at the target path. - // TODO(konsti): Remove when stabilizing packaged-init. - #[expect(clippy::fn_params_excessive_bools)] - fn init_library_old( - name: &PackageName, - path: &Path, - requires_python: &RequiresPython, - description: Option<&str>, - no_description: bool, - bare: bool, - vcs: Option, - build_backend: Option, - author_from: Option, - no_readme: bool, - package: bool, - ) -> Result<()> { - if !package { - return Err(anyhow!("Library projects must be packaged")); - } - - fs_err::create_dir_all(path)?; - - // Initialize the version control system first so that Git configuration can properly - // read conditional includes that depend on the repository path. - init_vcs(path, vcs)?; - - let author = get_author_info(path, author_from.unwrap_or_default()); - - // Create the `pyproject.toml` - let mut pyproject = pyproject_project( - name, - requires_python, - author.as_ref(), - description, - no_description, - no_readme || bare, - ); - - // Always include a build system if the project is packaged. - let build_backend = build_backend.unwrap_or(ProjectBuildBackend::Uv); - pyproject.push('\n'); - pyproject.push_str(&pyproject_build_system(name, build_backend)); - pyproject_build_backend_prerequisites(name, path, build_backend)?; - - fs_err::write(path.join("pyproject.toml"), pyproject)?; - - // Generate `src` files - if !bare { - generate_package_scripts(name, path, build_backend, true)?; - } - - Ok(()) - } - - /// Initialize this project kind at the target path. - #[expect(clippy::fn_params_excessive_bools)] fn init( self, name: &PackageName, @@ -951,25 +747,7 @@ impl InitProjectKind { build_backend: Option, author_from: Option, no_readme: bool, - package: bool, ) -> Result<()> { - // TODO(konsti): Remove when stabilizing. - if matches!(self, Self::ApplicationOld | Self::LibraryOld) { - return self.init_old( - name, - path, - requires_python, - description, - no_description, - bare, - vcs, - build_backend, - author_from, - no_readme, - package, - ); - } - fs_err::create_dir_all(path)?; // Initialize the version control system first so that Git configuration can properly @@ -982,7 +760,6 @@ impl InitProjectKind { AuthorFrom::default() } Self::Application | Self::Bare => AuthorFrom::None, - Self::ApplicationOld | Self::LibraryOld => unreachable!(), }); let author = get_author_info(path, author_from); @@ -1047,7 +824,6 @@ impl InitProjectKind { // Generate `src` files generate_package_scripts(name, path, build_backend, true)?; } - _ => unreachable!(), } fs_err::write(path.join("pyproject.toml"), pyproject)?; Ok(()) diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 4b32d17f51fcc..dfc9e1ac0bd6b 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -371,6 +371,10 @@ impl<'env> LockOperation<'env> { /// Perform a [`LockOperation`]. pub(crate) async fn execute(self, target: LockTarget<'_>) -> Result { + if !matches!(&self.mode, LockMode::Frozen(_)) { + target.validate_upgrade_groups(&self.settings.upgrade)?; + } + match self.mode { LockMode::Frozen(source) => { // Read the existing lockfile, but don't attempt to lock the project. @@ -953,6 +957,11 @@ async fn do_lock( // metadata that cannot be obtained under `--no-build`. return Err(ProjectError::Lock(err)); } + Err(ProjectError::Lock(err)) if err.is_not_pep625() => { + // A non-PEP 625-compliant sdist in the lockfile will also be rejected by a fresh + // resolve, so short-circuit rather than doing the extra work. + return Err(ProjectError::Lock(err)); + } Err(err) => { warn_user!("Failed to validate existing lockfile: {err}"); None diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index 5592f4c56adc9..7231f7c4adee1 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -1,13 +1,15 @@ -use itertools::Either; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; + +use itertools::Either; +use rustc_hash::FxHashSet; use toml_parser::Source; use toml_parser::lexer::TokenKind; use tracing::info_span; use uv_auth::CredentialsCache; use uv_cache::Cache; -use uv_configuration::{DependencyGroupsWithDefaults, ExcludeDependency, NoSources}; +use uv_configuration::{DependencyGroupsWithDefaults, ExcludeDependency, NoSources, Upgrade}; use uv_distribution::LoweredRequirement; use uv_distribution_types::{Index, IndexLocations, Requirement, RequiresPython}; use uv_normalize::{GroupName, PackageName}; @@ -15,7 +17,9 @@ use uv_pep508::RequirementOrigin; use uv_pypi_types::{Conflicts, SupportedEnvironments, VerbatimParsedUrl}; use uv_resolver::{Lock, LockVersion, VERSION}; use uv_scripts::Pep723Script; -use uv_workspace::dependency_groups::{DependencyGroupError, FlatDependencyGroup}; +use uv_workspace::dependency_groups::{ + DependencyGroupError, FlatDependencyGroup, FlatDependencyGroups, +}; use uv_workspace::pyproject::OverrideDependency; use uv_workspace::{Editability, Workspace, WorkspaceCache, WorkspaceMember}; @@ -129,6 +133,47 @@ impl<'lock> LockTarget<'lock> { } } + /// Validate the dependency groups requested by `--upgrade-group`. + pub(crate) fn validate_upgrade_groups(self, upgrade: &Upgrade) -> Result<(), ProjectError> { + let Some(groups) = upgrade.groups() else { + return Ok(()); + }; + + match self { + Self::Workspace(workspace) => { + let mut known_groups = FxHashSet::default(); + for member in workspace.packages().values() { + known_groups.extend( + FlatDependencyGroups::from_pyproject_toml( + member.root(), + member.pyproject_toml(), + )? + .into_iter() + .map(|(group, _)| group), + ); + } + known_groups.extend(workspace.workspace_dependency_groups()?.into_keys()); + + for group in groups { + if !known_groups.contains(group) { + return if workspace.packages().len() == 1 && !workspace.is_non_project() { + Err(ProjectError::MissingGroupProject(group.clone())) + } else { + Err(ProjectError::MissingGroupProjects(group.clone())) + }; + } + } + } + Self::Script(_) => { + if let Some(group) = groups.iter().next() { + return Err(ProjectError::MissingGroupScript(group.clone())); + } + } + } + + Ok(()) + } + /// Returns the set of all members within the target. pub(crate) fn members_requirements(self) -> impl Iterator + 'lock { match self { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index d915ec1b83d20..0d2a6ff828bac 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -49,6 +49,7 @@ use uv_types::SourceTreeEditablePolicy; use uv_warnings::warn_user; use uv_workspace::{DiscoveryOptions, VirtualProject, WorkspaceCache, WorkspaceErrorKind}; +use crate::base_client_builder; use crate::child::run_to_completion; /// GitHub Gist API response structure @@ -1472,19 +1473,8 @@ impl ParsedRunCommand { Ok((script, run_command)) } Self::PendingRemote(remote_command) => { - let settings = GlobalSettings::resolve(global_args, filesystem, environment)?; - let client_builder = BaseClientBuilder::new( - settings.network_settings.connectivity, - settings.network_settings.system_certs, - settings.network_settings.allow_insecure_host, - settings.preview, - settings.network_settings.read_timeout, - settings.network_settings.connect_timeout, - settings.network_settings.retries, - ) - .http_proxy(settings.network_settings.http_proxy) - .https_proxy(settings.network_settings.https_proxy) - .no_proxy(settings.network_settings.no_proxy); + let settings = GlobalSettings::resolve(global_args, filesystem, environment, None)?; + let client_builder = base_client_builder(&settings); let (url, downloaded_script, args) = remote_command.download(&client_builder).await?; diff --git a/crates/uv/src/commands/publish.rs b/crates/uv/src/commands/publish.rs index 8bfcac0cb56ff..2f72d2f0670e5 100644 --- a/crates/uv/src/commands/publish.rs +++ b/crates/uv/src/commands/publish.rs @@ -208,19 +208,11 @@ pub(crate) async fn publish( // Check if the filename is normalized (e.g., version `2025.09.4` should be `2025.9.4`). let normalized_filename = group.filename.to_string(); if group.raw_filename != normalized_filename { - if preview.is_enabled(PreviewFeature::PublishRequireNormalized) { - warn_user_once!( - "`{}` has a non-normalized filename (expected `{normalized_filename}`), skipping", - group.raw_filename - ); - continue; - } warn_user_once!( - "`{}` has a non-normalized filename (expected `{normalized_filename}`). \ - Pass `--preview-features {}` to skip such files.", - group.raw_filename, - PreviewFeature::PublishRequireNormalized + "`{}` has a non-normalized filename (expected `{normalized_filename}`), skipping", + group.raw_filename ); + continue; } let reporter = Arc::new(PublishReporter::single(printer)); diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 793512485d016..f7070ac3e30a3 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -486,38 +486,31 @@ async fn perform_install( for installation in matching_installations { changelog.existing.insert(installation.key().clone()); - if matches!(&request.request, &PythonRequest::Any) { - // Construct an install request matching the existing installation - match InstallRequest::new( - PythonRequest::Key(installation.into()), - &download_list, - ) { - Ok(request) => { - debug!("Will reinstall `{}`", installation.key()); - unsatisfied.push(Cow::Owned(request)); - } - Err(err) => { - // This shouldn't really happen, but maybe a new version of uv dropped - // support for a key we previously supported - warn_user!( - "Failed to create reinstall request for existing installation `{}`: {err}", - installation.key().green() - ); - } - } - } else { - // TODO(zanieb): This isn't really right! But we need `--upgrade` or similar - // to handle this case correctly without causing a breaking change. - - // If we have real requests, just ignore the existing installation - debug!( - "Ignoring match `{}` for request `{}` due to `--reinstall` flag", - installation.key(), - request - ); + + if matches!(upgrade, PythonUpgrade::Enabled(_)) + && !matches!(&request.request, &PythonRequest::Any) + { + // An upgrade must reinstall the latest patch, not every matching patch. + debug!("Will reinstall the latest patch for `{}`", request); unsatisfied.push(Cow::Borrowed(request)); break; } + + // Construct an install request matching the existing installation. + match InstallRequest::new(PythonRequest::Key(installation.into()), &download_list) { + Ok(request) => { + debug!("Will reinstall `{}`", installation.key()); + unsatisfied.push(Cow::Owned(request)); + } + Err(err) => { + // This shouldn't really happen, but maybe a new version of uv dropped + // support for a key we previously supported. + warn_user!( + "Failed to create reinstall request for existing installation `{}`: {err}", + installation.key().green() + ); + } + } } } (vec![], unsatisfied) diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index c244808e55a25..0e2e34f62da2f 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -68,6 +68,29 @@ mod logging; pub(crate) mod printer; pub(crate) mod settings; +/// Construct the shared HTTP client builder from the resolved global settings. +pub(crate) fn base_client_builder<'a>(globals: &GlobalSettings) -> BaseClientBuilder<'a> { + let client_builder = BaseClientBuilder::new( + globals.network_settings.connectivity, + globals.network_settings.system_certs, + globals.network_settings.allow_insecure_host.clone(), + globals.preview, + globals.network_settings.read_timeout, + globals.network_settings.connect_timeout, + globals.network_settings.retries, + ) + .cache_read_concurrency(globals.concurrency.cache_reads) + .http_proxy(globals.network_settings.http_proxy.clone()) + .https_proxy(globals.network_settings.https_proxy.clone()) + .no_proxy(globals.network_settings.no_proxy.clone()); + + if let Some(certificates) = &globals.network_settings.custom_certificates { + client_builder.custom_certificates(certificates.clone()) + } else { + client_builder + } +} + /// Whether to initialize process-global state. #[derive(Debug, Copy, Clone, Eq, PartialEq)] #[doc(hidden)] @@ -195,11 +218,10 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul path } } else if let Some(run_command) = &parsed_run_command - && early_preview.is_enabled(PreviewFeature::TargetWorkspaceDiscovery) && let Some(dir) = run_command.script_dir() { - // When running a target with the preview flag enabled, discover the workspace starting - // from the target's directory rather than the current working directory. + // When running a target, discover the workspace starting from the target's directory + // rather than the current working directory. Cow::Owned(std::path::absolute(dir)?) } else { Cow::Borrowed(&*CWD) @@ -215,31 +237,15 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul if !skip_project_validation { if let Some(project_path) = cli.top_level.global_args.project.as_ref() { if !project_dir.exists() { - if early_preview.is_enabled(PreviewFeature::ProjectDirectoryMustExist) { - bail!( - "Project directory `{}` does not exist", - project_path.user_display() - ); - } - warn_user_once!( - "Project directory `{}` does not exist. \ - This will become an error in a future release. \ - Use `--preview-features project-directory-must-exist` to error on this now.", + bail!( + "Project directory `{}` does not exist", project_path.user_display() ); } else if !project_dir.is_dir() { // `--project path/to/pyproject.toml` is resolved to its parent above, // so this only triggers for other file types (see #18508). - if early_preview.is_enabled(PreviewFeature::ProjectDirectoryMustExist) { - bail!( - "Project path `{}` is not a directory", - project_path.user_display() - ); - } - warn_user_once!( - "Project path `{}` is not a directory. \ - This will become an error in a future release. \ - Use `--preview-features project-directory-must-exist` to error on this now.", + bail!( + "Project path `{}` is not a directory", project_path.user_display() ); } @@ -478,11 +484,17 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul .map(FilesystemOptions::from) .combine(filesystem); + let custom_certificate_file = match &*cli.command { + Commands::Pip(PipNamespace { cert, .. }) => cert.as_deref(), + _ => None, + }; + // Resolve the global settings. let globals = GlobalSettings::resolve( &cli.top_level.global_args, filesystem.as_ref(), &environment, + custom_certificate_file, )?; if global_initialization.needs_initialization() { @@ -504,16 +516,12 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul ); } - // Adjust open file limits on Unix if the preview feature is enabled. + // Adjust open file limits on Unix. #[cfg(unix)] - if global_initialization.needs_initialization() - && globals.preview.is_enabled(PreviewFeature::AdjustUlimit) - { + if global_initialization.needs_initialization() { match uv_unix::adjust_open_file_limit() { Ok(_) | Err(uv_unix::OpenFileLimitError::AlreadySufficient { .. }) => {} - // TODO(zanieb): When moving out of preview, consider changing this to a log instead of - // a warning because it's okay if we fail here. - Err(err) => warn_user!("{err}"), + Err(err) => debug!("{err}"), } } @@ -629,19 +637,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul }; // Configure the global network settings. - let client_builder = BaseClientBuilder::new( - globals.network_settings.connectivity, - globals.network_settings.system_certs, - globals.network_settings.allow_insecure_host.clone(), - globals.preview, - globals.network_settings.read_timeout, - globals.network_settings.connect_timeout, - globals.network_settings.retries, - ) - .cache_read_concurrency(globals.concurrency.cache_reads) - .http_proxy(globals.network_settings.http_proxy.clone()) - .https_proxy(globals.network_settings.https_proxy.clone()) - .no_proxy(globals.network_settings.no_proxy.clone()); + let client_builder = base_client_builder(&globals); match *cli.command { Commands::Auth(AuthNamespace { @@ -723,6 +719,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul ), Commands::Pip(PipNamespace { command: PipCommand::Compile(args), + .. }) => { args.compat_args.validate()?; @@ -842,6 +839,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Sync(args), + .. }) => { args.compat_args.validate()?; @@ -931,6 +929,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Install(args), + .. }) => { args.compat_args.validate()?; @@ -1093,6 +1092,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Uninstall(args), + .. }) => { args.compat_args.validate()?; @@ -1130,6 +1130,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Freeze(args), + .. }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipFreezeSettings::resolve(args, filesystem, environment)?; @@ -1154,6 +1155,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::List(args), + .. }) => { args.compat_args.validate()?; @@ -1189,6 +1191,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Show(args), + .. }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipShowSettings::resolve(args, filesystem, environment)?; @@ -1212,6 +1215,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Tree(args), + .. }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipTreeSettings::resolve(args, filesystem, environment)?; @@ -1245,6 +1249,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Check(args), + .. }) => { // Resolve the settings from the command-line arguments and workspace configuration. let args = PipCheckSettings::resolve(args, filesystem, environment)?; @@ -1265,6 +1270,7 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul } Commands::Pip(PipNamespace { command: PipCommand::Debug(_), + .. }) => Err(anyhow!( "pip's `debug` is unsupported (consider using `uvx pip debug` instead)" )), @@ -1390,10 +1396,8 @@ pub async fn run(cli: Cli, global_initialization: GlobalInitialization) -> Resul args.no_clear, if args.force { uv_virtualenv::ClearNonVirtualenv::Allow - } else if globals.preview.is_enabled(PreviewFeature::VenvSafeClear) { - uv_virtualenv::ClearNonVirtualenv::Error } else { - uv_virtualenv::ClearNonVirtualenv::Warn + uv_virtualenv::ClearNonVirtualenv::Error }, ); @@ -2227,31 +2231,19 @@ async fn run_project( match *project_command { ProjectCommand::Init(args) => { // Resolve the settings from the command-line arguments and workspace configuration. - let args = - settings::InitSettings::resolve(args, filesystem, environment, globals.preview)?; + let args = settings::InitSettings::resolve(args, filesystem, environment)?; show_settings!(args); - // The `--project` arg is being deprecated for `init` with a warning now and an error in preview. + // The `--project` argument is not supported for `init`. if explicit_project { - if globals.preview.is_enabled(PreviewFeature::InitProjectFlag) { - bail!( - "The `--project` option cannot be used in `uv init`. {}", - if args.path.is_some() { - "Use `--directory` instead." - } else { - "Use `--directory` or a positional path instead." - } - ) - } - - warn_user!( - "Use of the `--project` option in `uv init` is deprecated and will be removed in a future release. {}", + bail!( + "The `--project` option cannot be used in `uv init`. {}", if args.path.is_some() { - "Since a positional path was provided, the `--project` option has no effect. Consider using `--directory` instead." + "Use `--directory` instead." } else { - "Consider using `uv init ` instead." + "Use `--directory` or a positional path instead." } - ); + ) } // Initialize the cache. @@ -2261,7 +2253,6 @@ async fn run_project( project_dir, args.path, args.name, - args.package, args.kind, args.bare, args.description, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 7a43a80b33dcf..e39dc2a7e9fa1 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -1,7 +1,7 @@ use std::env::VarError; use std::fmt; use std::num::NonZeroUsize; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use std::time::Duration; @@ -28,12 +28,11 @@ use uv_cli::{ PackageExcludeNewerArgs, PublishArgs, PythonDirArgs, RegistryClientArgs, ResolverInstallerArgs, ToolUpgradeArgs, options::{ - Flag, FlagSource, check_conflicts, flag, indexes_from_args, resolve_flag, - resolve_flag_pair, resolver_installer_options, resolver_installer_options_with_indexes, - resolver_options, + Flag, FlagSource, check_conflicts, flag, resolve_flag, resolve_flag_pair, + resolver_installer_options, resolver_options, }, }; -use uv_client::Connectivity; +use uv_client::{Certificates, Connectivity}; use uv_configuration::{ BuildIsolation, BuildOptions, Concurrency, DependencyGroups, DevMode, DryRun, EditableMode, EnvFile, ExcludeDependency, ExportFormat, ExtrasSpecification, GitLfsSetting, HashCheckingMode, @@ -49,7 +48,7 @@ use uv_install_wheel::LinkMode; use uv_normalize::{ExtraName, PackageName, PipGroupName}; use uv_pep440::Version; use uv_pep508::{MarkerTree, RequirementOrigin}; -use uv_preview::{Preview, PreviewFeature}; +use uv_preview::Preview; use uv_pypi_types::SupportedEnvironments; use uv_python::{Prefix, PythonDownloads, PythonPreference, PythonVersion, Target}; use uv_redacted::DisplaySafeUrl; @@ -99,8 +98,10 @@ impl GlobalSettings { args: &GlobalArgs, workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, + custom_certificate_file: Option<&Path>, ) -> anyhow::Result { - let network_settings = NetworkSettings::resolve(args, workspace, environment)?; + let network_settings = + NetworkSettings::resolve(args, workspace, environment, custom_certificate_file)?; let python_preference = resolve_python_preference(args, workspace, environment)?; let color = resolve_color(args); Ok(Self { @@ -274,6 +275,7 @@ pub(crate) struct NetworkSettings { pub(super) connectivity: Connectivity, pub(super) offline: Flag, pub(super) system_certs: bool, + pub(super) custom_certificates: Option, pub(super) http_proxy: Option, pub(super) https_proxy: Option, pub(super) no_proxy: Option>, @@ -289,6 +291,7 @@ impl NetworkSettings { args: &GlobalArgs, workspace: Option<&FilesystemOptions>, environment: &EnvironmentOptions, + custom_certificate_file: Option<&Path>, ) -> anyhow::Result { // Resolve offline flag from CLI, environment variable, and workspace config. // Precedence: CLI > Env var > Workspace config > default (false). @@ -389,10 +392,16 @@ impl NetworkSettings { let https_proxy = workspace.and_then(|workspace| workspace.globals.https_proxy.clone()); let no_proxy = workspace.and_then(|workspace| workspace.globals.no_proxy.clone()); + let custom_certificates = custom_certificate_file + .map(Certificates::from_file) + .transpose()? + .or_else(Certificates::from_env); + Ok(Self { connectivity, offline, system_certs, + custom_certificates, http_proxy, https_proxy, no_proxy, @@ -445,7 +454,6 @@ impl CacheSettings { pub(crate) struct InitSettings { pub(crate) path: Option, pub(crate) name: Option, - pub(crate) package: bool, pub(crate) kind: InitKind, pub(crate) bare: bool, pub(crate) description: Option, @@ -466,7 +474,6 @@ impl InitSettings { args: InitArgs, filesystem: Option, environment: EnvironmentOptions, - preview: Preview, ) -> Result { let InitArgs { path, @@ -499,94 +506,66 @@ impl InitSettings { let no_description = no_description || (bare && description.is_none()); - let (kind, package) = if preview.is_enabled(PreviewFeature::PackagedInit) { - if r#virtual && lib { - bail!("`--virtual` and `--lib` are mutually exclusive"); - } - if r#virtual && build_backend.is_some() { - bail!("`--virtual` and `--build-backend` are mutually exclusive"); - } + if r#virtual && lib { + bail!("`--virtual` and `--lib` are mutually exclusive"); + } + if r#virtual && build_backend.is_some() { + bail!("`--virtual` and `--build-backend` are mutually exclusive"); + } - let package_flag = flag( - package || build_backend.is_some(), - no_package || r#virtual, - "virtual", - )?; - - let kind = if script { - InitKind::Script - } else if bare { - if package_flag == Some(true) || lib { - InitKind::Project(InitProjectKind::BareWithBuildSystem) - } else { - InitKind::Project(InitProjectKind::Bare) - } + let package = flag( + package || build_backend.is_some(), + no_package || r#virtual, + "virtual", + )?; + + let kind = if script { + InitKind::Script + } else if bare { + if package == Some(true) || lib { + InitKind::Project(InitProjectKind::BareWithBuildSystem) } else { - // Merge `--app` and `--lib`. - let app_lib_kind = match (app, lib) { - (false, false) => InitProjectKind::ApplicationWithLibrary, - (true, false) => InitProjectKind::Application, - (false, true) => InitProjectKind::Library, - (true, true) => bail!("`app` and `lib` are mutually exclusive"), - }; - - // Apply overrides from `--package`/`--no-package`. - let app_lib_kind = match (app_lib_kind, package_flag) { - (InitProjectKind::ApplicationWithLibrary, None | Some(true)) => { - InitProjectKind::ApplicationWithLibrary - } - (InitProjectKind::ApplicationWithLibrary, Some(false)) => { - InitProjectKind::Application - } - // The user specifically asked for `--app`, so no library. - (InitProjectKind::Application, None | Some(false)) => { - InitProjectKind::Application - } - (InitProjectKind::Application, Some(true)) => { - InitProjectKind::ApplicationWithLibrary - } - (InitProjectKind::Library, None | Some(true)) => InitProjectKind::Library, - (InitProjectKind::Library, Some(false)) => { - bail!("`lib` and `no_package` are mutually exclusive"); - } - (InitProjectKind::Bare | InitProjectKind::BareWithBuildSystem, _) => { - unreachable!() - } - (InitProjectKind::ApplicationOld | InitProjectKind::LibraryOld, _) => { - unreachable!() - } - }; - InitKind::Project(app_lib_kind) - }; - - // Packaging is encoded in `kind`; `package` is only consumed by the old paths. - (kind, false) + InitKind::Project(InitProjectKind::Bare) + } } else { - // TODO(konsti): Remove when stabilizing packaged-init. - let kind = match (app, lib, script) { - (true, false, false) => InitKind::Project(InitProjectKind::ApplicationOld), - (false, true, false) => InitKind::Project(InitProjectKind::LibraryOld), - (false, false, true) => InitKind::Script, - (false, false, false) => InitKind::Project(InitProjectKind::ApplicationOld), - (_, _, _) => bail!("`app`, `lib`, and `script` are mutually exclusive"), + // Merge `--app` and `--lib`. + let app_lib_kind = match (app, lib) { + (false, false) => InitProjectKind::ApplicationWithLibrary, + (true, false) => InitProjectKind::Application, + (false, true) => InitProjectKind::Library, + (true, true) => bail!("`app` and `lib` are mutually exclusive"), }; - let package = flag( - package || build_backend.is_some(), - no_package || r#virtual, - "virtual", - )? - .unwrap_or(matches!( - kind, - InitKind::Project(InitProjectKind::LibraryOld) - )); - (kind, package) + // Apply overrides from `--package`/`--no-package`. + let app_lib_kind = match (app_lib_kind, package) { + (InitProjectKind::ApplicationWithLibrary, None | Some(true)) => { + InitProjectKind::ApplicationWithLibrary + } + (InitProjectKind::ApplicationWithLibrary, Some(false)) => { + InitProjectKind::Application + } + (InitProjectKind::Application, None | Some(true)) => { + InitProjectKind::ApplicationWithLibrary + } + (InitProjectKind::Application, Some(false)) => InitProjectKind::Application, + (InitProjectKind::Library, None | Some(true)) => InitProjectKind::Library, + (InitProjectKind::Library, Some(false)) => { + bail!("`lib` and `no_package` are mutually exclusive"); + } + (InitProjectKind::Bare | InitProjectKind::BareWithBuildSystem, _) => { + unreachable!() + } + }; + InitKind::Project(app_lib_kind) }; + if script && package == Some(true) { + warn_user_once!("`--package` is a no-op for Python scripts, which are standalone"); + } + Ok(Self { path, name, - package, kind, bare, description, @@ -2292,17 +2271,17 @@ impl AddSettings { DependencyType::Production }; - // Track the `--index` and `--default-index` arguments from the command-line. - let index = indexes_from_args( - installer.index_args.default_index.as_ref(), - installer.index_args.index.as_deref(), - ); - let indexes = index.clone().unwrap_or_default(); - // Warn user if an ambiguous relative path was passed as a value for // `--index` or `--default-index`. - for index in &indexes { - index.url().warn_on_disambiguated_relative_path(); + for index in installer + .index_args + .default_index + .iter() + .chain(installer.index_args.index.iter().flatten().flatten()) + { + if let Maybe::Some(index) = index { + index.url().warn_on_disambiguated_relative_path(); + } } // If the user passed an `--index-url` or `--extra-index-url`, warn. @@ -2392,11 +2371,20 @@ impl AddSettings { let only_install_local = only_install_local.is_enabled(); let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment); + let active = flag(active, no_active, "active")?; + let workspace = flag(workspace, no_workspace, "workspace")?; + let editable = EditableMode::from_args( + flag(editable.into(), no_editable.into(), "editable")?, + no_editable_package, + ); + let refresh = Refresh::try_from(refresh)?; + let options = resolver_installer_options(installer, build)?; + let indexes = options.index.clone().unwrap_or_default(); Ok(Self { lock_check: resolve_lock_check(locked), frozen: resolve_frozen(frozen), - active: flag(active, no_active, "active")?, + active, no_sync: no_sync.is_enabled(), packages, requirements, @@ -2415,7 +2403,7 @@ impl AddSettings { package, script, python: python.and_then(Maybe::into_option), - workspace: flag(workspace, no_workspace, "workspace")?, + workspace, no_install_project, only_install_project, no_install_workspace, @@ -2424,18 +2412,11 @@ impl AddSettings { only_install_local, no_install_package, only_install_package, - editable: EditableMode::from_args( - flag(editable.into(), no_editable.into(), "editable")?, - no_editable_package, - ), + editable, extras: extra.unwrap_or_default(), - refresh: Refresh::try_from(refresh)?, + refresh, indexes, - settings: ResolverInstallerSettings::combine( - resolver_installer_options_with_indexes(installer, build, index)?, - filesystem, - &environment, - ), + settings: ResolverInstallerSettings::combine(options, filesystem, &environment), install_mirrors: environment .install_mirrors .combine(filesystem_install_mirrors), @@ -3905,7 +3886,7 @@ impl PipListSettings { strict: flag(strict, no_strict, "strict")?, target, prefix, - ..PipOptions::from(fetch) + ..PipOptions::try_from(fetch)? }, filesystem, environment, @@ -4006,7 +3987,7 @@ impl PipTreeSettings { python: python.and_then(Maybe::into_option), system: flag(system, no_system, "system")?, strict: flag(strict, no_strict, "strict")?, - ..PipOptions::from(fetch) + ..PipOptions::try_from(fetch)? }, filesystem, environment, @@ -4261,7 +4242,7 @@ impl VenvSettings { exclude_newer_package: exclude_newer_package .map(ExcludeNewerPackage::from_iter), link_mode, - ..PipOptions::from(index_args) + ..PipOptions::try_from(index_args)? }, filesystem, environment, @@ -4321,6 +4302,18 @@ pub(crate) struct ResolverSettings { pub(crate) upgrade: Upgrade, } +#[allow(deprecated)] +fn warn_if_deprecated_prerelease_mode(prerelease: PrereleaseMode) -> PrereleaseMode { + if matches!(prerelease, PrereleaseMode::IfNecessaryOrExplicit) { + warn_user_once!( + "The `if-necessary-or-explicit` pre-release mode is deprecated and will be removed in a future release. Use `if-necessary` instead." + ); + PrereleaseMode::IfNecessary + } else { + prerelease + } +} + impl ResolverSettings { /// Resolve the [`ResolverSettings`] from the CLI and filesystem configuration. fn combine( @@ -4376,7 +4369,7 @@ impl From for ResolverSettings { Self { index_locations, resolution: value.resolution.unwrap_or_default(), - prerelease: value.prerelease.unwrap_or_default(), + prerelease: warn_if_deprecated_prerelease_mode(value.prerelease.unwrap_or_default()), fork_strategy: value.fork_strategy.unwrap_or_default(), dependency_metadata: DependencyMetadata::from_entries( value.dependency_metadata.into_iter().flatten(), @@ -4519,7 +4512,9 @@ impl From for ResolverInstallerSettings { build_isolation: value.build_isolation.unwrap_or_default(), extra_build_dependencies: value.extra_build_dependencies.unwrap_or_default(), extra_build_variables: value.extra_build_variables.unwrap_or_default(), - prerelease: value.prerelease.unwrap_or_default(), + prerelease: warn_if_deprecated_prerelease_mode( + value.prerelease.unwrap_or_default(), + ), resolution: value.resolution.unwrap_or_default(), sources: NoSources::from_args( value.no_sources, @@ -4793,7 +4788,9 @@ impl PipSettings { DependencyMode::Transitive }, resolution: args.resolution.combine(resolution).unwrap_or_default(), - prerelease: args.prerelease.combine(prerelease).unwrap_or_default(), + prerelease: warn_if_deprecated_prerelease_mode( + args.prerelease.combine(prerelease).unwrap_or_default(), + ), fork_strategy: args .fork_strategy .combine(fork_strategy) diff --git a/crates/uv/tests/build/build.rs b/crates/uv/tests/build/build.rs index 855cdc4afadf6..7ecf77468af10 100644 --- a/crates/uv/tests/build/build.rs +++ b/crates/uv/tests/build/build.rs @@ -2108,8 +2108,8 @@ fn build_fast_path() -> Result<()> { .arg(context.temp_dir.join("output1")), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built output1/built_by_uv-0.1.0.tar.gz Successfully built output1/built_by_uv-0.1.0-py3-none-any.whl "); @@ -2131,7 +2131,7 @@ fn build_fast_path() -> Result<()> { .arg("--sdist"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... Successfully built output2/built_by_uv-0.1.0.tar.gz "); context @@ -2147,7 +2147,7 @@ fn build_fast_path() -> Result<()> { .arg("--wheel"), @" exit_code: 0 (success) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl "); context @@ -2164,8 +2164,8 @@ fn build_fast_path() -> Result<()> { .arg("--wheel"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel (uv build backend)... + Building source distribution... + Building wheel... Successfully built output4/built_by_uv-0.1.0.tar.gz Successfully built output4/built_by_uv-0.1.0-py3-none-any.whl "); @@ -2183,6 +2183,57 @@ fn build_fast_path() -> Result<()> { Ok(()) } +/// Only mention the bundled build backend when verbose logging is enabled. +#[test] +fn build_fast_path_verbose() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let project = context.temp_dir.child("project"); + + project.child("pyproject.toml").write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [build-system] + requires = ["uv_build>=0.5.15,<10000"] + build-backend = "uv_build" + "#})?; + project.child("src/project/__init__.py").touch()?; + + let output = context + .build() + .arg("project") + .arg("--sdist") + .arg("--verbose") + .env_remove(EnvVars::RUST_LOG) + .output()?; + + let stderr = apply_filters( + String::from_utf8_lossy(&output.stderr).into_owned(), + context.filters(), + ); + assert!(output.status.success(), "build failed:\n{stderr}"); + + let messages = stderr + .lines() + .filter(|line| { + line.starts_with("DEBUG Using bundled `uv_build` backend for") + || line.starts_with("Building ") + || line.starts_with("Successfully built ") + }) + .collect::>() + .join("\n"); + + assert_snapshot!(messages, @r" + DEBUG Using bundled `uv_build` backend for `project` + Building source distribution... + Successfully built project/dist/project-0.1.0.tar.gz + "); + + Ok(()) +} + /// Reject path-shaped script entry point names before writing wheel metadata. #[test] fn build_unsafe_script_entry_point_name() -> Result<()> { @@ -2215,7 +2266,7 @@ fn build_unsafe_script_entry_point_name() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/` Caused by: Invalid project metadata Caused by: Script entry point name `../script` must include a non-dot character and consist only of letters, numbers, dots, underscores and dashes @@ -2256,7 +2307,7 @@ fn build_dot_script_entry_point_name() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/` Caused by: Invalid project metadata Caused by: Script entry point name `.` must include a non-dot character and consist only of letters, numbers, dots, underscores and dashes @@ -2297,7 +2348,7 @@ fn build_nested_script_entry_point_name() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/` Caused by: Invalid project metadata Caused by: Script entry point name `nested/script` must include a non-dot character and consist only of letters, numbers, dots, underscores and dashes @@ -2319,9 +2370,7 @@ fn build_list_files() -> Result<()> { .arg(&built_by_uv) .arg("--out-dir") .arg(context.temp_dir.join("output1")) - .arg("--list") - .arg("--preview-features") - .arg("toml-backwards-compatibility"), @" + .arg("--list"), @" exit_code: 0 (success) ----- stdout ----- Building built_by_uv-0.1.0.tar.gz will include the following files: @@ -2358,7 +2407,7 @@ fn build_list_files() -> Result<()> { built_by_uv-0.1.0.dist-info/METADATA (generated) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... Successfully built output1/built_by_uv-0.1.0.tar.gz "); context @@ -2378,9 +2427,7 @@ fn build_list_files() -> Result<()> { .arg(context.temp_dir.join("output2")) .arg("--list") .arg("--sdist") - .arg("--wheel") - .arg("--preview-features") - .arg("toml-backwards-compatibility"), @" + .arg("--wheel"), @" exit_code: 0 (success) ----- stdout ----- Building built_by_uv-0.1.0.tar.gz will include the following files: @@ -2746,7 +2793,7 @@ fn force_pep517() -> Result<()> { uv_snapshot!(context.filters(), context.build().env(EnvVars::RUST_BACKTRACE, "0"), @" exit_code: 2 (failure) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... error: Failed to build `[TEMP_DIR]/` Caused by: Expected a Python module at: src/does_not_exist/__init__.py "); @@ -2755,7 +2802,7 @@ fn force_pep517() -> Result<()> { exit_code: 2 (failure) ----- stderr ----- Building source distribution... - Error: Missing source directory at: `src` + Error: Missing module directory for `does_not_exist` in `src`. Found: `temp` error: Failed to build `[TEMP_DIR]/` Caused by: The build backend returned an error Caused by: Call to `uv_build.build_sdist` failed (exit status: 1) @@ -2852,8 +2899,8 @@ fn test_workspace_trailing_slash() { uv_snapshot!(context.filters(), context.build().arg("child"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/child-0.1.0.tar.gz Successfully built dist/child-0.1.0-py3-none-any.whl "); @@ -2862,8 +2909,8 @@ fn test_workspace_trailing_slash() { uv_snapshot!(context.filters(), context.build().arg("child/"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/child-0.1.0.tar.gz Successfully built dist/child-0.1.0-py3-none-any.whl "); @@ -2872,8 +2919,8 @@ fn test_workspace_trailing_slash() { uv_snapshot!(context.filters(), context.build().arg("./child/"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/child-0.1.0.tar.gz Successfully built dist/child-0.1.0-py3-none-any.whl "); @@ -2881,8 +2928,8 @@ fn test_workspace_trailing_slash() { uv_snapshot!(context.filters(), context.build().arg("./child/../child/"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/child-0.1.0.tar.gz Successfully built dist/child-0.1.0-py3-none-any.whl "); diff --git a/crates/uv/tests/build/build_backend.rs b/crates/uv/tests/build/build_backend.rs index bc0a495fecd75..db67122170b21 100644 --- a/crates/uv/tests/build/build_backend.rs +++ b/crates/uv/tests/build/build_backend.rs @@ -935,7 +935,7 @@ fn error_on_relative_module_root_outside_project_root() -> Result<()> { uv_snapshot!(context.filters(), context.build(), @" exit_code: 2 (failure) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... error: Failed to build `[TEMP_DIR]/` Caused by: Module root must be inside the project: .. "); @@ -943,7 +943,7 @@ fn error_on_relative_module_root_outside_project_root() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/` Caused by: Module root must be inside the project: .. "); @@ -984,7 +984,7 @@ fn error_on_relative_data_dir_outside_project_root() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("project"), @" exit_code: 2 (failure) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... error: Failed to build `[TEMP_DIR]/project` Caused by: The path for the data directory headers must be inside the project: ../header "); @@ -992,7 +992,7 @@ fn error_on_relative_data_dir_outside_project_root() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/project` Caused by: The path for the data directory headers must be inside the project: ../header "); @@ -1019,7 +1019,7 @@ fn error_on_relative_data_dir_outside_project_root() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/project` Caused by: The path for the data directory headers must be inside the project: ../outside "); @@ -1172,7 +1172,7 @@ fn venv_in_source_tree() { uv_snapshot!(context.filters(), context.build(), @" exit_code: 2 (failure) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... error: Failed to build `[TEMP_DIR]/` Caused by: Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv "); @@ -1180,7 +1180,7 @@ fn venv_in_source_tree() { uv_snapshot!(context.filters(), context.build().arg("--wheel"), @" exit_code: 2 (failure) ----- stderr ----- - Building wheel (uv build backend)... + Building wheel... error: Failed to build `[TEMP_DIR]/` Caused by: Virtual environments must not be added to source distributions or wheels, remove the directory or exclude it from the build: src/foo/.venv "); @@ -1226,9 +1226,9 @@ fn warn_on_redundant_module_names() -> Result<()> { uv_snapshot!(context.filters(), context.build(), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... warning: Ignoring redundant module names in `tool.uv.build-backend.module-name`: `foo.bar`, `foo`, `foo.bar.baz`, `foobar.baz` - Building wheel from source distribution (uv build backend)... + Building wheel from source distribution... Successfully built dist/project-0.1.0.tar.gz Successfully built dist/project-0.1.0-py3-none-any.whl "); @@ -1239,8 +1239,8 @@ fn warn_on_redundant_module_names() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("--no-sources"), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/project-0.1.0.tar.gz Successfully built dist/project-0.1.0-py3-none-any.whl "); @@ -1269,7 +1269,7 @@ fn invalid_pyproject_toml() -> Result<()> { uv_snapshot!(context.filters(), context.build().arg("child"), @" exit_code: 2 (failure) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... error: Failed to build `[TEMP_DIR]/child` Caused by: Invalid metadata format in: child/pyproject.toml Caused by: TOML parse error at line 2, column 8 @@ -1673,9 +1673,9 @@ fn warn_on_license_classifier() -> Result<()> { uv_snapshot!(context.filters(), context.build(), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... warning: Found license classifier `License :: OSI Approved :: MIT License`. License classifiers are ambiguous and deprecated per PEP 639; projects should use `project.license` and `project.license-files` instead. - Building wheel from source distribution (uv build backend)... + Building wheel from source distribution... Successfully built dist/foo-1.0.0.tar.gz Successfully built dist/foo-1.0.0-py3-none-any.whl "); @@ -1683,9 +1683,9 @@ fn warn_on_license_classifier() -> Result<()> { Ok(()) } -/// Auto-detect TOML 1.1 features in `pyproject.toml` and warn the user. +/// Rewrite TOML 1.1 features in `pyproject.toml` without a preview flag. #[test] -fn warn_on_toml_1_1_auto_detected() -> Result<()> { +fn rewrite_toml_1_1_by_default() -> Result<()> { let context = uv_test::test_context!("3.12"); context @@ -1705,23 +1705,11 @@ fn warn_on_toml_1_1_auto_detected() -> Result<()> { "#})?; context.temp_dir.child("src/foo/__init__.py").touch()?; - // Without the preview flag: auto-detection fires and a warning is shown. uv_snapshot!(context.filters(), context.build(), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... - warning: `pyproject.toml` uses TOML 1.1 features; rewriting to TOML 1.0 for compatibility with older build tools. Use `--preview-feature toml-backwards-compatibility` to suppress this warning. - Building wheel from source distribution (uv build backend)... - Successfully built dist/foo-1.0.0.tar.gz - Successfully built dist/foo-1.0.0-py3-none-any.whl - "); - - // With the preview flag set explicitly: rewrite still happens, but no warning. - uv_snapshot!(context.filters(), context.build().arg("--preview-feature").arg("toml-backwards-compatibility"), @" - exit_code: 0 (success) - ----- stderr ----- - Building source distribution (uv build backend)... - Building wheel from source distribution (uv build backend)... + Building source distribution... + Building wheel from source distribution... Successfully built dist/foo-1.0.0.tar.gz Successfully built dist/foo-1.0.0-py3-none-any.whl "); diff --git a/crates/uv/tests/build/cache.rs b/crates/uv/tests/build/cache.rs index b82965f76f99f..f9f5942c4931e 100644 --- a/crates/uv/tests/build/cache.rs +++ b/crates/uv/tests/build/cache.rs @@ -34,7 +34,7 @@ fn build_warns_cache_inside_source() -> Result<()> { exit_code: 0 (success) ----- stderr ----- warning: The cache directory `project/.uv-cache` is inside the build source directory `project` and may be included in distributions - Building source distribution (uv build backend)... + Building source distribution... Successfully built project/dist/project-0.1.0.tar.gz "); @@ -86,7 +86,7 @@ fn build_warns_symlinked_cache_inside_source() -> Result<()> { exit_code: 0 (success) ----- stderr ----- warning: The cache directory `cache-link` is inside the build source directory `project` and may be included in distributions - Building source distribution (uv build backend)... + Building source distribution... Successfully built project/dist/project-0.1.0.tar.gz "); @@ -133,7 +133,7 @@ fn build_allows_cache_outside_selected_source() -> Result<()> { .current_dir(&workspace), @" exit_code: 0 (success) ----- stderr ----- - Building source distribution (uv build backend)... + Building source distribution... Successfully built dist/member-0.1.0.tar.gz "); diff --git a/crates/uv/tests/build/cache_prune.rs b/crates/uv/tests/build/cache_prune.rs index 9151248043ecc..ffe2fca906f72 100644 --- a/crates/uv/tests/build/cache_prune.rs +++ b/crates/uv/tests/build/cache_prune.rs @@ -323,7 +323,6 @@ fn prune_unzipped() -> Result<()> { × No solution found when resolving dependencies: ╰─▶ Because all versions of iniconfig need to be downloaded from a registry and you require iniconfig, we can conclude that your requirements are unsatisfiable. - hint: Pre-releases are available for `iniconfig` in the requested range (e.g., 0.2.dev0), but pre-releases weren't enabled (try: `--prerelease=allow`) hint: Packages were unavailable because the network was disabled. When the network is disabled, registry packages may only be read from the cache. "); diff --git a/crates/uv/tests/build/extract.rs b/crates/uv/tests/build/extract.rs index 6386780fb69cb..18e9b6c5e0fac 100644 --- a/crates/uv/tests/build/extract.rs +++ b/crates/uv/tests/build/extract.rs @@ -23,7 +23,7 @@ async fn unzip(url: &str) -> anyhow::Result<(), uv_extract::Error> { .into_async_read(); let target = tempfile::TempDir::new().map_err(uv_extract::Error::Io)?; - uv_extract::stream::unzip(url, reader.compat(), target.path()).await?; + uv_extract::stream::unzip(reader.compat(), target.path()).await?; Ok(()) } diff --git a/crates/uv/tests/it/help.rs b/crates/uv/tests/it/help.rs index 6e046f83b0745..28c47d218eafb 100644 --- a/crates/uv/tests/it/help.rs +++ b/crates/uv/tests/it/help.rs @@ -2,6 +2,26 @@ use uv_static::EnvVars; use uv_test::uv_snapshot; +#[test] +fn cert_is_limited_to_pip() { + let context = uv_test::test_context_with_versions!(&[]); + + uv_snapshot!(context.filters(), context.command() + .arg("sync") + .arg("--cert") + .arg("ca-bundle.pem"), @" + exit_code: 2 (failure) + ----- stderr ----- + error: unexpected argument '--cert' found + + tip: a similar argument exists: '--script' + + Usage: uv sync --script