mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-03 05:03:46 +00:00
Add UV_SKIP_WHEEL_FILENAME_CHECK to allow installing invalid wheels (#16046)
## Summary This PR adds a user setting to allow (in rare cases) accepting wheels with mismatched filenames and internal metadata. Closes https://github.com/astral-sh/uv/issues/8082. Closes https://github.com/astral-sh/uv/issues/15647.
This commit is contained in:
parent
170ab1cd7f
commit
7d9ea797b0
12 changed files with 159 additions and 101 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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<Cache: serde::Serialize, Build: serde::Serialize>(
|
|||
// 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Self, Error> {
|
||||
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<Option<bool>, 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<str>) -> Option<bool> {
|
||||
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<Option<String>, 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(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Option<bool>, 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<str>) -> Option<bool> {
|
||||
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<Option<String>, 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()
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue