diff --git a/Cargo.lock b/Cargo.lock index cc0c05fe0..d45e9f4b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6070,6 +6070,7 @@ dependencies = [ "fs-err", "indoc", "mailparse", + "owo-colors", "pathdiff", "reflink-copy", "regex", @@ -6090,6 +6091,7 @@ dependencies = [ "uv-preview", "uv-pypi-types", "uv-shell", + "uv-static", "uv-trampoline-builder", "uv-warnings", "walkdir", diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index 5af347ff4..4b473e116 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -28,6 +28,7 @@ uv-pep440 = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-shell = { workspace = true } +uv-static = { workspace = true } uv-trampoline-builder = { workspace = true } uv-warnings = { workspace = true } @@ -37,6 +38,7 @@ csv = { workspace = true } data-encoding = { workspace = true } fs-err = { workspace = true } mailparse = { workspace = true } +owo-colors = { workspace = true } pathdiff = { workspace = true } reflink-copy = { workspace = true } regex = { workspace = true } diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index aab7d4229..b4c06a55e 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -11,6 +11,7 @@ use tracing::{instrument, trace}; use uv_distribution_filename::WheelFilename; use uv_pep440::Version; use uv_pypi_types::{DirectUrl, Metadata10}; +use uv_static::{EnvVars, parse_boolish_environment_variable}; use crate::linker::{LinkMode, Locks}; use crate::wheel::{ @@ -50,11 +51,21 @@ pub fn install_wheel( // Validate the wheel name and version. { if name != filename.name { - return Err(Error::MismatchedName(name, filename.name.clone())); + if !matches!( + parse_boolish_environment_variable(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK), + Ok(Some(true)) + ) { + return Err(Error::MismatchedName(name, filename.name.clone())); + } } if version != filename.version && version != filename.version.clone().without_local() { - return Err(Error::MismatchedVersion(version, filename.version.clone())); + if !matches!( + parse_boolish_environment_variable(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK), + Ok(Some(true)) + ) { + return Err(Error::MismatchedVersion(version, filename.version.clone())); + } } } diff --git a/crates/uv-install-wheel/src/lib.rs b/crates/uv-install-wheel/src/lib.rs index d79fd1ddd..97a1eb234 100644 --- a/crates/uv-install-wheel/src/lib.rs +++ b/crates/uv-install-wheel/src/lib.rs @@ -3,6 +3,7 @@ use std::io; use std::path::PathBuf; +use owo_colors::OwoColorize; use thiserror::Error; use uv_fs::Simplified; @@ -74,9 +75,9 @@ pub enum Error { MissingTopLevel(PathBuf), #[error("Invalid package version")] InvalidVersion(#[from] uv_pep440::VersionParseError), - #[error("Wheel package name does not match filename: {0} != {1}")] + #[error("Wheel package name does not match filename ({0} != {1}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] MismatchedName(PackageName, PackageName), - #[error("Wheel version does not match filename: {0} != {1}")] + #[error("Wheel version does not match filename ({0} != {1}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] MismatchedVersion(Version, Version), #[error("Invalid egg-link")] InvalidEggLink(PathBuf), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index c872ed3f8..47b0949f2 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -46,6 +46,7 @@ use uv_pypi_types::{ }; use uv_redacted::DisplaySafeUrl; use uv_small_str::SmallString; +use uv_static::{EnvVars, parse_boolish_environment_variable}; use uv_types::{BuildContext, HashStrategy}; use uv_workspace::{Editability, WorkspaceMember}; @@ -3245,11 +3246,16 @@ impl PackageWire { if *version != wheel.filename.version && *version != wheel.filename.version.clone().without_local() { - return Err(LockError::from(LockErrorKind::InconsistentVersions { - name: self.id.name, - version: version.clone(), - wheel: wheel.clone(), - })); + if !matches!( + parse_boolish_environment_variable(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK), + Ok(Some(true)) + ) { + return Err(LockError::from(LockErrorKind::InconsistentVersions { + name: self.id.name, + version: version.clone(), + wheel: wheel.clone(), + })); + } } } // We can't check the source dist version since it does not need to contain the version @@ -5866,7 +5872,7 @@ enum LockErrorKind { }, /// A package has inconsistent versions in a single entry // Using name instead of id since the version in the id is part of the conflict. - #[error("The entry for package `{name}` v{version} has wheel `{wheel_filename}` with inconsistent version: v{wheel_version} ", name = name.cyan(), wheel_filename = wheel.filename, wheel_version = wheel.filename.version)] + #[error("The entry for package `{name}` ({version}) has wheel `{wheel_filename}` with inconsistent version ({wheel_version}), which indicates a malformed wheel. If this is intentional, set `{env_var}`.", name = name.cyan(), wheel_filename = wheel.filename, wheel_version = wheel.filename.version, env_var = "UV_SKIP_WHEEL_FILENAME_CHECK=1".green())] InconsistentVersions { /// The name of the package with the inconsistent entry. name: PackageName, diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index 8f01087d1..ac851776b 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use uv_dirs::{system_config_file, user_config_dir}; use uv_fs::Simplified; -use uv_static::EnvVars; +use uv_static::{EnvVars, parse_boolish_environment_variable, parse_string_environment_variable}; use uv_warnings::warn_user; pub use crate::combine::*; @@ -554,12 +554,8 @@ pub enum Error { #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `uv.toml` file. `{}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display(), _1, _1)] PyprojectOnlyField(PathBuf, &'static str), - #[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")] - InvalidEnvironmentVariable { - name: String, - value: String, - err: String, - }, + #[error("{0}")] + InvalidEnvironmentVariable(String), } /// Options loaded from environment variables. @@ -578,95 +574,28 @@ impl EnvironmentOptions { /// Create a new [`EnvironmentOptions`] from environment variables. pub fn new() -> Result { Ok(Self { - python_install_bin: parse_boolish_environment_variable(EnvVars::UV_PYTHON_INSTALL_BIN)?, + python_install_bin: parse_boolish_environment_variable(EnvVars::UV_PYTHON_INSTALL_BIN) + .map_err(Error::InvalidEnvironmentVariable)?, python_install_registry: parse_boolish_environment_variable( EnvVars::UV_PYTHON_INSTALL_REGISTRY, - )?, + ) + .map_err(Error::InvalidEnvironmentVariable)?, install_mirrors: PythonInstallMirrors { python_install_mirror: parse_string_environment_variable( EnvVars::UV_PYTHON_INSTALL_MIRROR, - )?, + ) + .map_err(Error::InvalidEnvironmentVariable)?, pypy_install_mirror: parse_string_environment_variable( EnvVars::UV_PYPY_INSTALL_MIRROR, - )?, + ) + .map_err(Error::InvalidEnvironmentVariable)?, python_downloads_json_url: parse_string_environment_variable( EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL, - )?, + ) + .map_err(Error::InvalidEnvironmentVariable)?, }, - log_context: parse_boolish_environment_variable(EnvVars::UV_LOG_CONTEXT)?, + log_context: parse_boolish_environment_variable(EnvVars::UV_LOG_CONTEXT) + .map_err(Error::InvalidEnvironmentVariable)?, }) } } - -/// Parse a boolean environment variable. -/// -/// Adapted from Clap's `BoolishValueParser` which is dual licensed under the MIT and Apache-2.0. -fn parse_boolish_environment_variable(name: &'static str) -> Result, Error> { - // See `clap_builder/src/util/str_to_bool.rs` - // We want to match Clap's accepted values - - // True values are `y`, `yes`, `t`, `true`, `on`, and `1`. - const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"]; - - // False values are `n`, `no`, `f`, `false`, `off`, and `0`. - const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"]; - - // Converts a string literal representation of truth to true or false. - // - // `false` values are `n`, `no`, `f`, `false`, `off`, and `0` (case insensitive). - // - // Any other value will be considered as `true`. - fn str_to_bool(val: impl AsRef) -> Option { - let pat: &str = &val.as_ref().to_lowercase(); - if TRUE_LITERALS.contains(&pat) { - Some(true) - } else if FALSE_LITERALS.contains(&pat) { - Some(false) - } else { - None - } - } - - let Some(value) = std::env::var_os(name) else { - return Ok(None); - }; - - let Some(value) = value.to_str() else { - return Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: value.to_string_lossy().to_string(), - err: "expected a valid UTF-8 string".to_string(), - }); - }; - - let Some(value) = str_to_bool(value) else { - return Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: value.to_string(), - err: "expected a boolish value".to_string(), - }); - }; - - Ok(Some(value)) -} - -/// Parse a string environment variable. -fn parse_string_environment_variable(name: &'static str) -> Result, Error> { - match std::env::var(name) { - Ok(v) => { - if v.is_empty() { - Ok(None) - } else { - Ok(Some(v)) - } - } - Err(e) => match e { - std::env::VarError::NotPresent => Ok(None), - std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable { - name: name.to_string(), - value: err.to_string_lossy().to_string(), - err: "expected a valid UTF-8 string".to_string(), - }), - }, - } -} diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 97d0b9f28..5decfe439 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -937,4 +937,10 @@ impl EnvVars { /// The AWS shared credentials file to use when signing S3 requests. pub const AWS_SHARED_CREDENTIALS_FILE: &'static str = "AWS_SHARED_CREDENTIALS_FILE"; + + /// Avoid verifying that wheel filenames match their contents when installing wheels. This + /// is not recommended, as wheels with inconsistent filenames should be considered invalid and + /// corrected by the relevant package maintainers; however, this option can be used to work + /// around invalid artifacts in rare cases. + pub const UV_SKIP_WHEEL_FILENAME_CHECK: &'static str = "UV_SKIP_WHEEL_FILENAME_CHECK"; } diff --git a/crates/uv-static/src/lib.rs b/crates/uv-static/src/lib.rs index 153591db7..1e9209b6d 100644 --- a/crates/uv-static/src/lib.rs +++ b/crates/uv-static/src/lib.rs @@ -1,3 +1,74 @@ pub use env_vars::*; mod env_vars; + +/// Parse a boolean environment variable. +/// +/// Adapted from Clap's `BoolishValueParser` which is dual licensed under the MIT and Apache-2.0. +pub fn parse_boolish_environment_variable(name: &'static str) -> Result, String> { + // See `clap_builder/src/util/str_to_bool.rs` + // We want to match Clap's accepted values + + // True values are `y`, `yes`, `t`, `true`, `on`, and `1`. + const TRUE_LITERALS: [&str; 6] = ["y", "yes", "t", "true", "on", "1"]; + + // False values are `n`, `no`, `f`, `false`, `off`, and `0`. + const FALSE_LITERALS: [&str; 6] = ["n", "no", "f", "false", "off", "0"]; + + // Converts a string literal representation of truth to true or false. + // + // `false` values are `n`, `no`, `f`, `false`, `off`, and `0` (case insensitive). + // + // Any other value will be considered as `true`. + fn str_to_bool(val: impl AsRef) -> Option { + let pat: &str = &val.as_ref().to_lowercase(); + if TRUE_LITERALS.contains(&pat) { + Some(true) + } else if FALSE_LITERALS.contains(&pat) { + Some(false) + } else { + None + } + } + + let Some(value) = std::env::var_os(name) else { + return Ok(None); + }; + + let Some(value) = value.to_str() else { + return Err(format!( + "Failed to parse environment variable `{}` with invalid value `{}`: expected a valid UTF-8 string", + name, + value.to_string_lossy() + )); + }; + + let Some(value) = str_to_bool(value) else { + return Err(format!( + "Failed to parse environment variable `{name}` with invalid value `{value}`: expected a boolish value" + )); + }; + + Ok(Some(value)) +} + +/// Parse a string environment variable. +pub fn parse_string_environment_variable(name: &'static str) -> Result, String> { + match std::env::var(name) { + Ok(v) => { + if v.is_empty() { + Ok(None) + } else { + Ok(Some(v)) + } + } + Err(e) => match e { + std::env::VarError::NotPresent => Ok(None), + std::env::VarError::NotUnicode(err) => Err(format!( + "Failed to parse environment variable `{}` with invalid value `{}`: expected a valid UTF-8 string", + name, + err.to_string_lossy() + )), + }, + } +} diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index 663da1a11..6acd31714 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1233,7 +1233,7 @@ fn mismatched_version() -> Result<()> { uv_snapshot!(context.filters(), context.pip_sync() .arg("requirements.txt") - .arg("--strict"), @r###" + .arg("--strict"), @r" success: false exit_code: 2 ----- stdout ----- @@ -1242,8 +1242,23 @@ fn mismatched_version() -> Result<()> { Resolved 1 package in [TIME] Prepared 1 package in [TIME] error: Failed to install: tomli-3.7.2-py3-none-any.whl (tomli==3.7.2 (from file://[TEMP_DIR]/tomli-3.7.2-py3-none-any.whl)) - Caused by: Wheel version does not match filename: 2.0.1 != 3.7.2 - "### + Caused by: Wheel version does not match filename (2.0.1 != 3.7.2), which indicates a malformed wheel. If this is intentional, set `UV_SKIP_WHEEL_FILENAME_CHECK=1`. + " + ); + + uv_snapshot!(context.filters(), context.pip_sync() + .arg("requirements.txt") + .arg("--strict") + .env(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK, "1"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Installed 1 package in [TIME] + + tomli==3.7.2 (from file://[TEMP_DIR]/tomli-3.7.2-py3-none-any.whl) + " ); Ok(()) diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 8f8f33276..aa29d4652 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -11538,7 +11538,7 @@ fn locked_version_coherence() -> Result<()> { ----- stderr ----- error: Failed to parse `uv.lock` - Caused by: The entry for package `iniconfig` v1.0.0 has wheel `iniconfig-2.0.0-py3-none-any.whl` with inconsistent version: v2.0.0 + Caused by: The entry for package `iniconfig` (1.0.0) has wheel `iniconfig-2.0.0-py3-none-any.whl` with inconsistent version (2.0.0), which indicates a malformed wheel. If this is intentional, set `UV_SKIP_WHEEL_FILENAME_CHECK=1`. "); // Without `--locked`, we could fail or recreate the lockfile, currently, we fail. @@ -11549,7 +11549,7 @@ fn locked_version_coherence() -> Result<()> { ----- stderr ----- error: Failed to parse `uv.lock` - Caused by: The entry for package `iniconfig` v1.0.0 has wheel `iniconfig-2.0.0-py3-none-any.whl` with inconsistent version: v2.0.0 + Caused by: The entry for package `iniconfig` (1.0.0) has wheel `iniconfig-2.0.0-py3-none-any.whl` with inconsistent version (2.0.0), which indicates a malformed wheel. If this is intentional, set `UV_SKIP_WHEEL_FILENAME_CHECK=1`. "); Ok(()) diff --git a/docs/pip/compatibility.md b/docs/pip/compatibility.md index 2ce702006..09eac0d2b 100644 --- a/docs/pip/compatibility.md +++ b/docs/pip/compatibility.md @@ -481,3 +481,11 @@ is. For example, `uv pip install foo bar` prioritizes newer versions of `foo` over `bar` and could result in a different resolution than `uv pip install bar foo`. Similarly, this behavior applies to the ordering of requirements in input files for `uv pip compile`. + +## Wheel filename and metadata validation + +By default, uv will reject wheels whose filenames are inconsistent with the wheel metadata inside +the file. For example, a wheel named `foo-1.0.0-py3-none-any.whl` that contains metadata indicating +the version is `1.0.1` will be rejected by uv, but accepted by pip. + +To force uv to accept such wheels, set `UV_SKIP_WHEEL_FILENAME_CHECK=1` in the environment. diff --git a/docs/reference/environment.md b/docs/reference/environment.md index 7fd46a4a0..2ba729cd4 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -499,6 +499,13 @@ The URL to treat as an S3-compatible storage endpoint. Requests to this endpoint will be signed using AWS Signature Version 4 based on the `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_PROFILE`, and `AWS_CONFIG_FILE` environment variables. +### `UV_SKIP_WHEEL_FILENAME_CHECK` + +Avoid verifying that wheel filenames match their contents when installing wheels. This +is not recommended, as wheels with inconsistent filenames should be considered invalid and +corrected by the relevant package maintainers; however, this option can be used to work +around invalid artifacts in rare cases. + ### `UV_STACK_SIZE` Use to set the stack size used by uv.