Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/uv-install-wheel/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -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 }
Expand Down
15 changes: 13 additions & 2 deletions crates/uv-install-wheel/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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()));
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions crates/uv-install-wheel/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::io;
use std::path::PathBuf;

use owo_colors::OwoColorize;
use thiserror::Error;

use uv_fs::Simplified;
Expand Down Expand Up @@ -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),
Expand Down
18 changes: 12 additions & 6 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to do this once and pass it around, but it's legitimately annoying to do here, because this is ultimately triggered by a TryFrom on LockWire, so even if we did have it as a setting, we'd have to restructure the Serde setup to make it work.

Honestly, I kind of think we should parse these once (EnvironmentOptions) then set some global flags on a singleton that we can then access anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this out here: #16047

) {
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
Expand Down Expand Up @@ -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,
Expand Down
101 changes: 15 additions & 86 deletions crates/uv-settings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down Expand Up @@ -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.
Expand All @@ -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(),
}),
},
}
}
6 changes: 6 additions & 0 deletions crates/uv-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
71 changes: 71 additions & 0 deletions crates/uv-static/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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()
)),
},
}
}
21 changes: 18 additions & 3 deletions crates/uv/tests/it/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand All @@ -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(())
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/it/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(())
Expand Down
Loading
Loading