From ab2f394019903b2ea1245e3188b4deda65d6c6fe Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 29 Sep 2025 20:10:11 -0400 Subject: [PATCH] Use a global flags instance for wheel check (#16047) ## Summary This stands up the idea proposed in https://github.com/astral-sh/uv/pull/16046/files#r2384395797. --- Cargo.lock | 12 ++- Cargo.toml | 1 + crates/uv-flags/Cargo.toml | 24 +++++ crates/uv-flags/src/lib.rs | 21 +++++ crates/uv-install-wheel/Cargo.toml | 2 +- crates/uv-install-wheel/src/install.rs | 17 +--- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/lock/mod.rs | 20 ++--- crates/uv-settings/Cargo.toml | 1 + crates/uv-settings/src/lib.rs | 117 +++++++++++++++++++++---- crates/uv-static/src/lib.rs | 71 --------------- crates/uv/Cargo.toml | 1 + crates/uv/src/lib.rs | 5 ++ 13 files changed, 179 insertions(+), 114 deletions(-) create mode 100644 crates/uv-flags/Cargo.toml create mode 100644 crates/uv-flags/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d45e9f4b7..335a7790e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 8c0913c2c..529ffcd4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/crates/uv-flags/Cargo.toml b/crates/uv-flags/Cargo.toml new file mode 100644 index 000000000..df16040f0 --- /dev/null +++ b/crates/uv-flags/Cargo.toml @@ -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 = [] diff --git a/crates/uv-flags/src/lib.rs b/crates/uv-flags/src/lib.rs new file mode 100644 index 000000000..3c58882e9 --- /dev/null +++ b/crates/uv-flags/src/lib.rs @@ -0,0 +1,21 @@ +use std::sync::OnceLock; + +static FLAGS: OnceLock = 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) +} diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index 4b473e116..b56ea9a0b 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -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 } diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index b4c06a55e..0bc8b660b 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -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,23 +48,13 @@ pub fn install_wheel( 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())); - } + 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())); - } + return Err(Error::MismatchedVersion(version, filename.version.clone())); } } diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 77965eaa9..158e2967d 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -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 } diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 47b0949f2..2543d178f 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -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, ) -> Result { // Consistency 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)) - ) { + 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() + { return Err(LockError::from(LockErrorKind::InconsistentVersions { name: self.id.name, version: version.clone(), @@ -3257,9 +3253,9 @@ impl PackageWire { })); } } + // We can't check the source dist version since it does not need to contain the version + // in the filename. } - // 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| -> Result, LockError> { diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 1cc376eea..0793c9204 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -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 } diff --git a/crates/uv-settings/src/lib.rs b/crates/uv-settings/src/lib.rs index ac851776b..3e0196251 100644 --- a/crates/uv-settings/src/lib.rs +++ b/crates/uv-settings/src/lib.rs @@ -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, pub python_install_bin: Option, pub python_install_registry: Option, pub install_mirrors: PythonInstallMirrors, @@ -574,28 +580,109 @@ 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) - .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, 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(), + }), + }, + } +} + +/// 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 + } +} diff --git a/crates/uv-static/src/lib.rs b/crates/uv-static/src/lib.rs index 1e9209b6d..153591db7 100644 --- a/crates/uv-static/src/lib.rs +++ b/crates/uv-static/src/lib.rs @@ -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, 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/Cargo.toml b/crates/uv/Cargo.toml index 230a2bd10..c4c9af821 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -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 } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index ddb03c4bc..0b6673ecc 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -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 { // 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())?;