Use a global flags instance for wheel check (#16047)
Some checks failed
CI / Determine changes (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / cargo shear (push) Has been cancelled
CI / typos (push) Has been cancelled
CI / mkdocs (push) Has been cancelled
zizmor / Run zizmor (push) Has been cancelled
CI / cargo clippy | ubuntu (push) Has been cancelled
CI / cargo clippy | windows (push) Has been cancelled
CI / cargo dev generate-all (push) Has been cancelled
CI / cargo test | ubuntu (push) Has been cancelled
CI / cargo test | macos (push) Has been cancelled
CI / cargo test | windows (push) Has been cancelled
CI / check windows trampoline | aarch64 (push) Has been cancelled
CI / check windows trampoline | i686 (push) Has been cancelled
CI / check windows trampoline | x86_64 (push) Has been cancelled
CI / test windows trampoline | aarch64 (push) Has been cancelled
CI / test windows trampoline | i686 (push) Has been cancelled
CI / test windows trampoline | x86_64 (push) Has been cancelled
CI / build binary | linux libc (push) Has been cancelled
CI / build binary | linux aarch64 (push) Has been cancelled
CI / build binary | linux musl (push) Has been cancelled
CI / build binary | macos aarch64 (push) Has been cancelled
CI / build binary | macos x86_64 (push) Has been cancelled
CI / build binary | windows x86_64 (push) Has been cancelled
CI / build binary | windows aarch64 (push) Has been cancelled
CI / build binary | msrv (push) Has been cancelled
CI / build binary | freebsd (push) Has been cancelled
CI / ecosystem test | pydantic/pydantic-core (push) Has been cancelled
CI / ecosystem test | prefecthq/prefect (push) Has been cancelled
CI / ecosystem test | pallets/flask (push) Has been cancelled
CI / smoke test | linux (push) Has been cancelled
CI / smoke test | linux aarch64 (push) Has been cancelled
CI / check system | alpine (push) Has been cancelled
CI / smoke test | macos (push) Has been cancelled
CI / smoke test | windows x86_64 (push) Has been cancelled
CI / smoke test | windows aarch64 (push) Has been cancelled
CI / integration test | activate nushell venv (push) Has been cancelled
CI / integration test | conda on ubuntu (push) Has been cancelled
CI / integration test | deadsnakes python3.9 on ubuntu (push) Has been cancelled
CI / integration test | free-threaded on windows (push) Has been cancelled
CI / integration test | aarch64 windows implicit (push) Has been cancelled
CI / integration test | aarch64 windows explicit (push) Has been cancelled
CI / integration test | pypy on ubuntu (push) Has been cancelled
CI / integration test | pypy on windows (push) Has been cancelled
CI / integration test | graalpy on ubuntu (push) Has been cancelled
CI / integration test | graalpy on windows (push) Has been cancelled
CI / integration test | pyodide on ubuntu (push) Has been cancelled
CI / integration test | github actions (push) Has been cancelled
CI / integration test | free-threaded python on github actions (push) Has been cancelled
CI / integration test | pyenv on wsl x86-64 (push) Has been cancelled
CI / integration test | determine publish changes (push) Has been cancelled
CI / integration test | registries (push) Has been cancelled
CI / integration test | uv publish (push) Has been cancelled
CI / integration test | uv_build (push) Has been cancelled
CI / check cache | ubuntu (push) Has been cancelled
CI / check cache | macos aarch64 (push) Has been cancelled
CI / check system | python on debian (push) Has been cancelled
CI / check system | python on fedora (push) Has been cancelled
CI / check system | python on ubuntu (push) Has been cancelled
CI / check system | python on rocky linux 8 (push) Has been cancelled
CI / check system | python on rocky linux 9 (push) Has been cancelled
CI / check system | graalpy on ubuntu (push) Has been cancelled
CI / check system | pypy on ubuntu (push) Has been cancelled
CI / check system | pyston (push) Has been cancelled
CI / check system | python on macos aarch64 (push) Has been cancelled
CI / check system | homebrew python on macos aarch64 (push) Has been cancelled
CI / check system | x86-64 python on macos aarch64 (push) Has been cancelled
CI / check system | python on macos x86-64 (push) Has been cancelled
CI / check system | python3.10 on windows x86-64 (push) Has been cancelled
CI / check system | python3.10 on windows x86 (push) Has been cancelled
CI / check system | python3.13 on windows x86-64 (push) Has been cancelled
CI / check system | x86-64 python3.13 on windows aarch64 (push) Has been cancelled
CI / check system | aarch64 python3.13 on windows aarch64 (push) Has been cancelled
CI / check system | windows registry (push) Has been cancelled
CI / check system | python3.12 via chocolatey (push) Has been cancelled
CI / check system | python3.9 via pyenv (push) Has been cancelled
CI / check system | python3.13 (push) Has been cancelled
CI / check system | conda3.11 on macos aarch64 (push) Has been cancelled
CI / check system | conda3.8 on macos aarch64 (push) Has been cancelled
CI / check system | conda3.11 on linux x86-64 (push) Has been cancelled
CI / check system | conda3.8 on linux x86-64 (push) Has been cancelled
CI / check system | conda3.11 on windows x86-64 (push) Has been cancelled
CI / check system | conda3.8 on windows x86-64 (push) Has been cancelled
CI / check system | amazonlinux (push) Has been cancelled
CI / check system | embedded python3.10 on windows x86-64 (push) Has been cancelled
CI / benchmarks | walltime aarch64 linux (push) Has been cancelled
CI / benchmarks | instrumented (push) Has been cancelled

## Summary

This stands up the idea proposed in
https://github.com/astral-sh/uv/pull/16046/files#r2384395797.
This commit is contained in:
Charlie Marsh 2025-09-29 20:10:11 -04:00 committed by GitHub
parent 7d9ea797b0
commit ab2f394019
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 179 additions and 114 deletions

12
Cargo.lock generated
View file

@ -5351,6 +5351,7 @@ dependencies = [
"uv-distribution-filename",
"uv-distribution-types",
"uv-extract",
"uv-flags",
"uv-fs",
"uv-git",
"uv-git-types",
@ -5982,6 +5983,13 @@ dependencies = [
"zstd",
]
[[package]]
name = "uv-flags"
version = "0.0.1"
dependencies = [
"bitflags 2.9.4",
]
[[package]]
name = "uv-fs"
version = "0.0.1"
@ -6085,13 +6093,13 @@ dependencies = [
"thiserror 2.0.16",
"tracing",
"uv-distribution-filename",
"uv-flags",
"uv-fs",
"uv-normalize",
"uv-pep440",
"uv-preview",
"uv-pypi-types",
"uv-shell",
"uv-static",
"uv-trampoline-builder",
"uv-warnings",
"walkdir",
@ -6559,6 +6567,7 @@ dependencies = [
"uv-distribution",
"uv-distribution-filename",
"uv-distribution-types",
"uv-flags",
"uv-fs",
"uv-git",
"uv-git-types",
@ -6621,6 +6630,7 @@ dependencies = [
"uv-configuration",
"uv-dirs",
"uv-distribution-types",
"uv-flags",
"uv-fs",
"uv-install-wheel",
"uv-macros",

View file

@ -37,6 +37,7 @@ uv-distribution = { path = "crates/uv-distribution" }
uv-distribution-filename = { path = "crates/uv-distribution-filename" }
uv-distribution-types = { path = "crates/uv-distribution-types" }
uv-extract = { path = "crates/uv-extract" }
uv-flags = { path = "crates/uv-flags" }
uv-fs = { path = "crates/uv-fs", features = ["serde", "tokio"] }
uv-git = { path = "crates/uv-git" }
uv-git-types = { path = "crates/uv-git-types" }

View file

@ -0,0 +1,24 @@
[package]
name = "uv-flags"
version = "0.0.1"
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
authors = { workspace = true }
license = { workspace = true }
[lib]
doctest = false
[lints]
workspace = true
[dependencies]
bitflags = { workspace = true }
[dev-dependencies]
[features]
default = []

View file

@ -0,0 +1,21 @@
use std::sync::OnceLock;
static FLAGS: OnceLock<EnvironmentFlags> = OnceLock::new();
bitflags::bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EnvironmentFlags: u32 {
const SKIP_WHEEL_FILENAME_CHECK = 1 << 0;
}
}
/// Initialize the environment flags.
#[allow(clippy::result_unit_err)]
pub fn init(flags: EnvironmentFlags) -> Result<(), ()> {
FLAGS.set(flags).map_err(|_| ())
}
/// Check if a specific environment flag is set.
pub fn contains(flag: EnvironmentFlags) -> bool {
FLAGS.get_or_init(EnvironmentFlags::default).contains(flag)
}

View file

@ -22,13 +22,13 @@ name = "uv_install_wheel"
[dependencies]
uv-distribution-filename = { workspace = true }
uv-flags = { workspace = true }
uv-fs = { workspace = true }
uv-normalize = { workspace = true }
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 }

View file

@ -11,7 +11,6 @@ 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::{
@ -49,25 +48,15 @@ pub fn install_wheel<Cache: serde::Serialize, Build: serde::Serialize>(
let version = Version::from_str(&version)?;
// Validate the wheel name and version.
{
if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
if name != filename.name {
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() {
if !matches!(
parse_boolish_environment_variable(EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK),
Ok(Some(true))
) {
return Err(Error::MismatchedVersion(version, filename.version.clone()));
}
}
}
// We're going step by step though
// https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl

View file

@ -23,6 +23,7 @@ uv-configuration = { workspace = true }
uv-distribution = { workspace = true }
uv-distribution-filename = { workspace = true }
uv-distribution-types = { workspace = true }
uv-flags = { workspace = true }
uv-fs = { workspace = true, features = ["serde"] }
uv-git = { workspace = true }
uv-git-types = { workspace = true }

View file

@ -46,7 +46,6 @@ 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};
@ -3241,15 +3240,12 @@ impl PackageWire {
unambiguous_package_ids: &FxHashMap<PackageName, PackageId>,
) -> Result<Package, LockError> {
// Consistency check
if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
if let Some(version) = &self.id.version {
for wheel in &self.wheels {
if *version != wheel.filename.version
&& *version != wheel.filename.version.clone().without_local()
{
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(),
@ -3257,10 +3253,10 @@ impl PackageWire {
}));
}
}
}
// We can't check the source dist version since it does not need to contain the version
// in the filename.
}
}
let unwire_deps = |deps: Vec<DependencyWire>| -> Result<Vec<Dependency>, LockError> {
deps.into_iter()

View file

@ -20,6 +20,7 @@ uv-cache-info = { workspace = true, features = ["schemars"] }
uv-configuration = { workspace = true, features = ["schemars", "clap"] }
uv-dirs = { workspace = true }
uv-distribution-types = { workspace = true, features = ["schemars"] }
uv-flags = { workspace = true }
uv-fs = { workspace = true }
uv-install-wheel = { workspace = true, features = ["schemars", "clap"] }
uv-macros = { workspace = true }

View file

@ -2,8 +2,9 @@ use std::ops::Deref;
use std::path::{Path, PathBuf};
use uv_dirs::{system_config_file, user_config_dir};
use uv_flags::EnvironmentFlags;
use uv_fs::Simplified;
use uv_static::{EnvVars, parse_boolish_environment_variable, parse_string_environment_variable};
use uv_static::EnvVars;
use uv_warnings::warn_user;
pub use crate::combine::*;
@ -554,8 +555,12 @@ 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("{0}")]
InvalidEnvironmentVariable(String),
#[error("Failed to parse environment variable `{name}` with invalid value `{value}`: {err}")]
InvalidEnvironmentVariable {
name: String,
value: String,
err: String,
},
}
/// Options loaded from environment variables.
@ -564,6 +569,7 @@ pub enum Error {
/// the CLI level, however there are limited semantics in that context.
#[derive(Debug, Clone)]
pub struct EnvironmentOptions {
pub skip_wheel_filename_check: Option<bool>,
pub python_install_bin: Option<bool>,
pub python_install_registry: Option<bool>,
pub install_mirrors: PythonInstallMirrors,
@ -574,28 +580,109 @@ 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)
.map_err(Error::InvalidEnvironmentVariable)?,
skip_wheel_filename_check: parse_boolish_environment_variable(
EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK,
)?,
python_install_bin: parse_boolish_environment_variable(EnvVars::UV_PYTHON_INSTALL_BIN)?,
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)
.map_err(Error::InvalidEnvironmentVariable)?,
log_context: parse_boolish_environment_variable(EnvVars::UV_LOG_CONTEXT)?,
})
}
}
/// 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(),
}),
},
}
}
/// Populate the [`EnvironmentFlags`] from the given [`EnvironmentOptions`].
impl From<&EnvironmentOptions> for EnvironmentFlags {
fn from(options: &EnvironmentOptions) -> Self {
let mut flags = Self::empty();
if options.skip_wheel_filename_check == Some(true) {
flags.insert(Self::SKIP_WHEEL_FILENAME_CHECK);
}
flags
}
}

View file

@ -1,74 +1,3 @@
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()
)),
},
}
}

View file

@ -30,6 +30,7 @@ uv-distribution = { workspace = true }
uv-distribution-filename = { workspace = true }
uv-distribution-types = { workspace = true }
uv-extract = { workspace = true }
uv-flags = { workspace = true }
uv-fs = { workspace = true }
uv-git = { workspace = true }
uv-git-types = { workspace = true }

View file

@ -30,6 +30,7 @@ use uv_cli::{
};
use uv_client::BaseClientBuilder;
use uv_configuration::min_stack_size;
use uv_flags::EnvironmentFlags;
use uv_fs::{CWD, Simplified};
#[cfg(feature = "self-update")]
use uv_pep440::release_specifiers_to_ranges;
@ -314,6 +315,10 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
// Resolve the cache settings.
let cache_settings = CacheSettings::resolve(*cli.top_level.cache_args, filesystem.as_ref());
// Set the global flags.
uv_flags::init(EnvironmentFlags::from(&environment))
.map_err(|()| anyhow::anyhow!("Flags are already initialized"))?;
// Enforce the required version.
if let Some(required_version) = globals.required_version.as_ref() {
let package_version = uv_pep440::Version::from_str(uv_version::version())?;