From 4c614db9aed761d8335fc07dd7a21d86a0fc6748 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 14:04:51 -0400 Subject: [PATCH 01/56] Respect `required-environments` in `uv pip compile` (#19378) ## Summary Closes https://github.com/astral-sh/uv/issues/19372. --- crates/uv/src/commands/pip/compile.rs | 9 ++- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 11 +++ crates/uv/tests/it/pip_compile.rs | 53 ++++++++++++ crates/uv/tests/it/show_settings.rs | 111 ++++++++++++++++++++++++++ 5 files changed, 184 insertions(+), 1 deletion(-) diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index b0d38e6742e3c..ed9231d000338 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -72,6 +72,7 @@ pub(crate) async fn pip_compile( excludes_from_workspace: Vec, build_constraints_from_workspace: Vec, environments: SupportedEnvironments, + required_environments: SupportedEnvironments, extras: ExtrasSpecification, groups: GroupsSpecification, output_file: Option<&Path>, @@ -385,7 +386,13 @@ pub(crate) async fn pip_compile( }; let artifact_environments = if universal { - environments.clone() + SupportedEnvironments::from_markers( + environments + .iter() + .chain(required_environments.iter()) + .copied() + .collect(), + ) } else { SupportedEnvironments::default() }; diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 0ae0a316058ed..567beb9368376 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -657,6 +657,7 @@ async fn run(cli: Cli) -> Result { args.excludes_from_workspace, args.build_constraints_from_workspace, args.environments, + args.required_environments, args.settings.extras, groups, args.settings.output_file.as_deref(), diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 6d7075641ecdb..52413422180e4 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -2777,6 +2777,7 @@ pub(crate) struct PipCompileSettings { pub(crate) excludes_from_workspace: Vec, pub(crate) build_constraints_from_workspace: Vec, pub(crate) environments: SupportedEnvironments, + pub(crate) required_environments: SupportedEnvironments, pub(crate) refresh: Refresh, pub(crate) settings: PipSettings, } @@ -2899,6 +2900,15 @@ impl PipCompileSettings { SupportedEnvironments::default() }; + let required_environments = if let Some(configuration) = &filesystem { + configuration + .required_environments + .clone() + .unwrap_or_default() + } else { + SupportedEnvironments::default() + }; + Self { format, src_file, @@ -2923,6 +2933,7 @@ impl PipCompileSettings { excludes_from_workspace, build_constraints_from_workspace, environments, + required_environments, refresh: Refresh::from(refresh), settings: PipSettings::combine( PipOptions { diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index 67dac87b0ec74..d12eda1c4affa 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -14369,6 +14369,59 @@ fn universal_constrained_environment() -> Result<()> { Ok(()) } +/// Resolve with `--universal`, requiring artifact coverage for user-provided environments. +#[test] +fn universal_required_environment() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(&indoc::formatdoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["no-sdist-no-wheels-with-matching-platform-a"] + + [tool.uv] + required-environments = ["platform_machine == 'arm64'"] + + [[tool.uv.index]] + name = "packse" + url = "{}" + default = true + "#, + packse_index_url() + })?; + + let filters: Vec<_> = context + .filters() + .into_iter() + .chain([( + // This hint is only shown when the current platform doesn't match the target. + r"\n\n\s+hint: The resolution failed for an environment that is not the current one[^\n]*", + "", + )]) + .collect(); + + uv_snapshot!(filters, context.pip_compile() + .arg("pyproject.toml") + .arg("--universal") + .arg("--exclude-newer-package") + .arg("no-sdist-no-wheels-with-matching-platform-a=false") + .env_remove(EnvVars::UV_EXCLUDE_NEWER), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies for split (markers: platform_machine == 'arm64'): + ╰─▶ Because only no-sdist-no-wheels-with-matching-platform-a==1.0.0 is available and no-sdist-no-wheels-with-matching-platform-a==1.0.0 has no `platform_machine == 'arm64'`-compatible wheels, we can conclude that all versions of no-sdist-no-wheels-with-matching-platform-a cannot be used. + And because project depends on no-sdist-no-wheels-with-matching-platform-a, we can conclude that your requirements are unsatisfiable. + "); + + Ok(()) +} + /// Resolve a package that has no versions that satisfy the current Python version. #[test] fn compile_enumerate_no_versions() -> Result<()> { diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index d660a54a9895d..06b14cf53a749 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -106,6 +106,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -319,6 +322,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -533,6 +539,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -779,6 +788,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -994,6 +1006,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -1181,6 +1196,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -1419,6 +1437,9 @@ fn resolve_index_url() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -1667,6 +1688,9 @@ fn resolve_index_url() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -1975,6 +1999,9 @@ fn resolve_find_links() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -2210,6 +2237,9 @@ fn resolve_top_level() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -2402,6 +2432,9 @@ fn resolve_top_level() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -2648,6 +2681,9 @@ fn resolve_top_level() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -2917,6 +2953,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -3099,6 +3138,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -3281,6 +3323,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -3465,6 +3510,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -3866,6 +3914,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -4082,6 +4133,9 @@ fn resolve_both() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -4341,6 +4395,9 @@ fn resolve_both_special_fields() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -4679,6 +4736,9 @@ fn resolve_config_file() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -4990,6 +5050,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -5175,6 +5238,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -5379,6 +5445,9 @@ fn allow_insecure_host() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -5575,6 +5644,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -5825,6 +5897,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -6081,6 +6156,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -6332,6 +6410,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -6590,6 +6671,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -6841,6 +6925,9 @@ fn index_priority() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -9508,6 +9595,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -9698,6 +9788,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -9881,6 +9974,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -10062,6 +10158,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -10244,6 +10343,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -10427,6 +10529,9 @@ fn upgrade_pip_cli_config_interaction() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -11407,6 +11512,9 @@ fn build_isolation_override() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { @@ -11585,6 +11693,9 @@ fn build_isolation_override() -> anyhow::Result<()> { environments: SupportedEnvironments( [], ), + required_environments: SupportedEnvironments( + [], + ), refresh: None( Timestamp( SystemTime { From 390ae46203193ad34239a1aeca89cc5886f5e84e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 15:04:53 -0400 Subject: [PATCH 02/56] Apply stricter validation to all wheel filename segments (#19364) ## Summary Right now, we allow arbitrary characters in a few places: (1) `WheelTag::Large` segments, (2) build tags, (3) release tags on certain operating systems (like FreeBSD). The spec says that all wheel filenames must consist of alphanumeric characters and underscores, so if we see something that isn't alphanumeric or an underscore, we're justified in rejecting it. --------- Co-authored-by: Codex --- .../uv-distribution-filename/src/build_tag.rs | 56 +++++- crates/uv-distribution-filename/src/wheel.rs | 77 ++++++-- crates/uv-platform-tags/src/lib.rs | 2 +- crates/uv-platform-tags/src/platform.rs | 8 + crates/uv-platform-tags/src/platform_tag.rs | 177 ++++++++++++++---- crates/uv-platform-tags/src/tags.rs | 71 ++++++- crates/uv/tests/it/pip_sync.rs | 48 +++++ 7 files changed, 375 insertions(+), 64 deletions(-) diff --git a/crates/uv-distribution-filename/src/build_tag.rs b/crates/uv-distribution-filename/src/build_tag.rs index 30c048f5b8352..690142c2321d7 100644 --- a/crates/uv-distribution-filename/src/build_tag.rs +++ b/crates/uv-distribution-filename/src/build_tag.rs @@ -9,6 +9,8 @@ pub enum BuildTagError { Empty, #[error("must start with a digit")] NoLeadingDigit, + #[error("must contain only ASCII letters, digits, underscores, and periods")] + InvalidCharacters, #[error(transparent)] ParseInt(#[from] ParseIntError), } @@ -45,8 +47,18 @@ impl FromStr for BuildTag { return Err(BuildTagError::Empty); } + let mut prefix_end = None; + for (index, byte) in s.bytes().enumerate() { + if !is_build_tag_byte(byte) { + return Err(BuildTagError::InvalidCharacters); + } + if prefix_end.is_none() && !byte.is_ascii_digit() { + prefix_end = Some(index); + } + } + // A build tag must start with a digit. - let (prefix, suffix) = match s.find(|c: char| !c.is_ascii_digit()) { + let (prefix, suffix) = match prefix_end { // Ex) `abc` Some(0) => return Err(BuildTagError::NoLeadingDigit), // Ex) `123abc` @@ -70,3 +82,45 @@ impl std::fmt::Display for BuildTag { } } } + +fn is_build_tag_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'.') +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::BuildTag; + + #[test] + fn parse_periods() { + assert_eq!( + BuildTag::from_str("0.editable") + .map(|build_tag| build_tag.to_string()) + .map_err(|err| err.to_string()), + Ok("0.editable".to_string()) + ); + } + + #[test] + fn err_invalid_characters() { + let err = BuildTag::from_str("1/../../target").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + + let err = BuildTag::from_str(r"1..\..\target").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + + let err = BuildTag::from_str("1target:stream").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + + let err = BuildTag::from_str("1-target").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + + let err = BuildTag::from_str("1 target").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + + let err = BuildTag::from_str("1target\u{e9}").unwrap_err(); + insta::assert_snapshot!(err, @"must contain only ASCII letters, digits, underscores, and periods"); + } +} diff --git a/crates/uv-distribution-filename/src/wheel.rs b/crates/uv-distribution-filename/src/wheel.rs index f3be4a75370ff..3c5afb2ed56a6 100644 --- a/crates/uv-distribution-filename/src/wheel.rs +++ b/crates/uv-distribution-filename/src/wheel.rs @@ -15,7 +15,7 @@ use uv_platform_tags::{ }; use crate::splitter::MemchrSplitter; -use crate::wheel_tag::{WheelTag, WheelTagLarge, WheelTagSmall}; +use crate::wheel_tag::{TagSet, WheelTag, WheelTagLarge, WheelTagSmall}; use crate::{BuildTag, BuildTagError}; #[derive( @@ -262,18 +262,9 @@ impl WheelFilename { WheelTag::Large { large: Box::new(WheelTagLarge { build_tag, - python_tag: MemchrSplitter::split(python_tag, b'.') - .map(LanguageTag::from_str) - .filter_map(Result::ok) - .collect(), - abi_tag: MemchrSplitter::split(abi_tag, b'.') - .map(AbiTag::from_str) - .filter_map(Result::ok) - .collect(), - platform_tag: MemchrSplitter::split(platform_tag, b'.') - .map(PlatformTag::from_str) - .filter_map(Result::ok) - .collect(), + python_tag: parse_large_tag_component::(python_tag, filename)?, + abi_tag: parse_large_tag_component::(abi_tag, filename)?, + platform_tag: parse_large_tag_component::(platform_tag, filename)?, repr: repr.into(), }), } @@ -287,6 +278,39 @@ impl WheelFilename { } } +fn parse_large_tag_component( + component: &str, + filename: &str, +) -> Result, WheelFilenameError> { + if component.is_empty() { + return Err(invalid_tag_component(filename)); + } + + let mut tags = TagSet::new(); + for tag in MemchrSplitter::split(component, b'.') { + if tag.is_empty() || !tag.bytes().all(is_tag_atom_byte) { + return Err(invalid_tag_component(filename)); + } + if let Ok(tag) = T::from_str(tag) { + tags.push(tag); + } + } + + Ok(tags) +} + +fn invalid_tag_component(filename: &str) -> WheelFilenameError { + WheelFilenameError::InvalidWheelFileName( + filename.to_string(), + "Tag components must contain only ASCII letters, digits, underscores, and periods" + .to_string(), + ) +} + +fn is_tag_atom_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || byte == b'_' +} + impl<'de> Deserialize<'de> for WheelFilename { fn deserialize(deserializer: D) -> Result where @@ -408,6 +432,33 @@ mod tests { fn err_invalid_build_tag() { let err = WheelFilename::from_str("foo-1.2.3-tag-py3-none-any.whl").unwrap_err(); insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-tag-py3-none-any.whl" has an invalid build tag: must start with a digit"#); + + let err = WheelFilename::from_str("foo-1.2.3-1/../../target-py3-none-any.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-1/../../target-py3-none-any.whl" has an invalid build tag: must contain only ASCII letters, digits, underscores, and periods"#); + } + + #[test] + fn err_invalid_tag_component() { + let err = WheelFilename::from_str("foo-1.2.3-py3-none-../target.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-../target.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); + + let err = WheelFilename::from_str(r"foo-1.2.3-py3-none-..\target.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-..\target.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); + + let err = WheelFilename::from_str("foo-1.2.3-py3-none-target:stream.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-target:stream.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); + + let err = WheelFilename::from_str("foo-1.2.3-py3-none-freebsd_13_x86/64.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-freebsd_13_x86/64.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); + + let err = WheelFilename::from_str("foo-1.2.3-py3-none-unknown tag.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-unknown tag.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); + + let err = WheelFilename::from_str("foo-1.2.3-py3-none-unknown\u{e9}.whl").unwrap_err(); + insta::assert_snapshot!(err, @"The wheel filename \"foo-1.2.3-py3-none-unknown\u{e9}.whl\" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"); + + let err = WheelFilename::from_str("foo-1.2.3-py3-none-unknown..tag.whl").unwrap_err(); + insta::assert_snapshot!(err, @r#"The wheel filename "foo-1.2.3-py3-none-unknown..tag.whl" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods"#); } #[test] diff --git a/crates/uv-platform-tags/src/lib.rs b/crates/uv-platform-tags/src/lib.rs index 5350310b9b76c..a93a34983deee 100644 --- a/crates/uv-platform-tags/src/lib.rs +++ b/crates/uv-platform-tags/src/lib.rs @@ -1,7 +1,7 @@ pub use abi_tag::{AbiTag, CPythonAbiVariants, ParseAbiTagError}; pub use language_tag::{LanguageTag, ParseLanguageTagError}; pub use platform::{Arch, Os, Platform, PlatformError}; -pub use platform_tag::{ParsePlatformTagError, PlatformTag}; +pub use platform_tag::{ParsePlatformTagError, ParseReleaseArchError, PlatformTag, ReleaseArch}; pub use tags::{ BinaryFormat, IncompatibleTag, TagCompatibility, TagPriority, Tags, TagsError, TagsOptions, }; diff --git a/crates/uv-platform-tags/src/platform.rs b/crates/uv-platform-tags/src/platform.rs index d6eeb4c796ad2..bfea8c8604613 100644 --- a/crates/uv-platform-tags/src/platform.rs +++ b/crates/uv-platform-tags/src/platform.rs @@ -5,12 +5,20 @@ use std::{fmt, io}; use thiserror::Error; +use crate::ParseReleaseArchError; + #[derive(Error, Debug)] pub enum PlatformError { #[error(transparent)] IOError(#[from] io::Error), #[error("Failed to detect the operating system version: {0}")] OsVersionDetectionError(String), + #[error("Invalid platform release and architecture `{release_arch}`: {error}")] + InvalidReleaseArch { + release_arch: String, + #[source] + error: ParseReleaseArchError, + }, #[error("Invalid Android architecture: {0}")] InvalidAndroidArch(Arch), #[error("Invalid iOS simulator architecture: {0}")] diff --git a/crates/uv-platform-tags/src/platform_tag.rs b/crates/uv-platform-tags/src/platform_tag.rs index 34bb7f83af23e..420702118030e 100644 --- a/crates/uv-platform-tags/src/platform_tag.rs +++ b/crates/uv-platform-tags/src/platform_tag.rs @@ -7,6 +7,43 @@ use crate::tags::AndroidAbi; use crate::tags::IosMultiarch; use crate::{Arch, BinaryFormat}; +/// Opaque release-and-architecture suffix used by [`PlatformTag`] variants that preserve a native +/// platform-specific suffix. +#[derive( + Debug, + Clone, + Eq, + PartialEq, + Ord, + PartialOrd, + Hash, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] +#[rkyv(derive(Debug))] +pub struct ReleaseArch(SmallString); + +impl FromStr for ReleaseArch { + type Err = ParseReleaseArchError; + + fn from_str(s: &str) -> Result { + if s.bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + { + Ok(Self(SmallString::from(s))) + } else { + Err(ParseReleaseArchError) + } + } +} + +impl std::fmt::Display for ReleaseArch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + /// A tag to represent the platform compatibility of a Python distribution. /// /// This is the third segment in the wheel filename, following the language and ABI tags. For @@ -60,19 +97,19 @@ pub enum PlatformTag { /// Ex) `android_21_x86_64` Android { api_level: u16, abi: AndroidAbi }, /// Ex) `freebsd_12_x86_64` - FreeBsd { release_arch: SmallString }, + FreeBsd { release_arch: ReleaseArch }, /// Ex) `netbsd_9_x86_64` - NetBsd { release_arch: SmallString }, + NetBsd { release_arch: ReleaseArch }, /// Ex) `openbsd_6_x86_64` - OpenBsd { release_arch: SmallString }, + OpenBsd { release_arch: ReleaseArch }, /// Ex) `dragonfly_6_x86_64` - Dragonfly { release_arch: SmallString }, + Dragonfly { release_arch: ReleaseArch }, /// Ex) `haiku_1_x86_64` - Haiku { release_arch: SmallString }, + Haiku { release_arch: ReleaseArch }, /// Ex) `illumos_5_11_x86_64` - Illumos { release_arch: SmallString }, + Illumos { release_arch: ReleaseArch }, /// Ex) `solaris_11_4_x86_64` - Solaris { release_arch: SmallString }, + Solaris { release_arch: ReleaseArch }, /// Ex) `pyodide_2024_0_wasm32` Pyodide { major: u16, minor: u16 }, /// Ex) `ios_13_0_arm64_iphoneos` / `ios_13_0_arm64_iphonesimulator` @@ -688,9 +725,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::FreeBsd { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -702,9 +740,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::NetBsd { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -716,9 +755,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::OpenBsd { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -730,9 +770,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::Dragonfly { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -744,9 +785,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::Haiku { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -758,9 +800,10 @@ impl FromStr for PlatformTag { tag: s.to_string(), }); } - return Ok(Self::Illumos { - release_arch: SmallString::from(rest), + release_arch: rest + .parse::() + .map_err(|_| ParsePlatformTagError::InvalidCharacters { tag: s.to_string() })?, }); } @@ -776,7 +819,9 @@ impl FromStr for PlatformTag { if let Some(release_arch) = rest.strip_suffix("_64bit") { if !release_arch.is_empty() { return Ok(Self::Solaris { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|_| { + ParsePlatformTagError::InvalidCharacters { tag: s.to_string() } + })?, }); } } @@ -872,6 +917,10 @@ impl FromStr for PlatformTag { } } +#[derive(Debug, Clone, Copy, thiserror::Error, PartialEq, Eq)] +#[error("release and architecture must contain only ASCII letters, digits, and underscores")] +pub struct ParseReleaseArchError; + #[derive(Debug, thiserror::Error, PartialEq, Eq)] pub enum ParsePlatformTagError { #[error("Unknown platform tag format: {0}")] @@ -886,13 +935,17 @@ pub enum ParsePlatformTagError { InvalidArch { platform: &'static str, tag: String }, #[error("Invalid API level in {platform} platform tag: {tag}")] InvalidApiLevel { platform: &'static str, tag: String }, + #[error("Platform tag must contain only ASCII letters, digits, and underscores: {tag}")] + InvalidCharacters { tag: String }, } #[cfg(test)] mod tests { use std::str::FromStr; - use crate::platform_tag::{ParsePlatformTagError, PlatformTag}; + use crate::platform_tag::{ + ParsePlatformTagError, ParseReleaseArchError, PlatformTag, ReleaseArch, + }; use crate::tags::AndroidAbi; use crate::tags::IosMultiarch; use crate::{Arch, BinaryFormat}; @@ -1110,40 +1163,46 @@ mod tests { assert_eq!(PlatformTag::WinArm64.to_string(), "win_arm64"); } + #[test] + fn release_arch() { + assert_eq!( + ReleaseArch::from_str("13_14_x86_64").map(|release_arch| release_arch.to_string()), + Ok("13_14_x86_64".to_string()) + ); + assert_eq!( + ReleaseArch::from_str("13_x86/64"), + Err(ParseReleaseArchError) + ); + } + #[test] fn freebsd_platform() { - let tag = PlatformTag::FreeBsd { - release_arch: "13_14_x86_64".into(), - }; assert_eq!( - PlatformTag::from_str("freebsd_13_14_x86_64").as_ref(), - Ok(&tag) + PlatformTag::from_str("freebsd_13_14_x86_64") + .as_ref() + .map(ToString::to_string), + Ok("freebsd_13_14_x86_64".to_string()) ); - assert_eq!(tag.to_string(), "freebsd_13_14_x86_64"); } #[test] fn illumos_platform() { - let tag = PlatformTag::Illumos { - release_arch: "5_11_x86_64".into(), - }; assert_eq!( - PlatformTag::from_str("illumos_5_11_x86_64").as_ref(), - Ok(&tag) + PlatformTag::from_str("illumos_5_11_x86_64") + .as_ref() + .map(ToString::to_string), + Ok("illumos_5_11_x86_64".to_string()) ); - assert_eq!(tag.to_string(), "illumos_5_11_x86_64"); } #[test] fn solaris_platform() { - let tag = PlatformTag::Solaris { - release_arch: "11_4_x86_64".into(), - }; assert_eq!( - PlatformTag::from_str("solaris_11_4_x86_64_64bit").as_ref(), - Ok(&tag) + PlatformTag::from_str("solaris_11_4_x86_64_64bit") + .as_ref() + .map(ToString::to_string), + Ok("solaris_11_4_x86_64_64bit".to_string()) ); - assert_eq!(tag.to_string(), "solaris_11_4_x86_64_64bit"); assert_eq!( PlatformTag::from_str("solaris_11_4_x86_64"), @@ -1154,6 +1213,46 @@ mod tests { ); } + #[test] + fn invalid_characters_platform() { + assert_eq!( + PlatformTag::from_str("freebsd_13_x86/64"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: "freebsd_13_x86/64".to_string() + }) + ); + assert_eq!( + PlatformTag::from_str(r"netbsd_9_x86\64"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: r"netbsd_9_x86\64".to_string() + }) + ); + assert_eq!( + PlatformTag::from_str("solaris_11_4_x86:64_64bit"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: "solaris_11_4_x86:64_64bit".to_string() + }) + ); + assert_eq!( + PlatformTag::from_str("freebsd_13.14_x86_64"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: "freebsd_13.14_x86_64".to_string() + }) + ); + assert_eq!( + PlatformTag::from_str("freebsd_13 x86_64"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: "freebsd_13 x86_64".to_string() + }) + ); + assert_eq!( + PlatformTag::from_str("freebsd_13_x86\u{e9}"), + Err(ParsePlatformTagError::InvalidCharacters { + tag: "freebsd_13_x86\u{e9}".to_string() + }) + ); + } + #[test] fn pyodide_platform() { let tag = PlatformTag::Pyodide { diff --git a/crates/uv-platform-tags/src/tags.rs b/crates/uv-platform-tags/src/tags.rs index 532d5aaedbcbc..38e7f130d659b 100644 --- a/crates/uv-platform-tags/src/tags.rs +++ b/crates/uv-platform-tags/src/tags.rs @@ -6,10 +6,8 @@ use std::{cmp, num::NonZeroU32}; use rustc_hash::FxHashMap; -use uv_small_str::SmallString; - use crate::abi_tag::CPythonAbiVariants; -use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError, PlatformTag}; +use crate::{AbiTag, Arch, LanguageTag, Os, Platform, PlatformError, PlatformTag, ReleaseArch}; #[derive(Debug, thiserror::Error)] pub enum TagsError { @@ -680,7 +678,12 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro let arch_tag = arch.machine(); let release_arch = format!("{release_tag}_{arch_tag}"); vec![PlatformTag::FreeBsd { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::NetBsd { release }, arch) => { @@ -688,7 +691,12 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro let arch_tag = arch.machine(); let release_arch = format!("{release_tag}_{arch_tag}"); vec![PlatformTag::NetBsd { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::OpenBsd { release }, arch) => { @@ -696,21 +704,36 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro let arch_tag = arch.machine(); let release_arch = format!("{release_tag}_{arch_tag}"); vec![PlatformTag::OpenBsd { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::Dragonfly { release }, arch) => { let release = release.replace(['.', '-'], "_"); let release_arch = format!("{release}_{arch}"); vec![PlatformTag::Dragonfly { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::Haiku { release }, arch) => { let release = release.replace(['.', '-'], "_"); let release_arch = format!("{release}_{arch}"); vec![PlatformTag::Haiku { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::Illumos { release, arch }, _) => { @@ -727,14 +750,24 @@ fn compatible_tags(platform: &Platform) -> Result, PlatformErro let arch = format!("{arch}_64bit"); let release_arch = format!("{release}_{arch}"); return Ok(vec![PlatformTag::Solaris { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }]); } } let release_arch = format!("{release}_{arch}"); vec![PlatformTag::Illumos { - release_arch: SmallString::from(release_arch), + release_arch: release_arch.parse::().map_err(|error| { + PlatformError::InvalidReleaseArch { + release_arch, + error, + } + })?, }] } (Os::Android { api_level }, arch) => { @@ -1454,6 +1487,24 @@ mod tests { ); } + #[test] + fn test_platform_tags_invalid_release_arch() { + let error = compatible_tags(&Platform::new( + Os::FreeBsd { + release: "13/14".to_string(), + }, + Arch::X86_64, + )) + .unwrap_err(); + + assert_debug_snapshot!(error, @r#" + InvalidReleaseArch { + release_arch: "13/14_amd64", + error: ParseReleaseArchError, + } + "#); + } + #[test] fn test_platform_tags_android() { let tags = diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index a5bb0b832fab9..e1c39120bd4d1 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -1308,6 +1308,54 @@ fn install_local_wheel() -> Result<()> { Ok(()) } +/// Reject decoded path separators in an unnamed wheel URL before using the filename in cache paths. +#[test] +fn install_unnamed_wheel_url_rejects_path_traversal() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("https://example.com/packages/pkg-1.0-py3-none-..%2F..%2F..%2Ftarget.whl")?; + + uv_snapshot!(context.filters(), context.pip_sync() + .arg("requirements.txt") + .arg("--strict"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: The wheel filename \"pkg-1.0-py3-none-../../../target.whl\" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods + " + ); + + Ok(()) +} + +/// Reject decoded stream separators in an unnamed wheel URL before using the filename in cache paths. +#[test] +fn install_unnamed_wheel_url_rejects_stream_separator() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("https://example.com/packages/pkg-1.0-py3-none-target%3Astream.whl")?; + + uv_snapshot!(context.filters(), context.pip_sync() + .arg("requirements.txt") + .arg("--strict"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: The wheel filename \"pkg-1.0-py3-none-target:stream.whl\" is invalid: Tag components must contain only ASCII letters, digits, underscores, and periods + " + ); + + Ok(()) +} + /// Install a wheel whose actual version doesn't match the version encoded in the filename. #[test] fn mismatched_version() -> Result<()> { From f82a2cf7184dd551c314afc46b6b6601885faa70 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 16:47:58 -0400 Subject: [PATCH 03/56] Remove usages of `zip` crate (#19365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Historically, we've relied on both the synchronous `zip` crate and our own `rs-async-zip` fork. The async variant is used for streaming (i.e., unzipping a wheel as it's downloaded), while the sync variant is used to parallel-unzip local files, which turns out to be very fast. This PR removes all usages of the `zip` crate (apart from in development dependencies), in favor of using `rs-async-zip` everywhere. This appears to both reduce crate size _and_ improve performance while also cutting off a source of CVEs. (The `rs-async-zip` crate is, of course, also vulnerable to potential CVEs, but having _two_ crates is strictly greater exposure for us.) Per Codex, the parallel unzip is now a bit faster: ``` - 55 KB wheel: async -1.0% - 393 KB wheel: async -7.8% - 11.4 MB shellcheck wheel: async -3.8% - 12.5 MB Ruff wheel: async -9.1% - 21.3 MB NumPy wheel: async -11.0% - 66.5 MB pandas wheel: async -4.3% ``` Per Codex, the `uv` and `uv-build` binaries are both smaller: ``` ┌──────────┬──────────────┬──────────────┬─────────────────────────────────┐ │ Binary │ main │ branch │ delta │ ├──────────┼──────────────┼──────────────┼─────────────────────────────────┤ │ uv │ 16,422,784 B │ 16,274,608 B │ -148,176 B (-144.7 KiB, -0.90%) │ │ uv-build │ 2,683,584 B │ 2,652,512 B │ -31,072 B (-30.3 KiB, -1.16%) │ └──────────┴──────────────┴──────────────┴─────────────────────────────────┘ ``` Closes https://github.com/astral-sh/uv/issues/19363. --- Cargo.lock | 17 +- Cargo.toml | 3 +- crates/uv-build-backend/Cargo.toml | 4 +- crates/uv-build-backend/src/lib.rs | 4 +- crates/uv-build-backend/src/wheel.rs | 170 +++++++++++++----- crates/uv-distribution/Cargo.toml | 1 - crates/uv-distribution/src/error.rs | 3 - crates/uv-distribution/src/source/mod.rs | 4 +- crates/uv-extract/Cargo.toml | 7 +- crates/uv-extract/src/error.rs | 7 - crates/uv-extract/src/lib.rs | 14 -- crates/uv-extract/src/stream.rs | 40 ++--- crates/uv-extract/src/sync.rs | 86 ++++++--- .../src/vendor/cloneable_seekable_reader.rs | 63 ++++++- crates/uv-metadata/Cargo.toml | 1 - crates/uv-metadata/src/lib.rs | 39 ++-- crates/uv-trampoline-builder/Cargo.toml | 3 +- crates/uv-trampoline-builder/src/lib.rs | 33 ++-- crates/uv/Cargo.toml | 2 +- crates/uv/src/commands/build_frontend.rs | 2 +- crates/uv/src/commands/project/init.rs | 4 +- crates/uv/src/commands/project/run.rs | 23 ++- crates/uv/src/lib.rs | 20 +-- crates/uv/tests/it/extract.rs | 48 +++++ 24 files changed, 419 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f1d639b5fd82..fcfd7ce2bd8dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,15 +321,14 @@ dependencies = [ [[package]] name = "astral_async_zip" -version = "0.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab72a761e6085828cc8f0e05ed332b2554701368c5dc54de551bfaec466518ba" +version = "0.0.18-rc2" +source = "git+https://github.com/astral-sh/rs-async-zip.git?tag=v0.0.18-rc2#8eb9ed214128e6db76dc753bca43908f71e569a9" dependencies = [ "async-compression", "crc32fast", "futures-lite", "pin-project", - "thiserror 1.0.69", + "thiserror 2.0.18", "tokio", "tokio-util", ] @@ -5750,6 +5749,7 @@ dependencies = [ "assert_cmd", "assert_fs", "astral-version-ranges", + "astral_async_zip", "axoupdater", "backon", "base64 0.22.1", @@ -6012,10 +6012,12 @@ name = "uv-build-backend" version = "0.0.47" dependencies = [ "astral-version-ranges", + "astral_async_zip", "base64 0.22.1", "csv", "flate2", "fs-err", + "futures-lite", "globset", "indexmap", "indoc", @@ -6429,7 +6431,6 @@ dependencies = [ "uv-warnings", "uv-workspace", "walkdir", - "zip", ] [[package]] @@ -6497,6 +6498,7 @@ dependencies = [ "astral_async_zip", "async-compression", "blake2", + "flate2", "fs-err", "futures", "md-5", @@ -6515,7 +6517,6 @@ dependencies = [ "uv-static", "uv-warnings", "xz2", - "zip", ] [[package]] @@ -6752,7 +6753,6 @@ dependencies = [ "uv-distribution-filename", "uv-normalize", "uv-pypi-types", - "zip", ] [[package]] @@ -7352,7 +7352,9 @@ dependencies = [ "anyhow", "assert_cmd", "assert_fs", + "astral_async_zip", "fs-err", + "futures-lite", "goblin", "rcgen", "tempfile", @@ -7360,7 +7362,6 @@ dependencies = [ "uv-fs", "which", "windows", - "zip", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e639af0c61184..a5fda337842f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ async-compression = { version = "0.4.12", features = [ ] } async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.11.0", package = "astral_async_http_range_reader" } -async_zip = { version = "0.0.17", package = "astral_async_zip", features = [ +async_zip = { version = "0.0.18-rc2", git = "https://github.com/astral-sh/rs-async-zip.git", tag = "v0.0.18-rc2", package = "astral_async_zip", features = [ "bzip2", "deflate", "lzma", @@ -150,6 +150,7 @@ flate2 = { version = "1.0.33", default-features = false, features = [ ] } fs-err = { version = "3.2.2", features = ["tokio"] } futures = { version = "0.3.30" } +futures-lite = { version = "2.6.1" } glob = { version = "0.3.1" } globset = { version = "0.4.15" } globwalk = { version = "0.9.1" } diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 7695d8992f679..f596c69c462e8 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -27,10 +27,12 @@ uv-pypi-types = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } +async_zip = { workspace = true } base64 = { workspace = true } csv = { workspace = true } flate2 = { workspace = true, default-features = false } fs-err = { workspace = true } +futures-lite = { workspace = true } globset = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } @@ -47,7 +49,6 @@ toml = { workspace = true } tracing = { workspace = true } version-ranges = { workspace = true } walkdir = { workspace = true } -zip = { workspace = true } [lints] workspace = true @@ -65,3 +66,4 @@ uv-preview = { workspace = true, features = ["testing"] } indoc = { workspace = true } insta = { workspace = true } regex = { workspace = true } +zip = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index aa9222dd65ed4..56b93d4e6ec45 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -62,7 +62,7 @@ pub enum Error { err: walkdir::Error, }, #[error("Failed to write wheel zip archive")] - Zip(#[from] zip::result::ZipError), + AsyncZip(#[from] async_zip::error::ZipError), #[error("Failed to write RECORD file")] Csv(#[from] csv::Error), #[error("Failed to write JSON metadata file")] @@ -735,7 +735,7 @@ mod tests { // Check that the wheel is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())), - @"dbe56fd8bd52184095b2e0ea3e83c95d1bc8b4aa53cf469cec5af62251b24abb" + @"0affdf7d3fda7e0a27007f6bf61524cd58bf3fce8e456bafb58b4a22faf0d6d0" ); assert_snapshot!(build.wheel_contents.join("\n"), @" built_by_uv-0.1.0.data/data/ diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 6fba5fea23d27..1af40f7b48b0c 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -1,15 +1,20 @@ +use async_zip::base::write::{EntryStreamWriter, ZipFileWriter}; +use async_zip::{Compression, ZipEntryBuilder}; use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD as base64}; use fs_err::File; +use futures_lite::future::block_on; +use futures_lite::io::{AsyncWrite, AsyncWriteExt}; use globset::{GlobSet, GlobSetBuilder}; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; use std::fmt::{Display, Formatter}; -use std::io::{BufReader, Read, Seek, Write}; +use std::io::{BufReader, Read, Write}; use std::path::{Component, Path, PathBuf}; +use std::pin::Pin; +use std::task::{Context, Poll}; use std::{io, mem}; use tracing::{debug, trace}; use walkdir::WalkDir; -use zip::{CompressionMethod, ZipWriter}; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; @@ -697,20 +702,78 @@ impl Display for WheelInfo { } } -/// Zip archive (wheel) writer. -struct ZipDirectoryWriter { - writer: ZipWriter, - compression: CompressionMethod, +/// ZIP archive (wheel) writer. +struct ZipDirectoryWriter { + writer: ZipFileWriter, + compression: Compression, /// The entries in the `RECORD` file. record: Vec, } -impl ZipDirectoryWriter { +impl ZipDirectoryWriter { + // Include the Unix file type bits because `async_zip` writes this mode + // directly to the ZIP external attributes. The sync `zip` crate adds + // those bits internally when starting file and directory entries. + const REGULAR_FILE_MODE: u16 = 0o100_644; + const EXECUTABLE_FILE_MODE: u16 = 0o100_755; + const DIRECTORY_MODE: u16 = 0o040_755; + + fn entry(path: &str, compression: Compression, mode: u16) -> ZipEntryBuilder { + ZipEntryBuilder::new(path.to_string().into(), compression).unix_permissions(mode) + } + + /// Add a file with the given name and return a writer for it. + fn new_writer<'slf>( + &'slf mut self, + path: &str, + executable_bit: bool, + ) -> Result, Error> { + // Set file permissions: 644 (rw-r--r--) for regular files, 755 (rwxr-xr-x) for executables + let mode = if executable_bit { + Self::EXECUTABLE_FILE_MODE + } else { + Self::REGULAR_FILE_MODE + }; + let entry = Self::entry(path, self.compression, mode); + let writer = block_on(self.writer.write_entry_stream(entry))?; + Ok(EntryWriter::new(writer)) + } +} + +struct SyncWriter { + writer: W, +} + +impl SyncWriter { + fn new(writer: W) -> Self { + Self { writer } + } +} + +impl AsyncWrite for SyncWriter { + fn poll_write( + self: Pin<&mut Self>, + _context: &mut Context<'_>, + buffer: &[u8], + ) -> Poll> { + Poll::Ready(self.get_mut().writer.write(buffer)) + } + + fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll> { + Poll::Ready(self.get_mut().writer.flush()) + } + + fn poll_close(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { + self.poll_flush(context) + } +} + +impl ZipDirectoryWriter> { /// A wheel writer with deflate compression. fn new_wheel(writer: W) -> Self { Self { - writer: ZipWriter::new(writer), - compression: CompressionMethod::Deflated, + writer: ZipFileWriter::new(SyncWriter::new(writer)), + compression: Compression::Deflate, record: Vec::new(), } } @@ -721,39 +784,57 @@ impl ZipDirectoryWriter { #[expect(dead_code)] fn new_editable(writer: W) -> Self { Self { - writer: ZipWriter::new(writer), - compression: CompressionMethod::Stored, + writer: ZipFileWriter::new(SyncWriter::new(writer)), + compression: Compression::Stored, record: Vec::new(), } } +} - /// Add a file with the given name and return a writer for it. - fn new_writer<'slf>( - &'slf mut self, - path: &str, - executable_bit: bool, - ) -> Result, Error> { - // Set file permissions: 644 (rw-r--r--) for regular files, 755 (rwxr-xr-x) for executables - let permissions = if executable_bit { 0o755 } else { 0o644 }; - let options = zip::write::SimpleFileOptions::default() - .system(zip::System::Unix) - .unix_permissions(permissions) - .compression_method(self.compression); - self.writer.start_file(path, options)?; - Ok(Box::new(&mut self.writer)) +struct EntryWriter<'writer, W: AsyncWrite + Unpin> { + writer: Option>, +} + +impl<'writer, W: AsyncWrite + Unpin> EntryWriter<'writer, W> { + fn new(writer: EntryStreamWriter<'writer, W>) -> Self { + Self { + writer: Some(writer), + } + } + + fn close(mut self) -> Result<(), Error> { + let Some(writer) = self.writer.take() else { + return Ok(()); + }; + block_on(writer.close())?; + Ok(()) + } +} + +impl Write for EntryWriter<'_, W> { + fn write(&mut self, buffer: &[u8]) -> io::Result { + let Some(writer) = self.writer.as_mut() else { + return Err(io::Error::other( + "wheel ZIP entry writer was already closed", + )); + }; + block_on(writer.write(buffer)) + } + + fn flush(&mut self) -> io::Result<()> { + let Some(writer) = self.writer.as_mut() else { + return Ok(()); + }; + block_on(writer.flush()) } } -impl DirectoryWriter for ZipDirectoryWriter { +impl DirectoryWriter for ZipDirectoryWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { trace!("Adding {}", path); // Set appropriate permissions for metadata files (644 = rw-r--r--) - let options = zip::write::SimpleFileOptions::default() - .system(zip::System::Unix) - .unix_permissions(0o644) - .compression_method(self.compression); - self.writer.start_file(path, options)?; - self.writer.write_all(bytes)?; + let entry = Self::entry(path, self.compression, Self::REGULAR_FILE_MODE); + block_on(self.writer.write_entry_whole(entry, bytes))?; let hash = base64.encode(Sha256::new().chain_update(bytes).finalize()); self.record.push(RecordEntry { @@ -779,17 +860,21 @@ impl DirectoryWriter for ZipDirectoryWriter { let executable_bit = false; let mut writer = self.new_writer(path, executable_bit)?; let record = write_hashed(path, &mut reader, &mut writer)?; - drop(writer); + writer.close()?; self.record.push(record); Ok(()) } fn write_directory(&mut self, directory: &str) -> Result<(), Error> { trace!("Adding directory {}", directory); - let options = zip::write::SimpleFileOptions::default() - .compression_method(self.compression) - .system(zip::System::Unix); - Ok(self.writer.add_directory(directory, options)?) + let directory = if directory.ends_with('/') { + directory.to_string() + } else { + format!("{directory}/") + }; + let entry = Self::entry(&directory, Compression::Stored, Self::DIRECTORY_MODE); + block_on(self.writer.write_entry_whole(entry, &[]))?; + Ok(()) } /// Write the `RECORD` file and the central directory. @@ -797,14 +882,13 @@ impl DirectoryWriter for ZipDirectoryWriter { let record_path = format!("{dist_info_dir}/RECORD"); trace!("Adding {record_path}"); let record = mem::take(&mut self.record); - write_record( - &mut self.new_writer(&record_path, false)?, - dist_info_dir, - record, - )?; + let mut record_bytes = Vec::new(); + write_record(&mut record_bytes, dist_info_dir, record)?; + let entry = Self::entry(&record_path, self.compression, Self::REGULAR_FILE_MODE); + block_on(self.writer.write_entry_whole(entry, &record_bytes))?; trace!("Adding central directory"); - self.writer.finish()?; + block_on(self.writer.close())?; Ok(()) } } diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index f33ee254bae04..202d9fbb50193 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -60,7 +60,6 @@ toml = { workspace = true } tracing = { workspace = true } url = { workspace = true } walkdir = { workspace = true } -zip = { workspace = true } [dev-dependencies] indoc = { workspace = true } diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 5e9b265cfd642..a4311bdb8bd8f 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -3,7 +3,6 @@ use std::path::PathBuf; use owo_colors::OwoColorize; use tokio::task::JoinError; -use zip::result::ZipError; use crate::metadata::MetadataError; use uv_cache::Error as CacheError; @@ -125,8 +124,6 @@ pub enum Error { WheelMetadata(PathBuf, #[source] Box), #[error("Failed to read metadata from installed package `{0}`")] ReadInstalled(Box, #[source] InstalledDistError), - #[error("Failed to read zip archive from built wheel")] - Zip(#[from] ZipError), #[error("Failed to extract archive: {0}")] Extract(String, #[source] uv_extract::Error), #[error("The source distribution is missing a `PKG-INFO` file")] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 80df8bac38a86..4613fedeab1fb 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -20,7 +20,6 @@ use reqwest::{Response, StatusCode}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{Instrument, debug, info_span, instrument, warn}; use url::Url; -use zip::ZipArchive; use uv_auth::CredentialsCache; use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; @@ -3161,8 +3160,7 @@ fn read_wheel_metadata( ) -> Result { let file = fs_err::File::open(wheel).map_err(Error::CacheRead)?; let reader = std::io::BufReader::new(file); - let mut archive = ZipArchive::new(reader)?; - let dist_info = read_archive_metadata(filename, &mut archive) + let dist_info = read_archive_metadata(filename, reader) .map_err(|err| Error::WheelMetadata(wheel.to_path_buf(), Box::new(err)))?; Ok(ResolutionMetadata::parse_metadata(&dist_info)?) } diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index 552baa7596cd2..99ad315dd2ca3 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -26,6 +26,7 @@ astral-tokio-tar = { workspace = true } async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] } async_zip = { workspace = true } blake2 = { workspace = true } +flate2 = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } md-5 = { workspace = true } @@ -39,7 +40,6 @@ tokio = { workspace = true } tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } xz2 = { workspace = true } -zip = { workspace = true } [features] default = [] @@ -47,4 +47,7 @@ default = [] static = ["xz2/static"] [package.metadata.cargo-shear] -ignored = ["xz2"] +# NOTE: `async-compression` owns the actual Deflate codec, but we need to +# explicitly declare `flate2` here so the workspace's `zlib-rs` backend stays +# selected after removing the synchronous `zip` dependency. +ignored = ["flate2", "xz2"] diff --git a/crates/uv-extract/src/error.rs b/crates/uv-extract/src/error.rs index 9a5719eb14d34..93a830ca7df9e 100644 --- a/crates/uv-extract/src/error.rs +++ b/crates/uv-extract/src/error.rs @@ -4,8 +4,6 @@ use std::{ffi::OsString, path::PathBuf}; pub enum Error { #[error("I/O operation failed during extraction")] Io(#[source] std::io::Error), - #[error("Invalid zip file")] - Zip(#[from] zip::result::ZipError), #[error("Invalid zip file structure")] AsyncZip(#[from] async_zip::error::ZipError), #[error("Invalid tar file")] @@ -113,11 +111,6 @@ impl Error { Ok(zip_err) => return Self::AsyncZip(zip_err), Err(err) => err, }; - let err = match err.downcast::() { - Ok(zip_err) => return Self::Zip(zip_err), - Err(err) => err, - }; - Self::Io(err) } diff --git a/crates/uv-extract/src/lib.rs b/crates/uv-extract/src/lib.rs index 5bbee43980573..e04bfcba5b7fe 100644 --- a/crates/uv-extract/src/lib.rs +++ b/crates/uv-extract/src/lib.rs @@ -58,20 +58,6 @@ impl From for CompressionMethod { } } -impl From for CompressionMethod { - fn from(value: zip::CompressionMethod) -> Self { - match value { - zip::CompressionMethod::Stored => Self::Stored, - zip::CompressionMethod::Deflated => Self::Deflated, - zip::CompressionMethod::Zstd => Self::Zstd, - zip::CompressionMethod::Bzip2 => Self::Deprecated("bzip2"), - zip::CompressionMethod::Lzma => Self::Deprecated("lzma"), - zip::CompressionMethod::Xz => Self::Deprecated("xz"), - _ => Self::Deprecated("unknown"), - } - } -} - /// Validate that a given filename (e.g. reported by a ZIP archive's /// local file entries or central directory entries) is "safe" to use. /// diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 38295e6916cfd..5dc23504e014e 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -16,6 +16,26 @@ use crate::{CompressionMethod, Error, insecure_no_validate, validate_archive_mem const DEFAULT_BUF_SIZE: usize = 128 * 1024; +/// Ensure the file path is safe to use as a [`Path`]. +/// +/// See: +pub(crate) fn enclosed_name(file_name: &str) -> Option { + if file_name.contains('\0') { + return None; + } + let path = PathBuf::from(file_name); + let mut depth = 0usize; + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => return None, + Component::ParentDir => depth = depth.checked_sub(1)?, + Component::Normal(_) => depth += 1, + Component::CurDir => (), + } + } + Some(path) +} + #[derive(Debug, Clone, PartialEq, Eq)] struct LocalHeaderEntry { /// The relative path of the entry, as computed from the local file header. @@ -55,26 +75,6 @@ pub async fn unzip( reader: R, target: impl AsRef, ) -> Result, Error> { - /// Ensure the file path is safe to use as a [`Path`]. - /// - /// See: - pub(crate) fn enclosed_name(file_name: &str) -> Option { - if file_name.contains('\0') { - return None; - } - let path = PathBuf::from(file_name); - let mut depth = 0usize; - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir => return None, - Component::ParentDir => depth = depth.checked_sub(1)?, - Component::Normal(_) => depth += 1, - Component::CurDir => (), - } - } - Some(path) - } - // Determine whether ZIP validation is disabled. let skip_validation = insecure_no_validate(); diff --git a/crates/uv-extract/src/sync.rs b/crates/uv-extract/src/sync.rs index 34ba536c5bd04..795db51a828d8 100644 --- a/crates/uv-extract/src/sync.rs +++ b/crates/uv-extract/src/sync.rs @@ -3,12 +3,15 @@ use std::sync::Mutex; use crate::vendor::CloneableSeekableReader; use crate::{CompressionMethod, Error, insecure_no_validate, validate_archive_member_name}; +use async_zip::base::read::seek::ZipFileReader; +use async_zip::error::ZipError; +use futures::executor::block_on; +use futures::io::{AllowStdIo, AsyncReadExt, AsyncWriteExt}; use rayon::prelude::*; use rustc_hash::FxHashSet; use tracing::warn; use uv_configuration::initialize_rayon_once; use uv_warnings::warn_user_once; -use zip::ZipArchive; /// Unzip a `.zip` archive into the target directory. /// @@ -16,20 +19,31 @@ use zip::ZipArchive; pub fn unzip(reader: fs_err::File, target: &Path) -> Result, Error> { let (reader, filename) = reader.into_parts(); - // Unzip in parallel. - let reader = std::io::BufReader::new(reader); - let archive = ZipArchive::new(CloneableSeekableReader::new(reader))?; + // Parse the central directory once, then clone the archive reader per Rayon worker so + // extraction stays parallel for already-downloaded wheels. + let archive = block_on(ZipFileReader::new(AllowStdIo::new( + CloneableSeekableReader::new(reader), + )))?; let directories = Mutex::new(FxHashSet::default()); let skip_validation = insecure_no_validate(); // Initialize the threadpool with the user settings. initialize_rayon_once(); - (0..archive.len()) + (0..archive.file().entries().len()) .into_par_iter() .map(|file_number| { let mut archive = archive.clone(); - let mut file = archive.by_index(file_number)?; + let entry = archive.file().entries()[file_number].clone(); + let file_name = match entry.filename().as_str() { + Ok(file_name) => file_name, + Err(ZipError::StringNotUtf8) => { + return Err(Error::CentralDirectoryEntryNotUtf8 { + index: file_number as u64, + }); + } + Err(err) => return Err(err.into()), + }; - let compression = CompressionMethod::from(file.compression()); + let compression = CompressionMethod::from(entry.compression()); if !compression.is_well_known() { warn_user_once!( "One or more file entries in '{filename}' use the '{compression}' compression method, which is not widely supported. A future version of uv will reject ZIP archives containing entries compressed with this method. Entries must be compressed with the '{stored}', '{deflate}', or '{zstd}' compression methods.", @@ -40,21 +54,21 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result, ); } - if let Err(e) = validate_archive_member_name(file.name()) { + if let Err(e) = validate_archive_member_name(file_name) { if !skip_validation { return Err(e); } } // Determine the path of the file within the wheel. - let Some(enclosed_name) = file.enclosed_name() else { - warn!("Skipping unsafe file name: {}", file.name()); + let Some(enclosed_name) = crate::stream::enclosed_name(file_name) else { + warn!("Skipping unsafe file name: {file_name}"); return Ok(None); }; // Create necessary parent directories. let path = target.join(&enclosed_name); - if file.is_dir() { + if entry.dir()? { let mut directories = directories.lock().unwrap(); if directories.insert(path.clone()) { fs_err::create_dir_all(path).map_err(Error::Io)?; @@ -71,14 +85,46 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result, // Copy the file contents. let outfile = fs_err::File::create(&path).map_err(Error::Io)?; - let size = file.size(); - if size > 0 { - let mut writer = if let Ok(size) = usize::try_from(size) { - std::io::BufWriter::with_capacity(std::cmp::min(size, 1024 * 1024), outfile) - } else { - std::io::BufWriter::new(outfile) - }; - std::io::copy(&mut file, &mut writer).map_err(Error::io_or_compression)?; + let size = entry.uncompressed_size(); + let writer = if let Ok(size) = usize::try_from(size) { + std::io::BufWriter::with_capacity(std::cmp::min(size, 1024 * 1024), outfile) + } else { + std::io::BufWriter::new(outfile) + }; + let (copied, computed_crc32) = block_on(async { + let mut file = archive.reader_with_entry(file_number).await?; + let mut writer = AllowStdIo::new(writer); + let mut copied = 0; + let mut buffer = vec![0; 128 * 1024]; + loop { + let read = file + .read(&mut buffer) + .await + .map_err(Error::io_or_compression)?; + if read == 0 { + break; + } + writer.write_all(&buffer[..read]).await.map_err(Error::Io)?; + copied += read as u64; + } + writer.flush().await.map_err(Error::Io)?; + Ok::<_, Error>((copied, file.compute_hash())) + })?; + + if copied != size && !skip_validation { + return Err(Error::BadUncompressedSize { + path: enclosed_name.clone(), + computed: copied, + expected: size, + }); + } + + if computed_crc32 != entry.crc32() && !skip_validation { + return Err(Error::BadCrc32 { + path: enclosed_name.clone(), + computed: computed_crc32, + expected: entry.crc32(), + }); } // See `uv_extract::stream::unzip`. For simplicity, this is identical with the code there except for being @@ -88,7 +134,7 @@ pub fn unzip(reader: fs_err::File, target: &Path) -> Result, use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - if let Some(mode) = file.unix_mode() { + if let Some(mode) = entry.unix_permissions() { // https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/utils/unpacking.py#L88-L100 let has_any_executable_bit = mode & 0o111; if has_any_executable_bit != 0 { diff --git a/crates/uv-extract/src/vendor/cloneable_seekable_reader.rs b/crates/uv-extract/src/vendor/cloneable_seekable_reader.rs index f62e8f0e963a6..080b1aff20664 100644 --- a/crates/uv-extract/src/vendor/cloneable_seekable_reader.rs +++ b/crates/uv-extract/src/vendor/cloneable_seekable_reader.rs @@ -9,10 +9,14 @@ #![expect(clippy::cast_sign_loss)] use std::{ - io::{BufReader, Cursor, Read, Seek, SeekFrom}, + io::{BufRead, BufReader, Cursor, Read, Seek, SeekFrom}, sync::{Arc, Mutex}, }; +// Chosen from extraction benchmarks to reduce read calls without adding too +// much per-clone buffering. +const BUFFER_SIZE: usize = 64 * 1024; + /// A trait to represent some reader which has a total length known in /// advance. This is roughly equivalent to the nightly /// [`Seek::stream_len`] API. @@ -30,6 +34,9 @@ pub(crate) struct CloneableSeekableReader { pos: u64, // TODO determine and store this once instead of per cloneable file file_length: Option, + buffer: Box<[u8; BUFFER_SIZE]>, + buffer_position: usize, + buffer_length: usize, } impl Clone for CloneableSeekableReader { @@ -38,6 +45,9 @@ impl Clone for CloneableSeekableReader { file: self.file.clone(), pos: self.pos, file_length: self.file_length, + buffer: Box::new([0; BUFFER_SIZE]), + buffer_position: 0, + buffer_length: 0, } } } @@ -53,6 +63,9 @@ impl CloneableSeekableReader { file: Arc::new(Mutex::new(file)), pos: 0u64, file_length: None, + buffer: Box::new([0; BUFFER_SIZE]), + buffer_position: 0, + buffer_length: 0, } } @@ -64,11 +77,39 @@ impl CloneableSeekableReader { len }) } + + fn buffered_len(&self) -> usize { + self.buffer_length.saturating_sub(self.buffer_position) + } + + fn consume_buffer(&mut self, amount: usize) { + let amount = amount.min(self.buffered_len()); + self.buffer_position += amount; + self.pos += amount as u64; + + if self.buffer_position == self.buffer_length { + self.buffer_position = 0; + self.buffer_length = 0; + } + } + + fn clear_buffer(&mut self) { + self.buffer_position = 0; + self.buffer_length = 0; + } } impl Read for CloneableSeekableReader { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - let mut underlying_file = self.file.lock().expect("Unable to get underlying file"); + if self.buffered_len() > 0 { + let amount = buf.len().min(self.buffered_len()); + buf[..amount] + .copy_from_slice(&self.buffer[self.buffer_position..self.buffer_position + amount]); + self.consume_buffer(amount); + return Ok(amount); + } + + let mut underlying_file = self.file.lock().unwrap(); // TODO share an object which knows current position to avoid unnecessary // seeks underlying_file.seek(SeekFrom::Start(self.pos))?; @@ -106,10 +147,28 @@ impl Seek for CloneableSeekableReader { } }; self.pos = new_pos; + self.clear_buffer(); Ok(new_pos) } } +impl BufRead for CloneableSeekableReader { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + if self.buffered_len() == 0 { + let mut underlying_file = self.file.lock().unwrap(); + underlying_file.seek(SeekFrom::Start(self.pos))?; + self.buffer_length = underlying_file.read(&mut *self.buffer)?; + self.buffer_position = 0; + } + + Ok(&self.buffer[self.buffer_position..self.buffer_length]) + } + + fn consume(&mut self, amount: usize) { + self.consume_buffer(amount); + } +} + impl HasLength for BufReader { fn len(&self) -> u64 { self.get_ref().len() diff --git a/crates/uv-metadata/Cargo.toml b/crates/uv-metadata/Cargo.toml index 8c4633aa8c7df..f007955f37c55 100644 --- a/crates/uv-metadata/Cargo.toml +++ b/crates/uv-metadata/Cargo.toml @@ -24,7 +24,6 @@ thiserror = { workspace = true } tokio = { workspace = true } tokio-util = { workspace = true } tracing = { workspace = true } -zip = { workspace = true } [lints] workspace = true diff --git a/crates/uv-metadata/src/lib.rs b/crates/uv-metadata/src/lib.rs index b5e0ea854333c..cf8b2046ecb42 100644 --- a/crates/uv-metadata/src/lib.rs +++ b/crates/uv-metadata/src/lib.rs @@ -3,8 +3,9 @@ //! This module reads all fields exhaustively. The fields are defined in the [Core metadata //! specification](https://packaging.python.org/en/latest/specifications/core-metadata/). +use futures::executor::block_on; +use futures::io::AllowStdIo; use std::io; -use std::io::{Read, Seek}; use std::path::Path; use thiserror::Error; use tokio::io::AsyncReadExt; @@ -12,7 +13,6 @@ use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use uv_distribution_filename::WheelFilename; use uv_normalize::{DistInfoName, InvalidNameError}; use uv_pypi_types::ResolutionMetadata; -use zip::ZipArchive; /// The caller is responsible for attaching the path or url we failed to read. #[derive(Debug, Error)] @@ -40,8 +40,6 @@ pub enum Error { expected: u32, }, #[error("Failed to read from zip file")] - Zip(#[from] zip::result::ZipError), - #[error("Failed to read from zip file")] AsyncZip(#[from] async_zip::error::ZipError), // No `#[from]` to enforce manual review of `io::Error` sources. #[error(transparent)] @@ -133,18 +131,31 @@ pub fn is_metadata_entry(path: &str, filename: &WheelFilename) -> Result, + reader: impl std::io::BufRead + std::io::Seek + Unpin, ) -> Result, Error> { - let dist_info_prefix = - find_archive_dist_info(filename, archive.file_names().map(|name| (name, name)))?.1; - - let mut file = archive.by_name(&format!("{dist_info_prefix}.dist-info/METADATA"))?; - - #[expect(clippy::cast_possible_truncation)] - let mut buffer = Vec::with_capacity(file.size() as usize); - file.read_to_end(&mut buffer).map_err(Error::Io)?; + block_on(async { + let mut zip_reader = + async_zip::base::read::seek::ZipFileReader::new(AllowStdIo::new(reader)).await?; + + let (metadata_index, _dist_info_prefix) = find_archive_dist_info( + filename, + zip_reader + .file() + .entries() + .iter() + .enumerate() + .filter_map(|(index, entry)| Some((index, entry.filename().as_str().ok()?))), + )?; + + let mut buffer = Vec::new(); + zip_reader + .reader_with_entry(metadata_index) + .await? + .read_to_end_checked(&mut buffer) + .await?; - Ok(buffer) + Ok(buffer) + }) } /// Find the `.dist-info` directory in an unzipped wheel. diff --git a/crates/uv-trampoline-builder/Cargo.toml b/crates/uv-trampoline-builder/Cargo.toml index 189c31d001912..43b446bc38fea 100644 --- a/crates/uv-trampoline-builder/Cargo.toml +++ b/crates/uv-trampoline-builder/Cargo.toml @@ -31,9 +31,10 @@ fs-err = {workspace = true } goblin = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } -zip = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +async_zip = { workspace = true } +futures-lite = { workspace = true } windows = { workspace = true } [dev-dependencies] diff --git a/crates/uv-trampoline-builder/src/lib.rs b/crates/uv-trampoline-builder/src/lib.rs index 6dc67226911d7..e43faaf2cf4fb 100644 --- a/crates/uv-trampoline-builder/src/lib.rs +++ b/crates/uv-trampoline-builder/src/lib.rs @@ -230,6 +230,9 @@ pub enum Error { NotWindows, #[error("Cannot process launcher metadata from resource")] UnprocessableMetadata, + #[cfg(windows)] + #[error("Failed to write Windows launcher ZIP payload")] + AsyncZip(#[from] async_zip::error::ZipError), #[error("Resources over 2^32 bytes are not supported")] ResourceTooLarge, #[error("Failed to update Windows PE resources: {}", path.user_display())] @@ -380,30 +383,22 @@ pub fn windows_script_launcher( is_gui: bool, python_executable: impl AsRef, ) -> Result, Error> { - use std::io::{Cursor, Write}; - - use zip::ZipWriter; - use zip::write::SimpleFileOptions; + use async_zip::base::write::ZipFileWriter; + use async_zip::{Compression, ZipEntryBuilder}; + use futures_lite::future::block_on; + use futures_lite::io::Cursor; use uv_fs::Simplified; let launcher_bin: &[u8] = get_launcher_bin(is_gui)?; - let mut payload: Vec = Vec::new(); - { - // We're using the zip writer, but with stored compression - // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82 - // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271 - let stored = - SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); - let mut archive = ZipWriter::new(Cursor::new(&mut payload)); - let error_msg = "Writing to Vec should never fail"; - archive.start_file("__main__.py", stored).expect(error_msg); - archive - .write_all(launcher_python_script.as_bytes()) - .expect(error_msg); - archive.finish().expect(error_msg); - } + // Store the launcher script without compression. + // https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82 + // https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271 + let mut archive = ZipFileWriter::new(Cursor::new(Vec::new())); + let entry = ZipEntryBuilder::new("__main__.py".to_string().into(), Compression::Stored); + block_on(archive.write_entry_whole(entry, launcher_python_script.as_bytes()))?; + let payload = block_on(archive.close())?.into_inner(); let python = python_executable.as_ref(); let python_path = python.simplified_display().to_string(); diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 10a25b19e9b24..0040659ede642 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -69,6 +69,7 @@ uv-workspace = { workspace = true, features = ["clap"] } anstream = { workspace = true } anyhow = { workspace = true } +async_zip = { workspace = true } axoupdater = { workspace = true, features = [ "github_releases", "tokio", @@ -113,7 +114,6 @@ uuid = { workspace = true, features = ["v4"] } version-ranges = { workspace = true } walkdir = { workspace = true } which = { workspace = true } -zip = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] self-replace = { workspace = true } diff --git a/crates/uv/src/commands/build_frontend.rs b/crates/uv/src/commands/build_frontend.rs index 6fa061fc5936d..37a04791630eb 100644 --- a/crates/uv/src/commands/build_frontend.rs +++ b/crates/uv/src/commands/build_frontend.rs @@ -382,7 +382,7 @@ async fn build_impl( preview, ); async { - let result = future.await; + let result = Box::pin(future).await; (source, result) } })) diff --git a/crates/uv/src/commands/project/init.rs b/crates/uv/src/commands/project/init.rs index 90b7375edd718..1dc8b24712d78 100644 --- a/crates/uv/src/commands/project/init.rs +++ b/crates/uv/src/commands/project/init.rs @@ -142,7 +142,7 @@ pub(crate) async fn init( } }; - init_project( + Box::pin(init_project( &path, &name, package, @@ -165,7 +165,7 @@ pub(crate) async fn init( cache, printer, preview, - ) + )) .await?; // Create the `README.md` if it does not already exist. diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 6f47762aa31af..7fc8832010bb1 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -2005,9 +2005,26 @@ async fn resolve_gist_url( /// Returns `true` if the target is a ZIP archive containing a `__main__.py` file. fn is_python_zipapp(target: &Path) -> bool { if let Ok(file) = fs_err::File::open(target) { - if let Ok(mut archive) = zip::ZipArchive::new(file) { - return archive.by_name("__main__.py").is_ok_and(|f| f.is_file()); - } + let reader = std::io::BufReader::new(file); + return futures::executor::block_on(async { + let archive = async_zip::base::read::seek::ZipFileReader::new( + futures::io::AllowStdIo::new(reader), + ) + .await + .ok()?; + archive + .file() + .entries() + .iter() + .find(|entry| { + entry + .filename() + .as_str() + .is_ok_and(|name| name == "__main__.py") + }) + .map(|entry| entry.dir().is_ok_and(|is_dir| !is_dir)) + }) + .unwrap_or(false); } false } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 567beb9368376..572c91127e448 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -646,7 +646,7 @@ async fn run(cli: Cli) -> Result { groups: args.settings.groups, }; - commands::pip_compile( + Box::pin(commands::pip_compile( &requirements, &constraints, &overrides, @@ -709,7 +709,7 @@ async fn run(cli: Cli) -> Result { workspace_cache, printer, globals.preview, - ) + )) .await } Commands::Pip(PipNamespace { @@ -753,7 +753,7 @@ async fn run(cli: Cli) -> Result { groups: args.settings.groups, }; - commands::pip_sync( + Box::pin(commands::pip_sync( &requirements, &constraints, &build_constraints, @@ -796,7 +796,7 @@ async fn run(cli: Cli) -> Result { args.dry_run, printer, globals.preview, - ) + )) .await } Commands::Pip(PipNamespace { @@ -1264,7 +1264,7 @@ async fn run(cli: Cli) -> Result { args.no_clear, ); - commands::venv( + Box::pin(commands::venv( &project_dir, args.path, python_request, @@ -1294,7 +1294,7 @@ async fn run(cli: Cli) -> Result { .is_enabled(PreviewFeature::RelocatableEnvsDefault) && !args.no_relocatable), globals.preview, - ) + )) .await } Commands::Project(project) => { @@ -1836,7 +1836,7 @@ async fn run(cli: Cli) -> Result { // Initialize the cache. let cache = cache.init().await?; - commands::python_pin( + Box::pin(commands::python_pin( &project_dir, args.request, args.resolved, @@ -1851,7 +1851,7 @@ async fn run(cli: Cli) -> Result { &workspace_cache, printer, globals.preview, - ) + )) .await } Commands::Python(PythonNamespace { @@ -2113,7 +2113,7 @@ async fn run_project( // Initialize the cache. let cache = cache.init().await?; - commands::init( + Box::pin(commands::init( project_dir, args.path, args.name, @@ -2137,7 +2137,7 @@ async fn run_project( &cache, printer, globals.preview, - ) + )) .await } ProjectCommand::Run(args) => { diff --git a/crates/uv/tests/it/extract.rs b/crates/uv/tests/it/extract.rs index 1bf3479445002..4d8e7b92cac16 100644 --- a/crates/uv/tests/it/extract.rs +++ b/crates/uv/tests/it/extract.rs @@ -27,6 +27,36 @@ async fn unzip(url: &str) -> anyhow::Result<(), uv_extract::Error> { Ok(()) } +async fn unzip_seekable(url: &str) -> anyhow::Result<(), uv_extract::Error> { + let backoff = backon::ExponentialBuilder::default() + .with_min_delay(std::time::Duration::from_millis(500)) + .with_max_times(5) + .build(); + + let download = || async { + let response = reqwest::get(url).await?; + Ok::<_, reqwest::Error>(response) + }; + + let response = download.retry(backoff).await.unwrap(); + let bytes = response + .bytes() + .await + .map_err(std::io::Error::other) + .map_err(uv_extract::Error::Io)?; + + let archive = tempfile::NamedTempFile::new().map_err(uv_extract::Error::Io)?; + fs_err::write(archive.path(), bytes).map_err(uv_extract::Error::Io)?; + let archive = fs_err::File::open(archive.path()).map_err(uv_extract::Error::Io)?; + + let target = tempfile::TempDir::new().map_err(uv_extract::Error::Io)?; + let target_path = target.path().to_path_buf(); + tokio::task::spawn_blocking(move || uv_extract::unzip(archive, &target_path)) + .await + .expect("seekable ZIP extraction task should not panic")?; + Ok(()) +} + #[tokio::test] async fn malo_accept_comment() { unzip("https://pub-c6f28d316acd406eae43501e51ad30fa.r2.dev/0723f54ceb33a4fdc7f2eddc19635cd704d61c84/accept/comment.zip").await.unwrap(); @@ -51,6 +81,12 @@ async fn malo_accept_deflate() { insta::assert_debug_snapshot!((), @"()"); } +#[tokio::test] +async fn malo_seekable_accept_deflate() { + unzip_seekable("https://pub-c6f28d316acd406eae43501e51ad30fa.r2.dev/0723f54ceb33a4fdc7f2eddc19635cd704d61c84/accept/deflate.zip").await.unwrap(); + insta::assert_debug_snapshot!((), @"()"); +} + #[tokio::test] async fn malo_accept_normal_deflate_zip64_extra() { unzip("https://pub-c6f28d316acd406eae43501e51ad30fa.r2.dev/0723f54ceb33a4fdc7f2eddc19635cd704d61c84/accept/normal_deflate_zip64_extra.zip").await.unwrap(); @@ -233,6 +269,18 @@ async fn malo_reject_data_descriptor_bad_crc() { "#); } +#[tokio::test] +async fn malo_seekable_malicious_short_usize() { + let result = unzip_seekable("https://pub-c6f28d316acd406eae43501e51ad30fa.r2.dev/0723f54ceb33a4fdc7f2eddc19635cd704d61c84/malicious/short_usize.zip").await.unwrap_err(); + insta::assert_debug_snapshot!(result, @r#" + BadUncompressedSize { + path: "file", + computed: 51, + expected: 9, + } + "#); +} + #[tokio::test] async fn malo_reject_data_descriptor_bad_csize() { let result = unzip("https://pub-c6f28d316acd406eae43501e51ad30fa.r2.dev/0723f54ceb33a4fdc7f2eddc19635cd704d61c84/reject/data_descriptor_bad_csize.zip").await.unwrap_err(); From 60e45d709a3f1c1718193e6e76bbe4d363a91426 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 16:59:40 -0400 Subject: [PATCH 04/56] Use the crates.io version of `astral_async_zip` (#19381) ## Summary A staged change that I forgot to push prior to https://github.com/astral-sh/uv/pull/19365. --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- crates/uv-build-backend/src/lib.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcfd7ce2bd8dd..c155e144f3346 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,8 +321,9 @@ dependencies = [ [[package]] name = "astral_async_zip" -version = "0.0.18-rc2" -source = "git+https://github.com/astral-sh/rs-async-zip.git?tag=v0.0.18-rc2#8eb9ed214128e6db76dc753bca43908f71e569a9" +version = "0.0.18-rc3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f773362bd6d5b14e1cb03473e26a1932884107f98f77f45c443e7ca85c9d6c43" dependencies = [ "async-compression", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index a5fda337842f8..689050149a494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ async-compression = { version = "0.4.12", features = [ ] } async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.11.0", package = "astral_async_http_range_reader" } -async_zip = { version = "0.0.18-rc2", git = "https://github.com/astral-sh/rs-async-zip.git", tag = "v0.0.18-rc2", package = "astral_async_zip", features = [ +async_zip = { version = "0.0.18-rc3", package = "astral_async_zip", features = [ "bzip2", "deflate", "lzma", diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 56b93d4e6ec45..02991f9e53781 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -735,7 +735,7 @@ mod tests { // Check that the wheel is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())), - @"0affdf7d3fda7e0a27007f6bf61524cd58bf3fce8e456bafb58b4a22faf0d6d0" + @"17d4946da272e3fee31f9e8261472f06f115cc3c2178e0a5cddbebb83e91fd04" ); assert_snapshot!(build.wheel_contents.join("\n"), @" built_by_uv-0.1.0.data/data/ From a625358edd53010953776a923fa2f21bc112f15f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 17:20:56 -0400 Subject: [PATCH 05/56] Avoid walking nested directories in linker conflict registration (#19382) ## Summary In the linker, we now only store top-level wheel entries when registering conflicts. The subsequent hook (`register_installed_path`) already ignores paths with more than one component... But on `main`, we recursively walk every file in the unpacked wheel before discarding nested paths. In Flask, this speeds up the warm install by nearly 10%. Co-authored-by: Codex --- crates/uv-install-wheel/src/install.rs | 6 +++--- crates/uv-install-wheel/src/linker.rs | 27 +++++++++----------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index e14031afbe3da..64b20246fb568 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -69,13 +69,13 @@ pub fn install_wheel( // > 1.c If Root-Is-Purelib == ‘true’, unpack archive into purelib (site-packages). // > 1.d Else unpack archive into platlib (site-packages). - trace!(?name, "Extracting file"); + trace!(?name, "Extracting wheel files"); let site_packages = match lib_kind { LibKind::Pure => &layout.scheme.purelib, LibKind::Plat => &layout.scheme.platlib, }; - let num_unpacked = link_wheel_files(link_mode, site_packages, &wheel, state, filename)?; - trace!(?name, "Extracted {num_unpacked} files"); + link_wheel_files(link_mode, site_packages, &wheel, state, filename)?; + trace!(?name, "Extracted wheel files"); // Read the RECORD file. let mut record_file = File::open( diff --git a/crates/uv-install-wheel/src/linker.rs b/crates/uv-install-wheel/src/linker.rs index 1590cb0829592..736e412e486e3 100644 --- a/crates/uv-install-wheel/src/linker.rs +++ b/crates/uv-install-wheel/src/linker.rs @@ -8,7 +8,6 @@ use fs_err as fs; use itertools::Itertools; use rustc_hash::FxHashMap; use tracing::{debug, instrument}; -use walkdir::WalkDir; use uv_distribution_filename::WheelFilename; use uv_fs::Simplified; @@ -249,8 +248,6 @@ impl InstallState { } /// Extract a wheel by linking all of its files into site packages. -/// -/// Returns the number of files extracted. #[instrument(skip_all)] pub fn link_wheel_files( link_mode: LinkMode, @@ -258,10 +255,10 @@ pub fn link_wheel_files( wheel: impl AsRef, state: &InstallState, filename: &WheelFilename, -) -> Result { +) -> Result<(), Error> { let wheel = wheel.as_ref(); let site_packages = site_packages.as_ref(); - let count = register_installed_paths(wheel, state, filename)?; + register_installed_paths(wheel, state, filename)?; // The `RECORD` file is modified during installation, so it needs a real // copy rather than a link back to the cache. @@ -281,7 +278,7 @@ pub fn link_wheel_files( update_site_packages_mtime(site_packages); } - Ok(count) + Ok(()) } /// Update the mtime of the site-packages directory to the current time. @@ -303,23 +300,17 @@ fn update_site_packages_mtime(site_packages: &Path) { } } -/// Walk the wheel directory and register all paths for conflict detection. -/// -/// Returns the number of files (not directories) in the wheel. +/// Register top-level wheel paths for conflict detection. fn register_installed_paths( wheel: &Path, state: &InstallState, filename: &WheelFilename, -) -> Result { - let mut count = 0; - for entry in WalkDir::new(wheel) { +) -> Result<(), Error> { + for entry in fs::read_dir(wheel)? { let entry = entry?; let path = entry.path(); - let relative = path.strip_prefix(wheel).expect("walkdir starts with root"); - state.register_installed_path(relative, path, filename); - if entry.file_type().is_file() { - count += 1; - } + let relative = PathBuf::from(entry.file_name()); + state.register_installed_path(&relative, &path, filename); } - Ok(count) + Ok(()) } From 7147eda60f7cf60f02b347d1591fe4ce90e5da68 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 12 May 2026 17:44:29 -0400 Subject: [PATCH 06/56] Remove `zip` crate from dev dependencies (#19384) ## Summary Disappears from the `Cargo.lock`! --- Cargo.lock | 63 +----------------------- Cargo.toml | 7 --- crates/uv-build-backend/Cargo.toml | 1 - crates/uv-build-backend/src/lib.rs | 78 +++++++++++++++++++----------- crates/uv/Cargo.toml | 1 - crates/uv/tests/it/build.rs | 26 +++++++--- crates/uv/tests/it/lock.rs | 4 +- crates/uv/tests/it/pip_install.rs | 23 ++++----- 8 files changed, 83 insertions(+), 120 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c155e144f3346..1fe9682f98d9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,7 +364,7 @@ version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06575e6a9673580f52661c92107baabffbf41e2141373441cbcdc47cb733003c" dependencies = [ - "bzip2 0.5.2", + "bzip2", "flate2", "futures-core", "futures-io", @@ -650,15 +650,6 @@ dependencies = [ "bzip2-sys", ] -[[package]] -name = "bzip2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" -dependencies = [ - "libbz2-rs-sys", -] - [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -2572,12 +2563,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" -[[package]] -name = "libbz2-rs-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" - [[package]] name = "libc" version = "0.2.183" @@ -2644,15 +2629,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rust2" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae" -dependencies = [ - "sha2", -] - [[package]] name = "lzma-sys" version = "0.1.20" @@ -5527,12 +5503,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "typed-path" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" - [[package]] name = "typeid" version = "1.0.3" @@ -5864,7 +5834,6 @@ dependencies = [ "walkdir", "which", "wiremock", - "zip", ] [[package]] @@ -6050,7 +6019,6 @@ dependencies = [ "uv-version", "uv-warnings", "walkdir", - "zip", ] [[package]] @@ -8455,23 +8423,6 @@ dependencies = [ "syn", ] -[[package]] -name = "zip" -version = "8.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e499faf5c6b97a0d086f4a8733de6d47aee2252b8127962439d8d4311a73f72" -dependencies = [ - "bzip2 0.6.1", - "crc32fast", - "flate2", - "indexmap", - "lzma-rust2", - "memchr", - "typed-path", - "zopfli", - "zstd", -] - [[package]] name = "zlib-rs" version = "0.6.3" @@ -8484,18 +8435,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zopfli" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" -dependencies = [ - "bumpalo", - "crc32fast", - "log", - "simd-adler32", -] - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 689050149a494..3dbcfb3b214b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -308,13 +308,6 @@ wiremock = { version = "0.6.4" } wmi = { version = "0.18.3", default-features = false } xz2 = { version = "0.1.7", features = ["static"] } zeroize = { version = "1.8.1" } -zip = { version = "8.1.0", default-features = false, features = [ - "deflate", - "zstd", - "bzip2", - "lzma", - "xz", -] } # dev-dependencies assert_cmd = { version = "2.0.16" } diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index f596c69c462e8..da898e5971b68 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -66,4 +66,3 @@ uv-preview = { workspace = true, features = ["testing"] } indoc = { workspace = true } insta = { workspace = true } regex = { workspace = true } -zip = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 02991f9e53781..47a7c34bfa569 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -439,14 +439,16 @@ pub(crate) fn error_on_venv(file_name: &OsStr, path: &Path) -> Result<(), Error> #[cfg(test)] mod tests { use super::*; + use async_zip::base::read::mem::ZipFileReader; use flate2::bufread::GzDecoder; use fs_err::File; + use futures_lite::future::block_on; use indoc::indoc; use insta::assert_snapshot; use itertools::Itertools; use regex::Regex; use sha2::Digest; - use std::io::{BufReader, Read}; + use std::io::BufReader; use std::iter; use tempfile::TempDir; use uv_distribution_filename::{SourceDistFilename, WheelFilename}; @@ -582,15 +584,57 @@ mod tests { } fn wheel_contents(direct_output_dir: &Path) -> Vec { - let wheel = zip::ZipArchive::new(File::open(direct_output_dir).unwrap()).unwrap(); + let wheel = block_on(read_wheel(direct_output_dir)); let mut wheel_contents: Vec<_> = wheel - .file_names() - .map(|path| path.replace('\\', "/")) + .file() + .entries() + .iter() + .map(|entry| { + entry + .filename() + .as_str() + .expect("wheel filenames should be UTF-8") + .replace('\\', "/") + }) .collect(); wheel_contents.sort_unstable(); wheel_contents } + fn wheel_entry(wheel_path: &Path, filename: &str) -> String { + block_on(async { + let wheel = read_wheel(wheel_path).await; + let index = wheel + .file() + .entries() + .iter() + .position(|entry| { + entry + .filename() + .as_str() + .is_ok_and(|entry_filename| entry_filename == filename) + }) + .expect("wheel entry should exist"); + + let mut reader = wheel + .reader_with_entry(index) + .await + .expect("wheel entry should be readable"); + let mut contents = String::new(); + reader + .read_to_string_checked(&mut contents) + .await + .expect("wheel entry should be valid UTF-8"); + contents + }) + } + + async fn read_wheel(wheel_path: &Path) -> ZipFileReader { + ZipFileReader::new(fs_err::read(wheel_path).expect("wheel should be readable")) + .await + .expect("wheel should be valid") + } + fn format_file_list(file_list: FileList, src: &Path) -> String { file_list .into_iter() @@ -779,13 +823,7 @@ mod tests { built_by_uv-0.1.0.dist-info/METADATA (generated) "); - let mut wheel = zip::ZipArchive::new(File::open(wheel_path).unwrap()).unwrap(); - let mut record = String::new(); - wheel - .by_name("built_by_uv-0.1.0.dist-info/RECORD") - .unwrap() - .read_to_string(&mut record) - .unwrap(); + let record = wheel_entry(&wheel_path, "built_by_uv-0.1.0.dist-info/RECORD"); assert_snapshot!(record, @" built_by_uv/__init__.py,sha256=AJ7XpTNWxYktP97ydb81UpnNqoebH7K4sHRakAMQKG4,44 built_by_uv/arithmetic/__init__.py,sha256=x2agwFbJAafc9Z6TdJ0K6b6bLMApQdvRSQjP4iy7IEI,67 @@ -860,14 +898,7 @@ mod tests { let wheel = output_dir .path() .join("pep_pep639_license-1.0.0-py3-none-any.whl"); - let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap(); - - let mut metadata = String::new(); - wheel - .by_name("pep_pep639_license-1.0.0.dist-info/METADATA") - .unwrap() - .read_to_string(&mut metadata) - .unwrap(); + let metadata = wheel_entry(&wheel, "pep_pep639_license-1.0.0.dist-info/METADATA"); assert_snapshot!(metadata, @" Metadata-Version: 2.3 @@ -934,14 +965,7 @@ mod tests { let wheel = output_dir .path() .join("two_step_build-1.0.0-py3-none-any.whl"); - let mut wheel = zip::ZipArchive::new(File::open(wheel).unwrap()).unwrap(); - - let mut metadata_wheel = String::new(); - wheel - .by_name("two_step_build-1.0.0.dist-info/METADATA") - .unwrap() - .read_to_string(&mut metadata_wheel) - .unwrap(); + let metadata_wheel = wheel_entry(&wheel, "two_step_build-1.0.0.dist-info/METADATA"); assert_eq!(metadata_prepared, metadata_wheel); diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 0040659ede642..b51ffd686632a 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -151,7 +151,6 @@ tempfile = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } wiremock = { workspace = true } -zip = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true } diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 48c5d0d3cbc8d..ceab878114d1c 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1,11 +1,13 @@ use anyhow::Result; use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; -use fs_err::File; +use async_zip::base::read::mem::ZipFileReader; +use futures::executor::block_on; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use predicates::prelude::predicate; use std::env::current_dir; +use std::path::Path; use url::Url; use uv_static::EnvVars; use uv_test::{DEFAULT_PYTHON_VERSION, apply_filters, uv_snapshot}; @@ -13,7 +15,20 @@ use wiremock::{ Mock, MockServer, ResponseTemplate, matchers::{method, path as url_path}, }; -use zip::ZipArchive; + +fn zip_file_names(path: &Path) -> Result> { + block_on(async { + let wheel = ZipFileReader::new(fs_err::read(path)?).await?; + let mut files: Vec<_> = wheel + .file() + .entries() + .iter() + .map(|entry| Ok(entry.filename().as_str()?.to_string())) + .collect::>()?; + files.sort(); + Ok(files) + }) +} #[test] fn build_basic() -> Result<()> { @@ -1836,12 +1851,7 @@ fn build_git_boundary_in_dist_build() -> Result<()> { "); // Check that the source file is included - let reader = File::open(project.join("dist/demo-0.1.0-py3-none-any.whl"))?; - let mut files: Vec<_> = ZipArchive::new(reader)? - .file_names() - .map(ToString::to_string) - .collect(); - files.sort(); + let files = zip_file_names(&project.join("dist/demo-0.1.0-py3-none-any.whl"))?; assert_snapshot!(files.join("\n"), @" demo-0.1.0.dist-info/METADATA demo-0.1.0.dist-info/RECORD diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 3850490f5cddd..96d773effbc9c 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -3,7 +3,6 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::prelude::*; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; -use std::io::BufReader; use url::Url; use uv_fs::Simplified; @@ -13646,8 +13645,7 @@ fn lock_sources_source_tree() -> Result<()> { // Unzip the file. let file = fs_err::File::open(&*workspace_archive)?; - let mut archive = zip::ZipArchive::new(BufReader::new(file))?; - archive.extract(&context.temp_dir)?; + uv_extract::unzip(file, &context.temp_dir)?; let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str(&formatdoc! { diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 9ac05a06d8165..69784ca76e1bf 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -1,4 +1,3 @@ -use std::io; use std::io::Cursor; use std::path::PathBuf; use std::process::Command; @@ -6,9 +5,12 @@ use std::process::Command; use anyhow::{Result, anyhow}; use assert_cmd::prelude::*; use assert_fs::prelude::*; +use async_zip::base::write::ZipFileWriter; +use async_zip::{Compression, ZipEntryBuilder}; use flate2::write::GzEncoder; use fs_err as fs; use fs_err::File; +use futures::executor::block_on; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use predicates::prelude::predicate; @@ -18,8 +20,6 @@ use wiremock::{ Mock, MockServer, ResponseTemplate, matchers::{basic_auth, method, path}, }; -use zip::write::SimpleFileOptions; -use zip::{ZipArchive, ZipWriter}; use uv_fs::{PortablePath, Simplified}; use uv_static::EnvVars; @@ -15424,7 +15424,7 @@ fn handle_record_mismatches() -> Result<()> { context.build().arg("--wheel").arg("foo").assert().success(); let built_wheel = context.temp_dir.join("foo/dist/foo-0.1.0-py3-none-any.whl"); let unpacked = context.temp_dir.join("foo-unpacked"); - ZipArchive::new(File::open(built_wheel)?)?.extract(&unpacked)?; + uv_extract::unzip(File::open(&built_wheel)?, &unpacked)?; // Snapshot the current (correct) RECORD. let record = unpacked.join("foo-0.1.0.dist-info/RECORD"); @@ -15451,8 +15451,7 @@ fn handle_record_mismatches() -> Result<()> { // Repack the wheel. let repacked_wheel = context.temp_dir.join("foo-0.1.0-py3-none-any.whl"); - let mut writer = ZipWriter::new(File::create(&repacked_wheel)?); - let options = SimpleFileOptions::default(); + let mut writer = ZipFileWriter::new(Vec::new()); for entry in WalkDir::new(&unpacked) { let entry = entry?; let path = entry.path(); @@ -15461,15 +15460,17 @@ fn handle_record_mismatches() -> Result<()> { continue; } // Zip entries must use forward slashes, even on Windows. - let name = PortablePath::from(name).to_string(); + let mut name = PortablePath::from(name).to_string(); if path.is_dir() { - writer.add_directory(&name, options)?; + name.push('/'); + let entry = ZipEntryBuilder::new(name.into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, &[]))?; } else { - writer.start_file(&name, options)?; - io::copy(&mut File::open(path)?, &mut writer)?; + let entry = ZipEntryBuilder::new(name.into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, &fs_err::read(path)?))?; } } - writer.finish()?; + fs_err::write(&repacked_wheel, block_on(writer.close())?)?; uv_snapshot!(context.filters(), context.pip_install() .arg("--find-links") From 9b78b7926f5364dfffcfc78b2dd07ccd36efb79c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 13 May 2026 09:37:18 -0400 Subject: [PATCH 07/56] Bump `cyclonedx-bom` to remove duplicate `spdx`, `base64`, and `bitflags` crates (#19386) --- Cargo.lock | 126 +++++++++----------- Cargo.toml | 2 +- crates/uv/tests/it/export.rs | 216 ++++++++++++----------------------- 3 files changed, 129 insertions(+), 215 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fe9682f98d9a..09a2e97925087 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -505,12 +505,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -523,12 +517,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.11.1" @@ -571,6 +559,12 @@ dependencies = [ "objc2", ] +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + [[package]] name = "boxcar" version = "0.2.14" @@ -743,7 +737,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e" dependencies = [ - "base64 0.22.1", + "base64", "encoding_rs", ] @@ -1156,11 +1150,11 @@ dependencies = [ [[package]] name = "cyclonedx-bom" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce2ec98a191e17f63b92b132f6852462de9eaee03ca8dbf2df401b9fd809bcac" +checksum = "3132b69ba8c13808bd2fa5748ac5b9816eb4f4e1f0bff6b7f9254a5940dcdeef" dependencies = [ - "base64 0.21.7", + "base64", "cyclonedx-bom-macros", "fluent-uri", "indexmap", @@ -1170,9 +1164,9 @@ dependencies = [ "regex", "serde", "serde_json", - "spdx 0.10.9", + "spdx", "strum", - "thiserror 1.0.69", + "thiserror 2.0.18", "time", "uuid", "xml-rs", @@ -1328,7 +1322,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags", "block2", "libc", "objc2", @@ -1597,11 +1591,13 @@ dependencies = [ [[package]] name = "fluent-uri" -version = "0.1.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" +checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" dependencies = [ - "bitflags 1.3.2", + "borrow-or-share", + "ref-cast", + "serde", ] [[package]] @@ -1856,7 +1852,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.11.1", + "bitflags", "ignore", "walkdir", ] @@ -2092,7 +2088,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-util", @@ -2517,7 +2513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "aws-lc-rs", - "base64 0.22.1", + "base64", "getrandom 0.2.16", "js-sys", "pem", @@ -2590,7 +2586,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.11.1", + "bitflags", "libc", "plain", "redox_syscall 0.7.0", @@ -2828,7 +2824,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -2840,7 +2836,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ - "bitflags 2.11.1", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -3044,9 +3040,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "4.6.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] @@ -3143,7 +3139,7 @@ version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ - "base64 0.22.1", + "base64", "serde_core", ] @@ -3311,7 +3307,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags", "crc32fast", "fdeflate", "flate2", @@ -3451,7 +3447,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.11.1", + "bitflags", "flate2", "procfs-core", "rustix", @@ -3463,7 +3459,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hex", ] @@ -3704,7 +3700,7 @@ version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3713,7 +3709,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -3849,7 +3845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b10302cf0a7d7e7352ba211fc92c3c5bebf1286153e49cc5aa87348078a8e102" dependencies = [ "anyhow", - "base64 0.22.1", + "base64", "bytes", "form_urlencoded", "futures", @@ -3917,7 +3913,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ - "base64 0.22.1", + "base64", "bytes", "futures-channel", "futures-core", @@ -4131,7 +4127,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -4225,7 +4221,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.11.1", + "bitflags", "bytemuck", "core_maths", "log", @@ -4373,7 +4369,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4683,15 +4679,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "spdx" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e17e880bafaeb362a7b751ec46bdc5b61445a188f80e0606e68167cd540fa3" -dependencies = [ - "smallvec", -] - [[package]] name = "spdx" version = "0.13.4" @@ -4750,23 +4737,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn", ] @@ -4850,7 +4836,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.11.1", + "bitflags", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5335,7 +5321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags", "bytes", "futures-core", "futures-util", @@ -5659,7 +5645,7 @@ version = "0.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d46cf96c5f498d36b7a9693bc6a7075c0bb9303189d61b2249b0dc3d309c07de" dependencies = [ - "base64 0.22.1", + "base64", "data-url", "flate2", "fontdb", @@ -5723,7 +5709,7 @@ dependencies = [ "astral_async_zip", "axoupdater", "backon", - "base64 0.22.1", + "base64", "byteorder", "bytes", "clap", @@ -5872,7 +5858,7 @@ dependencies = [ "arcstr", "astral-reqwest-middleware", "async-trait", - "base64 0.22.1", + "base64", "etcetera", "fs-err", "futures", @@ -5983,7 +5969,7 @@ version = "0.0.47" dependencies = [ "astral-version-ranges", "astral_async_zip", - "base64 0.22.1", + "base64", "csv", "flate2", "fs-err", @@ -5999,7 +5985,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "spdx 0.13.4", + "spdx", "tar", "tempfile", "thiserror 2.0.18", @@ -6425,7 +6411,7 @@ version = "0.0.47" dependencies = [ "arcstr", "astral-version-ranges", - "bitflags 2.11.1", + "bitflags", "fs-err", "http", "itertools 0.14.0", @@ -6502,7 +6488,7 @@ dependencies = [ name = "uv-flags" version = "0.0.47" dependencies = [ - "bitflags 2.11.1", + "bitflags", ] [[package]] @@ -6827,7 +6813,7 @@ dependencies = [ name = "uv-platform-tags" version = "0.0.47" dependencies = [ - "bitflags 2.11.1", + "bitflags", "insta", "memchr", "rkyv", @@ -6858,7 +6844,7 @@ dependencies = [ "astral-reqwest-retry", "astral-tokio-tar", "async-compression", - "base64 0.22.1", + "base64", "fastrand", "fs-err", "futures", @@ -7242,7 +7228,7 @@ dependencies = [ "anyhow", "assert_cmd", "assert_fs", - "base64 0.22.1", + "base64", "fs-err", "futures", "ignore", @@ -7621,7 +7607,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags", "hashbrown 0.15.5", "indexmap", "semver", @@ -8076,7 +8062,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" dependencies = [ "assert-json-diff", - "base64 0.22.1", + "base64", "deadpool", "futures", "http", @@ -8150,7 +8136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags", "indexmap", "log", "serde", diff --git a/Cargo.toml b/Cargo.toml index 3dbcfb3b214b6..34f50cfc029ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,7 +133,7 @@ configparser = { version = "3.1.0" } console = { version = "0.16.0", default-features = false, features = ["std"] } csv = { version = "1.3.0" } ctrlc = { version = "3.4.5" } -cyclonedx-bom = { version = "0.8.0" } +cyclonedx-bom = { version = "0.8.1" } dashmap = { version = "6.1.0" } data-encoding = { version = "2.6.0" } diskus = { version = "0.9.0", default-features = false } diff --git a/crates/uv/tests/it/export.rs b/crates/uv/tests/it/export.rs index 4ec59672972c6..208598e350300 100644 --- a/crates/uv/tests/it/export.rs +++ b/crates/uv/tests/it/export.rs @@ -5416,8 +5416,7 @@ fn cyclonedx_export_basic() -> Result<()> { ] }, { - "ref": "urllib3-2@2.2.0", - "dependsOn": [] + "ref": "urllib3-2@2.2.0" } ] } @@ -5492,8 +5491,7 @@ fn cyclonedx_export_direct_url() -> Result<()> { ], "dependencies": [ { - "ref": "idna-2@3.6", - "dependsOn": [] + "ref": "idna-2@3.6" }, { "ref": "project-1@0.1.0", @@ -5581,8 +5579,7 @@ fn cyclonedx_export_git_dependency() -> Result<()> { ] }, { - "ref": "urllib3-2@2.2.0", - "dependsOn": [] + "ref": "urllib3-2@2.2.0" } ] } @@ -5649,8 +5646,7 @@ fn cyclonedx_export_no_dependencies() -> Result<()> { "components": [], "dependencies": [ { - "ref": "standalone-project-1@1.0.0", - "dependsOn": [] + "ref": "standalone-project-1@1.0.0" } ] } @@ -5744,12 +5740,10 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { ], "dependencies": [ { - "ref": "idna-2@3.6", - "dependsOn": [] + "ref": "idna-2@3.6" }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { "ref": "mixed-project-1@0.1.0", @@ -5760,8 +5754,7 @@ fn cyclonedx_export_mixed_source_types() -> Result<()> { ] }, { - "ref": "urllib3-4@2.2.0", - "dependsOn": [] + "ref": "urllib3-4@2.2.0" } ] } @@ -5846,8 +5839,7 @@ fn cyclonedx_export_project_extra() -> Result<()> { ] }, { - "ref": "typing-extensions-2@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-2@4.10.0" } ] } @@ -5940,8 +5932,7 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { ], "dependencies": [ { - "ref": "iniconfig-2@2.0.0", - "dependsOn": [] + "ref": "iniconfig-2@2.0.0" }, { "ref": "project-1@0.1.0", @@ -5952,12 +5943,10 @@ fn cyclonedx_export_project_extra_with_optional_flag() -> Result<()> { ] }, { - "ref": "typing-extensions-3@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-3@4.10.0" }, { - "ref": "urllib3-4@2.2.0", - "dependsOn": [] + "ref": "urllib3-4@2.2.0" } ] } @@ -6106,12 +6095,10 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { ] }, { - "ref": "child2-3@0.2.9", - "dependsOn": [] + "ref": "child2-3@0.2.9" }, { - "ref": "iniconfig-4@2.0.0", - "dependsOn": [] + "ref": "iniconfig-4@2.0.0" }, { "ref": "project-1@0.1.0", @@ -6122,8 +6109,7 @@ fn cyclonedx_export_with_workspace_member() -> Result<()> { ] }, { - "ref": "urllib3-5@2.2.0", - "dependsOn": [] + "ref": "urllib3-5@2.2.0" } ] } @@ -6225,8 +6211,7 @@ fn cyclonedx_export_workspace_non_root() -> Result<()> { ] }, { - "ref": "iniconfig-2@2.0.0", - "dependsOn": [] + "ref": "iniconfig-2@2.0.0" } ] } @@ -6350,8 +6335,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ] }, { - "ref": "typing-extensions-3@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-3@4.10.0" } ] } @@ -6434,8 +6418,7 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ] }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { "ref": "project-1@0.1.0", @@ -6446,12 +6429,10 @@ fn cyclonedx_export_workspace_with_extras() -> Result<()> { ] }, { - "ref": "typing-extensions-4@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-4@4.10.0" }, { - "ref": "urllib3-5@2.2.0", - "dependsOn": [] + "ref": "urllib3-5@2.2.0" } ] } @@ -6597,8 +6578,7 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { ] }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { "ref": "project-1@0.1.0", @@ -6608,8 +6588,7 @@ fn cyclonedx_export_workspace_frozen() -> Result<()> { ] }, { - "ref": "urllib3-4@2.2.0", - "dependsOn": [] + "ref": "urllib3-4@2.2.0" }, { "ref": "project-5", @@ -6784,8 +6763,7 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ] }, { - "ref": "iniconfig-4@2.0.0", - "dependsOn": [] + "ref": "iniconfig-4@2.0.0" }, { "ref": "project-1@0.1.0", @@ -6794,12 +6772,10 @@ fn cyclonedx_export_workspace_all_packages() -> Result<()> { ] }, { - "ref": "sniffio-5@1.3.1", - "dependsOn": [] + "ref": "sniffio-5@1.3.1" }, { - "ref": "urllib3-6@2.2.0", - "dependsOn": [] + "ref": "urllib3-6@2.2.0" }, { "ref": "project-7", @@ -6899,8 +6875,7 @@ fn cyclonedx_export_all_packages_non_workspace_root_dependency() -> Result<()> { ] }, { - "ref": "urllib3-2@2.2.0", - "dependsOn": [] + "ref": "urllib3-2@2.2.0" }, { "ref": "my-project-3", @@ -7072,8 +7047,7 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { ] }, { - "ref": "iniconfig-4@2.0.0", - "dependsOn": [] + "ref": "iniconfig-4@2.0.0" }, { "ref": "project-1@0.1.0", @@ -7083,12 +7057,10 @@ fn cyclonedx_export_workspace_mixed_dependencies() -> Result<()> { ] }, { - "ref": "sniffio-5@1.3.1", - "dependsOn": [] + "ref": "sniffio-5@1.3.1" }, { - "ref": "urllib3-6@2.2.0", - "dependsOn": [] + "ref": "urllib3-6@2.2.0" } ] } @@ -7179,8 +7151,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { ], "dependencies": [ { - "ref": "iniconfig-2@2.0.0", - "dependsOn": [] + "ref": "iniconfig-2@2.0.0" }, { "ref": "project-1@0.1.0", @@ -7190,8 +7161,7 @@ fn cyclonedx_export_dependency_marker() -> Result<()> { ] }, { - "ref": "urllib3-3@2.2.1", - "dependsOn": [] + "ref": "urllib3-3@2.2.1" } ] } @@ -7319,8 +7289,7 @@ fn cyclonedx_export_multiple_dependency_markers() -> Result<()> { ] }, { - "ref": "pycparser-4@2.21", - "dependsOn": [] + "ref": "pycparser-4@2.21" } ] } @@ -7428,8 +7397,7 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { ], "dependencies": [ { - "ref": "bcrypt-2@4.1.2", - "dependsOn": [] + "ref": "bcrypt-2@4.1.2" }, { "ref": "cffi-3@1.16.0", @@ -7451,8 +7419,7 @@ fn cyclonedx_export_dependency_extra() -> Result<()> { ] }, { - "ref": "pycparser-5@2.21", - "dependsOn": [] + "ref": "pycparser-5@2.21" } ] } @@ -7614,8 +7581,7 @@ fn cyclonedx_export_prune() -> Result<()> { ] }, { - "ref": "pycparser-4@2.21", - "dependsOn": [] + "ref": "pycparser-4@2.21" }, { "ref": "python-dateutil-5@2.9.0.post0", @@ -7630,16 +7596,13 @@ fn cyclonedx_export_prune() -> Result<()> { ] }, { - "ref": "six-7@1.16.0", - "dependsOn": [] + "ref": "six-7@1.16.0" }, { - "ref": "tornado-8@6.4", - "dependsOn": [] + "ref": "tornado-8@6.4" }, { - "ref": "traitlets-9@5.14.2", - "dependsOn": [] + "ref": "traitlets-9@5.14.2" } ] } @@ -7731,12 +7694,10 @@ fn cyclonedx_export_group() -> Result<()> { ] }, { - "ref": "sniffio-2@1.3.1", - "dependsOn": [] + "ref": "sniffio-2@1.3.1" }, { - "ref": "typing-extensions-3@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-3@4.10.0" } ] } @@ -7788,8 +7749,7 @@ fn cyclonedx_export_group() -> Result<()> { ], "dependencies": [ { - "ref": "iniconfig-2@2.0.0", - "dependsOn": [] + "ref": "iniconfig-2@2.0.0" } ] } @@ -7869,16 +7829,13 @@ fn cyclonedx_export_group() -> Result<()> { ] }, { - "ref": "sniffio-2@1.3.1", - "dependsOn": [] + "ref": "sniffio-2@1.3.1" }, { - "ref": "typing-extensions-3@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-3@4.10.0" }, { - "ref": "urllib3-4@2.2.1", - "dependsOn": [] + "ref": "urllib3-4@2.2.1" } ] } @@ -7941,8 +7898,7 @@ fn cyclonedx_export_non_project() -> Result<()> { "components": [], "dependencies": [ { - "ref": "uv-workspace-1", - "dependsOn": [] + "ref": "uv-workspace-1" } ] } @@ -7994,12 +7950,10 @@ fn cyclonedx_export_non_project() -> Result<()> { ], "dependencies": [ { - "ref": "urllib3-1@2.2.1", - "dependsOn": [] + "ref": "urllib3-1@2.2.1" }, { - "ref": "uv-workspace-2", - "dependsOn": [] + "ref": "uv-workspace-2" } ] } @@ -8115,8 +8069,7 @@ fn cyclonedx_export_no_emit() -> Result<()> { ] }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { "ref": "project-1@0.1.0", @@ -8199,12 +8152,10 @@ fn cyclonedx_export_no_emit() -> Result<()> { ] }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { - "ref": "urllib3-4@2.2.0", - "dependsOn": [] + "ref": "urllib3-4@2.2.0" } ] } @@ -8309,8 +8260,7 @@ fn cyclonedx_export_relative_path() -> Result<()> { ] }, { - "ref": "iniconfig-3@2.0.0", - "dependsOn": [] + "ref": "iniconfig-3@2.0.0" }, { "ref": "project-1@0.1.0", @@ -8454,12 +8404,10 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "argparse-2@1.4.0", - "dependsOn": [] + "ref": "argparse-2@1.4.0" }, { - "ref": "extras-3@1.0.0", - "dependsOn": [] + "ref": "extras-3@1.0.0" }, { "ref": "fixtures-4@3.0.0", @@ -8470,12 +8418,10 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { ] }, { - "ref": "linecache2-5@1.0.0", - "dependsOn": [] + "ref": "linecache2-5@1.0.0" }, { - "ref": "pbr-6@6.0.0", - "dependsOn": [] + "ref": "pbr-6@6.0.0" }, { "ref": "project-1@0.1.0", @@ -8485,12 +8431,10 @@ fn cyclonedx_export_cyclic_dependencies() -> Result<()> { ] }, { - "ref": "python-mimeparse-7@1.6.0", - "dependsOn": [] + "ref": "python-mimeparse-7@1.6.0" }, { - "ref": "six-8@1.16.0", - "dependsOn": [] + "ref": "six-8@1.16.0" }, { "ref": "testtools-9@2.3.0", @@ -8609,12 +8553,10 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ] }, { - "ref": "typing-extensions-2@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-2@4.10.0" }, { - "ref": "urllib3-3@2.2.1", - "dependsOn": [] + "ref": "urllib3-3@2.2.1" } ] } @@ -8673,8 +8615,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ] }, { - "ref": "typing-extensions-2@4.10.0", - "dependsOn": [] + "ref": "typing-extensions-2@4.10.0" } ] } @@ -8727,8 +8668,7 @@ fn cyclonedx_export_dev_dependencies() -> Result<()> { ], "dependencies": [ { - "ref": "urllib3-2@2.2.1", - "dependsOn": [] + "ref": "urllib3-2@2.2.1" } ] } @@ -8873,12 +8813,10 @@ fn cyclonedx_export_all_packages_conflicting_workspace_members() -> Result<()> { ] }, { - "ref": "sortedcontainers-3@2.3.0", - "dependsOn": [] + "ref": "sortedcontainers-3@2.3.0" }, { - "ref": "sortedcontainers-4@2.4.0", - "dependsOn": [] + "ref": "sortedcontainers-4@2.4.0" }, { "ref": "project-5", @@ -9481,12 +9419,10 @@ fn cyclonedx_export_alternative_registry() -> Result<()> { ], "dependencies": [ { - "ref": "filelock-2@3.13.1", - "dependsOn": [] + "ref": "filelock-2@3.13.1" }, { - "ref": "fsspec-3@2024.6.1", - "dependsOn": [] + "ref": "fsspec-3@2024.6.1" }, { "ref": "jinja2-4@3.1.4", @@ -9495,16 +9431,13 @@ fn cyclonedx_export_alternative_registry() -> Result<()> { ] }, { - "ref": "markupsafe-5@3.0.2", - "dependsOn": [] + "ref": "markupsafe-5@3.0.2" }, { - "ref": "mpmath-6@1.3.0", - "dependsOn": [] + "ref": "mpmath-6@1.3.0" }, { - "ref": "networkx-7@3.3", - "dependsOn": [] + "ref": "networkx-7@3.3" }, { "ref": "project-1@0.1.0", @@ -9514,8 +9447,7 @@ fn cyclonedx_export_alternative_registry() -> Result<()> { ] }, { - "ref": "setuptools-8@70.2.0", - "dependsOn": [] + "ref": "setuptools-8@70.2.0" }, { "ref": "sympy-9@1.13.1", @@ -9548,8 +9480,7 @@ fn cyclonedx_export_alternative_registry() -> Result<()> { ] }, { - "ref": "typing-extensions-12@4.12.2", - "dependsOn": [] + "ref": "typing-extensions-12@4.12.2" } ] } @@ -9697,12 +9628,10 @@ fn cyclonedx_export_virtual_workspace_fixture() -> Result<()> { ] }, { - "ref": "idna-4@3.6", - "dependsOn": [] + "ref": "idna-4@3.6" }, { - "ref": "iniconfig-5@2.0.0", - "dependsOn": [] + "ref": "iniconfig-5@2.0.0" }, { "ref": "seeds-6@1.0.0", @@ -9711,8 +9640,7 @@ fn cyclonedx_export_virtual_workspace_fixture() -> Result<()> { ] }, { - "ref": "sniffio-7@1.3.1", - "dependsOn": [] + "ref": "sniffio-7@1.3.1" }, { "ref": "uv-workspace-8", From 77ef90777c07ad4728824a6b1f8d2c0de5863b77 Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Wed, 13 May 2026 15:04:46 +0100 Subject: [PATCH 08/56] Move Bazel auth helper setup into integration guide (#19392) Make a new Bazel guide, that links to two of the most popular Bazel rulesets that support uv. Move the Bazel specific authentication setup to this new guide. This is a followup to #19358 --- docs/concepts/authentication/cli.md | 32 ++------------------- docs/guides/integration/bazel.md | 44 +++++++++++++++++++++++++++++ docs/guides/integration/index.md | 1 + mkdocs.yml | 2 ++ 4 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 docs/guides/integration/bazel.md diff --git a/docs/concepts/authentication/cli.md b/docs/concepts/authentication/cli.md index 2a8d85661d0c4..e9d94dcd73cdb 100644 --- a/docs/concepts/authentication/cli.md +++ b/docs/concepts/authentication/cli.md @@ -87,36 +87,8 @@ If no credentials are found, uv will return an empty set of headers: `uv auth helper` is experimental. Use `--preview-features auth-helper` or `UV_PREVIEW_FEATURES=auth-helper` to disable the warning. -### Bazel - -Bazel 7 and newer supports credential helpers via the `--credential_helper` option. First, -authenticate uv with the service that hosts the files Bazel needs to fetch: - -```console -$ uv auth login https://packages.example.com -``` - -Then, configure Bazel to invoke uv for matching hosts: - -```text title=".bazelrc" -common --credential_helper=packages.example.com=%workspace%/bazel/uv-auth-helper -common --credential_helper=files.example.com=%workspace%/bazel/uv-auth-helper -``` - -Replace the host patterns with the hosts that serve the index and files Bazel will fetch. - -Finally, add the wrapper script referenced by `.bazelrc`: - -```bash title="bazel/uv-auth-helper" -#!/usr/bin/env bash -exec uv --preview-features auth-helper auth helper --protocol=bazel "$@" -``` - -The script must be executable: - -```console -$ chmod +x bazel/uv-auth-helper -``` +The [Bazel integration guide](../../guides/integration/bazel.md) explains how to use this command +with Bazel. ## Configuring the storage backend diff --git a/docs/guides/integration/bazel.md b/docs/guides/integration/bazel.md new file mode 100644 index 0000000000000..471740eda65e4 --- /dev/null +++ b/docs/guides/integration/bazel.md @@ -0,0 +1,44 @@ +--- +title: Using uv with Bazel +description: Using uv to power package resolution with Bazel +--- + +# Using uv with Bazel + +For broader Bazel workflows with uv, see the +[`rules_py` uv guide](https://github.com/aspect-build/rules_py#dependency-resolution-with-uv) or the +[`rules_python` uv guide](https://rules-python.readthedocs.io/en/latest/pypi/lock.html#uv-pip-compile-bzlmod-only). + +## Authentication + +Bazel 7 and newer supports credential helpers via the `--credential_helper` option. To use +credentials stored by uv for Bazel fetches, first authenticate uv with the service that hosts the +files Bazel needs to fetch: + +```console +$ uv auth login https://packages.example.com +``` + +Then, configure Bazel to invoke +[`uv auth helper`](../../concepts/authentication/cli.md#using-credentials-with-external-tools) for +matching hosts: + +```text title=".bazelrc" +common --credential_helper=packages.example.com=%workspace%/bazel/uv-auth-helper +common --credential_helper=files.example.com=%workspace%/bazel/uv-auth-helper +``` + +Replace the host patterns with the hosts that serve the index and files Bazel will fetch. + +Finally, add the wrapper script referenced by `.bazelrc`: + +```bash title="bazel/uv-auth-helper" +#!/usr/bin/env bash +exec uv --preview-features auth-helper auth helper --protocol=bazel "$@" +``` + +The script must be executable: + +```console +$ chmod +x bazel/uv-auth-helper +``` diff --git a/docs/guides/integration/index.md b/docs/guides/integration/index.md index d7b7c70a96e7c..dc15a32654afa 100644 --- a/docs/guides/integration/index.md +++ b/docs/guides/integration/index.md @@ -10,6 +10,7 @@ Learn how to integrate uv with other software: - [Using in GitLab CI/CD](./gitlab.md) - [Installing PyTorch](./pytorch.md) - [Building a FastAPI application](./fastapi.md) +- [Using with Bazel](./bazel.md) - [Using with Azure Artifacts](./azure.md) - [Using with Google Artifact Registry](./google.md) - [Using with AWS CodeArtifact](./aws.md) diff --git a/mkdocs.yml b/mkdocs.yml index 56dcd5399196c..fea141974afec 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ plugins: - guides/integration/pre-commit.md - guides/integration/pytorch.md - guides/integration/fastapi.md + - guides/integration/bazel.md - guides/integration/azure.md - guides/integration/google.md - guides/integration/aws.md @@ -203,6 +204,7 @@ nav: - Pre-commit: guides/integration/pre-commit.md - PyTorch: guides/integration/pytorch.md - FastAPI: guides/integration/fastapi.md + - Bazel: guides/integration/bazel.md - Azure Artifacts: guides/integration/azure.md - Google Artifact Registry: guides/integration/google.md - AWS CodeArtifact: guides/integration/aws.md From 16302873e48ce4f3aaf97cb73d15e7556494413a Mon Sep 17 00:00:00 2001 From: vip892766gma Date: Wed, 13 May 2026 12:45:42 -0500 Subject: [PATCH 09/56] Fix article grammar in comments (#19394) Fixes small article/grammar issues in comments and assertion text. No behavior change. Co-authored-by: Aiden Park <275402320+vip892766gma@users.noreply.github.com> --- crates/uv-pypi-types/src/metadata/metadata23.rs | 2 +- crates/uv-python/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/uv-pypi-types/src/metadata/metadata23.rs b/crates/uv-pypi-types/src/metadata/metadata23.rs index 72b3ee90a68f1..9dee09a27e45a 100644 --- a/crates/uv-pypi-types/src/metadata/metadata23.rs +++ b/crates/uv-pypi-types/src/metadata/metadata23.rs @@ -61,7 +61,7 @@ pub struct Metadata23 { /// it should be omitted if it is identical to `author`. pub maintainer: Option, /// A string containing the maintainer's e-mail address. - /// It can contain a name and e-mail address in the legal forms for a RFC-822 `From:` header. + /// It can contain a name and e-mail address in the legal forms for an RFC-822 `From:` header. /// /// Note that this field is intended for use when a project is being maintained by someone other /// than the original author: it should be omitted if it is identical to `author_email`. diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index f25b9d7283cb3..327630987ee24 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -618,7 +618,7 @@ mod tests { }); assert!( matches!(result, Ok(Err(PythonNotFound { .. }))), - "With an non-executable Python, no Python installation should be detected; got {result:?}" + "With a non-executable Python, no Python installation should be detected; got {result:?}" ); Ok(()) From 941142a92b9e5be28b350a59e58ebb0a408b4e85 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 13 May 2026 15:03:28 -0400 Subject: [PATCH 10/56] Optimize async wheel ZIP writing (#19383) --- Cargo.lock | 4 +- Cargo.toml | 2 +- crates/uv-build-backend/src/lib.rs | 2 +- crates/uv-build-backend/src/wheel.rs | 81 ++++++++++++++++++++-------- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09a2e97925087..9c5ad619d3e26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,9 +321,9 @@ dependencies = [ [[package]] name = "astral_async_zip" -version = "0.0.18-rc3" +version = "0.0.18-rc4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f773362bd6d5b14e1cb03473e26a1932884107f98f77f45c443e7ca85c9d6c43" +checksum = "9d523231a4339307c7699aa5d9492fe6873906afefc9d7853b5e7ffbfee3c7f1" dependencies = [ "async-compression", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 34f50cfc029ca..0b282c5188b68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ async-compression = { version = "0.4.12", features = [ ] } async-trait = { version = "0.1.82" } async_http_range_reader = { version = "0.11.0", package = "astral_async_http_range_reader" } -async_zip = { version = "0.0.18-rc3", package = "astral_async_zip", features = [ +async_zip = { version = "0.0.18-rc4", package = "astral_async_zip", features = [ "bzip2", "deflate", "lzma", diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 47a7c34bfa569..add85236c233f 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -779,7 +779,7 @@ mod tests { // Check that the wheel is reproducible across platforms. assert_snapshot!( format!("{:x}", sha2::Sha256::digest(fs_err::read(&wheel_path).unwrap())), - @"17d4946da272e3fee31f9e8261472f06f115cc3c2178e0a5cddbebb83e91fd04" + @"9e8d80fef76be79a7fe73a2ccac3bdd0132b10fdcff7271ca8b868c99061b8ce" ); assert_snapshot!(build.wheel_contents.join("\n"), @" built_by_uv-0.1.0.data/data/ diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 1af40f7b48b0c..fbeff5af7c7a0 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -1,14 +1,14 @@ -use async_zip::base::write::{EntryStreamWriter, ZipFileWriter}; +use async_zip::base::write::{EntrySeekableWriter, ZipFileWriter}; use async_zip::{Compression, ZipEntryBuilder}; use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD as base64}; use fs_err::File; use futures_lite::future::block_on; -use futures_lite::io::{AsyncWrite, AsyncWriteExt}; +use futures_lite::io::{AsyncSeek, AsyncWrite, AsyncWriteExt}; use globset::{GlobSet, GlobSetBuilder}; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; use std::fmt::{Display, Formatter}; -use std::io::{BufReader, Read, Write}; +use std::io::{BufReader, Read, Seek, SeekFrom, Write}; use std::path::{Component, Path, PathBuf}; use std::pin::Pin; use std::task::{Context, Poll}; @@ -29,6 +29,16 @@ use crate::{ error_on_venv, find_roots, }; +// Files at or below this size are buffered and written with `write_entry_whole`, +// which was fastest in wheel-writer benchmarks because it can write final ZIP +// headers without per-chunk async writes. The 16 MiB limit keeps typical source +// files on that fast path without buffering very large data files wholesale. +const WHOLE_FILE_ZIP_ENTRY_LIMIT: u64 = 16 * 1024 * 1024; +// Buffer size for the large-file streaming fallback. 128 KiB was enough to cut +// down read/write loop overhead compared to the 8 KiB default while remaining a +// small fixed allocation for entries that are too large for `write_entry_whole`. +const ZIP_STREAM_BUFFER_SIZE: usize = 128 * 1024; + /// Build a wheel from the source tree and place it in the output directory. pub fn build_wheel( source_tree: &Path, @@ -439,8 +449,7 @@ fn write_hashed( ) -> Result { let mut hasher = Sha256::new(); let mut size: u64 = 0; - // 8KB is the default defined in `std::sys_common::io`. - let mut buffer = vec![0; 8 * 1024]; + let mut buffer = vec![0; ZIP_STREAM_BUFFER_SIZE]; loop { let read = match reader.read(&mut buffer) { Ok(read) => read, @@ -703,14 +712,14 @@ impl Display for WheelInfo { } /// ZIP archive (wheel) writer. -struct ZipDirectoryWriter { +struct ZipDirectoryWriter { writer: ZipFileWriter, compression: Compression, /// The entries in the `RECORD` file. record: Vec, } -impl ZipDirectoryWriter { +impl ZipDirectoryWriter { // Include the Unix file type bits because `async_zip` writes this mode // directly to the ZIP external attributes. The sync `zip` crate adds // those bits internally when starting file and directory entries. @@ -735,7 +744,7 @@ impl ZipDirectoryWriter { Self::REGULAR_FILE_MODE }; let entry = Self::entry(path, self.compression, mode); - let writer = block_on(self.writer.write_entry_stream(entry))?; + let writer = block_on(self.writer.write_entry_seekable(entry))?; Ok(EntryWriter::new(writer)) } } @@ -768,7 +777,17 @@ impl AsyncWrite for SyncWriter { } } -impl ZipDirectoryWriter> { +impl AsyncSeek for SyncWriter { + fn poll_seek( + self: Pin<&mut Self>, + _context: &mut Context<'_>, + position: SeekFrom, + ) -> Poll> { + Poll::Ready(self.get_mut().writer.seek(position)) + } +} + +impl ZipDirectoryWriter> { /// A wheel writer with deflate compression. fn new_wheel(writer: W) -> Self { Self { @@ -791,12 +810,12 @@ impl ZipDirectoryWriter> { } } -struct EntryWriter<'writer, W: AsyncWrite + Unpin> { - writer: Option>, +struct EntryWriter<'writer, W: AsyncWrite + AsyncSeek + Unpin> { + writer: Option>, } -impl<'writer, W: AsyncWrite + Unpin> EntryWriter<'writer, W> { - fn new(writer: EntryStreamWriter<'writer, W>) -> Self { +impl<'writer, W: AsyncWrite + AsyncSeek + Unpin> EntryWriter<'writer, W> { + fn new(writer: EntrySeekableWriter<'writer, W>) -> Self { Self { writer: Some(writer), } @@ -811,7 +830,7 @@ impl<'writer, W: AsyncWrite + Unpin> EntryWriter<'writer, W> { } } -impl Write for EntryWriter<'_, W> { +impl Write for EntryWriter<'_, W> { fn write(&mut self, buffer: &[u8]) -> io::Result { let Some(writer) = self.writer.as_mut() else { return Err(io::Error::other( @@ -829,7 +848,7 @@ impl Write for EntryWriter<'_, W> { } } -impl DirectoryWriter for ZipDirectoryWriter { +impl DirectoryWriter for ZipDirectoryWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { trace!("Adding {}", path); // Set appropriate permissions for metadata files (644 = rw-r--r--) @@ -848,20 +867,40 @@ impl DirectoryWriter for ZipDirectoryWriter { fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error> { trace!("Adding {} from {}", path, file.user_display()); - let mut reader = BufReader::new(File::open(file)?); + let metadata = file.metadata()?; // Preserve the executable bit, especially for scripts #[cfg(unix)] let executable_bit = { use std::os::unix::fs::PermissionsExt; - file.metadata()?.permissions().mode() & 0o111 != 0 + metadata.permissions().mode() & 0o111 != 0 }; // Windows has no executable bit #[cfg(not(unix))] let executable_bit = false; - let mut writer = self.new_writer(path, executable_bit)?; - let record = write_hashed(path, &mut reader, &mut writer)?; - writer.close()?; - self.record.push(record); + let mode = if executable_bit { + Self::EXECUTABLE_FILE_MODE + } else { + Self::REGULAR_FILE_MODE + }; + + if metadata.len() <= WHOLE_FILE_ZIP_ENTRY_LIMIT { + let bytes = fs_err::read(file)?; + let entry = Self::entry(path, self.compression, mode); + block_on(self.writer.write_entry_whole(entry, &bytes))?; + + let hash = base64.encode(Sha256::new().chain_update(&bytes).finalize()); + self.record.push(RecordEntry { + path: path.to_string(), + hash, + size: bytes.len() as u64, + }); + } else { + let mut reader = BufReader::new(File::open(file)?); + let mut writer = self.new_writer(path, executable_bit)?; + let record = write_hashed(path, &mut reader, &mut writer)?; + writer.close()?; + self.record.push(record); + } Ok(()) } From d2e91ac0a7053c3acdc76779380f99c7bf61f469 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 13 May 2026 15:33:11 -0400 Subject: [PATCH 11/56] Remove `wmi` dependency (#19387) ## Summary This is entirely written by Codex, but the intent is to use ~~`windows_registry`~~ the configuration manager APIs (from Win32) instead of `wmi` for Intel XPU detection. (We _only_ use `wmi` for this one use-case, which seems not-worth-it.) --------- Signed-off-by: William Woodruff Co-authored-by: William Woodruff --- Cargo.lock | 16 +-- Cargo.toml | 2 +- crates/uv-torch/Cargo.toml | 2 +- crates/uv-torch/src/accelerator.rs | 163 +++++++++++++++++++++-------- 4 files changed, 123 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9c5ad619d3e26..28fee09fa2fc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7297,7 +7297,7 @@ dependencies = [ "uv-pep440", "uv-platform-tags", "uv-static", - "wmi", + "windows", ] [[package]] @@ -8166,20 +8166,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "wmi" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" -dependencies = [ - "futures", - "log", - "serde", - "thiserror 2.0.18", - "windows", - "windows-core", -] - [[package]] name = "writeable" version = "0.6.2" diff --git a/Cargo.toml b/Cargo.toml index 0b282c5188b68..3340d9968c72b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -286,6 +286,7 @@ webpki-root-certs = { version = "1" } which = { version = "8.0.0", features = ["regex"] } windows = { version = "0.61.0", features = [ "std", + "Win32_Devices_DeviceAndDriverInstallation", "Win32_Foundation", "Win32_Globalization", "Win32_Security", @@ -305,7 +306,6 @@ windows = { version = "0.61.0", features = [ windows-registry = { version = "0.5.0" } windows-version = { version = "0.1.6" } wiremock = { version = "0.6.4" } -wmi = { version = "0.18.3", default-features = false } xz2 = { version = "0.1.7", features = ["static"] } zeroize = { version = "1.8.1" } diff --git a/crates/uv-torch/Cargo.toml b/crates/uv-torch/Cargo.toml index 199e0a1ad7827..dc81f5680b7ee 100644 --- a/crates/uv-torch/Cargo.toml +++ b/crates/uv-torch/Cargo.toml @@ -26,7 +26,7 @@ tracing = { workspace = true } url = { workspace = true } [target.'cfg(all(target_os = "windows"))'.dependencies] -wmi = { workspace = true } +windows = { workspace = true } [lints] workspace = true diff --git a/crates/uv-torch/src/accelerator.rs b/crates/uv-torch/src/accelerator.rs index 1b70b071405ad..13419255e972d 100644 --- a/crates/uv-torch/src/accelerator.rs +++ b/crates/uv-torch/src/accelerator.rs @@ -7,9 +7,19 @@ use uv_pep440::Version; use uv_static::EnvVars; #[cfg(windows)] -use serde::Deserialize; +use windows::Win32::Devices::DeviceAndDriverInstallation::{ + CM_GETIDLIST_FILTER_CLASS, CM_GETIDLIST_FILTER_PRESENT, CM_Get_Device_ID_List_SizeW, + CM_Get_Device_ID_ListW, CR_SUCCESS, +}; + +// Constants used for PCI device detection. +const PCI_BASE_CLASS_MASK: u32 = 0x00ff_0000; +const PCI_BASE_CLASS_DISPLAY: u32 = 0x0003_0000; +const PCI_VENDOR_ID_INTEL: u32 = 0x8086; +// https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors #[cfg(windows)] -use wmi::WMIConnection; +const WINDOWS_DISPLAY_ADAPTER_CLASS_GUID: windows::core::PCWSTR = + windows::core::w!("{4d36e968-e325-11ce-bfc1-08002be10318}"); #[derive(Debug, thiserror::Error)] pub enum AcceleratorError { @@ -65,13 +75,8 @@ impl Accelerator { /// 5. `nvidia-smi --query-gpu=driver_version --format=csv,noheader`. /// 6. `rocm_agent_enumerator`, which lists the AMD GPU architectures. /// 7. `/sys/bus/pci/devices`, filtering for the Intel GPU via PCI. - /// 8. Windows Managmeent Instrumentation (WMI), filtering for the Intel GPU via PCI. + /// 8. The Windows device tree, filtering for present Intel display adapters via PCI. pub fn detect() -> Result, AcceleratorError> { - // Constants used for PCI device detection. - const PCI_BASE_CLASS_MASK: u32 = 0x00ff_0000; - const PCI_BASE_CLASS_DISPLAY: u32 = 0x0003_0000; - const PCI_VENDOR_ID_INTEL: u32 = 0x8086; - // Read from `UV_CUDA_DRIVER_VERSION`. if let Ok(driver_version) = std::env::var(EnvVars::UV_CUDA_DRIVER_VERSION) { let driver_version = Version::from_str(&driver_version)?; @@ -196,42 +201,12 @@ impl Accelerator { Err(e) => return Err(e.into()), } - // Detect Intel GPU via WMI on Windows + // Detect Intel GPU via present Windows display adapters. + // TODO: Consider rejecting display adapters with disabled/problem devnode status. + // See: `CM_Locate_DevNodeW` + `CM_Get_DevNode_Status` #[cfg(windows)] - { - #[derive(Deserialize, Debug)] - #[serde(rename = "Win32_VideoController")] - #[serde(rename_all = "PascalCase")] - struct VideoController { - #[serde(rename = "PNPDeviceID")] - pnp_device_id: Option, - name: Option, - } - - match WMIConnection::new() { - Ok(wmi_connection) => match wmi_connection.query::() { - Ok(gpu_controllers) => { - for gpu_controller in gpu_controllers { - if let Some(pnp_device_id) = &gpu_controller.pnp_device_id { - if pnp_device_id.contains(&format!("VEN_{PCI_VENDOR_ID_INTEL:04X}")) - { - debug!( - "Detected Intel GPU from WMI: PNPDeviceID={}, Name={:?}", - pnp_device_id, gpu_controller.name - ); - return Ok(Some(Self::Xpu)); - } - } - } - } - Err(e) => { - debug!("Failed to query WMI for video controllers: {e}"); - } - }, - Err(e) => { - debug!("Failed to create WMI connection: {e}"); - } - } + if detect_intel_gpu_from_windows_devices() { + return Ok(Some(Self::Xpu)); } debug!("Failed to detect GPU driver version"); @@ -280,6 +255,77 @@ fn parse_pci_device_ids(device_path: &Path) -> Result<(u32, u32), AcceleratorErr Ok((pci_class, pci_vendor)) } +#[cfg(windows)] +#[allow(unsafe_code)] +fn detect_intel_gpu_from_windows_devices() -> bool { + const FLAGS: u32 = CM_GETIDLIST_FILTER_CLASS | CM_GETIDLIST_FILTER_PRESENT; + + let mut device_ids_len = 0; + // SAFETY: The class GUID is a static null-terminated UTF-16 string, `device_ids_len` is a + // valid out pointer, and Configuration Manager writes only that scalar result here. + let result = unsafe { + CM_Get_Device_ID_List_SizeW( + &raw mut device_ids_len, + WINDOWS_DISPLAY_ADAPTER_CLASS_GUID, + FLAGS, + ) + }; + if result != CR_SUCCESS { + debug!("Failed to query Windows display adapter device list length: {result:?}"); + return false; + } + + let Ok(device_ids_len) = usize::try_from(device_ids_len) else { + debug!("Windows display adapter device list length does not fit in memory"); + return false; + }; + let mut encoded_device_ids = vec![0; device_ids_len]; + + // SAFETY: The class GUID is a static null-terminated UTF-16 string, and the writable buffer + // length matches the size returned by `CM_Get_Device_ID_List_SizeW` for the same filter. + let result = unsafe { + CM_Get_Device_ID_ListW( + WINDOWS_DISPLAY_ADAPTER_CLASS_GUID, + &mut encoded_device_ids, + FLAGS, + ) + }; + if result != CR_SUCCESS { + debug!("Failed to query present Windows display adapters: {result:?}"); + return false; + } + + for device_id in windows_device_ids(&encoded_device_ids) { + if contains_intel_vendor_id(&device_id) { + debug!("Detected Intel GPU from present Windows display adapter: {device_id}"); + return true; + } + } + + false +} + +#[cfg(any(windows, test))] +fn contains_intel_vendor_id(pnp_device_id: &str) -> bool { + pnp_device_id + .split(['\\', '&']) + .any(|segment| segment.eq_ignore_ascii_case("VEN_8086")) +} + +#[cfg(any(windows, test))] +fn windows_device_ids(encoded_device_ids: &[u16]) -> impl Iterator + '_ { + encoded_device_ids + .split(|code_unit| *code_unit == 0) + .filter(|device_id| !device_id.is_empty()) + .filter_map(|device_id| match String::from_utf16(device_id) { + Ok(device_id) => Some(device_id), + Err(err) => { + debug!("Failed to decode Windows device instance ID: {err}"); + None + } + }) +} + /// A GPU architecture for AMD GPUs. /// /// See: @@ -376,4 +422,35 @@ mod tests { assert_eq!(version, Version::from_str("572.60").unwrap()); } } + + #[test] + fn intel_vendor_id_from_pnp_device_id() { + assert!(contains_intel_vendor_id( + r"PCI\VEN_8086&DEV_9A49&SUBSYS_00000000" + )); + assert!(contains_intel_vendor_id( + r"pci\ven_8086&dev_9a49&subsys_00000000" + )); + assert!(!contains_intel_vendor_id( + r"PCI\VEN_10DE&DEV_2504&SUBSYS_00000000" + )); + assert!(!contains_intel_vendor_id(r"PCI\DEV_8086&SUBSYS_00000000")); + } + + #[test] + fn windows_device_instance_ids() { + let intel_gpu = r"PCI\VEN_8086&DEV_9A49&SUBSYS_00000000\3&11583659&0&10"; + let nvidia_gpu = r"PCI\VEN_10DE&DEV_2504&SUBSYS_00000000\4&12AB34CD&0&0008"; + + let mut encoded_device_ids = Vec::new(); + encoded_device_ids.extend(intel_gpu.encode_utf16()); + encoded_device_ids.push(0); + encoded_device_ids.extend(nvidia_gpu.encode_utf16()); + encoded_device_ids.extend([0, 0]); + + assert_eq!( + windows_device_ids(&encoded_device_ids).collect::>(), + vec![intel_gpu.to_string(), nvidia_gpu.to_string()] + ); + } } From 5aba1bde10b8a397ea4faff5f83a9504345f4dac Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 14 May 2026 16:22:38 +0100 Subject: [PATCH 12/56] Avoid parsing JSON manifest when local Python is available (#19398) ## Summary Right now, it appears that we parse the JSON manifest for ~every lock and sync (I _think_ this regressed in b9826778b). By avoiding that work, this PR improves (warm) `uv lock` from 38.94ms to 24.06ms (38.2% faster!), and (warm) `uv sync` from 146.96ms to 129.80ms. Note that we _do_ need to read it if you're using a pre-release, since we have this dedicated pre-release warning. Closes https://github.com/astral-sh/uv/issues/19397. --- .github/workflows/build-dev-binaries.yml | 2 + crates/uv-python/src/discovery.rs | 27 ++++- crates/uv-python/src/installation.rs | 136 ++++++++++++++++------- crates/uv-python/src/lib.rs | 66 ++++++++--- crates/uv/src/commands/python/find.rs | 17 +-- crates/uv/src/commands/python/pin.rs | 30 +++-- crates/uv/tests/it/python_find.rs | 18 +++ crates/uv/tests/it/python_pin.rs | 44 ++++++++ 8 files changed, 265 insertions(+), 75 deletions(-) diff --git a/.github/workflows/build-dev-binaries.yml b/.github/workflows/build-dev-binaries.yml index b2519636c038a..f76aaec9d06de 100644 --- a/.github/workflows/build-dev-binaries.yml +++ b/.github/workflows/build-dev-binaries.yml @@ -152,6 +152,7 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ inputs.save-rust-cache == 'true' }} + cache-bin: false - name: "Build" run: cargo build --profile no-debug --bin uv --bin uvx @@ -176,6 +177,7 @@ jobs: - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: save-if: ${{ inputs.save-rust-cache == 'true' }} + cache-bin: false - name: "Build" run: cargo build --profile no-debug --bin uv --bin uvx diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index cfc894b8cf682..9663944c3e2c4 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1,7 +1,6 @@ use itertools::{Either, Itertools}; use owo_colors::AnsiColors; use regex::Regex; -use reqwest_retry::policies::ExponentialBackoff; use rustc_hash::{FxBuildHasher, FxHashSet}; use same_file::is_same_file; use std::borrow::Cow; @@ -12,7 +11,7 @@ use std::{path::Path, path::PathBuf, str::FromStr}; use thiserror::Error; use tracing::{debug, instrument, trace}; use uv_cache::Cache; -use uv_client::BaseClient; +use uv_client::BaseClientBuilder; use uv_distribution_types::RequiresPython; use uv_fs::Simplified; use uv_fs::which::is_executable; @@ -1391,19 +1390,19 @@ pub(crate) async fn find_best_python_installation( environments: EnvironmentPreference, preference: PythonPreference, downloads_enabled: bool, - download_list: &ManagedPythonDownloadList, - client: &BaseClient, - retry_policy: &ExponentialBackoff, + client_builder: &BaseClientBuilder<'_>, cache: &Cache, reporter: Option<&dyn crate::downloads::Reporter>, python_install_mirror: Option<&str>, pypy_install_mirror: Option<&str>, + python_downloads_json_url: Option<&str>, preview: Preview, ) -> Result { debug!("Starting Python discovery for {request}"); let original_request = request; let mut previous_fetch_failed = false; + let mut download_state = None; let request_without_patch = match request { PythonRequest::Version(version) => { @@ -1449,6 +1448,24 @@ pub(crate) async fn find_best_python_installation( && !previous_fetch_failed && let Some(download_request) = PythonDownloadRequest::from_request(request) { + let (client, retry_policy, download_list) = + if let Some(download_state) = &mut download_state { + download_state + } else { + let download_list_client = client_builder.build()?; + let download_list = ManagedPythonDownloadList::new( + &download_list_client, + python_downloads_json_url, + ) + .await?; + let retry_policy = client_builder.retry_policy(); + + // Python downloads are performing their own retries to catch stream errors, disable + // the default retries to avoid the middleware performing uncontrolled retries. + let client = client_builder.clone().retries(0).build()?; + download_state.insert((client, retry_policy, download_list)) + }; + let download = download_request .clone() .fill() diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index dcfd97915e0b4..93da9fe806b39 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -66,12 +66,28 @@ impl PythonInstallation { cache: &Cache, preview: Preview, ) -> Result { - let installation = - find_python_installation(request, environments, preference, cache, preview)??; + let installation = Self::find_existing(request, environments, preference, cache, preview)?; installation.warn_if_outdated_prerelease(request, download_list); Ok(installation) } + /// Find an existing [`PythonInstallation`]. + pub fn find_existing( + request: &PythonRequest, + environments: EnvironmentPreference, + preference: PythonPreference, + cache: &Cache, + preview: Preview, + ) -> Result { + Ok(find_python_installation( + request, + environments, + preference, + cache, + preview, + )??) + } + /// Find or download a [`PythonInstallation`] that satisfies a requested version, if the request /// cannot be satisfied, fallback to the best available Python installation. pub async fn find_best( @@ -87,10 +103,6 @@ impl PythonInstallation { python_downloads_json_url: Option<&str>, preview: Preview, ) -> Result { - let retry_policy = client_builder.retry_policy(); - let client = client_builder.clone().retries(0).build()?; - let download_list = - ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; let downloads_enabled = preference.allows_managed() && python_downloads.is_automatic() && client_builder.connectivity.is_online(); @@ -99,17 +111,22 @@ impl PythonInstallation { environments, preference, downloads_enabled, - &download_list, - &client, - &retry_policy, + client_builder, cache, reporter, python_install_mirror, pypy_install_mirror, + python_downloads_json_url, preview, ) .await?; - installation.warn_if_outdated_prerelease(request, &download_list); + installation + .download_and_warn_if_outdated_prerelease( + request, + client_builder, + python_downloads_json_url, + ) + .await?; Ok(installation) } @@ -131,23 +148,17 @@ impl PythonInstallation { ) -> Result { let request = request.unwrap_or(&PythonRequest::Default); - // Python downloads are performing their own retries to catch stream errors, disable the - // default retries to avoid the middleware performing uncontrolled retries. - let retry_policy = client_builder.retry_policy(); - let client = client_builder.clone().retries(0).build()?; - let download_list = - ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; - - // Search for the installation - let err = match Self::find( - request, - environments, - preference, - &download_list, - cache, - preview, - ) { - Ok(installation) => return Ok(installation), + let err = match Self::find_existing(request, environments, preference, cache, preview) { + Ok(installation) => { + installation + .download_and_warn_if_outdated_prerelease( + request, + client_builder, + python_downloads_json_url, + ) + .await?; + return Ok(installation); + } Err(err) => err, }; @@ -165,6 +176,11 @@ impl PythonInstallation { return Err(err); }; + let download_list_client = client_builder.build()?; + let download_list = + ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url) + .await?; + let downloads_enabled = preference.allows_managed() && python_downloads.is_automatic() && client_builder.connectivity.is_online(); @@ -251,9 +267,14 @@ impl PythonInstallation { return Err(err); } + // Python downloads are performing their own retries to catch stream errors, disable the + // default retries to avoid the middleware performing uncontrolled retries. + let retry_policy = client_builder.retry_policy(); + let download_client = client_builder.clone().retries(0).build()?; + let installation = Self::fetch( download, - &client, + &download_client, &retry_policy, cache, reporter, @@ -400,26 +421,20 @@ impl PythonInstallation { self.interpreter } - /// Emit a warning when the interpreter is a managed prerelease and a matching stable - /// build can be installed via `uv python upgrade`. - pub(crate) fn warn_if_outdated_prerelease( - &self, - request: &PythonRequest, - download_list: &ManagedPythonDownloadList, - ) { + /// Return `true` when checking for an outdated managed prerelease warning may be necessary. + fn should_check_outdated_prerelease_warning(&self, request: &PythonRequest) -> bool { if request.allows_prereleases() { - return; + return false; } let interpreter = self.interpreter(); - let version = interpreter.python_version(); - if version.pre().is_none() { - return; + if interpreter.python_version().pre().is_none() { + return false; } if !interpreter.is_managed() { - return; + return false; } // Transparent upgrades only exist for CPython, so skip the warning for other @@ -430,9 +445,26 @@ impl PythonInstallation { .implementation_name() .eq_ignore_ascii_case("cpython") { + return false; + } + + true + } + + /// Emit a warning when the interpreter is a managed prerelease and a matching stable + /// build can be installed via `uv python upgrade`. + pub(crate) fn warn_if_outdated_prerelease( + &self, + request: &PythonRequest, + download_list: &ManagedPythonDownloadList, + ) { + if !self.should_check_outdated_prerelease_warning(request) { return; } + let interpreter = self.interpreter(); + let version = interpreter.python_version(); + let release = version.only_release(); let Ok(download_request) = PythonDownloadRequest::try_from(&interpreter.key()) else { @@ -471,6 +503,30 @@ impl PythonInstallation { ); } } + + /// Emit a warning when the interpreter is a managed prerelease and a matching stable + /// build can be installed via `uv python upgrade`. + /// + /// Avoids loading the Python download list unless the discovered interpreter could require + /// the warning. + pub async fn download_and_warn_if_outdated_prerelease( + &self, + request: &PythonRequest, + client_builder: &BaseClientBuilder<'_>, + python_downloads_json_url: Option<&str>, + ) -> Result<(), Error> { + if !self.should_check_outdated_prerelease_warning(request) { + return Ok(()); + } + + let download_list_client = client_builder.build()?; + let download_list = + ManagedPythonDownloadList::new(&download_list_client, python_downloads_json_url) + .await?; + self.warn_if_outdated_prerelease(request, &download_list); + + Ok(()) + } } #[derive(Error, Debug)] diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 327630987ee24..8525605fd2b44 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -145,10 +145,9 @@ mod tests { use uv_cache::Cache; use crate::{ - PythonNotFound, PythonRequest, PythonSource, PythonVersion, - downloads::ManagedPythonDownloadList, implementation::ImplementationName, - installation::PythonInstallation, managed::ManagedPythonInstallations, - virtualenv::virtualenv_python_executable, + PythonDownloads, PythonNotFound, PythonRequest, PythonSource, PythonVersion, + implementation::ImplementationName, installation::PythonInstallation, + managed::ManagedPythonInstallations, virtualenv::virtualenv_python_executable, }; use crate::{ PythonPreference, @@ -652,6 +651,54 @@ mod tests { Ok(()) } + #[test] + fn find_or_download_skips_download_metadata_when_python_is_found() -> Result<()> { + let mut context = TestContext::new()?; + context.add_python_versions(&["3.12.1"])?; + // Pass a missing metadata file to assert that an already-installed Python can + // be returned without reading the download list. + let missing_downloads = context.tempdir.child("missing-downloads.json"); + + let interpreter = context.run(|| { + let client_builder = BaseClientBuilder::default(); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("Failed to build runtime") + .block_on(PythonInstallation::find_or_download( + None, + EnvironmentPreference::OnlySystem, + PythonPreference::OnlySystem, + PythonDownloads::Never, + &client_builder, + &context.cache, + None, + None, + None, + missing_downloads.path().to_str(), + Preview::default(), + )) + })?; + + assert!( + matches!( + interpreter, + PythonInstallation { + source: PythonSource::SearchPathFirst, + interpreter: _ + } + ), + "We should find the local Python without reading download metadata; got {interpreter:?}" + ); + assert_eq!( + &interpreter.interpreter().python_full_version().to_string(), + "3.12.1", + "We should find the local interpreter" + ); + + Ok(()) + } + #[test] fn find_python_valid_executable_after_invalid() -> Result<()> { let mut context = TestContext::new()?; @@ -1004,12 +1051,6 @@ mod tests { preview: Preview, ) -> Result { let client_builder = BaseClientBuilder::default(); - let download_list = ManagedPythonDownloadList::new_only_embedded()?; - let client = client_builder - .clone() - .retries(0) - .build() - .expect("failed to build base client"); tokio::runtime::Builder::new_current_thread() .enable_all() .build() @@ -1019,13 +1060,12 @@ mod tests { environments, preference, false, - &download_list, - &client, - &client_builder.retry_policy(), + &client_builder, cache, None, None, None, + None, preview, )) } diff --git a/crates/uv/src/commands/python/find.rs b/crates/uv/src/commands/python/find.rs index d093f4921bfe2..2939e4026ceed 100644 --- a/crates/uv/src/commands/python/find.rs +++ b/crates/uv/src/commands/python/find.rs @@ -7,7 +7,6 @@ use uv_client::BaseClientBuilder; use uv_configuration::DependencyGroupsWithDefaults; use uv_fs::Simplified; use uv_preview::Preview; -use uv_python::downloads::ManagedPythonDownloadList; use uv_python::{ EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest, }; @@ -78,17 +77,21 @@ pub(crate) async fn find( ) .await?; - let client = client_builder.clone().retries(0).build()?; - let download_list = ManagedPythonDownloadList::new(&client, python_downloads_json_url).await?; - - let python = PythonInstallation::find( - &python_request.unwrap_or_default(), + let python_request = python_request.unwrap_or_default(); + let python = PythonInstallation::find_existing( + &python_request, environment_preference, python_preference, - &download_list, cache, preview, )?; + python + .download_and_warn_if_outdated_prerelease( + &python_request, + client_builder, + python_downloads_json_url, + ) + .await?; // Warn if the discovered Python version is incompatible with the current workspace if let Some(requires_python) = requires_python { diff --git a/crates/uv/src/commands/python/pin.rs b/crates/uv/src/commands/python/pin.rs index f592e24db9831..5c6325d979433 100644 --- a/crates/uv/src/commands/python/pin.rs +++ b/crates/uv/src/commands/python/pin.rs @@ -93,20 +93,30 @@ pub(crate) async fn pin( let Some(request) = request else { // Display the current pinned Python version if let Some(file) = version_file? { - for pin in file.versions() { - writeln!(printer.stdout(), "{}", pin.to_canonical_string())?; - if let Some(virtual_project) = &virtual_project { - let client = client_builder.clone().retries(0).build()?; - let download_list = ManagedPythonDownloadList::new( - &client, + let mut pins = file.versions().peekable(); + let download_list = if virtual_project.is_some() && pins.peek().is_some() { + let download_list_client = client_builder.build()?; + Some( + ManagedPythonDownloadList::new( + &download_list_client, install_mirrors.python_downloads_json_url.as_deref(), ) - .await?; + .await?, + ) + } else { + None + }; + + for pin in pins { + writeln!(printer.stdout(), "{}", pin.to_canonical_string())?; + if let Some(virtual_project) = &virtual_project + && let Some(download_list) = &download_list + { warn_if_existing_pin_incompatible_with_project( pin, virtual_project, python_preference, - &download_list, + download_list, cache, preview, ); @@ -268,8 +278,8 @@ fn warn_if_existing_pin_incompatible_with_project( } } - // If there is not a version in the pinned request, attempt to resolve the pin into an - // interpreter to check for compatibility on the current system. + // If the request itself didn't prove an incompatibility, resolve the pin into an + // interpreter to check the concrete version on the current system. match PythonInstallation::find( pin, EnvironmentPreference::OnlySystem, diff --git a/crates/uv/tests/it/python_find.rs b/crates/uv/tests/it/python_find.rs index bf997f6dda8cf..cafdafcb96b2b 100644 --- a/crates/uv/tests/it/python_find.rs +++ b/crates/uv/tests/it/python_find.rs @@ -150,6 +150,24 @@ fn python_find() { "); } +#[test] +fn python_find_skips_download_metadata_when_python_is_found() { + let context = uv_test::test_context_with_versions!(&["3.12"]); + let missing_downloads = context.temp_dir.child("missing-downloads.json"); + + uv_snapshot!(context.filters(), context + .python_find() + .arg("--python-downloads-json-url") + .arg(missing_downloads.path()), @" + success: true + exit_code: 0 + ----- stdout ----- + [PYTHON-3.12] + + ----- stderr ----- + "); +} + #[test] fn python_find_pin() { let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]); diff --git a/crates/uv/tests/it/python_pin.rs b/crates/uv/tests/it/python_pin.rs index 81a3dee3b69c9..eaac508dbc8b2 100644 --- a/crates/uv/tests/it/python_pin.rs +++ b/crates/uv/tests/it/python_pin.rs @@ -8,6 +8,7 @@ use uv_platform::{Arch, Os}; use uv_python::{PYTHON_VERSION_FILENAME, PYTHON_VERSIONS_FILENAME}; use uv_static::EnvVars; use uv_test::uv_snapshot; +use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; #[test] fn python_pin() { @@ -203,6 +204,49 @@ fn python_pin_uses_python_downloads_json_url() { "); } +#[tokio::test] +async fn python_pin_downloads_metadata_once_for_multiple_pins() -> Result<()> { + let context = uv_test::test_context_with_versions!(&["3.11", "3.12"]); + + context.temp_dir.child("pyproject.toml").write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.11" + dependencies = [] + "#, + )?; + + context + .temp_dir + .child(PYTHON_VERSION_FILENAME) + .write_str("3.11\n3.12\n")?; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with(ResponseTemplate::new(200).set_body_raw("{}", "application/json")) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .python_pin() + .arg("--python-downloads-json-url") + .arg(server.uri()), @" + success: true + exit_code: 0 + ----- stdout ----- + 3.11 + 3.12 + + ----- stderr ----- + "); + + assert_eq!(server.received_requests().await.unwrap().len(), 1); + + Ok(()) +} + // If there is no project-level `.python-version` file, respect the global pin. #[test] fn python_pin_global_if_no_local() -> Result<()> { From e187049247c13c04e5f53b06bf2d405a5acbd78e Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Thu, 14 May 2026 19:06:50 +0100 Subject: [PATCH 13/56] Verify created junctions (#19402) ## Summary Verify Windows junction creation after `junction::create`. If junction creation leaves behind a broken reparse point or empty directory, clean it up and return the original creation error instead of leaving a broken link behind. ## Test Plan Tested manually on windows, requires setting up a network or WINFSP drive to really test. Planning on actually making that happen in CI. --- crates/uv-fs/src/lib.rs | 61 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 71e34c3790978..261b949231f32 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -78,6 +78,63 @@ pub async fn read_to_string_transcode(path: impl AsRef) -> std::io::Result Ok(buf) } +/// Create a junction at `path` pointing to `target`. +/// +/// Junctions can be silently broken when involving network paths or non-NTFS filesystems. +/// +/// If creation fails but leaves behind an empty directory, it is cleaned up and the original +/// creation error is propagated. +#[cfg(windows)] +fn create_junction(target: &Path, path: &Path) -> std::io::Result<()> { + use windows::Win32::Foundation::{ + ERROR_ALREADY_EXISTS, ERROR_INVALID_NAME, ERROR_INVALID_PARAMETER, + ERROR_INVALID_REPARSE_DATA, ERROR_NOT_A_REPARSE_POINT, WIN32_ERROR, + }; + + let create_result = junction::create(target, path); + + match path.metadata() { + Ok(_) if create_result.is_ok() => Ok(()), + Ok(_) => { + // Creation failed but left behind an empty directory. Only clean + // it up if the directory wasn't already there before we tried. + if let Err(ref create_err) = create_result { + if !matches!( + create_err + .raw_os_error() + .map(|err| WIN32_ERROR(err.cast_unsigned())), + Some(ERROR_ALREADY_EXISTS) + ) { + // Not a junction (metadata succeeded normally), just + // an empty directory left behind by junction::create. + let _ = fs_err::remove_dir(path); + } + } + create_result + } + Err(err) + if matches!( + err.raw_os_error() + .map(|err| WIN32_ERROR(err.cast_unsigned())), + Some( + ERROR_INVALID_PARAMETER + | ERROR_INVALID_NAME + | ERROR_NOT_A_REPARSE_POINT + | ERROR_INVALID_REPARSE_DATA + ) + ) => + { + // Broken reparse point. Once junction::delete strips the reparse data, only an empty + // directory shell remains. + if junction::delete(path).is_ok() { + let _ = fs_err::remove_dir(path); + } + Err(create_result.err().unwrap_or(err)) + } + Err(err) => Err(create_result.err().unwrap_or(err)), + } +} + /// Create a directory link at `dst` pointing to `src`, replacing any existing link. /// /// On Windows, this normally creates an NTFS junction, since junctions don't @@ -128,7 +185,7 @@ fn replace_with_junction(src: &Path, dst: &Path) -> std::io::Result<()> { } // Replace it with a new junction. - junction::create(dunce::simplified(src), dunce::simplified(dst)) + create_junction(src, dst) } #[cfg(windows)] @@ -218,7 +275,7 @@ pub fn create_symlink(src: impl AsRef, dst: impl AsRef) -> std::io:: if uv_windows::is_wine() { fs_err::os::windows::fs::symlink_dir(dunce::simplified(src), dunce::simplified(dst)) } else { - junction::create(dunce::simplified(src), dunce::simplified(dst)) + create_junction(src, dst) } } From 79b5b6a9312895262a1db6c8082fa46b050df0bd Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 14 May 2026 11:10:48 -0700 Subject: [PATCH 14/56] uv audit: JSON output (#19305) ## Summary This adds `--output-format json` to `uv audit`. Like other subcommands that support JSON, we're keeping this in preview (so it's in double preview with `uv audit` itself). I'm not sure about the JSON layout itself yet. See #18506. ## Test Plan Added some integration coverage. --------- Signed-off-by: William Woodruff --- crates/uv-audit/src/types.rs | 10 + crates/uv-cli/src/lib.rs | 13 ++ crates/uv/src/commands/project/audit.rs | 288 +++++++++++++++++++++--- crates/uv/src/lib.rs | 1 + crates/uv/src/settings.rs | 17 +- crates/uv/tests/it/audit.rs | 237 +++++++++++++++++++ 6 files changed, 522 insertions(+), 44 deletions(-) diff --git a/crates/uv-audit/src/types.rs b/crates/uv-audit/src/types.rs index 69cc31a472b39..6fd9ffc48f34f 100644 --- a/crates/uv-audit/src/types.rs +++ b/crates/uv-audit/src/types.rs @@ -68,6 +68,16 @@ pub enum AdverseStatus { Deprecated, } +impl std::fmt::Display for AdverseStatus { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Archived => "archived", + Self::Quarantined => "quarantined", + Self::Deprecated => "deprecated", + }) + } +} + /// A vulnerability within a dependency. #[derive(Debug)] pub struct Vulnerability { diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 3f5b8be2ed602..5c5e72750a2b2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -68,6 +68,15 @@ pub enum SyncFormat { Json, } +#[derive(Debug, Default, Clone, Copy, clap::ValueEnum)] +pub enum AuditOutputFormat { + /// Display the result in a human-readable format. + #[default] + Text, + /// Display the result in JSON format. + Json, +} + #[derive(Debug, Default, Clone, clap::ValueEnum)] pub enum ListFormat { /// Display the list of packages in a human-readable table. @@ -5219,6 +5228,10 @@ pub struct AuditArgs { #[arg(long, conflicts_with_all = ["locked", "upgrade", "no_sources"])] pub frozen: bool, + /// Select the output format. + #[arg(long, value_enum, default_value_t = AuditOutputFormat::default())] + pub output_format: AuditOutputFormat, + #[command(flatten)] pub build: BuildOptionsArgs, diff --git a/crates/uv/src/commands/project/audit.rs b/crates/uv/src/commands/project/audit.rs index c06b7a1ad981f..93063ddb9aeda 100644 --- a/crates/uv/src/commands/project/audit.rs +++ b/crates/uv/src/commands/project/audit.rs @@ -1,5 +1,6 @@ use itertools::Itertools as _; use owo_colors::OwoColorize; +use serde::Serialize; use std::fmt::Write as _; use std::path::Path; @@ -22,8 +23,11 @@ use rustc_hash::FxHashSet; use tracing::trace; use uv_audit::service::project_status::ProjectStatusAudit; use uv_audit::service::{VulnerabilityServiceFormat, osv}; -use uv_audit::types::{AdverseStatus, Dependency, Finding, VulnerabilityID}; +use uv_audit::types::{ + AdverseStatus, Dependency, Finding, ProjectStatus, Vulnerability, VulnerabilityID, +}; use uv_cache::Cache; +use uv_cli::AuditOutputFormat; use uv_client::{BaseClientBuilder, RegistryClientBuilder}; use uv_configuration::{Concurrency, DependencyGroups, ExtrasSpecification, TargetTriple}; use uv_distribution_types::{IndexCapabilities, IndexUrl}; @@ -54,6 +58,7 @@ pub(crate) async fn audit( cache: Cache, printer: Printer, preview: Preview, + output_format: AuditOutputFormat, service: VulnerabilityServiceFormat, service_url: Option, ignore: Vec, @@ -66,6 +71,14 @@ pub(crate) async fn audit( PreviewFeature::Audit ); } + if matches!(output_format, AuditOutputFormat::Json) + && !preview.is_enabled(PreviewFeature::JsonOutput) + { + warn_user!( + "The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeature::JsonOutput + ); + } let workspace_cache = WorkspaceCache::default(); let workspace; @@ -297,6 +310,7 @@ pub(crate) async fn audit( let display = AuditResults { printer, n_packages: auditable.len(), + output_format, findings: all_findings, }; display.render() @@ -305,20 +319,52 @@ pub(crate) async fn audit( struct AuditResults { printer: Printer, n_packages: usize, + output_format: AuditOutputFormat, findings: Vec, } impl AuditResults { fn render(&self) -> Result { - let (vulns, statuses): (Vec<_>, Vec<_>) = - self.findings.iter().partition_map(|finding| match finding { - Finding::Vulnerability(vuln) => itertools::Either::Left(vuln), - Finding::ProjectStatus(status) => itertools::Either::Right(status), - }); + match self.output_format { + AuditOutputFormat::Text => self.render_text(), + AuditOutputFormat::Json => self.render_json(), + } + } - let vuln_banner = if !vulns.is_empty() { - let s = if vulns.len() == 1 { "y" } else { "ies" }; - format!("{} known vulnerabilit{s}", vulns.len()) + fn split_findings(&self) -> (Vec<&Vulnerability>, Vec<&ProjectStatus>) { + self.findings.iter().partition_map(|finding| match finding { + Finding::Vulnerability(vulnerability) => { + itertools::Either::Left(vulnerability.as_ref()) + } + Finding::ProjectStatus(status) => itertools::Either::Right(status), + }) + } + + fn exit_status(&self) -> ExitStatus { + // NOTE: intentional: we don't currently fail if there are any adverse statuses, + // only when there are vulnerabilities. We will likely change this once we allow users + // to ignore adverse statuses and configure policies. + if self + .findings + .iter() + .any(|finding| matches!(finding, Finding::Vulnerability(_))) + { + ExitStatus::Failure + } else { + ExitStatus::Success + } + } + + fn render_text(&self) -> Result { + let (vulnerabilities, statuses) = self.split_findings(); + + let vulnerability_banner = if !vulnerabilities.is_empty() { + let suffix = if vulnerabilities.len() == 1 { + "y" + } else { + "ies" + }; + format!("{} known vulnerabilit{suffix}", vulnerabilities.len()) .yellow() .to_string() } else { @@ -338,7 +384,7 @@ impl AuditResults { writeln!( self.printer.stderr(), - "Found {vuln_banner} and {status_banner} in {packages}", + "Found {vulnerability_banner} and {status_banner} in {packages}", packages = format!( "{npackages} {label}", npackages = self.n_packages, @@ -351,37 +397,45 @@ impl AuditResults { .bold() )?; - let has_vulnerabilities = !vulns.is_empty(); - - if !vulns.is_empty() { + if !vulnerabilities.is_empty() { writeln!(self.printer.stdout_important(), "\nVulnerabilities:\n")?; // Group vulnerabilities by (dependency name, version). - let groups = vulns - .into_iter() - .chunk_by(|vuln| (vuln.dependency.name(), vuln.dependency.version())); + let groups = vulnerabilities.into_iter().chunk_by(|vulnerability| { + ( + vulnerability.dependency.name(), + vulnerability.dependency.version(), + ) + }); - for (dependency, vulns) in &groups { - let vulns: Vec<_> = vulns.collect(); + for (dependency, vulnerabilities) in &groups { + let vulnerabilities: Vec<_> = vulnerabilities.collect(); let (name, version) = dependency; writeln!( self.printer.stdout_important(), "{name_version} has {n} known vulnerabilit{ies}:\n", name_version = format!("{name} {version}").bold(), - n = vulns.len(), - ies = if vulns.len() == 1 { "y" } else { "ies" }, + n = vulnerabilities.len(), + ies = if vulnerabilities.len() == 1 { + "y" + } else { + "ies" + }, )?; - for vuln in vulns { + for vulnerability in vulnerabilities { writeln!( self.printer.stdout_important(), "- {id}: {description}", - id = vuln.best_id().as_str().bold(), - description = vuln.summary.as_deref().unwrap_or("No summary provided"), + id = vulnerability.best_id().as_str().bold(), + description = vulnerability + .summary + .as_deref() + .unwrap_or("No summary provided"), )?; - if vuln.fix_versions.is_empty() { + if vulnerability.fix_versions.is_empty() { writeln!( self.printer.stdout_important(), "\n No fix versions available\n" @@ -390,7 +444,8 @@ impl AuditResults { writeln!( self.printer.stdout_important(), "\n Fixed in: {}\n", - vuln.fix_versions + vulnerability + .fix_versions .iter() .map(std::string::ToString::to_string) .join(", ") @@ -398,7 +453,7 @@ impl AuditResults { )?; } - if let Some(link) = &vuln.link { + if let Some(link) = &vulnerability.link { writeln!( self.printer.stdout_important(), " Advisory information: {link}\n", @@ -413,10 +468,11 @@ impl AuditResults { writeln!(self.printer.stdout_important(), "\nAdverse statuses:\n")?; for status in statuses { - let label = match status.status { - AdverseStatus::Archived => "archived".yellow().to_string(), - AdverseStatus::Deprecated => "deprecated".yellow().to_string(), - AdverseStatus::Quarantined => "quarantined".red().to_string(), + let label = match &status.status { + AdverseStatus::Archived | AdverseStatus::Deprecated => { + status.status.to_string().yellow().to_string() + } + AdverseStatus::Quarantined => status.status.to_string().red().to_string(), }; let name = status.name.bold(); if let Some(reason) = &status.reason { @@ -430,13 +486,171 @@ impl AuditResults { } } - // NOTE: intentional: we don't currently fail if there are any adverse statuses, - // only when there are vulnerabilities. We will likely change this once we allow users - // to ignore adverse statuses and configure policies. - if has_vulnerabilities { - Ok(ExitStatus::Failure) - } else { - Ok(ExitStatus::Success) + Ok(self.exit_status()) + } + + fn render_json(&self) -> Result { + let (vulnerabilities, statuses) = self.split_findings(); + let report = JsonReport::from_findings(self.n_packages, &vulnerabilities, &statuses); + + writeln!( + self.printer.stdout_important(), + "{}", + serde_json::to_string_pretty(&report)? + )?; + + Ok(self.exit_status()) + } +} + +#[derive(Debug, Serialize)] +struct JsonReport { + schema: JsonSchema, + summary: JsonSummary, + vulnerabilities: Vec, + adverse_statuses: Vec, +} + +impl JsonReport { + fn from_findings( + n_packages: usize, + vulnerabilities: &[&Vulnerability], + statuses: &[&ProjectStatus], + ) -> Self { + let mut vulnerabilities = vulnerabilities + .iter() + .copied() + .map(JsonVulnerability::from) + .collect::>(); + vulnerabilities.sort_by(|first, second| { + first + .dependency + .name + .cmp(&second.dependency.name) + .then_with(|| first.dependency.version.cmp(&second.dependency.version)) + .then_with(|| first.display_id.cmp(&second.display_id)) + }); + + let mut adverse_statuses = statuses + .iter() + .copied() + .map(JsonAdverseStatus::from) + .collect::>(); + adverse_statuses.sort_by(|first, second| { + first + .name + .cmp(&second.name) + .then_with(|| first.status.cmp(&second.status)) + }); + + Self { + schema: JsonSchema::default(), + summary: JsonSummary { + audited_packages: n_packages, + vulnerabilities: vulnerabilities.len(), + adverse_statuses: adverse_statuses.len(), + }, + vulnerabilities, + adverse_statuses, + } + } +} + +#[derive(Debug, Serialize, Default)] +struct JsonSchema { + version: JsonSchemaVersion, +} + +#[derive(Debug, Serialize, Default)] +#[serde(rename_all = "snake_case")] +enum JsonSchemaVersion { + #[default] + Preview, +} + +#[derive(Debug, Serialize)] +struct JsonSummary { + audited_packages: usize, + vulnerabilities: usize, + adverse_statuses: usize, +} + +#[derive(Debug, Serialize)] +struct JsonDependency { + name: String, + version: String, +} + +impl From<&Dependency> for JsonDependency { + fn from(dependency: &Dependency) -> Self { + Self { + name: dependency.name().to_string(), + version: dependency.version().to_string(), + } + } +} + +#[derive(Debug, Serialize)] +struct JsonVulnerability { + dependency: JsonDependency, + id: String, + display_id: String, + aliases: Vec, + summary: Option, + description: Option, + link: Option, + fix_versions: Vec, + published: Option, + modified: Option, +} + +impl From<&Vulnerability> for JsonVulnerability { + fn from(vulnerability: &Vulnerability) -> Self { + Self { + dependency: JsonDependency::from(&vulnerability.dependency), + id: vulnerability.id.as_str().to_string(), + display_id: vulnerability.best_id().as_str().to_string(), + aliases: vulnerability + .aliases + .iter() + .map(|id| id.as_str().to_string()) + .collect(), + summary: vulnerability.summary.clone(), + description: vulnerability.description.clone(), + link: vulnerability + .link + .as_ref() + .map(|link| link.as_str().to_string()), + fix_versions: vulnerability + .fix_versions + .iter() + .map(std::string::ToString::to_string) + .collect(), + published: vulnerability + .published + .as_ref() + .map(std::string::ToString::to_string), + modified: vulnerability + .modified + .as_ref() + .map(std::string::ToString::to_string), + } + } +} + +#[derive(Debug, Serialize)] +struct JsonAdverseStatus { + name: String, + status: String, + reason: Option, +} + +impl From<&ProjectStatus> for JsonAdverseStatus { + fn from(status: &ProjectStatus) -> Self { + Self { + name: status.name.to_string(), + status: status.status.to_string(), + reason: status.reason.clone(), } } } diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 572c91127e448..fc3dedaefcd8a 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -2697,6 +2697,7 @@ async fn run_project( cache, printer, globals.preview, + args.output_format, args.service_format, args.service_url, args.ignore, diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index 52413422180e4..8aa81077e3579 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -14,13 +14,13 @@ use uv_auth::Service; use uv_cache::{CacheArgs, Refresh}; use uv_cli::comma::CommaSeparatedRequirements; use uv_cli::{ - AddArgs, AuditArgs, AuthLoginArgs, AuthLogoutArgs, AuthTokenArgs, ColorChoice, ExternalCommand, - GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, MetadataArgs, PipCheckArgs, PipCompileArgs, - PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs, - PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, PythonListFormat, - PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, SyncArgs, - SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, ToolUninstallArgs, - TreeArgs, VenvArgs, VersionArgs, VersionBumpSpec, VersionFormat, + AddArgs, AuditArgs, AuditOutputFormat, AuthLoginArgs, AuthLogoutArgs, AuthTokenArgs, + ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe, MetadataArgs, + PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs, PipListArgs, PipShowArgs, + PipSyncArgs, PipTreeArgs, PipUninstallArgs, PythonFindArgs, PythonInstallArgs, PythonListArgs, + PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs, RemoveArgs, RunArgs, + SyncArgs, SyncFormat, ToolDirArgs, ToolInstallArgs, ToolListArgs, ToolRunArgs, + ToolUninstallArgs, TreeArgs, VenvArgs, VersionArgs, VersionBumpSpec, VersionFormat, }; use uv_cli::{ AuthorFrom, BuildArgs, ExportArgs, FormatArgs, PublishArgs, PythonDirArgs, @@ -2664,6 +2664,7 @@ pub(crate) struct AuditSettings { pub(crate) python_platform: Option, pub(crate) install_mirrors: PythonInstallMirrors, pub(crate) settings: ResolverSettings, + pub(crate) output_format: AuditOutputFormat, pub(crate) service_format: VulnerabilityServiceFormat, pub(crate) service_url: Option, pub(crate) ignore: Vec, @@ -2689,6 +2690,7 @@ impl AuditSettings { python_platform, locked, frozen, + output_format, build, resolver, ignore, @@ -2744,6 +2746,7 @@ impl AuditSettings { .install_mirrors .combine(filesystem_install_mirrors), settings: ResolverSettings::combine(resolver_options(resolver, build), filesystem), + output_format, service_format, service_url, ignore: { diff --git a/crates/uv/tests/it/audit.rs b/crates/uv/tests/it/audit.rs index 9a39981e10efb..603216b0e4392 100644 --- a/crates/uv/tests/it/audit.rs +++ b/crates/uv/tests/it/audit.rs @@ -7,6 +7,57 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; use uv_test::uv_snapshot; +fn write_audit_json_project(context: &uv_test::TestContext, index_url: &str) { + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml + .write_str(&formatdoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig==2.0.0"] + + [[tool.uv.index]] + url = "{index_url}" + default = true + "#}) + .unwrap(); + + let lockfile = context.temp_dir.child("uv.lock"); + lockfile + .write_str(&formatdoc! {r#" + version = 1 + revision = 3 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = {{ registry = "{index_url}" }} + sdist = {{ url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }} + wheels = [ + {{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }}, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = {{ virtual = "." }} + dependencies = [ + {{ name = "iniconfig" }}, + ] + + [package.metadata] + requires-dist = [ + {{ name = "iniconfig", specifier = "==2.0.0" }}, + ] + "#}) + .unwrap(); +} + /// Audit a project with no vulnerabilities found. #[tokio::test] async fn audit_no_vulnerabilities() { @@ -50,6 +101,99 @@ async fn audit_no_vulnerabilities() { "); } +/// Audit a project with no vulnerabilities found, emitting JSON output. +#[tokio::test] +async fn audit_json_no_vulnerabilities() { + let context = uv_test::test_context!("3.12"); + let proxy = crate::pypi_proxy::start().await; + write_audit_json_project(&context, &proxy.url("/simple")); + + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/v1/querybatch")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "results": [{"vulns": []}] + }))) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .audit() + .arg("--preview-features") + .arg("audit,json-output") + .arg("--output-format") + .arg("json") + .arg("--frozen") + .arg("--service-url") + .arg(server.uri()), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "schema": { + "version": "preview" + }, + "summary": { + "audited_packages": 1, + "vulnerabilities": 0, + "adverse_statuses": 0 + }, + "vulnerabilities": [], + "adverse_statuses": [] + } + + ----- stderr ----- + "#); +} + +/// Requesting JSON output warns unless the JSON preview feature is enabled. +#[tokio::test] +async fn audit_json_preview_warning() { + let context = uv_test::test_context!("3.12"); + let proxy = crate::pypi_proxy::start().await; + write_audit_json_project(&context, &proxy.url("/simple")); + + let server = MockServer::start().await; + + Mock::given(method("POST")) + .and(path("/v1/querybatch")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "results": [{"vulns": []}] + }))) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .audit() + .arg("--preview-features") + .arg("audit") + .arg("--output-format") + .arg("json") + .arg("--frozen") + .arg("--service-url") + .arg(server.uri()), @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "schema": { + "version": "preview" + }, + "summary": { + "audited_packages": 1, + "vulnerabilities": 0, + "adverse_statuses": 0 + }, + "vulnerabilities": [], + "adverse_statuses": [] + } + + ----- stderr ----- + warning: The `--output-format json` option is experimental and the schema may change without warning. Pass `--preview-features json-output` to disable this warning. + "#); +} + /// Audit a project and find a single vulnerability with summary, fix version, and advisory link. #[tokio::test] async fn audit_vulnerability_found() { @@ -2003,3 +2147,96 @@ async fn audit_vulnerability_and_project_status() { Found 1 known vulnerability and 1 adverse project status in 1 package "); } + +/// JSON output includes vulnerabilities and adverse project statuses in the +/// same audit report. +#[tokio::test] +async fn audit_json_vulnerability_and_project_status() { + let context = uv_test::test_context!("3.12"); + let proxy = crate::pypi_proxy::start().await; + write_audit_json_project(&context, &proxy.url("/status/archived/simple")); + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/v1/querybatch")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "results": [{"vulns": [{"id": "PYSEC-2023-0001"}]}] + }))) + .mount(&server) + .await; + + Mock::given(method("GET")) + .and(path("/v1/vulns/PYSEC-2023-0001")) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "id": "PYSEC-2023-0001", + "modified": "2026-01-01T00:00:00Z", + "summary": "A test vulnerability in iniconfig", + "affected": [{ + "ranges": [{ + "type": "ECOSYSTEM", + "events": [ + {"introduced": "0"}, + {"fixed": "2.1.0"} + ] + }] + }], + "references": [{ + "type": "ADVISORY", + "url": "https://example.com/advisory/PYSEC-2023-0001" + }] + }))) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context + .audit() + .arg("--preview-features") + .arg("audit,json-output") + .arg("--output-format") + .arg("json") + .arg("--frozen") + .arg("--service-url") + .arg(server.uri()), @r#" + success: false + exit_code: 1 + ----- stdout ----- + { + "schema": { + "version": "preview" + }, + "summary": { + "audited_packages": 1, + "vulnerabilities": 1, + "adverse_statuses": 1 + }, + "vulnerabilities": [ + { + "dependency": { + "name": "iniconfig", + "version": "2.0.0" + }, + "id": "PYSEC-2023-0001", + "display_id": "PYSEC-2023-0001", + "aliases": [], + "summary": "A test vulnerability in iniconfig", + "description": null, + "link": "https://example.com/advisory/PYSEC-2023-0001", + "fix_versions": [ + "2.1.0" + ], + "published": null, + "modified": "2026-01-01T00:00:00Z" + } + ], + "adverse_statuses": [ + { + "name": "iniconfig", + "status": "archived", + "reason": null + } + ] + } + + ----- stderr ----- + "#); +} From da8fb4c9ef76b99d127eb84365ab1517c2bcaba6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 14 May 2026 20:39:59 +0100 Subject: [PATCH 15/56] Update toml-edit to v0.25 (#19405) --- Cargo.lock | 32 ++++++------------- Cargo.toml | 2 +- .../src/lock/export/pylock_toml.rs | 18 ++++++++--- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28fee09fa2fc6..8a012aab1d3fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3429,7 +3429,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.4+spec-1.1.0", + "toml_edit", ] [[package]] @@ -5257,21 +5257,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_edit" -version = "0.24.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c740b185920170a6d9191122cafef7010bd6270a3824594bff6784c04d7f09e" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow", -] - [[package]] name = "toml_edit" version = "0.25.4+spec-1.1.0" @@ -5279,8 +5264,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap", + "serde_core", + "serde_spanned", "toml_datetime 1.0.0+spec-1.1.0", "toml_parser", + "toml_writer", "winnow", ] @@ -5756,7 +5744,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "tracing-durations-export", "tracing-subscriber", @@ -6024,7 +6012,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "uv-auth", "uv-cache-key", @@ -6896,7 +6884,7 @@ dependencies = [ "serde-untagged", "serde_json", "thiserror 2.0.18", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "url", "uv-cache-key", @@ -7081,7 +7069,7 @@ dependencies = [ "tokio", "tokio-stream", "toml", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "url", "uv-cache-key", @@ -7261,7 +7249,7 @@ dependencies = [ "serde", "thiserror 2.0.18", "toml", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "uv-cache", "uv-dirs", @@ -7415,7 +7403,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "toml", - "toml_edit 0.24.0+spec-1.1.0", + "toml_edit", "tracing", "uv-build-backend", "uv-cache-key", diff --git a/Cargo.toml b/Cargo.toml index 3340d9968c72b..5369d5b524331 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -270,7 +270,7 @@ tokio = { version = "1.40.0", features = [ tokio-stream = { version = "0.1.16" } tokio-util = { version = "0.7.12", features = ["compat", "io"] } toml = { version = "0.9.2", features = ["fast_hash"] } -toml_edit = { version = "0.24.0", features = ["serde"] } +toml_edit = { version = "0.25.4", features = ["serde"] } tracing = { version = "0.1.40" } tracing-durations-export = { version = "0.3.0", features = ["plot"] } tracing-subscriber = { version = "0.3.18" } # Default feature set for uv_build, uv activates extra features diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index 831510f4c0168..313b605139c35 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -1709,8 +1709,16 @@ where time: Some(toml_edit::Time { hour: u8::try_from(timestamp.hour()).map_err(serde::ser::Error::custom)?, minute: u8::try_from(timestamp.minute()).map_err(serde::ser::Error::custom)?, - second: u8::try_from(timestamp.second()).map_err(serde::ser::Error::custom)?, - nanosecond: u32::try_from(timestamp.nanosecond()).map_err(serde::ser::Error::custom)?, + second: Some(u8::try_from(timestamp.second()).map_err(serde::ser::Error::custom)?), + nanosecond: { + let nanosecond = + u32::try_from(timestamp.nanosecond()).map_err(serde::ser::Error::custom)?; + if nanosecond == 0 { + None + } else { + Some(nanosecond) + } + }, }), offset: Some(toml_edit::Offset::Z), }; @@ -1753,8 +1761,10 @@ where let time = if let Some(time) = datetime.time { let hour = i8::try_from(time.hour).map_err(serde::de::Error::custom)?; let minute = i8::try_from(time.minute).map_err(serde::de::Error::custom)?; - let second = i8::try_from(time.second).map_err(serde::de::Error::custom)?; - let nanosecond = i32::try_from(time.nanosecond).map_err(serde::de::Error::custom)?; + let second = + i8::try_from(time.second.unwrap_or_default()).map_err(serde::de::Error::custom)?; + let nanosecond = + i32::try_from(time.nanosecond.unwrap_or_default()).map_err(serde::de::Error::custom)?; Time::new(hour, minute, second, nanosecond).map_err(serde::de::Error::custom)? } else { Time::midnight() From 0784c7f269dd907b28e79341e2150e638e5a61b9 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Thu, 14 May 2026 12:45:11 -0700 Subject: [PATCH 16/56] Pin to cargo-shear 1.11.2 (#19406) See https://github.com/astral-sh/ruff/pull/25166 --- .github/workflows/check-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index b56132f4875f2..c8dd7cb180d89 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -153,7 +153,7 @@ jobs: - name: "Install cargo shear" uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 with: - tool: cargo-shear + tool: cargo-shear@1.11.2 - run: cargo shear --deny-warnings typos: From 1356b1b2b3ff1f8adbafc0c6bc730ca9034058aa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 14 May 2026 23:35:28 +0100 Subject: [PATCH 17/56] Bump `rustls-platform-verifier` to v0.7.0 (#19407) ## Summary This will ultimately enable us to remove a copy of `thiserror`. (I think this doesn't get bumped by Renovate because it's a transitive dependency.) --- Cargo.lock | 150 ++++++++++++++++++----------------------------------- 1 file changed, 51 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a012aab1d3fa..4e96417c32ad3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -101,7 +101,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -713,12 +713,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" version = "1.0.4" @@ -1303,7 +1297,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1482,7 +1476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2352,7 +2346,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2413,7 +2407,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2444,27 +2438,32 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link 0.2.1", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] @@ -2530,7 +2529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "160f2eade097f30263b548aae5deb12ad349c909baa710fa24b92c9090b2e006" dependencies = [ "scopeguard", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2785,7 +2784,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2864,7 +2863,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4112,6 +4111,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -4131,7 +4139,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4172,9 +4180,9 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", @@ -4188,7 +4196,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4594,6 +4602,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -4676,7 +4694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4894,7 +4912,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5503,7 +5521,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7672,7 +7690,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7794,15 +7812,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -7830,21 +7839,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -7896,12 +7890,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -7914,12 +7902,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -7932,12 +7914,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -7962,12 +7938,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -7980,12 +7950,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -7998,12 +7962,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -8016,12 +7974,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" From 1898337844afc1267766049c21163e2502e853de Mon Sep 17 00:00:00 2001 From: Choudhry Abdullah Date: Thu, 14 May 2026 18:11:10 -0500 Subject: [PATCH 18/56] Fix Git submodule checkouts with relative paths (#12156) ## Summary This fixes installations using git which involved submodules declared through relative paths, fixes #9822 . ## Test Plan I did testing using a private gitlab server that I was running locally, to recreate the issue. I have also added tests that involve installing from a repository that I created on my GitHub. Adding that repository and it's accompanying submodule to uv-test (I see there is already a repository - `uv_submodule_pypackage` that tests submodule installation but that doesn't use relative path) should work. --------- Signed-off-by: William Woodruff Co-authored-by: Charlie Marsh Co-authored-by: William Woodruff --- Cargo.lock | 1 + crates/uv-git/Cargo.toml | 2 +- crates/uv-git/src/git.rs | 161 ++++++++++++++++++++--- crates/uv/tests/it/pip_install.rs | 212 ++++++++++++++++++++++++++++++ 4 files changed, 360 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4e96417c32ad3..aeb99e17b9637 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6541,6 +6541,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "url", "uv-auth", "uv-cache-key", "uv-fs", diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index 105e129b6e8b7..b4ef3893e9a05 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -11,7 +11,6 @@ license = { workspace = true } [lib] doctest = false -test = false [lints] workspace = true @@ -36,4 +35,5 @@ reqwest-middleware = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } +url = { workspace = true } which = { workspace = true } diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index 36c498f3ed0db..3ae0408d7c6e7 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -10,6 +10,7 @@ use anyhow::{Context, Result, anyhow}; use cargo_util::{ProcessBuilder, paths}; use owo_colors::OwoColorize; use tracing::{debug, instrument, warn}; +use url::Url; use uv_fs::Simplified; use uv_git_types::{GitOid, GitReference}; @@ -160,6 +161,8 @@ pub(crate) struct GitRemote { /// A local clone of a remote repository's database. Multiple [`GitCheckout`]s /// can be cloned from a single [`GitDatabase`]. pub(crate) struct GitDatabase { + /// The remote repository where this database is fetched from. + remote: GitRemote, /// Underlying Git repository instance for this database. repo: GitRepository, /// Git LFS artifacts have been initialized (if requested). @@ -294,7 +297,7 @@ impl GitRemote { /// populated the database with the latest version of `reference`, so /// return that database and the rev we resolve to. pub(crate) fn checkout( - &self, + self, into: &Path, db: Option, reference: &GitReference, @@ -349,14 +352,21 @@ impl GitRemote { }) .transpose()?; - Ok((GitDatabase { repo, lfs_ready }, rev)) + Ok(( + GitDatabase { + remote: self, + repo, + lfs_ready, + }, + rev, + )) } /// Creates a [`GitDatabase`] of this remote at `db_path`. - #[expect(clippy::unused_self)] pub(crate) fn db_at(&self, db_path: &Path) -> Result { let repo = GitRepository::open(db_path)?; Ok(GitDatabase { + remote: self.clone(), repo, lfs_ready: None, }) @@ -376,7 +386,7 @@ impl GitDatabase { .filter(GitCheckout::is_fresh) { Some(co) => co.with_lfs_ready(self.lfs_ready), - None => GitCheckout::clone_into(destination, self, rev)?, + None => GitCheckout::clone_into(destination, self, rev, self.remote.url())?, }; Ok(checkout) } @@ -430,7 +440,12 @@ impl GitCheckout { /// Clone a repo for a `revision` into a local path from a `database`. /// This is a filesystem-to-filesystem clone. - fn clone_into(into: &Path, database: &GitDatabase, revision: GitOid) -> Result { + fn clone_into( + into: &Path, + database: &GitDatabase, + revision: GitOid, + original_remote_url: &DisplaySafeUrl, + ) -> Result { let dirname = into.parent().unwrap(); fs_err::create_dir_all(dirname)?; match fs_err::remove_dir_all(into) { @@ -468,7 +483,7 @@ impl GitCheckout { let repo = GitRepository::open(into)?; let checkout = Self::new(revision, repo); - let lfs_ready = checkout.reset(database.lfs_ready)?; + let lfs_ready = checkout.reset(database.lfs_ready, original_remote_url)?; Ok(checkout.with_lfs_ready(lfs_ready)) } @@ -495,20 +510,26 @@ impl GitCheckout { self } - /// This performs `git reset --hard` to the revision of this checkout, with - /// additional interrupt protection by a dummy file [`CHECKOUT_READY_LOCK`]. + /// This performs `git reset --hard` to the revision of this checkout and updates submodules, + /// with additional interrupt protection by a dummy file [`CHECKOUT_READY_LOCK`]. /// - /// If we're interrupted while performing a `git reset` (e.g., we die + /// If we're interrupted while performing any of the processes in this method (e.g., we die /// because of a signal) uv needs to be sure to try to check out this /// repo again on the next go-round. /// /// To enable this we have a dummy file in our checkout, [`.ok`], - /// which if present means that the repo has been successfully reset and is - /// ready to go. Hence, if we start to do a reset, we make sure this file + /// which if present means that the repo has been successfully checked out and is + /// ready to go. Hence if we start to update submodules, we make sure this file /// *doesn't* exist, and then once we're done we create the file. /// /// [`.ok`]: CHECKOUT_READY_LOCK - fn reset(&self, with_lfs: Option) -> Result> { + /// `git reset --hard []` can break relative submodule URLs, so we update submodules + /// using the original remote URL. + fn reset( + &self, + with_lfs: Option, + original_remote_url: &DisplaySafeUrl, + ) -> Result> { let ok_file = self.repo.path.join(CHECKOUT_READY_LOCK); let _ = paths::remove_file(&ok_file); @@ -516,6 +537,7 @@ impl GitCheckout { // as smudge filters can trigger on a reset even if lfs artifacts // were not originally "fetched". let lfs_skip_smudge = if with_lfs == Some(true) { "0" } else { "1" }; + debug!("Reset {} to {}", self.repo.path.display(), self.revision); // Perform the hard reset. @@ -528,9 +550,37 @@ impl GitCheckout { .cwd(&self.repo.path) .exec_with_output()?; - // Update submodules (`git submodule update --recursive`). - GIT.as_ref() - .cloned()? + // Initialize direct submodules using the original remote URL so Git can resolve relative + // submodule URLs, but don't write it to `remote.origin.url`. Git persists resolved submodule + // URLs during initialization, so writing a credentialed parent remote can leak credentials + // into checkout configuration. + // + // Do not use `--recursive` here: command-local `remote.origin.url` config is inherited by + // Git commands run inside submodules, which would make nested relative URLs resolve against + // the top-level remote instead of their immediate parent submodule. + let mut submodule_update = GIT.as_ref().cloned()?; + for config in submodule_update_config(original_remote_url) { + submodule_update.arg("-c").arg(config); + } + + submodule_update + .arg("submodule") + .arg("update") + .arg("--init") + .env(EnvVars::GIT_LFS_SKIP_SMUDGE, lfs_skip_smudge) + .cwd(&self.repo.path) + .exec_with_output() + .map(drop)?; + + // Recursively update nested submodules without overriding `remote.origin.url`, so each + // nested relative URL resolves against its immediate parent submodule. The transient + // credential rewrite is still safe to inherit because it only affects transport. + let mut submodule_update = GIT.as_ref().cloned()?; + for config in submodule_auth_config(original_remote_url) { + submodule_update.arg("-c").arg(config); + } + + submodule_update .arg("submodule") .arg("update") .arg("--recursive") @@ -559,6 +609,59 @@ impl GitCheckout { } } +/// Return command-local Git configuration for initializing direct submodules in a checkout. +/// +/// Relative submodule URLs are resolved from `remote.origin.url`, but writing the original remote +/// URL into checkout configuration can persist credentials in the parent repository or submodule +/// remotes. Instead, callers pass these values via `git -c`, using a credential-stripped origin URL +/// for resolution and a transient `url.*.insteadOf` rewrite when credentials are needed for +/// transport. +fn submodule_update_config(original_remote_url: &DisplaySafeUrl) -> Vec { + let remote_url = original_remote_url.without_credentials(); + let mut config = vec![format!("remote.origin.url={}", remote_url.as_str())]; + + config.extend(submodule_auth_config(original_remote_url)); + config +} + +/// Return command-local Git authentication configuration for updating submodules. +/// +/// Unlike `remote.origin.url`, these rewrites are safe to inherit during recursive submodule +/// updates: they rewrite transport URLs for authentication, but do not change the base URL that Git +/// uses to resolve nested relative submodule URLs. +fn submodule_auth_config(original_remote_url: &DisplaySafeUrl) -> Vec { + let remote_url = original_remote_url.without_credentials(); + let mut config = Vec::new(); + + if remote_url.as_str() != original_remote_url.as_str() { + let safe_root = remote_url_root(remote_url.as_ref()); + let credentialed_root = remote_url_root(original_remote_url); + + if safe_root.as_str() != credentialed_root.as_str() { + config.push(format!( + "url.{}.insteadOf={}", + credentialed_root.as_str(), + safe_root.as_str() + )); + } + } + + config +} + +/// Return the scheme, authority, and root path of a remote URL. +/// +/// This is used as the rewrite prefix for `url.*.insteadOf`, so a credentialed parent URL can +/// authenticate sibling submodule URLs without making the credentials part of any persisted +/// submodule URL. +fn remote_url_root(url: &Url) -> Url { + let mut root = url.clone(); + root.set_path("/"); + root.set_query(None); + root.set_fragment(None); + root +} + /// Attempts to fetch the given git `reference` for a Git repository. /// /// This is the main entry for git clone/fetch. It does the following: @@ -833,3 +936,31 @@ fn is_short_hash_of(rev: &str, oid: GitOid) -> bool { None => false, } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn submodule_update_config_strips_credentials_from_origin_override() { + let url = DisplaySafeUrl::parse("https://user:password@example.com/org/repo.git").unwrap(); + + assert_eq!( + submodule_update_config(&url), + vec![ + "remote.origin.url=https://example.com/org/repo.git".to_string(), + "url.https://user:password@example.com/.insteadOf=https://example.com/".to_string(), + ] + ); + } + + #[test] + fn submodule_update_config_preserves_git_ssh_user() { + let url = DisplaySafeUrl::parse("ssh://git@example.com/org/repo.git").unwrap(); + + assert_eq!( + submodule_update_config(&url), + vec!["remote.origin.url=ssh://git@example.com/org/repo.git".to_string()] + ); + } +} diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index 69784ca76e1bf..bf39d27d4346f 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -11777,6 +11777,218 @@ fn unsupported_git_scheme() { ); } +#[test] +#[cfg(unix)] +#[cfg(feature = "test-git")] +fn install_git_submodule_relative_url() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let temp_dir = &context.temp_dir; + let utilities_dir = temp_dir.child("utilities"); + utilities_dir.create_dir_all()?; + + // Create and initialize the helper repository + let helpers_dir = utilities_dir.child("helpers"); + helpers_dir.create_dir_all()?; + + // Initialize helpers as a git repo + Command::new("git") + .args(["init"]) + .current_dir(helpers_dir.path()) + .output()?; + + // Create a simple file in helpers + helpers_dir + .child("helper.py") + .write_str("def help():\n return 'I am a helper'")?; + + // Create a nested dependency of the helper repository. This lives outside the parent + // repository so the helper must resolve it through its own relative submodule URL. + let grandchild_dir = temp_dir.child("grandchild"); + grandchild_dir.create_dir_all()?; + + Command::new("git") + .args(["init"]) + .current_dir(grandchild_dir.path()) + .output()?; + + grandchild_dir + .child("grandchild.py") + .write_str("def help():\n return 'I am a nested helper'")?; + + Command::new("git") + .args(["add", "."]) + .current_dir(grandchild_dir.path()) + .output()?; + + Command::new("git") + .args(["commit", "-m", "Initial nested helpers commit"]) + .current_dir(grandchild_dir.path()) + .env("GIT_AUTHOR_NAME", "Test") + .env("GIT_AUTHOR_EMAIL", "test@example.com") + .env("GIT_COMMITTER_NAME", "Test") + .env("GIT_COMMITTER_EMAIL", "test@example.com") + .output()?; + + Command::new("git") + .args(["submodule", "add", "../../grandchild", "nested/grandchild"]) + .env("GIT_ALLOW_PROTOCOL", "file:ext:http:https:ssh") + .current_dir(helpers_dir.path()) + .assert() + .success(); + helpers_dir + .child(".gitmodules") + .assert(predicate::path::is_file()); + helpers_dir + .child("nested") + .child("grandchild") + .child("grandchild.py") + .assert(predicate::path::is_file()); + + // Add and commit in helpers + Command::new("git") + .args(["add", "."]) + .current_dir(helpers_dir.path()) + .output()?; + + Command::new("git") + .args(["commit", "-m", "Initial helpers commit"]) + .current_dir(helpers_dir.path()) + .env("GIT_AUTHOR_NAME", "Test") + .env("GIT_AUTHOR_EMAIL", "test@example.com") + .env("GIT_COMMITTER_NAME", "Test") + .env("GIT_COMMITTER_EMAIL", "test@example.com") + .output()?; + + // Create and initialize the main repository + let mylib_dir = temp_dir.child("mylib"); + mylib_dir.create_dir_all()?; + + // Initialize mylib as a git repo + Command::new("git") + .args(["init"]) + .current_dir(mylib_dir.path()) + .output()?; + + // Create a simple setup.py + mylib_dir.child("setup.py").write_str( + r#" +from setuptools import setup, find_packages + +setup( + name="mylib", + version="0.1.0", + packages=find_packages(), +) +"#, + )?; + + // Create a simple module file + let mylib_package = mylib_dir.child("mylib"); + mylib_package.create_dir_all()?; + mylib_package.child("__init__.py").write_str( + r#" +from .helpers import helper + +def main(): + return f"Hello from mylib, {helper.help()}" +"#, + )?; + + // Set helper as a submodule + Command::new("git") + .args(["submodule", "add", "../utilities/helpers", "mylib/helpers"]) + .env("GIT_ALLOW_PROTOCOL", "file:ext:http:https:ssh") + .current_dir(mylib_dir.path()) + .assert() + .success(); + mylib_dir + .child(".gitmodules") + .assert(predicate::path::is_file()); + mylib_package + .child("helpers") + .child("helper.py") + .assert(predicate::path::is_file()); + + // Add and commit in mylib + Command::new("git") + .args(["add", "."]) + .current_dir(mylib_dir.path()) + .output()?; + + Command::new("git") + .args(["commit", "-m", "Initial mylib commit with submodule"]) + .current_dir(mylib_dir.path()) + .env("GIT_AUTHOR_NAME", "Test") + .env("GIT_AUTHOR_EMAIL", "test@example.com") + .env("GIT_COMMITTER_NAME", "Test") + .env("GIT_COMMITTER_EMAIL", "test@example.com") + .output()?; + + let mut filters = context.filters(); + filters.push((r"@[0-9a-f]{40}", "@COMMIT_HASH")); + + uv_snapshot!(filters, context.pip_install() + .arg(format!("git+file://{}", mylib_dir.path().display())) + // Pass through environment variable to allow file:// URLs in Git subprocesses + .env("GIT_ALLOW_PROTOCOL", "file:ext:http:https:ssh"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + mylib==0.1.0 (from git+file://[TEMP_DIR]/mylib@COMMIT_HASH) + "); + + Ok(()) +} + +#[test] +#[cfg(feature = "test-git")] +fn install_git_submodule_remote() { + const TEST_REPO: &str = "astral-test/uv-submodule-pypackage.git"; + const TEST_REV: &str = "d2c44b87eef25896dd30a6e55d4689b918180c7b"; + let context = uv_test::test_context!("3.13"); + + uv_snapshot!(context.filters(), context.pip_install() + .arg(format!("git+https://github.com/{TEST_REPO}@{TEST_REV}")), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-submodule-pypackage.git@d2c44b87eef25896dd30a6e55d4689b918180c7b) + "); +} + +/// Like `install_git_submodule_remote` but the submodule URL is relative. +/// See: +#[test] +#[cfg(feature = "test-git")] +fn install_git_submodule_remote_relative() { + const TEST_REPO: &str = "astral-test/uv-submodule-pypackage.git"; + const TEST_REV: &str = "377720a1a7f48279bbfc17988454132f13644e84"; + let context = uv_test::test_context!("3.13"); + + uv_snapshot!(context.filters(), context.pip_install() + .arg(format!("git+https://github.com/{TEST_REPO}@{TEST_REV}")), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-public-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-submodule-pypackage.git@377720a1a7f48279bbfc17988454132f13644e84) + "); +} + /// Modify a project to use a `src` layout. #[test] fn change_layout_src() -> Result<()> { From 15689b2a2b5152362d420598a469382f3cf8f8b3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 15 May 2026 00:46:51 +0100 Subject: [PATCH 19/56] Vendor netrc parser crate (#19409) ## Summary This is a small crate which nothing else in our tree depends on. By vendoring it, we pull in some fixes that never got released (see @terror's work from https://github.com/astral-sh/uv/issues/16083), and we get to remove `thiserror` v1 from our dependency tree. Closes https://github.com/astral-sh/uv/issues/16083. --- Cargo.lock | 170 ++++----- Cargo.toml | 7 +- crates/README.md | 4 + crates/uv-auth/Cargo.toml | 2 +- crates/uv-auth/src/credentials.rs | 2 +- crates/uv-auth/src/middleware.rs | 4 +- crates/uv-netrc/Cargo.toml | 25 ++ crates/uv-netrc/LICENSE | 21 + crates/uv-netrc/README.md | 14 + crates/uv-netrc/src/lex.rs | 89 +++++ crates/uv-netrc/src/lib.rs | 177 +++++++++ crates/uv-netrc/src/netrc.rs | 612 ++++++++++++++++++++++++++++++ 12 files changed, 1027 insertions(+), 100 deletions(-) create mode 100644 crates/uv-netrc/Cargo.toml create mode 100644 crates/uv-netrc/LICENSE create mode 100644 crates/uv-netrc/README.md create mode 100644 crates/uv-netrc/src/lex.rs create mode 100644 crates/uv-netrc/src/lib.rs create mode 100644 crates/uv-netrc/src/netrc.rs diff --git a/Cargo.lock b/Cargo.lock index aeb99e17b9637..21e7cd7748413 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ dependencies = [ "secrecy", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -149,7 +149,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -227,7 +227,7 @@ dependencies = [ "log", "priority-queue", "rustc-hash", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -241,7 +241,7 @@ dependencies = [ "http", "reqwest", "serde", - "thiserror 2.0.18", + "thiserror", "tower-service", ] @@ -260,7 +260,7 @@ dependencies = [ "hyper", "reqwest", "retry-policies", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "wasmtimer", @@ -312,7 +312,7 @@ dependencies = [ "itertools 0.14.0", "memmap2", "reqwest", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tokio-util", @@ -329,7 +329,7 @@ dependencies = [ "crc32fast", "futures-lite", "pin-project", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", ] @@ -447,7 +447,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "url", "walkdir", ] @@ -459,7 +459,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a4b4798a6c02e91378537c63cd6e91726900b595450daa5d487bc3c11e95e1b" dependencies = [ "miette", - "thiserror 2.0.18", + "thiserror", "tracing", ] @@ -471,7 +471,7 @@ checksum = "dc923121fbc4cc72e9008436b5650b98e56f94b5799df59a1b4f572b5c6a7e6b" dependencies = [ "miette", "semver", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -489,7 +489,7 @@ dependencies = [ "self-replace", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "url", ] @@ -1160,7 +1160,7 @@ dependencies = [ "serde_json", "spdx", "strum", - "thiserror 2.0.18", + "thiserror", "time", "uuid", "xml-rs", @@ -2448,7 +2448,7 @@ dependencies = [ "jni-sys", "log", "simd_cesu8", - "thiserror 2.0.18", + "thiserror", "walkdir", "windows-link 0.2.1", ] @@ -3490,7 +3490,7 @@ checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad" dependencies = [ "hex", "percent-encoding", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -3529,7 +3529,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "web-time", @@ -3551,7 +3551,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.18", + "thiserror", "tinyvec", "tracing", "web-time", @@ -3719,7 +3719,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -4095,16 +4095,6 @@ dependencies = [ "ordered-multimap", ] -[[package]] -name = "rust-netrc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e98097f62769f92dbf95fb51f71c0a68ec18a4ee2e70e0d3e4f47ac005d63e9" -dependencies = [ - "shellexpand", - "thiserror 1.0.69", -] - [[package]] name = "rustc-hash" version = "2.1.2" @@ -4641,7 +4631,7 @@ checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.18", + "thiserror", "time", ] @@ -5006,33 +4996,13 @@ dependencies = [ "unicode-width 0.2.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -5757,7 +5727,7 @@ dependencies = [ "tar", "tempfile", "textwrap", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tokio-util", @@ -5842,7 +5812,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uv-client", @@ -5874,14 +5844,13 @@ dependencies = [ "percent-encoding", "reqsign", "reqwest", - "rust-netrc", "rustc-hash", "schemars", "serde", "serde_json", "tempfile", "test-log", - "thiserror 2.0.18", + "thiserror", "tokio", "toml", "tracing", @@ -5889,6 +5858,7 @@ dependencies = [ "uv-cache-key", "uv-fs", "uv-keyring", + "uv-netrc", "uv-once-map", "uv-preview", "uv-redacted", @@ -5939,7 +5909,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "url", @@ -5994,7 +5964,7 @@ dependencies = [ "spdx", "tar", "tempfile", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "uv-distribution-filename", @@ -6028,7 +5998,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "toml_edit", "tracing", @@ -6061,7 +6031,7 @@ dependencies = [ "same-file", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "tracing", "uv-cache-info", "uv-cache-key", @@ -6086,7 +6056,7 @@ dependencies = [ "schemars", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "uv-fs", @@ -6176,7 +6146,7 @@ dependencies = [ "serde_json", "temp-env", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-rustls", "tokio-util", @@ -6225,7 +6195,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -6318,7 +6288,7 @@ dependencies = [ "futures", "itertools 0.14.0", "rustc-hash", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uv-build-backend", @@ -6360,7 +6330,7 @@ dependencies = [ "rustc-hash", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "toml", @@ -6403,7 +6373,7 @@ dependencies = [ "rkyv", "serde", "smallvec", - "thiserror 2.0.18", + "thiserror", "uv-cache-key", "uv-normalize", "uv-pep440", @@ -6430,7 +6400,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "url", @@ -6468,7 +6438,7 @@ dependencies = [ "reqwest", "rustc-hash", "sha2", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -6517,7 +6487,7 @@ dependencies = [ "schemars", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "uv-static", @@ -6538,7 +6508,7 @@ dependencies = [ "fs-err", "owo-colors", "reqwest", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -6559,7 +6529,7 @@ version = "0.0.47" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.18", + "thiserror", "tracing", "url", "uv-cache-key", @@ -6579,7 +6549,7 @@ dependencies = [ "regex", "regex-automata", "tempfile", - "thiserror 2.0.18", + "thiserror", "tracing", "walkdir", ] @@ -6606,7 +6576,7 @@ dependencies = [ "serde", "serde_json", "sha2", - "thiserror 2.0.18", + "thiserror", "tracing", "uv-distribution-filename", "uv-flags", @@ -6636,7 +6606,7 @@ dependencies = [ "rustc-hash", "same-file", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "url", @@ -6674,7 +6644,7 @@ dependencies = [ "fastrand", "secret-service", "security-framework", - "thiserror 2.0.18", + "thiserror", "tokio", "windows", "zeroize", @@ -6708,7 +6678,7 @@ dependencies = [ "astral_async_zip", "fs-err", "futures", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -6717,6 +6687,16 @@ dependencies = [ "uv-pypi-types", ] +[[package]] +name = "uv-netrc" +version = "0.0.47" +dependencies = [ + "fs-err", + "shellexpand", + "temp-env", + "thiserror", +] + [[package]] name = "uv-normalize" version = "0.0.47" @@ -6775,7 +6755,7 @@ dependencies = [ "serde_json", "smallvec", "temp-env", - "thiserror 2.0.18", + "thiserror", "tracing", "tracing-test", "unicode-width 0.2.2", @@ -6808,7 +6788,7 @@ dependencies = [ "rustix", "target-lexicon", "tempfile", - "thiserror 2.0.18", + "thiserror", "tracing", "uv-fs", "uv-platform-tags", @@ -6826,7 +6806,7 @@ dependencies = [ "rkyv", "rustc-hash", "serde", - "thiserror 2.0.18", + "thiserror", "uv-small-str", ] @@ -6836,7 +6816,7 @@ version = "0.0.47" dependencies = [ "enumflags2", "itertools 0.14.0", - "thiserror 2.0.18", + "thiserror", "uv-preview", "uv-warnings", ] @@ -6862,7 +6842,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -6902,7 +6882,7 @@ dependencies = [ "serde", "serde-untagged", "serde_json", - "thiserror 2.0.18", + "thiserror", "toml_edit", "tracing", "url", @@ -6947,7 +6927,7 @@ dependencies = [ "temp-env", "tempfile", "test-log", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-util", "tracing", @@ -6985,7 +6965,7 @@ dependencies = [ "ref-cast", "schemars", "serde", - "thiserror 2.0.18", + "thiserror", "url", ] @@ -6999,7 +6979,7 @@ dependencies = [ "fs-err", "futures", "rustc-hash", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "url", @@ -7040,7 +7020,7 @@ dependencies = [ "rustc-hash", "tempfile", "test-case", - "thiserror 2.0.18", + "thiserror", "tokio", "tracing", "unscanny", @@ -7084,7 +7064,7 @@ dependencies = [ "serde_json", "smallvec", "textwrap", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "toml", @@ -7131,7 +7111,7 @@ dependencies = [ "memchr", "regex", "serde", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "url", @@ -7156,7 +7136,7 @@ dependencies = [ "schemars", "serde", "textwrap", - "thiserror 2.0.18", + "thiserror", "toml", "tracing", "url", @@ -7224,7 +7204,7 @@ dependencies = [ name = "uv-static" version = "0.0.47" dependencies = [ - "thiserror 2.0.18", + "thiserror", "uv-macros", ] @@ -7266,7 +7246,7 @@ dependencies = [ "owo-colors", "pathdiff", "serde", - "thiserror 2.0.18", + "thiserror", "toml", "toml_edit", "tracing", @@ -7296,7 +7276,7 @@ dependencies = [ "fs-err", "schemars", "serde", - "thiserror 2.0.18", + "thiserror", "tracing", "url", "uv-distribution-types", @@ -7320,7 +7300,7 @@ dependencies = [ "goblin", "rcgen", "tempfile", - "thiserror 2.0.18", + "thiserror", "uv-fs", "which", "windows", @@ -7333,7 +7313,7 @@ dependencies = [ "anyhow", "dashmap", "rustc-hash", - "thiserror 2.0.18", + "thiserror", "uv-cache", "uv-configuration", "uv-distribution-filename", @@ -7353,7 +7333,7 @@ name = "uv-unix" version = "0.0.47" dependencies = [ "nix 0.31.2", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -7370,7 +7350,7 @@ dependencies = [ "owo-colors", "pathdiff", "self-replace", - "thiserror 2.0.18", + "thiserror", "tracing", "uv-console", "uv-fs", @@ -7419,7 +7399,7 @@ dependencies = [ "schemars", "serde", "tempfile", - "thiserror 2.0.18", + "thiserror", "tokio", "toml", "toml_edit", @@ -8127,7 +8107,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror 2.0.18", + "thiserror", "time", ] diff --git a/Cargo.toml b/Cargo.toml index 5369d5b524331..97d5ce83d59ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ uv-keyring = { version = "0.0.47", path = "crates/uv-keyring" } uv-logging = { version = "0.0.47", path = "crates/uv-logging" } uv-macros = { version = "0.0.47", path = "crates/uv-macros" } uv-metadata = { version = "0.0.47", path = "crates/uv-metadata" } +uv-netrc = { version = "0.0.47", path = "crates/uv-netrc" } uv-normalize = { version = "0.0.47", path = "crates/uv-normalize" } uv-once-map = { version = "0.0.47", path = "crates/uv-once-map" } uv-options-metadata = { version = "0.0.47", path = "crates/uv-options-metadata" } @@ -229,7 +230,6 @@ reqwest-retry = { version = "0.9.1", package = "astral-reqwest-retry", features ] } rkyv = { version = "0.8.14", features = ["bytecheck"] } rmp-serde = { version = "1.3.0" } -rust-netrc = { version = "0.1.2" } rustc-hash = { version = "2.0.0" } rustls-native-certs = { version = "0.8.3" } rustls-pki-types = { version = "1.14.0" } @@ -242,6 +242,11 @@ same-file = { version = "1.0.6" } schemars = { version = "1.0.0", features = ["url2"] } seahash = { version = "4.1.0" } secret-service = { version = "5.0.0", features = ["rt-tokio-crypto-rust"] } +shellexpand = { version = "3.1.0", default-features = false, features = [ + "base-0", + "tilde", + "path", +] } security-framework = { version = "3" } self-replace = { version = "1.5.0" } serde = { version = "1.0.210", features = ["derive", "rc"] } diff --git a/crates/README.md b/crates/README.md index b691afa348528..380b959e4a918 100644 --- a/crates/README.md +++ b/crates/README.md @@ -92,6 +92,10 @@ Functionality for installing Python packages into a virtual environment. Functionality for detecting and leveraging the current Python interpreter. +## [uv-netrc](./uv-netrc) + +A vendored netrc parser for uv. + ## [uv-normalize](./uv-normalize) Normalize package and extra names as per Python specifications. diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index 974cebf6a744f..e7c46e39860b7 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -19,6 +19,7 @@ workspace = true uv-cache-key = { workspace = true } uv-fs = { workspace = true } uv-keyring = { workspace = true, features = ["apple-native", "secret-service", "windows-native"] } +uv-netrc = { workspace = true } uv-once-map = { workspace = true } uv-preview = { workspace = true } uv-redacted = { workspace = true } @@ -40,7 +41,6 @@ percent-encoding = { workspace = true } reqsign = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } -rust-netrc = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index 5f48a036b6872..f7a7257d44f2e 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -8,7 +8,6 @@ use base64::prelude::BASE64_STANDARD; use base64::read::DecoderReader; use base64::write::EncoderWriter; use http::Uri; -use netrc::Netrc; use reqsign::aws::DefaultSigner as AwsDefaultSigner; use reqsign::google::DefaultSigner as GcsDefaultSigner; use reqwest::Request; @@ -16,6 +15,7 @@ use reqwest::header::HeaderValue; use serde::{Deserialize, Serialize}; use url::Url; +use uv_netrc::Netrc; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index b9ab7a4d3496e..0efabab5504ba 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -2,12 +2,12 @@ use std::sync::{Arc, LazyLock}; use anyhow::{anyhow, format_err}; use http::{Extensions, StatusCode}; -use netrc::Netrc; use reqwest::{Request, Response}; use reqwest_middleware::{ClientWithMiddleware, Error, Middleware, Next}; use tokio::sync::Mutex; use tracing::{debug, trace, warn}; +use uv_netrc::Netrc; use uv_preview::{Preview, PreviewFeature}; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; @@ -40,7 +40,7 @@ impl Default for NetrcMode { fn default() -> Self { Self::Automatic(LazyLock::new(|| match Netrc::new() { Ok(netrc) => Some(netrc), - Err(netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + Err(uv_netrc::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { debug!("No netrc file found"); None } diff --git a/crates/uv-netrc/Cargo.toml b/crates/uv-netrc/Cargo.toml new file mode 100644 index 0000000000000..bcebe15e24180 --- /dev/null +++ b/crates/uv-netrc/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "uv-netrc" +version = "0.0.47" +description = "A vendored netrc parser for uv" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = "MIT" + +[lib] +name = "uv_netrc" +doctest = false + +[lints] +workspace = true + +[dependencies] +shellexpand = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +fs-err = { workspace = true } +temp-env = { workspace = true } diff --git a/crates/uv-netrc/LICENSE b/crates/uv-netrc/LICENSE new file mode 100644 index 0000000000000..472a683c79f03 --- /dev/null +++ b/crates/uv-netrc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Gribouille + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/uv-netrc/README.md b/crates/uv-netrc/README.md new file mode 100644 index 0000000000000..6ca857a2dab8e --- /dev/null +++ b/crates/uv-netrc/README.md @@ -0,0 +1,14 @@ +# uv-netrc + +This crate vendors the [`rust-netrc`](https://crates.io/crates/rust-netrc) parser for use by uv. + +The source was vendored from +[`gribouille/netrc`](https://github.com/gribouille/netrc/tree/e5f96cc9cc78931b949b48c0758f15da331e9761), +as published in `rust-netrc` 0.1.2 with checksum +`7e98097f62769f92dbf95fb51f71c0a68ec18a4ee2e70e0d3e4f47ac005d63e9`. + +The package is named `uv-netrc`, and the library target is `uv_netrc`. + +## License + +The vendored source is licensed under MIT. See [LICENSE](./LICENSE). diff --git a/crates/uv-netrc/src/lex.rs b/crates/uv-netrc/src/lex.rs new file mode 100644 index 0000000000000..bd4cc7edcf7ed --- /dev/null +++ b/crates/uv-netrc/src/lex.rs @@ -0,0 +1,89 @@ +use std::collections::VecDeque; +use std::str::Chars; + +pub(crate) struct Lex<'a> { + pub(crate) lineno: u32, + instream: Chars<'a>, + pushback: VecDeque, +} + +impl<'a> Lex<'a> { + pub(crate) fn new(content: &'a str) -> Self { + Lex { + lineno: 1, + instream: content.chars(), + pushback: VecDeque::new(), + } + } + + fn read_char(&mut self) -> Option { + let ch = self.instream.next(); + if ch == Some('\n') { + self.lineno += 1; + } + ch + } + + pub(crate) fn read_line(&mut self) -> String { + let mut s = String::new(); + for ch in &mut self.instream { + if ch == '\n' { + return s; + } + s.push(ch); + } + s + } + + pub(crate) fn get_token(&mut self) -> String { + let p = self.pushback.pop_front(); + if let Some(x) = p { + return x; + } + let mut token = String::new(); + + while let Some(ch) = self.read_char() { + match ch { + '\n' | '\t' | '\r' | ' ' => {} + '"' => { + while let Some(ch) = self.read_char() { + match ch { + '"' => { + return token; + } + '\\' => { + token.push(self.read_char().unwrap_or(' ')); + } + _ => { + token.push(ch); + } + } + } + } + _ => { + let c = if ch == '\\' { + self.read_char().unwrap_or(' ') + } else { + ch + }; + token.push(c); + while let Some(ch) = self.read_char() { + let c = match ch { + '\n' | '\t' | '\r' | ' ' => { + return token; + } + '\\' => self.read_char().unwrap_or(' '), + _ => ch, + }; + token.push(c); + } + } + } + } + token + } + + pub(crate) fn push_token(&mut self, token: &str) { + self.pushback.push_back(token.to_owned()); + } +} diff --git a/crates/uv-netrc/src/lib.rs b/crates/uv-netrc/src/lib.rs new file mode 100644 index 0000000000000..f84d040d93375 --- /dev/null +++ b/crates/uv-netrc/src/lib.rs @@ -0,0 +1,177 @@ +/*! +The `uv-netrc` crate provides a parser for the netrc file. + +# Setup + +```text +$ cargo add uv-netrc +``` + +# Example + +```ignore +use uv_netrc::Netrc; + +// ... + +let nrc = Netrc::new().unwrap(); + +// ... +println!( + "login = {}\naccount = {}\npassword = {}", + nrc.hosts["my.host"].login, + nrc.hosts["my.host"].account, + nrc.hosts["my.host"].password, +); +``` + +*/ + +pub use netrc::{Authenticator, Netrc}; +use std::fs; +use std::io; +use std::io::ErrorKind; +#[cfg(windows)] +use std::iter::repeat; +use std::path::{Path, PathBuf}; +use std::result; + +mod lex; +mod netrc; + +pub type Result = result::Result; + +/// An error that can occur when processing a Netrc file. +#[derive(thiserror::Error, Debug)] +pub enum Error { + /// Wrap `std::io::Error` when we try to open the netrc file. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + /// Parsing error. + #[error("{parser} in the file '{filename}'")] + Parsing { + parser: netrc::ParsingError, + filename: String, + }, +} + +impl Netrc { + /// Create a new `Netrc` object. + /// + /// Look up the `NETRC` environment variable if it is defined else that the + /// default `~/.netrc` file. + pub fn new() -> Result { + Self::get_file() + .ok_or(Error::Io(io::Error::new( + ErrorKind::NotFound, + "no netrc file found", + ))) + .and_then(|f| Self::from_file(f.as_path())) + } + + /// Create a new `Netrc` object from a file. + #[expect( + clippy::disallowed_methods, + reason = "Preserve the upstream I/O error surface." + )] + pub fn from_file(file: &Path) -> Result { + String::from_utf8_lossy(&fs::read(file)?) + .parse() + .map_err(|e| Error::Parsing { + parser: e, + filename: file.display().to_string(), + }) + } + + /// Search a netrc file. + /// + /// Look up the `NETRC` environment variable if it is defined else use the .netrc (or _netrc + /// file on windows) in the user's home directory. + pub fn get_file() -> Option { + let env_var = std::env::var("NETRC") + .map(PathBuf::from) + .map(|f| shellexpand::path::tilde(&f).into_owned()); + + #[cfg(windows)] + let default = std::env::var("USERPROFILE") + .into_iter() + .flat_map(|home| repeat(home).zip([".netrc", "_netrc"])) + .map(|(home, file)| PathBuf::from(home).join(file)); + + #[cfg(not(windows))] + let default = std::env::var("HOME").map(|home| PathBuf::from(home).join(".netrc")); + + env_var.into_iter().chain(default).find(|f| f.exists()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + static NETRC_COUNTER: AtomicUsize = AtomicUsize::new(0); + + const CONTENT: &str = "\ +machine cocolog-nifty.com +login jmarten0 +password cC2&yt7ZX + +machine wired.com +login mstanlack1 +password gH4={wx=>VixU + +machine joomla.org +login mbutterley2 +password hY5>yKqU&$vq&0 +"; + + fn create_netrc_file() -> PathBuf { + let id = NETRC_COUNTER.fetch_add(1, Ordering::Relaxed); + let dest = std::env::temp_dir().join(format!("mynetrc-{}-{id}", std::process::id())); + fs_err::write(&dest, CONTENT).unwrap(); + dest + } + + fn check_nrc(nrc: &Netrc) { + assert_eq!(nrc.hosts.len(), 3); + assert_eq!( + nrc.hosts["cocolog-nifty.com"], + Authenticator::new("jmarten0", "", "cC2&yt7ZX") + ); + assert_eq!( + nrc.hosts["wired.com"], + Authenticator::new("mstanlack1", "", "gH4={wx=>VixU") + ); + assert_eq!( + nrc.hosts["joomla.org"], + Authenticator::new("mbutterley2", "", "hY5>yKqU&$vq&0") + ); + } + + #[test] + fn test_new_env() { + let fi = create_netrc_file(); + temp_env::with_var("NETRC", Some(fi.as_os_str()), || { + let nrc = Netrc::new().unwrap(); + check_nrc(&nrc); + }); + } + + #[test] + fn test_from_file_failed() { + let err = Netrc::from_file(Path::new("/netrc/file/not/exists/on/no/netrc")).unwrap_err(); + assert!( + matches!(&err, Error::Io(err) if err.kind() == ErrorKind::NotFound), + "expected NotFound I/O error, got {err}", + ); + } + + #[test] + fn test_from_file() { + let fi = create_netrc_file(); + let nrc = Netrc::from_file(fi.as_path()).unwrap(); + check_nrc(&nrc); + } +} diff --git a/crates/uv-netrc/src/netrc.rs b/crates/uv-netrc/src/netrc.rs new file mode 100644 index 0000000000000..905e0700ea5d8 --- /dev/null +++ b/crates/uv-netrc/src/netrc.rs @@ -0,0 +1,612 @@ +//! This parser and the tests are a translation of the official Python netrc library. + +use crate::lex::Lex; +use std::collections::HashMap; + +#[derive(Debug)] +pub struct ParsingError { + lineno: u32, + message: String, +} + +impl std::fmt::Display for ParsingError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "parsing error: {} (line {})", self.message, self.lineno) + } +} + +/// Authenticators for host. +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Authenticator { + /// Identify a user on the remote machine. + pub login: String, + + /// Supply an additional account password. + pub account: String, + + /// Supply a password + pub password: String, +} + +impl Authenticator { + #[allow(dead_code)] + pub fn new(login: &str, account: &str, password: &str) -> Self { + Self { + login: login.to_owned(), + account: account.to_owned(), + password: password.to_owned(), + } + } +} + +/// Represents the netrc file. +#[derive(Debug, Default)] +pub struct Netrc { + /// Dictionary mapping host names to the authenticators. + pub hosts: HashMap, + + /// Dictionary mapping macro names to string lists. + pub macros: HashMap>, +} + +impl std::fmt::Display for Netrc { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (host, attrs) in &self.hosts { + writeln!(f, "machine {host}")?; + writeln!(f, "\tlogin {}", attrs.login)?; + if !attrs.account.is_empty() { + writeln!(f, "\taccount {}", attrs.account)?; + } + writeln!(f, "\tpassword {}", attrs.password)?; + } + for (macro_, lines) in &self.macros { + writeln!(f, "macdef {macro_}")?; + for line in lines { + writeln!(f, "{line}")?; + } + } + Ok(()) + } +} + +impl std::str::FromStr for Netrc { + type Err = ParsingError; + + fn from_str(s: &str) -> Result { + let mut res = Self::default(); + let mut lexer = Lex::new(s); + + loop { + let saved_lineno = lexer.lineno; + let tt = lexer.get_token(); + if tt.is_empty() { + break; + } + if tt.chars().nth(0) == Some('#') { + if lexer.lineno == saved_lineno && tt.len() == 1 { + lexer.read_line(); + } + continue; + } + + let entryname = match tt.as_str() { + "" => { + break; + } + "machine" => lexer.get_token(), + "default" => String::from("default"), + "macdef" => { + let entryname = lexer.get_token(); + let mut v = Vec::new(); + loop { + let line = lexer.read_line(); + if line.trim().is_empty() { + break; + } + v.push(line.trim().to_owned()); + } + res.macros.insert(entryname, v); + continue; + } + _ => { + return Err(ParsingError { + lineno: lexer.lineno, + message: format!("bad toplevel token '{tt}'"), + }); + } + }; + if entryname.is_empty() { + return Err(ParsingError { + lineno: lexer.lineno, + message: format!("missing '{tt}' name"), + }); + } + + let mut auth = Authenticator::default(); + + loop { + let prev_lineno = lexer.lineno; + let tt = lexer.get_token(); + if tt.starts_with('#') { + if lexer.lineno == prev_lineno { + lexer.read_line(); + } + continue; + } + match tt.as_str() { + "" | "machine" | "default" | "macdef" => { + res.hosts.insert(entryname, auth); + lexer.push_token(&tt); + break; + } + "login" | "user" => { + auth.login = lexer.get_token(); + } + "account" => { + auth.account = lexer.get_token(); + } + "password" => { + auth.password = lexer.get_token(); + } + _ => { + return Err(ParsingError { + lineno: lexer.lineno, + message: format!("bad follower token '{tt}'"), + }); + } + } + } + } + + Ok(res) + } +} + +#[cfg(test)] +#[expect( + clippy::needless_raw_string_hashes, + reason = "Keep the vendored parser tests close to upstream." +)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[test] + fn test_toplevel_non_ordered_tokens() { + let nrc = Netrc::from_str( + "\ + machine host.domain.com password pass1 login log1 account acct1 + default login log2 password pass2 account acct2 + ", + ) + .unwrap(); + + assert_eq!( + nrc.hosts["host.domain.com"], + Authenticator::new("log1", "acct1", "pass1") + ); + assert_eq!( + nrc.hosts["default"], + Authenticator::new("log2", "acct2", "pass2") + ); + } + + #[test] + fn test_toplevel_tokens() { + let nrc = Netrc::from_str( + "\ + machine host.domain.com login log1 password pass1 account acct1 + default login log2 password pass2 account acct2 + ", + ) + .unwrap(); + assert_eq!( + nrc.hosts["host.domain.com"], + Authenticator::new("log1", "acct1", "pass1") + ); + assert_eq!( + nrc.hosts["default"], + Authenticator::new("log2", "acct2", "pass2") + ); + } + + #[test] + fn test_macros() { + let nrc = Netrc::from_str( + "\ + macdef macro1 + line1 + line2 + + macdef macro2 + line3 + line4 + ", + ) + .unwrap(); + assert_eq!(nrc.macros["macro1"], vec!["line1", "line2"]); + assert_eq!(nrc.macros["macro2"], vec!["line3", "line4"]); + } + + #[test] + fn test_optional_tokens_machine() { + let data = vec![ + "machine host.domain.com", + "machine host.domain.com login", + "machine host.domain.com account", + "machine host.domain.com password", + "machine host.domain.com login \"\" account", + "machine host.domain.com login \"\" password", + "machine host.domain.com account \"\" password", + ]; + + for item in data { + let nrc = Netrc::from_str(item).unwrap(); + assert_eq!(nrc.hosts["host.domain.com"], Authenticator::new("", "", "")); + } + } + + #[test] + fn test_optional_tokens_default() { + let data = vec![ + "default", + "default login", + "default account", + "default password", + "default login \"\" account", + "default login \"\" password", + "default account \"\" password", + ]; + + for item in data { + let nrc = Netrc::from_str(item).unwrap(); + assert_eq!(nrc.hosts["default"], Authenticator::new("", "", "")); + } + } + + #[test] + fn test_invalid_tokens() { + let data = vec![ + ( + "invalid host.domain.com", + "parsing error: bad toplevel token 'invalid' (line 1)", + ), + ( + "machine host.domain.com invalid", + "parsing error: bad follower token 'invalid' (line 1)", + ), + ( + "machine host.domain.com login log password pass account acct invalid", + "parsing error: bad follower token 'invalid' (line 1)", + ), + ( + "default host.domain.com invalid", + "parsing error: bad follower token 'host.domain.com' (line 1)", + ), + ( + "default host.domain.com login log password pass account acct invalid", + "parsing error: bad follower token 'host.domain.com' (line 1)", + ), + ]; + + for (item, msg) in data { + let nrc = Netrc::from_str(item); + assert_eq!(nrc.unwrap_err().to_string(), msg); + } + } + + fn test_token_x(data: &str, token: &str, value: &str) { + let nrc = Netrc::from_str(data).unwrap(); + match token { + "login" => { + assert_eq!( + nrc.hosts["host.domain.com"], + Authenticator::new(value, "acct", "pass") + ); + } + "account" => { + assert_eq!( + nrc.hosts["host.domain.com"], + Authenticator::new("log", value, "pass") + ); + } + "password" => { + assert_eq!( + nrc.hosts["host.domain.com"], + Authenticator::new("log", "acct", value) + ); + } + _ => {} + } + } + + #[test] + fn test_token_value_quotes() { + test_token_x( + "\ + machine host.domain.com login \"log\" password pass account acct + ", + "login", + "log", + ); + test_token_x( + "\ + machine host.domain.com login log password pass account \"acct\" + ", + "account", + "acct", + ); + test_token_x( + "\ + machine host.domain.com login log password \"pass\" account acct + ", + "password", + "pass", + ); + } + + #[test] + fn test_token_value_escape() { + test_token_x( + r#"machine host.domain.com login \"log password pass account acct"#, + "login", + "\"log", + ); + test_token_x( + "\ + machine host.domain.com login \"\\\"log\" password pass account acct + ", + "login", + "\"log", + ); + test_token_x( + "\ + machine host.domain.com login log password pass account \\\"acct + ", + "account", + "\"acct", + ); + test_token_x( + "\ + machine host.domain.com login log password pass account \"\\\"acct\" + ", + "account", + "\"acct", + ); + test_token_x( + "\ + machine host.domain.com login log password \\\"pass account acct + ", + "password", + "\"pass", + ); + test_token_x( + "\ + machine host.domain.com login log password \"\\\"pass\" account acct + ", + "password", + "\"pass", + ); + } + + #[test] + fn test_token_value_whitespace() { + test_token_x( + r#"machine host.domain.com login "lo g" password pass account acct"#, + "login", + "lo g", + ); + test_token_x( + r#"machine host.domain.com login log password "pas s" account acct"#, + "password", + "pas s", + ); + test_token_x( + r#"machine host.domain.com login log password pass account "acc t""#, + "account", + "acc t", + ); + } + + #[test] + fn test_token_value_non_ascii() { + test_token_x( + r#"machine host.domain.com login ¡¢ password pass account acct"#, + "login", + "¡¢", + ); + test_token_x( + r#"machine host.domain.com login log password pass account ¡¢"#, + "account", + "¡¢", + ); + test_token_x( + r#"machine host.domain.com login log password ¡¢ account acct"#, + "password", + "¡¢", + ); + } + + #[test] + fn test_token_value_leading_hash() { + test_token_x( + r#"machine host.domain.com login #log password pass account acct"#, + "login", + "#log", + ); + test_token_x( + r#"machine host.domain.com login log password pass account #acct"#, + "account", + "#acct", + ); + test_token_x( + r#"machine host.domain.com login log password #pass account acct"#, + "password", + "#pass", + ); + } + + #[test] + fn test_token_value_trailing_hash() { + test_token_x( + r#"machine host.domain.com login log# password pass account acct"#, + "login", + "log#", + ); + test_token_x( + r#"machine host.domain.com login log password pass account acct#"#, + "account", + "acct#", + ); + test_token_x( + r#"machine host.domain.com login log password pass# account acct"#, + "password", + "pass#", + ); + } + + #[test] + fn test_token_value_internal_hash() { + test_token_x( + r#"machine host.domain.com login lo#g password pass account acct"#, + "login", + "lo#g", + ); + test_token_x( + r#"machine host.domain.com login log password pass account ac#ct"#, + "account", + "ac#ct", + ); + test_token_x( + r#"machine host.domain.com login log password pa#ss account acct"#, + "password", + "pa#ss", + ); + } + + fn test_comment(data: &str) { + let nrc = Netrc::from_str(data).unwrap(); + assert_eq!( + nrc.hosts["foo.domain.com"], + Authenticator::new("bar", "", "pass") + ); + assert_eq!( + nrc.hosts["bar.domain.com"], + Authenticator::new("foo", "", "pass") + ); + } + + #[test] + fn test_comment_before_machine_line() { + test_comment( + r#"# comment + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + "#, + ); + } + #[test] + fn test_comment_before_machine_line_no_space() { + test_comment( + r#"#comment + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + "#, + ); + } + + #[test] + fn test_comment_before_machine_line_hash_only() { + test_comment( + r#"# + machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + "#, + ); + } + + #[test] + fn test_comment_after_machine_line() { + test_comment( + r#"machine foo.domain.com login bar password pass + # comment + machine bar.domain.com login foo password pass + "#, + ); + test_comment( + r#"machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + # comment + "#, + ); + } + + #[test] + fn test_comment_after_machine_line_no_space() { + test_comment( + r#"machine foo.domain.com login bar password pass + #comment + machine bar.domain.com login foo password pass + "#, + ); + test_comment( + r#"machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + #comment + "#, + ); + } + + #[test] + fn test_comment_after_machine_line_hash_only() { + test_comment( + r#"machine foo.domain.com login bar password pass + # + machine bar.domain.com login foo password pass + "#, + ); + test_comment( + r#"machine foo.domain.com login bar password pass + machine bar.domain.com login foo password pass + # + "#, + ); + } + + #[test] + fn test_comment_at_end_of_machine_line() { + test_comment( + r#"machine foo.domain.com login bar password pass # comment + machine bar.domain.com login foo password pass + "#, + ); + } + + #[test] + fn test_comment_at_end_of_machine_line_no_space() { + test_comment( + r#"machine foo.domain.com login bar password pass #comment + machine bar.domain.com login foo password pass + "#, + ); + } + + #[test] + fn test_comment_at_end_of_machine_line_pass_has_hash() { + let nrc = Netrc::from_str( + r#"machine foo.domain.com login bar password #pass #comment + machine bar.domain.com login foo password pass + "#, + ) + .unwrap(); + assert_eq!( + nrc.hosts["foo.domain.com"], + Authenticator::new("bar", "", "#pass") + ); + assert_eq!( + nrc.hosts["bar.domain.com"], + Authenticator::new("foo", "", "pass") + ); + } +} From a151ff400861544c21fcbe18c3e80c26fcde3484 Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Fri, 15 May 2026 14:12:05 +0100 Subject: [PATCH 20/56] Move `cache_name` to uv-cache-key (#19414) ## Summary A small refactor which aims to slim down the centralised envs PR. I also think it makes more sense for this to be in the uv-cache-key crate. Also add an optional parameter for length limits (currently unused, specifically for centralised envs). ## Test Plan Existing tests + new tests. --- crates/uv-cache-key/src/digest.rs | 80 +++++++++++++++++++++++++++ crates/uv-cache-key/src/lib.rs | 2 +- crates/uv/src/commands/project/mod.rs | 58 +------------------ 3 files changed, 83 insertions(+), 57 deletions(-) diff --git a/crates/uv-cache-key/src/digest.rs b/crates/uv-cache-key/src/digest.rs index a2387868d03a0..5afffd2cba59e 100644 --- a/crates/uv-cache-key/src/digest.rs +++ b/crates/uv-cache-key/src/digest.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::hash::{Hash, Hasher}; use seahash::SeaHasher; @@ -34,3 +35,82 @@ pub fn hash_digest(hashable: &H) -> String { fn to_hex(num: u64) -> String { hex::encode(num.to_le_bytes()) } + +/// Normalize a name for use in a cache entry. +/// +/// Replaces non-alphanumeric characters with dashes, and lowercases the name. +/// +/// If `max_len` is provided, the output is truncated to at most that many bytes +/// (trailing dashes from truncation are stripped). +pub fn cache_name(name: &str, max_len: Option) -> Option> { + let limit = max_len.unwrap_or(usize::MAX); + + if name + .bytes() + .all(|char| matches!(char, b'0'..=b'9' | b'a'..=b'f')) + { + return if name.is_empty() { + None + } else { + Some(Cow::Borrowed(name.get(..limit).unwrap_or(name))) + }; + } + let mut normalized = String::with_capacity(name.len().min(limit)); + let mut dash = false; + for char in name.bytes().take(limit) { + match char { + b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => { + dash = false; + normalized.push(char.to_ascii_lowercase() as char); + } + _ => { + if !dash { + normalized.push('-'); + dash = true; + } + } + } + } + if normalized.ends_with('-') { + normalized.pop(); + } + if normalized.is_empty() { + None + } else { + Some(Cow::Owned(normalized)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cache_name() { + assert_eq!(cache_name("foo", None), Some("foo".into())); + assert_eq!(cache_name("foo-bar", None), Some("foo-bar".into())); + assert_eq!(cache_name("foo_bar", None), Some("foo-bar".into())); + assert_eq!(cache_name("foo-bar_baz", None), Some("foo-bar-baz".into())); + assert_eq!(cache_name("foo-bar_baz_", None), Some("foo-bar-baz".into())); + assert_eq!(cache_name("foo-_bar_baz", None), Some("foo-bar-baz".into())); + assert_eq!(cache_name("_+-_", None), None); + } + + #[test] + fn test_cache_name_max_len() { + let long = "a".repeat(300); + assert_eq!( + cache_name(&long, Some(100)).as_deref().map(str::len), + Some(100) + ); + + let long_hex = "abcdef".repeat(50); + assert_eq!( + cache_name(&long_hex, Some(100)).as_deref().map(str::len), + Some(100) + ); + + assert_eq!(cache_name("aaaa_bbbb", Some(5)), Some("aaaa".into())); + assert_eq!(cache_name(&long, None).as_deref().map(str::len), Some(300)); + } +} diff --git a/crates/uv-cache-key/src/lib.rs b/crates/uv-cache-key/src/lib.rs index 8ada055795864..848656ffc3a3f 100644 --- a/crates/uv-cache-key/src/lib.rs +++ b/crates/uv-cache-key/src/lib.rs @@ -1,6 +1,6 @@ pub use cache_key::{CacheKey, CacheKeyHasher}; pub use canonical_url::{CanonicalUrl, RepositoryUrl}; -pub use digest::{cache_digest, hash_digest}; +pub use digest::{cache_digest, cache_name, hash_digest}; mod cache_key; mod canonical_url; diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 5a53d2f42568c..200be3e73ce49 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write; use std::path::{Path, PathBuf}; @@ -9,7 +8,7 @@ use owo_colors::OwoColorize; use tracing::{debug, trace, warn}; use uv_auth::CredentialsCache; use uv_cache::{Cache, CacheBucket}; -use uv_cache_key::cache_digest; +use uv_cache_key::{cache_digest, cache_name}; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ Concurrency, Constraints, DependencyGroupsWithDefaults, DryRun, ExtrasSpecification, @@ -664,7 +663,7 @@ impl ScriptInterpreter { .path .file_stem() .and_then(|name| name.to_str()) - .and_then(cache_name) + .and_then(|name| cache_name(name, None)) { format!("{file_name}-{digest}") } else { @@ -2960,43 +2959,6 @@ fn warn_on_requirements_txt_setting(spec: &RequirementsSpecification, settings: } } -/// Normalize a filename for use in a cache entry. -/// -/// Replaces non-alphanumeric characters with dashes, and lowercases the filename. -fn cache_name(name: &str) -> Option> { - if name.bytes().all(|c| matches!(c, b'0'..=b'9' | b'a'..=b'f')) { - return if name.is_empty() { - None - } else { - Some(Cow::Borrowed(name)) - }; - } - let mut normalized = String::with_capacity(name.len()); - let mut dash = false; - for char in name.bytes() { - match char { - b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' => { - dash = false; - normalized.push(char.to_ascii_lowercase() as char); - } - _ => { - if !dash { - normalized.push('-'); - dash = true; - } - } - } - } - if normalized.ends_with('-') { - normalized.pop(); - } - if normalized.is_empty() { - None - } else { - Some(Cow::Owned(normalized)) - } -} - fn format_requires_python_sources(conflicts: &RequiresPythonSources) -> String { conflicts .iter() @@ -3040,19 +3002,3 @@ fn format_optional_requires_python_sources( // Otherwise don't elaborate String::new() } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cache_name() { - assert_eq!(cache_name("foo"), Some("foo".into())); - assert_eq!(cache_name("foo-bar"), Some("foo-bar".into())); - assert_eq!(cache_name("foo_bar"), Some("foo-bar".into())); - assert_eq!(cache_name("foo-bar_baz"), Some("foo-bar-baz".into())); - assert_eq!(cache_name("foo-bar_baz_"), Some("foo-bar-baz".into())); - assert_eq!(cache_name("foo-_bar_baz"), Some("foo-bar-baz".into())); - assert_eq!(cache_name("_+-_"), None); - } -} From a4d9e42197d606474f085cff93caa200666512df Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Fri, 15 May 2026 14:33:26 +0100 Subject: [PATCH 21/56] Move `remove_virtualenv` to uv-fs (#19413) ## Summary A small refactor which aims to slim down the centralised envs PR. ## Test Plan Code moved, existing tests. --- Cargo.lock | 2 +- crates/uv-fs/Cargo.toml | 1 + crates/uv-fs/src/lib.rs | 55 ++++++++++++++++++++++++- crates/uv-tool/src/lib.rs | 9 ++--- crates/uv-virtualenv/Cargo.toml | 3 -- crates/uv-virtualenv/src/lib.rs | 2 +- crates/uv-virtualenv/src/virtualenv.rs | 56 +------------------------- crates/uv/src/commands/project/mod.rs | 18 +++------ 8 files changed, 69 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21e7cd7748413..2627b488cd145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6485,6 +6485,7 @@ dependencies = [ "rustix", "same-file", "schemars", + "self-replace", "serde", "tempfile", "thiserror", @@ -7349,7 +7350,6 @@ dependencies = [ "itertools 0.14.0", "owo-colors", "pathdiff", - "self-replace", "thiserror", "tracing", "uv-console", diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 198dc95440563..93f276deeecda 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -44,6 +44,7 @@ rustix = { workspace = true } backon = { workspace = true } junction = { workspace = true } uv-windows = { workspace = true } +self-replace = { workspace = true } windows = { workspace = true } [features] diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 261b949231f32..7691a812cd91c 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -1,3 +1,4 @@ +use std::io; use std::path::{Path, PathBuf}; #[cfg(feature = "tokio")] @@ -6,7 +7,7 @@ use std::io::Read; #[cfg(feature = "tokio")] use encoding_rs_io::DecodeReaderBytes; use tempfile::NamedTempFile; -use tracing::warn; +use tracing::{debug, warn}; pub use crate::locked_file::*; pub use crate::path::*; @@ -835,3 +836,55 @@ pub fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Re } Ok(()) } + +/// Perform a safe removal of a virtual environment. +pub fn remove_virtualenv(location: &Path) -> io::Result<()> { + // On Windows, if the current executable is in the directory, defer self-deletion since Windows + // won't let you unlink a running executable. + #[cfg(windows)] + if let Ok(itself) = std::env::current_exe() { + let target = std::path::absolute(location)?; + if itself.starts_with(&target) { + debug!("Detected self-delete of executable: {}", itself.display()); + self_replace::self_delete_outside_path(location)?; + } + } + + // We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment, + // uv can still identify it as a Python virtual environment that can be deleted. + for entry in fs_err::read_dir(location)? { + let entry = entry?; + let path = entry.path(); + if path == location.join("pyvenv.cfg") { + continue; + } + if path.is_dir() { + fs_err::remove_dir_all(&path)?; + } else { + fs_err::remove_file(&path)?; + } + } + + match fs_err::remove_file(location.join("pyvenv.cfg")) { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => return Err(err), + } + + // Remove the virtual environment directory itself + match fs_err::remove_dir_all(location) { + Ok(()) => {} + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + // If the virtual environment is a mounted file system, e.g., in a Docker container, we + // cannot delete it — but that doesn't need to be a fatal error + Err(err) if err.kind() == io::ErrorKind::ResourceBusy => { + debug!( + "Skipping removal of `{}` directory due to {err}", + location.display(), + ); + } + Err(err) => return Err(err), + } + + Ok(()) +} diff --git a/crates/uv-tool/src/lib.rs b/crates/uv-tool/src/lib.rs index 3e0a11bae4d53..09cd29b0b35dd 100644 --- a/crates/uv-tool/src/lib.rs +++ b/crates/uv-tool/src/lib.rs @@ -18,7 +18,6 @@ use uv_pep440::Version; use uv_python::{BrokenLink, Interpreter, PythonEnvironment}; use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; -use uv_virtualenv::remove_virtualenv; pub use receipt::ToolReceipt; pub use tool::{Tool, ToolEntrypoint}; @@ -251,7 +250,7 @@ impl InstalledTools { environment_path.user_display() ); - remove_virtualenv(environment_path.as_path())?; + uv_fs::remove_virtualenv(environment_path.as_path()).map_err(uv_virtualenv::Error::from)?; Ok(()) } @@ -324,15 +323,15 @@ impl InstalledTools { let environment_path = self.tool_dir(name); // Remove any existing environment. - match remove_virtualenv(&environment_path) { + match uv_fs::remove_virtualenv(&environment_path) { Ok(()) => { debug!( "Removed existing environment for tool `{name}`: {}", environment_path.user_display() ); } - Err(uv_virtualenv::Error::Io(err)) if err.kind() == io::ErrorKind::NotFound => (), - Err(err) => return Err(err.into()), + Err(err) if err.kind() == io::ErrorKind::NotFound => (), + Err(err) => return Err(uv_virtualenv::Error::from(err).into()), } debug!( diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index 9d57f3a7c8259..d0776574efc36 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -34,6 +34,3 @@ owo-colors = { workspace = true } pathdiff = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } - -[target.'cfg(target_os = "windows")'.dependencies] -self-replace = { workspace = true } diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index 79f1168b7c470..93b553bef0e46 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -5,7 +5,7 @@ use thiserror::Error; use uv_python::{Interpreter, PythonEnvironment}; -pub use virtualenv::{OnExisting, RemovalReason, remove_virtualenv}; +pub use virtualenv::{OnExisting, RemovalReason}; mod virtualenv; diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 68bec538ef023..ba6b8a3f1b3f8 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -142,7 +142,7 @@ pub(crate) fn create( let location = location .canonicalize() .unwrap_or_else(|_| location.to_path_buf()); - remove_virtualenv(&location)?; + uv_fs::remove_virtualenv(&location)?; fs_err::create_dir_all(&location)?; } OnExisting::Fail => return err, @@ -158,7 +158,7 @@ pub(crate) fn create( let location = location .canonicalize() .unwrap_or_else(|_| location.to_path_buf()); - remove_virtualenv(&location)?; + uv_fs::remove_virtualenv(&location)?; fs_err::create_dir_all(&location)?; } Some(false) => return err, @@ -610,58 +610,6 @@ fn confirm_clear(location: &Path, name: &'static str) -> Result, io } } -/// Perform a safe removal of a virtual environment. -pub fn remove_virtualenv(location: &Path) -> Result<(), Error> { - // On Windows, if the current executable is in the directory, defer self-deletion since Windows - // won't let you unlink a running executable. - #[cfg(windows)] - if let Ok(itself) = std::env::current_exe() { - let target = std::path::absolute(location)?; - if itself.starts_with(&target) { - debug!("Detected self-delete of executable: {}", itself.display()); - self_replace::self_delete_outside_path(location)?; - } - } - - // We defer removal of the `pyvenv.cfg` until the end, so if we fail to remove the environment, - // uv can still identify it as a Python virtual environment that can be deleted. - for entry in fs_err::read_dir(location)? { - let entry = entry?; - let path = entry.path(); - if path == location.join("pyvenv.cfg") { - continue; - } - if path.is_dir() { - fs_err::remove_dir_all(&path)?; - } else { - fs_err::remove_file(&path)?; - } - } - - match fs_err::remove_file(location.join("pyvenv.cfg")) { - Ok(()) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), - } - - // Remove the virtual environment directory itself - match fs_err::remove_dir_all(location) { - Ok(()) => {} - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - // If the virtual environment is a mounted file system, e.g., in a Docker container, we - // cannot delete it — but that doesn't need to be a fatal error - Err(err) if err.kind() == io::ErrorKind::ResourceBusy => { - debug!( - "Skipping removal of `{}` directory due to {err}", - location.display(), - ); - } - Err(err) => return Err(err.into()), - } - - Ok(()) -} - #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum RemovalReason { /// The removal was explicitly requested, i.e., with `--clear`. diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 200be3e73ce49..289da6704b0db 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -44,7 +44,6 @@ use uv_settings::PythonInstallMirrors; use uv_static::EnvVars; use uv_torch::{TorchSource, TorchStrategy}; use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceTreeEditablePolicy}; -use uv_virtualenv::remove_virtualenv; use uv_warnings::{warn_user, warn_user_once}; use uv_workspace::dependency_groups::DependencyGroupError; use uv_workspace::pyproject::{ExtraBuildDependency, PyProjectToml}; @@ -1508,7 +1507,7 @@ impl ProjectEnvironment { // Remove the existing virtual environment if it doesn't meet the requirements. if replace { - match remove_virtualenv(&root) { + match uv_fs::remove_virtualenv(&root) { Ok(()) => { writeln!( printer.stderr(), @@ -1516,9 +1515,8 @@ impl ProjectEnvironment { root.user_display().cyan() )?; } - Err(uv_virtualenv::Error::Io(err)) - if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(err.into()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} + Err(err) => return Err(uv_virtualenv::Error::from(err).into()), } } @@ -1700,7 +1698,7 @@ impl ScriptEnvironment { } // Remove the existing virtual environment. - let replaced = match remove_virtualenv(&root) { + let replaced = match uv_fs::remove_virtualenv(&root) { Ok(()) => { debug!( "Removed virtual environment at: {}", @@ -1708,12 +1706,8 @@ impl ScriptEnvironment { ); true } - Err(uv_virtualenv::Error::Io(err)) - if err.kind() == std::io::ErrorKind::NotFound => - { - false - } - Err(err) => return Err(err.into()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => false, + Err(err) => return Err(uv_virtualenv::Error::from(err).into()), }; debug!( From 30a9143a13a43bcbc2cf0caf209e54614f3d4d2a Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 15 May 2026 20:33:36 +0100 Subject: [PATCH 22/56] Add support for Azure request signing (#19421) ## Summary This follows the pattern we use for GCS and AWS: set `UV_AZURE_ENDPOINT_URL`, then we use `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, and `AZURE_FEDERATED_TOKEN_FILE` to sign requests to that endpoint (or the CLI authentication, based on `reqsign`'s default precedence). Closes https://github.com/astral-sh/uv/issues/19420. --- Cargo.lock | 23 ++++++++ Cargo.toml | 1 + crates/uv-auth/src/credentials.rs | 91 ++++++++++++++++++++++++++--- crates/uv-auth/src/middleware.rs | 38 +++++++++++- crates/uv-auth/src/providers.rs | 43 +++++++++++++- crates/uv-preview/src/lib.rs | 4 ++ crates/uv-static/src/env_vars.rs | 6 ++ crates/uv/tests/it/show_settings.rs | 2 + docs/concepts/preview.md | 1 + 9 files changed, 200 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2627b488cd145..0d05b93d7ad82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3799,6 +3799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f343280e2e5ce07f97f32ead07a68824cb9cea60093ad78f22664011f90ae47" dependencies = [ "reqsign-aws-v4", + "reqsign-azure-storage", "reqsign-command-execute-tokio", "reqsign-core", "reqsign-file-read-tokio", @@ -3827,6 +3828,28 @@ dependencies = [ "sha1", ] +[[package]] +name = "reqsign-azure-storage" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a321980405d596bd34aaf95c4722a3de4128a67fd19e74a81a83aa3fdf082e6" +dependencies = [ + "anyhow", + "base64", + "bytes", + "form_urlencoded", + "http", + "jsonwebtoken", + "log", + "pem", + "percent-encoding", + "reqsign-core", + "rsa", + "serde", + "serde_json", + "sha1", +] + [[package]] name = "reqsign-command-execute-tokio" version = "3.0.0" diff --git a/Cargo.toml b/Cargo.toml index 97d5ce83d59ca..f440145702531 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,6 +203,7 @@ regex-automata = { version = "0.4.8", default-features = false, features = [ ] } reqsign = { version = "0.20.0", features = [ "aws", + "azure", "google", "default-context", ], default-features = false } diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index f7a7257d44f2e..e02319dff5967 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -9,9 +9,10 @@ use base64::read::DecoderReader; use base64::write::EncoderWriter; use http::Uri; use reqsign::aws::DefaultSigner as AwsDefaultSigner; +use reqsign::azure::DefaultSigner as AzureDefaultSigner; use reqsign::google::DefaultSigner as GcsDefaultSigner; use reqwest::Request; -use reqwest::header::HeaderValue; +use reqwest::header::{HeaderName, HeaderValue}; use serde::{Deserialize, Serialize}; use url::Url; @@ -19,6 +20,8 @@ use uv_netrc::Netrc; use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; +const AZURE_STORAGE_VERSION: &str = "2023-11-03"; + #[derive(Clone, Debug, PartialEq, Eq)] pub enum Credentials { /// RFC 7617 HTTP Basic Authentication @@ -393,6 +396,9 @@ pub(crate) enum Authentication { /// Google Cloud signing. GcsSigner(GcsDefaultSigner), + + /// Azure Storage signing. + AzureSigner(AzureDefaultSigner), } impl PartialEq for Authentication { @@ -401,6 +407,7 @@ impl PartialEq for Authentication { (Self::Credentials(a), Self::Credentials(b)) => a == b, (Self::AwsSigner(..), Self::AwsSigner(..)) => true, (Self::GcsSigner(..), Self::GcsSigner(..)) => true, + (Self::AzureSigner(..), Self::AzureSigner(..)) => true, _ => false, } } @@ -426,12 +433,18 @@ impl From for Authentication { } } +impl From for Authentication { + fn from(signer: AzureDefaultSigner) -> Self { + Self::AzureSigner(signer) + } +} + impl Authentication { /// Return the password used for authentication, if any. pub(crate) fn password(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.password(), - Self::AwsSigner(..) | Self::GcsSigner(..) => None, + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None, } } @@ -439,7 +452,7 @@ impl Authentication { pub(crate) fn username(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.username(), - Self::AwsSigner(..) | Self::GcsSigner(..) => None, + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => None, } } @@ -447,7 +460,9 @@ impl Authentication { pub(crate) fn as_username(&self) -> Cow<'_, Username> { match self { Self::Credentials(credentials) => credentials.as_username(), - Self::AwsSigner(..) | Self::GcsSigner(..) => Cow::Owned(Username::none()), + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => { + Cow::Owned(Username::none()) + } } } @@ -455,7 +470,7 @@ impl Authentication { pub(crate) fn to_username(&self) -> Username { match self { Self::Credentials(credentials) => credentials.to_username(), - Self::AwsSigner(..) | Self::GcsSigner(..) => Username::none(), + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => Username::none(), } } @@ -463,7 +478,7 @@ impl Authentication { pub(crate) fn is_authenticated(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_authenticated(), - Self::AwsSigner(..) | Self::GcsSigner(..) => true, + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => true, } } @@ -471,7 +486,7 @@ impl Authentication { pub(crate) fn is_empty(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_empty(), - Self::AwsSigner(..) | Self::GcsSigner(..) => false, + Self::AwsSigner(..) | Self::GcsSigner(..) | Self::AzureSigner(..) => false, } } @@ -531,6 +546,38 @@ impl Authentication { // Copy over the signed headers. request.headers_mut().extend(parts.headers); + // Copy over the signed path and query, if any. + if let Some(path_and_query) = parts.uri.path_and_query() { + request.url_mut().set_path(path_and_query.path()); + request.url_mut().set_query(path_and_query.query()); + } + request + } + Self::AzureSigner(signer) => { + // Build an `http::Request` from the `reqwest::Request`. + // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. + let uri = Uri::from_str(request.url().as_str()).unwrap(); + let mut http_req = http::Request::builder() + .method(request.method().clone()) + .uri(uri) + .body(()) + .unwrap(); + *http_req.headers_mut() = request.headers().clone(); + http_req + .headers_mut() + .entry(HeaderName::from_static("x-ms-version")) + .or_insert(HeaderValue::from_static(AZURE_STORAGE_VERSION)); + + // Sign the parts. + let (mut parts, ()) = http_req.into_parts(); + signer + .sign(&mut parts, None) + .await + .expect("Azure signing should succeed"); + + // Copy over the signed headers. + request.headers_mut().extend(parts.headers); + // Copy over the signed path and query, if any. if let Some(path_and_query) = parts.uri.path_and_query() { request.url_mut().set_path(path_and_query.path()); @@ -668,6 +715,36 @@ mod tests { assert_eq!(Credentials::from_header_value(&header), Some(credentials)); } + #[tokio::test] + async fn authenticated_request_with_azure_signer() { + let signer = reqsign::azure::default_signer().with_credential_provider( + reqsign::azure::StaticCredentialProvider::new_bearer_token("token"), + ); + let authentication = Authentication::from(signer); + + let request = Request::new( + reqwest::Method::GET, + Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(), + ); + let request = authentication.authenticate(request).await; + + let authorization = request + .headers() + .get(reqwest::header::AUTHORIZATION) + .expect("Authorization header should be set"); + assert_eq!(authorization.to_str().unwrap(), "Bearer token"); + assert!(request.headers().contains_key("x-ms-date")); + assert_eq!( + request + .headers() + .get("x-ms-version") + .expect("x-ms-version header should be set") + .to_str() + .unwrap(), + AZURE_STORAGE_VERSION + ); + } + /// Passwords should be redacted in debug output. #[test] fn test_password_redaction() { diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 0efabab5504ba..33a651a7c0840 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -14,7 +14,9 @@ use uv_static::EnvVars; use uv_warnings::owo_colors::OwoColorize; use crate::credentials::Authentication; -use crate::providers::{GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider}; +use crate::providers::{ + AzureEndpointProvider, GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider, +}; use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::{ AccessToken, CredentialsCache, KeyringProvider, @@ -147,6 +149,15 @@ enum GcsCredentialState { Initialized(Option>), } +#[derive(Clone)] +enum AzureCredentialState { + /// The Azure credential state has not yet been initialized. + Uninitialized, + /// The Azure credential state has been initialized, with either a signer or `None` if + /// no Azure endpoint is configured. + Initialized(Option>), +} + /// A middleware that adds basic authentication to requests. /// /// Uses a cache to propagate credentials from previously seen requests and @@ -172,6 +183,8 @@ pub struct AuthMiddleware { s3_credential_state: Mutex, /// Cached GCS credentials to avoid running the credential helper multiple times. gcs_credential_state: Mutex, + /// Cached Azure credentials to avoid running the credential helper multiple times. + azure_credential_state: Mutex, preview: Preview, } @@ -196,6 +209,7 @@ impl AuthMiddleware { pyx_token_state: Mutex::new(TokenState::Uninitialized), s3_credential_state: Mutex::new(S3CredentialState::Uninitialized), gcs_credential_state: Mutex::new(GcsCredentialState::Uninitialized), + azure_credential_state: Mutex::new(AzureCredentialState::Uninitialized), preview: Preview::default(), } } @@ -750,6 +764,28 @@ impl AuthMiddleware { } } + if AzureEndpointProvider::is_azure_endpoint(url, self.preview) { + let mut azure_state = self.azure_credential_state.lock().await; + + // If the Azure credential state is uninitialized, initialize it. + let credentials = match &*azure_state { + AzureCredentialState::Uninitialized => { + trace!("Initializing Azure credentials for {url}"); + let signer = AzureEndpointProvider::create_signer(); + let credentials = Arc::new(Authentication::from(signer)); + *azure_state = AzureCredentialState::Initialized(Some(credentials.clone())); + Some(credentials) + } + AzureCredentialState::Initialized(credentials) => credentials.clone(), + }; + + if let Some(credentials) = credentials { + debug!("Found Azure credentials for {url}"); + self.cache().fetches.done(key, Some(credentials.clone())); + return Some(credentials); + } + } + // If this is a known URL, authenticate it via the token store. let credentials = if let Some(credentials) = async { let base_client = self.base_client.as_ref()?; diff --git a/crates/uv-auth/src/providers.rs b/crates/uv-auth/src/providers.rs index 9585018b39726..5691c8846c935 100644 --- a/crates/uv-auth/src/providers.rs +++ b/crates/uv-auth/src/providers.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use std::sync::LazyLock; use reqsign::aws::DefaultSigner as AwsDefaultSigner; +use reqsign::azure::DefaultSigner as AzureDefaultSigner; use reqsign::google::DefaultSigner as GcsDefaultSigner; use tracing::debug; use url::Url; @@ -145,7 +146,47 @@ impl GcsEndpointProvider { } } -/// Returns `true` if `url` is within the configured S3 or GCS-compatible endpoint URL. +/// The [`Url`] for the Azure endpoint, if set. +static AZURE_ENDPOINT_URL: LazyLock> = LazyLock::new(|| { + let azure_endpoint_url = std::env::var(EnvVars::UV_AZURE_ENDPOINT_URL).ok()?; + let url = Url::parse(&azure_endpoint_url).expect("Failed to parse Azure endpoint URL"); + Some(url) +}); + +/// A provider for authentication credentials for Azure endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct AzureEndpointProvider; + +impl AzureEndpointProvider { + /// Returns `true` if the URL matches the configured Azure endpoint. + pub(crate) fn is_azure_endpoint(url: &Url, preview: Preview) -> bool { + if let Some(azure_endpoint_url) = AZURE_ENDPOINT_URL.as_ref() { + if !preview.is_enabled(PreviewFeature::AzureEndpoint) { + warn_user_once!( + "The `azure-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeature::AzureEndpoint + ); + } + + // Treat any URL under the endpoint path on the same domain or subdomain as available + // for Azure signing. + if is_endpoint_url(url, azure_endpoint_url) { + return true; + } + } + false + } + + /// Creates a new Azure signer using the default Azure credential chain. + /// + /// This is potentially expensive as it may invoke credential helpers, so the result + /// should be cached. + pub(crate) fn create_signer() -> AzureDefaultSigner { + reqsign::azure::default_signer() + } +} + +/// Returns `true` if `url` is within the configured S3, GCS, or Azure-compatible endpoint URL. /// /// The URL must be in the same realm, or a subdomain of the endpoint realm, and must be under the /// endpoint path using complete path-segment prefix matching. diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index d68b60bea9962..b599687c3ea26 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -256,6 +256,7 @@ pub enum PreviewFeature { Audit = 1 << 26, ProjectDirectoryMustExist = 1 << 27, IndexExcludeNewer = 1 << 28, + AzureEndpoint = 1 << 29, } impl PreviewFeature { @@ -291,6 +292,7 @@ impl PreviewFeature { Self::Audit => "audit", Self::ProjectDirectoryMustExist => "project-directory-must-exist", Self::IndexExcludeNewer => "index-exclude-newer", + Self::AzureEndpoint => "azure-endpoint", } } } @@ -339,6 +341,7 @@ impl FromStr for PreviewFeature { "audit" => Self::Audit, "project-directory-must-exist" => Self::ProjectDirectoryMustExist, "index-exclude-newer" => Self::IndexExcludeNewer, + "azure-endpoint" => Self::AzureEndpoint, _ => return Err(PreviewFeatureParseError), }) } @@ -592,6 +595,7 @@ mod tests { PreviewFeature::IndexExcludeNewer.as_str(), "index-exclude-newer" ); + assert_eq!(PreviewFeature::AzureEndpoint.as_str(), "azure-endpoint"); } #[test] diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 0a3e9f7b0d2c7..97414185c424c 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -1351,6 +1351,12 @@ impl EnvVars { #[attr_added_in("0.9.26")] pub const UV_GCS_ENDPOINT_URL: &'static str = "UV_GCS_ENDPOINT_URL"; + /// The URL to treat as an Azure Blob Storage endpoint. Requests to this endpoint will be signed + /// using Azure credentials from the default credential chain, including Azure CLI credentials + /// and workload identity. + #[attr_added_in("0.11.14")] + pub const UV_AZURE_ENDPOINT_URL: &'static str = "UV_AZURE_ENDPOINT_URL"; + /// The URL of the pyx Simple API server. #[attr_added_in("0.8.15")] pub const PYX_API_URL: &'static str = "PYX_API_URL"; diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 06b14cf53a749..744ec6f935230 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -8258,6 +8258,7 @@ fn preview_features() { Audit, ProjectDirectoryMustExist, IndexExcludeNewer, + AzureEndpoint, ], }, python_preference: Managed, @@ -8530,6 +8531,7 @@ fn preview_features() { Audit, ProjectDirectoryMustExist, IndexExcludeNewer, + AzureEndpoint, ], }, python_preference: Managed, diff --git a/docs/concepts/preview.md b/docs/concepts/preview.md index 46c7eb99cc24f..7703ed1010724 100644 --- a/docs/concepts/preview.md +++ b/docs/concepts/preview.md @@ -64,6 +64,7 @@ The following preview features are available: [installing `python` and `python3` executables](./python-versions.md#installing-python-executables). - `format`: Allows using `uv format`. - `index-exclude-newer`: Allows setting `exclude-newer` on configured package indexes. +- `azure-endpoint`: Allows signing requests to Azure Blob Storage endpoints with Azure credentials. - `native-auth`: Enables storage of credentials in a [system-native location](../concepts/authentication/http.md#the-uv-credentials-store). - `auth-helper`: Allows using `uv auth helper` as a credential helper for external tools. From cb7b769b2d8ce187f3d1c208586b5326715ce9fb Mon Sep 17 00:00:00 2001 From: Tomasz Kramkowski Date: Fri, 15 May 2026 20:34:01 +0100 Subject: [PATCH 23/56] Use audit preview feature in audit tests (#19418) --- crates/uv/tests/it/audit.rs | 108 ++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 36 deletions(-) diff --git a/crates/uv/tests/it/audit.rs b/crates/uv/tests/it/audit.rs index 603216b0e4392..b1168d249ff01 100644 --- a/crates/uv/tests/it/audit.rs +++ b/crates/uv/tests/it/audit.rs @@ -88,7 +88,8 @@ async fn audit_no_vulnerabilities() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -247,7 +248,8 @@ async fn audit_vulnerability_found() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: false @@ -295,7 +297,8 @@ async fn audit_no_dependencies() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -351,7 +354,8 @@ async fn audit_best_id_selection() { // The output should show PYSEC-2023-0042 as the display ID (PYSEC preferred over GHSA, CVE). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: false @@ -415,7 +419,8 @@ async fn audit_no_fix_versions() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: false @@ -515,7 +520,8 @@ async fn audit_multiple_vulnerabilities_same_package() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: false @@ -579,7 +585,8 @@ async fn audit_no_dev() { // With --no-dev, only "iniconfig" should be audited (not "typing-extensions"). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--no-dev") .arg("--service-url") .arg(server.uri()), @" @@ -595,7 +602,8 @@ async fn audit_no_dev() { // Without --no-dev, both packages should be audited. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -642,7 +650,8 @@ async fn audit_extras() { // By default, extras are included: both iniconfig and typing-extensions are audited. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -657,7 +666,8 @@ async fn audit_extras() { // With --no-extra web, only iniconfig should be audited. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--no-extra") .arg("web") .arg("--service-url") @@ -708,7 +718,8 @@ async fn audit_dependency_groups() { // Default: all groups are included (iniconfig + typing-extensions + sniffio = 3). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -723,7 +734,8 @@ async fn audit_dependency_groups() { // --no-dev: excludes the dev group (iniconfig + sniffio = 2). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--no-dev") .arg("--service-url") .arg(server.uri()), @" @@ -739,7 +751,8 @@ async fn audit_dependency_groups() { // --no-group lint: excludes the lint group (iniconfig + typing-extensions = 2). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--no-group") .arg("lint") .arg("--service-url") @@ -756,7 +769,8 @@ async fn audit_dependency_groups() { // --only-group lint: only the "lint" group, project deps omitted (sniffio = 1). uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--only-group") .arg("lint") .arg("--service-url") @@ -821,7 +835,8 @@ async fn audit_ignore_by_id() { // Without --ignore, the vulnerability is reported. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: false @@ -847,7 +862,8 @@ async fn audit_ignore_by_id() { // With --ignore, the vulnerability is suppressed. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore") .arg("PYSEC-2023-0001") .arg("--service-url") @@ -904,7 +920,8 @@ async fn audit_ignore_by_alias() { // Ignoring by alias (CVE-2023-9999) should suppress the vulnerability. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore") .arg("CVE-2023-9999") .arg("--service-url") @@ -961,7 +978,8 @@ async fn audit_ignore_until_fixed() { // With --ignore-until-fixed and no fix versions, the vulnerability is suppressed. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore-until-fixed") .arg("VULN-NO-FIX") .arg("--service-url") @@ -1027,7 +1045,8 @@ async fn audit_ignore_until_fixed_with_fix() { // With --ignore-until-fixed but a fix IS available, the vulnerability is still reported. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore-until-fixed") .arg("PYSEC-2023-0001") .arg("--service-url") @@ -1106,7 +1125,8 @@ async fn audit_ignore_config() { // The vulnerability is suppressed by the config. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -1163,7 +1183,8 @@ async fn audit_ignore_until_fixed_config() { // The vulnerability is suppressed because no fix is available. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -1245,7 +1266,8 @@ async fn audit_ignore_partial() { // Ignoring VULN-A should still report VULN-B. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore") .arg("VULN-A") .arg("--service-url") @@ -1302,7 +1324,8 @@ async fn audit_ignore_unmatched() { // Ignoring a non-existent vulnerability should warn. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore") .arg("CVE-XXXX-YYYY") .arg("--service-url") @@ -1349,7 +1372,8 @@ async fn audit_ignore_until_fixed_unmatched() { // Ignoring a non-existent vulnerability with --ignore-until-fixed should warn. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore-until-fixed") .arg("CVE-XXXX-YYYY") .arg("--service-url") @@ -1416,7 +1440,8 @@ async fn audit_ignore_mixed_matched_unmatched() { // and the non-existent one triggers a warning. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--ignore") .arg("PYSEC-2023-0001") .arg("--ignore") @@ -1488,7 +1513,8 @@ async fn audit_script_no_vulnerabilities() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1580,7 +1606,8 @@ async fn audit_script_vulnerability_found() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1641,7 +1668,8 @@ async fn audit_script_no_dependencies() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1680,7 +1708,8 @@ async fn audit_script_frozen_missing_lockfile() { uv_snapshot!(context.filters(), context .audit() .arg("--frozen") - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1761,7 +1790,8 @@ async fn audit_script_multiple_dependencies() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1850,7 +1880,8 @@ async fn audit_script_extras() { // typing-extensions (reachable only via the `test` extra) should be audited. uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--script") .arg("script.py") .arg("--service-url") @@ -1900,7 +1931,8 @@ async fn audit_project_status_deprecated_with_reason() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @r" success: true @@ -1951,7 +1983,8 @@ async fn audit_project_status_archived_no_reason() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @r" success: true @@ -2002,7 +2035,8 @@ async fn audit_project_status_quarantined() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @r" success: true @@ -2053,7 +2087,8 @@ async fn audit_project_status_active_not_reported() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @" success: true @@ -2120,7 +2155,8 @@ async fn audit_vulnerability_and_project_status() { uv_snapshot!(context.filters(), context .audit() - .arg("--preview") + .arg("--preview-features") + .arg("audit") .arg("--service-url") .arg(server.uri()), @r" success: false From 4da52a35b720360d57e67fe073afee069c9f0157 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 15 May 2026 22:01:08 +0100 Subject: [PATCH 24/56] Use async tar crate in `uv-build-backend` (#19417) ## Summary The motivation and approach here is identical to #19365, but for tar. This doesn't have a meaningful effect on build size (since we _only_ use `tar` in `uv-build` and `astral-tokio-tar` in `uv`), but it does mean we aren't exposed to CVEs in `tar`. --- Cargo.lock | 16 +-- Cargo.toml | 1 - crates/uv-build-backend/Cargo.toml | 3 +- crates/uv-build-backend/src/lib.rs | 57 +++++++---- crates/uv-build-backend/src/source_dist.rs | 109 +++++++++++++++++---- crates/uv/Cargo.toml | 2 +- crates/uv/tests/it/build_backend.rs | 21 +++- crates/uv/tests/it/pip_compile.rs | 45 ++++++--- crates/uv/tests/it/pip_install.rs | 44 ++++++--- 9 files changed, 210 insertions(+), 88 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d05b93d7ad82..21bd2fb4e4150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4888,17 +4888,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddb6b06d20fba9ed21fca3d696ee1b6e870bca0bcf9fa2971f6ae2436de576a" -[[package]] -name = "tar" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "target-lexicon" version = "0.13.5" @@ -5704,6 +5693,7 @@ dependencies = [ "anyhow", "assert_cmd", "assert_fs", + "astral-tokio-tar", "astral-version-ranges", "astral_async_zip", "axoupdater", @@ -5747,7 +5737,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "tar", "tempfile", "textwrap", "thiserror", @@ -5966,6 +5955,7 @@ dependencies = [ name = "uv-build-backend" version = "0.0.47" dependencies = [ + "astral-tokio-tar", "astral-version-ranges", "astral_async_zip", "base64", @@ -5985,9 +5975,9 @@ dependencies = [ "serde_json", "sha2", "spdx", - "tar", "tempfile", "thiserror", + "tokio", "toml", "tracing", "uv-distribution-filename", diff --git a/Cargo.toml b/Cargo.toml index f440145702531..088318e92f5e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -257,7 +257,6 @@ sha2 = { version = "0.10.8" } smallvec = { version = "1.13.2" } spdx = { version = "0.13.0" } syn = { version = "2.0.77" } -tar = { version = "0.4.43" } target-lexicon = { version = "0.13.0" } tempfile = { version = "3.14.0" } textwrap = { version = "0.16.1" } diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index da898e5971b68..3354bbae6fbd4 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -27,6 +27,7 @@ uv-pypi-types = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } +astral-tokio-tar = { workspace = true } async_zip = { workspace = true } base64 = { workspace = true } csv = { workspace = true } @@ -42,9 +43,9 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } spdx = { workspace = true } -tar = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } +tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } version-ranges = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index add85236c233f..5fa26f37ea974 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -442,7 +442,7 @@ mod tests { use async_zip::base::read::mem::ZipFileReader; use flate2::bufread::GzDecoder; use fs_err::File; - use futures_lite::future::block_on; + use futures_lite::{StreamExt, future::block_on}; use indoc::indoc; use insta::assert_snapshot; use itertools::Itertools; @@ -450,11 +450,14 @@ mod tests { use sha2::Digest; use std::io::BufReader; use std::iter; + use std::pin::Pin; use tempfile::TempDir; use uv_distribution_filename::{SourceDistFilename, WheelFilename}; use uv_fs::{copy_dir_all, relative_to}; use uv_preview::PreviewFeature; + use crate::source_dist::SyncReader; + const MOCK_UV_VERSION: &str = "1.0.0+test"; fn format_err(err: &Error) -> String { @@ -515,9 +518,7 @@ mod tests { // Unpack the source distribution and build a wheel from it. let sdist_tree = TempDir::new()?; - let sdist_reader = BufReader::new(File::open(&source_dist_path)?); - let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); - source_dist.unpack(sdist_tree.path())?; + unpack_sdist(&source_dist_path, sdist_tree.path())?; let sdist_top_level_directory = sdist_tree.path().join(format!( "{}-{}", source_dist_filename.name.as_dist_info_name(), @@ -565,24 +566,40 @@ mod tests { fn sdist_contents(source_dist_path: &Path) -> Vec { let sdist_reader = BufReader::new(File::open(source_dist_path).unwrap()); - let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); - let mut source_dist_contents: Vec<_> = source_dist - .entries() - .unwrap() - .map(|entry| { - entry - .unwrap() - .path() - .unwrap() - .to_str() - .unwrap() - .replace('\\', "/") - }) - .collect(); + let mut source_dist = + tokio_tar::Archive::new(SyncReader::new(GzDecoder::new(sdist_reader))); + let mut source_dist_contents = block_on(async { + let mut entries = source_dist.entries().unwrap(); + let mut entries = Pin::new(&mut entries); + let mut contents = Vec::new(); + while let Some(entry) = entries.next().await { + contents.push( + entry + .unwrap() + .path() + .unwrap() + .to_str() + .unwrap() + .replace('\\', "/"), + ); + } + contents + }); source_dist_contents.sort(); source_dist_contents } + fn unpack_sdist(source_dist_path: &Path, target: &Path) -> Result<(), Error> { + let sdist_reader = BufReader::new(File::open(source_dist_path)?); + let mut source_dist = + tokio_tar::Archive::new(SyncReader::new(GzDecoder::new(sdist_reader))); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(source_dist.unpack(target))?; + Ok(()) + } + fn wheel_contents(direct_output_dir: &Path) -> Vec { let wheel = block_on(read_wheel(direct_output_dir)); let mut wheel_contents: Vec<_> = wheel @@ -881,9 +898,7 @@ mod tests { build_source_dist(src.path(), output_dir.path(), "0.5.15", false).unwrap(); let sdist_tree = TempDir::new().unwrap(); let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); - let sdist_reader = BufReader::new(File::open(&source_dist_path).unwrap()); - let mut source_dist = tar::Archive::new(GzDecoder::new(sdist_reader)); - source_dist.unpack(sdist_tree.path()).unwrap(); + unpack_sdist(&source_dist_path, sdist_tree.path()).unwrap(); { let _preview = uv_preview::test::with_features(&[]); build_wheel( diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 84224941e737b..d0a34a92bc825 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -7,11 +7,15 @@ use crate::{ use flate2::Compression; use flate2::write::GzEncoder; use fs_err::File; +use futures_lite::future::block_on; use globset::{Glob, GlobSet}; use std::io; -use std::io::{BufReader, Cursor, Write}; +use std::io::{BufReader, Cursor, Read, Write}; use std::path::{Component, Path, PathBuf}; -use tar::{EntryType, Header}; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_tar::{EntryType, Header}; use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; use uv_fs::{Simplified, normalize_path}; @@ -288,21 +292,78 @@ fn write_source_dist( Ok(filename) } -struct TarGzWriter { +pub(crate) struct SyncReader { + reader: R, +} + +impl SyncReader { + pub(crate) fn new(reader: R) -> Self { + Self { reader } + } +} + +impl AsyncRead for SyncReader { + fn poll_read( + mut self: Pin<&mut Self>, + _context: &mut Context<'_>, + buffer: &mut ReadBuf<'_>, + ) -> Poll> { + let read = self.reader.read(buffer.initialize_unfilled())?; + buffer.advance(read); + Poll::Ready(Ok(())) + } +} + +struct SyncWriter { + writer: W, +} + +impl SyncWriter { + fn new(writer: W) -> Self { + Self { writer } + } + + fn into_inner(self) -> W { + self.writer + } +} + +impl AsyncWrite for SyncWriter { + fn poll_write( + mut self: Pin<&mut Self>, + _context: &mut Context<'_>, + buffer: &[u8], + ) -> Poll> { + Poll::Ready(self.writer.write(buffer)) + } + + fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll> { + // `tokio::io::copy` flushes after each copied entry. Forwarding those flushes to the gzip + // encoder changes the deflate stream, even though the tar payload is identical. The + // encoder is finalized by `GzEncoder::finish` when the archive is closed. + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll> { + self.poll_flush(context) + } +} + +struct TarGzWriter { path: PathBuf, - tar: tar::Builder>, + tar: tokio_tar::Builder>>, } -impl TarGzWriter { +impl TarGzWriter { fn new(writer: W, path: impl Into) -> Self { let path = path.into(); let enc = GzEncoder::new(writer, Compression::default()); - let tar = tar::Builder::new(enc); + let tar = tokio_tar::Builder::new_non_terminated(SyncWriter::new(enc)); Self { path, tar } } } -impl DirectoryWriter for TarGzWriter { +impl DirectoryWriter for TarGzWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { let mut header = Header::new_gnu(); // Work around bug in Python's std tar module @@ -313,9 +374,11 @@ impl DirectoryWriter for TarGzWriter { // Reasonable default to avoid 0o000 permissions, the user's umask will be applied on // unpacking. header.set_mode(0o644); - self.tar - .append_data(&mut header, path, Cursor::new(bytes)) - .map_err(|err| Error::TarWrite(self.path.clone(), err))?; + block_on( + self.tar + .append_data(&mut header, path, SyncReader::new(Cursor::new(bytes))), + ) + .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } @@ -346,9 +409,11 @@ impl DirectoryWriter for TarGzWriter { } header.set_size(metadata.len()); let reader = BufReader::new(File::open(file)?); - self.tar - .append_data(&mut header, path, reader) - .map_err(|err| Error::TarWrite(self.path.clone(), err))?; + block_on( + self.tar + .append_data(&mut header, path, SyncReader::new(reader)), + ) + .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } @@ -358,16 +423,22 @@ impl DirectoryWriter for TarGzWriter { header.set_mode(0o755); header.set_entry_type(EntryType::Directory); header.set_size(0); - self.tar - .append_data(&mut header, directory, io::empty()) - .map_err(|err| Error::TarWrite(self.path.clone(), err))?; + block_on( + self.tar + .append_data(&mut header, directory, SyncReader::new(io::empty())), + ) + .map_err(|err| Error::TarWrite(self.path.clone(), err))?; Ok(()) } - fn close(mut self, _dist_info_dir: &str) -> Result<(), Error> { - self.tar + fn close(self, _dist_info_dir: &str) -> Result<(), Error> { + let path = self.path; + let writer = + block_on(self.tar.into_inner()).map_err(|err| Error::TarWrite(path.clone(), err))?; + writer + .into_inner() .finish() - .map_err(|err| Error::TarWrite(self.path.clone(), err))?; + .map_err(|err| Error::TarWrite(path, err))?; Ok(()) } } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index b51ffd686632a..a7526be510d33 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -146,7 +146,7 @@ predicates = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, features = ["blocking"], default-features = false } sha2 = { workspace = true } -tar = { workspace = true } +astral-tokio-tar = { workspace = true } tempfile = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index 20ca09d361a43..e897347c437ba 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -3,12 +3,14 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileTouch, FileWriteBin, FileWriteStr, PathChild, PathCreateDir}; use flate2::bufread::GzDecoder; use fs_err::File; +use futures::io::AllowStdIo; use indoc::{formatdoc, indoc}; use insta::{assert_json_snapshot, assert_snapshot}; use std::io::BufReader; use std::path::Path; use std::process::Command; use tempfile::TempDir; +use tokio_util::compat::FuturesAsyncReadCompatExt; use uv_static::EnvVars; use uv_test::{uv_snapshot, venv_bin_path}; @@ -20,6 +22,17 @@ const BUILT_BY_UV_TEST_SCRIPT: &str = indoc! {r#" print(f"Area of a circle with r=2: {area(2)}") "#}; +fn unpack_tar_gz(source_dist_path: &Path, target: &Path) -> Result<()> { + let sdist_reader = BufReader::new(File::open(source_dist_path)?); + let mut source_dist = + tokio_tar::Archive::new(AllowStdIo::new(GzDecoder::new(sdist_reader)).compat()); + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(source_dist.unpack(target))?; + Ok(()) +} + /// Test that build backend works if we invoke it directly. /// /// We can't test end-to-end here including the PEP 517 bridge code since we don't have a uv wheel. @@ -102,10 +115,10 @@ fn built_by_uv_direct() -> Result<()> { let sdist_tree = TempDir::new()?; - let sdist_reader = BufReader::new(File::open( - sdist_dir.path().join("built_by_uv-0.1.0.tar.gz"), - )?); - tar::Archive::new(GzDecoder::new(sdist_reader)).unpack(sdist_tree.path())?; + unpack_tar_gz( + &sdist_dir.path().join("built_by_uv-0.1.0.tar.gz"), + sdist_tree.path(), + )?; drop(sdist_dir); diff --git a/crates/uv/tests/it/pip_compile.rs b/crates/uv/tests/it/pip_compile.rs index d12eda1c4affa..b3d4ae78c70c8 100644 --- a/crates/uv/tests/it/pip_compile.rs +++ b/crates/uv/tests/it/pip_compile.rs @@ -8,8 +8,11 @@ use anyhow::Result; use assert_fs::prelude::*; use flate2::write::GzEncoder; use fs_err::File; +use futures::executor::block_on; +use futures::io::AllowStdIo; use http::StatusCode; use indoc::indoc; +use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; use url::Url; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -21,6 +24,27 @@ use uv_test::{ DEFAULT_PYTHON_VERSION, TestContext, download_to_disk, packse_index_url, uv_snapshot, }; +fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> { + let enc = GzEncoder::new(file, flate2::Compression::default()); + let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(enc).compat_write()); + + for (path, contents) in entries { + let mut header = tokio_tar::Header::new_gnu(); + header.set_size(contents.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + block_on(tar.append_data( + &mut header, + path, + AllowStdIo::new(Cursor::new(contents)).compat(), + ))?; + } + + let writer = block_on(tar.into_inner())?; + writer.into_inner().into_inner().finish()?; + Ok(()) +} + #[test] fn compile_requirements_in() -> Result<()> { let context = uv_test::test_context!("3.12"); @@ -15444,20 +15468,13 @@ fn dynamic_version_source_dist() -> Result<()> { // Flush the file after we're done. { let file = File::create(source_dist.path())?; - let enc = GzEncoder::new(file, flate2::Compression::default()); - let mut tar = tar::Builder::new(enc); - - for (path, contents) in [ - ("foo-1.2.3/pyproject.toml", pyproject_toml), - ("foo-1.2.3/setup.py", setup_py), - ] { - let mut header = tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - tar.append_data(&mut header, path, Cursor::new(contents))?; - } - tar.finish()?; + write_tar_gz( + file, + &[ + ("foo-1.2.3/pyproject.toml", pyproject_toml), + ("foo-1.2.3/setup.py", setup_py), + ], + )?; } let requirements_in = context.temp_dir.child("requirements.in"); diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index bf39d27d4346f..a07d70e423285 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -11,9 +11,11 @@ use flate2::write::GzEncoder; use fs_err as fs; use fs_err::File; use futures::executor::block_on; +use futures::io::AllowStdIo; use indoc::{formatdoc, indoc}; use insta::assert_snapshot; use predicates::prelude::predicate; +use tokio_util::compat::{FuturesAsyncReadCompatExt, FuturesAsyncWriteCompatExt}; use url::Url; use walkdir::WalkDir; use wiremock::{ @@ -30,6 +32,27 @@ use uv_test::{ get_bin, packse_index_url, uv_snapshot, venv_bin_path, }; +fn write_tar_gz(file: File, entries: &[(&str, &str)]) -> Result<()> { + let enc = GzEncoder::new(file, flate2::Compression::default()); + let mut tar = tokio_tar::Builder::new_non_terminated(AllowStdIo::new(enc).compat_write()); + + for (path, contents) in entries { + let mut header = tokio_tar::Header::new_gnu(); + header.set_size(contents.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + block_on(tar.append_data( + &mut header, + path, + AllowStdIo::new(Cursor::new(contents)).compat(), + ))?; + } + + let writer = block_on(tar.into_inner())?; + writer.into_inner().into_inner().finish()?; + Ok(()) +} + #[test] fn missing_requirements_txt() { let context = uv_test::test_context!("3.12"); @@ -10071,20 +10094,13 @@ fn test_dynamic_version_sdist_wrong_version() -> Result<()> { // Flush the file after we're done. { let file = File::create(source_dist.path())?; - let enc = GzEncoder::new(file, flate2::Compression::default()); - let mut tar = tar::Builder::new(enc); - - for (path, contents) in [ - ("foo-1.2.3/pyproject.toml", pyproject_toml), - ("foo-1.2.3/setup.py", setup_py), - ] { - let mut header = tar::Header::new_gnu(); - header.set_size(contents.len() as u64); - header.set_mode(0o644); - header.set_cksum(); - tar.append_data(&mut header, path, Cursor::new(contents))?; - } - tar.finish()?; + write_tar_gz( + file, + &[ + ("foo-1.2.3/pyproject.toml", pyproject_toml), + ("foo-1.2.3/setup.py", setup_py), + ], + )?; } uv_snapshot!(context.filters(), context From de4b1af484fde7fa270a55caa41353ff1bf483ca Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 15 May 2026 22:50:56 +0100 Subject: [PATCH 25/56] Use structured errors for signing authentication failures (#19422) ## Summary Right now, these paths panic if you don't have credentials set. --- crates/uv-auth/src/credentials.rs | 158 ++++++++++++++++++++++++------ crates/uv-auth/src/middleware.rs | 25 +++-- 2 files changed, 145 insertions(+), 38 deletions(-) diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index e02319dff5967..f307079874e73 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -14,6 +14,7 @@ use reqsign::google::DefaultSigner as GcsDefaultSigner; use reqwest::Request; use reqwest::header::{HeaderName, HeaderValue}; use serde::{Deserialize, Serialize}; +use thiserror::Error; use url::Url; use uv_netrc::Netrc; @@ -401,6 +402,26 @@ pub(crate) enum Authentication { AzureSigner(AzureDefaultSigner), } +#[derive(Debug, Error)] +pub(crate) enum AuthenticationError { + #[error("Failed to convert request URL to URI")] + InvalidUri(#[from] http::uri::InvalidUri), + + #[error("Failed to build request for {provider} signing")] + BuildRequest { + provider: &'static str, + #[source] + source: http::Error, + }, + + #[error("Failed to sign request with {provider} credentials")] + Sign { + provider: &'static str, + #[source] + source: reqsign::Error, + }, +} + impl PartialEq for Authentication { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -493,27 +514,33 @@ impl Authentication { /// Apply the authentication to the given request. /// /// Any existing credentials will be overridden. - #[must_use] - pub(crate) async fn authenticate(&self, mut request: Request) -> Request { + pub(crate) async fn authenticate( + &self, + mut request: Request, + ) -> Result { match self { - Self::Credentials(credentials) => credentials.authenticate(request), + Self::Credentials(credentials) => Ok(credentials.authenticate(request)), Self::AwsSigner(signer) => { // Build an `http::Request` from the `reqwest::Request`. - // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. - let uri = Uri::from_str(request.url().as_str()).unwrap(); + let uri = Uri::from_str(request.url().as_str())?; let mut http_req = http::Request::builder() .method(request.method().clone()) .uri(uri) .body(()) - .unwrap(); + .map_err(|source| AuthenticationError::BuildRequest { + provider: "AWS", + source, + })?; *http_req.headers_mut() = request.headers().clone(); // Sign the parts. let (mut parts, ()) = http_req.into_parts(); - signer - .sign(&mut parts, None) - .await - .expect("AWS signing should succeed"); + signer.sign(&mut parts, None).await.map_err(|source| { + AuthenticationError::Sign { + provider: "AWS", + source, + } + })?; // Copy over the signed headers. request.headers_mut().extend(parts.headers); @@ -523,25 +550,29 @@ impl Authentication { request.url_mut().set_path(path_and_query.path()); request.url_mut().set_query(path_and_query.query()); } - request + Ok(request) } Self::GcsSigner(signer) => { // Build an `http::Request` from the `reqwest::Request`. - // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. - let uri = Uri::from_str(request.url().as_str()).unwrap(); + let uri = Uri::from_str(request.url().as_str())?; let mut http_req = http::Request::builder() .method(request.method().clone()) .uri(uri) .body(()) - .unwrap(); + .map_err(|source| AuthenticationError::BuildRequest { + provider: "GCS", + source, + })?; *http_req.headers_mut() = request.headers().clone(); // Sign the parts. let (mut parts, ()) = http_req.into_parts(); - signer - .sign(&mut parts, None) - .await - .expect("GCS signing should succeed"); + signer.sign(&mut parts, None).await.map_err(|source| { + AuthenticationError::Sign { + provider: "GCS", + source, + } + })?; // Copy over the signed headers. request.headers_mut().extend(parts.headers); @@ -551,17 +582,19 @@ impl Authentication { request.url_mut().set_path(path_and_query.path()); request.url_mut().set_query(path_and_query.query()); } - request + Ok(request) } Self::AzureSigner(signer) => { // Build an `http::Request` from the `reqwest::Request`. - // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. - let uri = Uri::from_str(request.url().as_str()).unwrap(); + let uri = Uri::from_str(request.url().as_str())?; let mut http_req = http::Request::builder() .method(request.method().clone()) .uri(uri) .body(()) - .unwrap(); + .map_err(|source| AuthenticationError::BuildRequest { + provider: "Azure", + source, + })?; *http_req.headers_mut() = request.headers().clone(); http_req .headers_mut() @@ -570,10 +603,12 @@ impl Authentication { // Sign the parts. let (mut parts, ()) = http_req.into_parts(); - signer - .sign(&mut parts, None) - .await - .expect("Azure signing should succeed"); + signer.sign(&mut parts, None).await.map_err(|source| { + AuthenticationError::Sign { + provider: "Azure", + source, + } + })?; // Copy over the signed headers. request.headers_mut().extend(parts.headers); @@ -583,7 +618,7 @@ impl Authentication { request.url_mut().set_path(path_and_query.path()); request.url_mut().set_query(path_and_query.query()); } - request + Ok(request) } } } @@ -592,9 +627,40 @@ impl Authentication { #[cfg(test)] mod tests { use insta::assert_debug_snapshot; + use reqsign::aws::Credential as AwsCredential; + use reqsign::azure::Credential as AzureCredential; + use reqsign::{Context, ProvideCredential}; use super::*; + #[derive(Debug)] + struct EmptyAwsCredentialProvider; + + impl ProvideCredential for EmptyAwsCredentialProvider { + type Credential = AwsCredential; + + async fn provide_credential( + &self, + _ctx: &Context, + ) -> reqsign::Result> { + Ok(None) + } + } + + #[derive(Debug)] + struct EmptyAzureCredentialProvider; + + impl ProvideCredential for EmptyAzureCredentialProvider { + type Credential = AzureCredential; + + async fn provide_credential( + &self, + _ctx: &Context, + ) -> reqsign::Result> { + Ok(None) + } + } + #[test] fn from_url_no_credentials() { let url = &Url::parse("https://example.com/simple/first/").unwrap(); @@ -726,7 +792,7 @@ mod tests { reqwest::Method::GET, Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(), ); - let request = authentication.authenticate(request).await; + let request = authentication.authenticate(request).await.unwrap(); let authorization = request .headers() @@ -745,6 +811,42 @@ mod tests { ); } + #[tokio::test] + async fn authenticated_request_with_aws_signer_missing_credentials() { + let signer = reqsign::aws::default_signer("s3", "us-east-1") + .with_credential_provider(EmptyAwsCredentialProvider); + let authentication = Authentication::from(signer); + + let request = Request::new( + reqwest::Method::GET, + Url::parse("https://s3.amazonaws.com/bucket/blob.whl").unwrap(), + ); + let err = authentication.authenticate(request).await.unwrap_err(); + + insta::assert_snapshot!( + err.to_string(), + @"Failed to sign request with AWS credentials" + ); + } + + #[tokio::test] + async fn authenticated_request_with_azure_signer_missing_credentials() { + let signer = + reqsign::azure::default_signer().with_credential_provider(EmptyAzureCredentialProvider); + let authentication = Authentication::from(signer); + + let request = Request::new( + reqwest::Method::GET, + Url::parse("https://account.blob.core.windows.net/container/blob.whl").unwrap(), + ); + let err = authentication.authenticate(request).await.unwrap_err(); + + insta::assert_snapshot!( + err.to_string(), + @"Failed to sign request with Azure credentials" + ); + } + /// Passwords should be redacted in debug output. #[test] fn test_password_redaction() { diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 33a651a7c0840..f9ee1cf3ca686 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -13,7 +13,6 @@ use uv_redacted::DisplaySafeUrl; use uv_static::EnvVars; use uv_warnings::owo_colors::OwoColorize; -use crate::credentials::Authentication; use crate::providers::{ AzureEndpointProvider, GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider, }; @@ -21,7 +20,7 @@ use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::{ AccessToken, CredentialsCache, KeyringProvider, cache::FetchUrl, - credentials::{Credentials, Username}, + credentials::{Authentication, AuthenticationError, Credentials, Username}, index::{AuthPolicy, Indexes}, realm::Realm, }; @@ -31,6 +30,12 @@ use crate::{Index, TextCredentialStore}; static IS_DEPENDABOT: LazyLock = LazyLock::new(|| std::env::var(EnvVars::DEPENDABOT).is_ok_and(|value| value == "true")); +impl From for Error { + fn from(err: AuthenticationError) -> Self { + Self::middleware(err) + } +} + /// Strategy for loading netrc files. enum NetrcMode { Automatic(LazyLock>), @@ -383,7 +388,7 @@ impl Middleware for AuthMiddleware { .cache() .get_url(DisplaySafeUrl::ref_cast(request.url()), &Username::none()); if let Some(credentials) = credentials.as_ref() { - request = credentials.authenticate(request).await; + request = credentials.authenticate(request).await?; // If it's fully authenticated, finish the request if credentials.is_authenticated() { @@ -484,7 +489,7 @@ impl Middleware for AuthMiddleware { if let Some(credentials) = credentials.as_ref() { if credentials.is_authenticated() { trace!("Retrying request for {url} with credentials from cache {credentials:?}"); - retry_request = credentials.authenticate(retry_request).await; + retry_request = credentials.authenticate(retry_request).await?; return self .complete_request(None, retry_request, extensions, next, auth_policy) .await; @@ -502,7 +507,7 @@ impl Middleware for AuthMiddleware { ) .await { - retry_request = credentials.authenticate(retry_request).await; + retry_request = credentials.authenticate(retry_request).await?; trace!("Retrying request for {url} with {credentials:?}"); return self .complete_request( @@ -518,7 +523,7 @@ impl Middleware for AuthMiddleware { if let Some(credentials) = credentials.as_ref() { if !attempt_has_username { trace!("Retrying request for {url} with username from cache {credentials:?}"); - retry_request = credentials.authenticate(retry_request).await; + retry_request = credentials.authenticate(retry_request).await?; return self .complete_request(None, retry_request, extensions, next, auth_policy) .await; @@ -623,7 +628,7 @@ impl AuthMiddleware { .get_realm(Realm::from(request.url()), credentials.to_username()) }; if let Some(credentials) = maybe_cached_credentials { - request = credentials.authenticate(request).await; + request = credentials.authenticate(request).await?; // Do not insert already-cached credentials let credentials = None; return self @@ -635,7 +640,7 @@ impl AuthMiddleware { DisplaySafeUrl::ref_cast(request.url()), credentials.as_username().as_ref(), ) { - request = credentials.authenticate(request).await; + request = credentials.authenticate(request).await?; // Do not insert already-cached credentials None } else if let Some(credentials) = self @@ -647,7 +652,7 @@ impl AuthMiddleware { ) .await { - request = credentials.authenticate(request).await; + request = credentials.authenticate(request).await?; Some(credentials) } else if index.is_some() { // If this is a known index, we fall back to checking for the realm. @@ -655,7 +660,7 @@ impl AuthMiddleware { .cache() .get_realm(Realm::from(request.url()), credentials.to_username()) { - request = credentials.authenticate(request).await; + request = credentials.authenticate(request).await?; Some(credentials) } else { Some(credentials) From 1e99086e645038804c3f479ef24cc50f4ec74a96 Mon Sep 17 00:00:00 2001 From: Dev-X25874 <283057883+Dev-X25874@users.noreply.github.com> Date: Sat, 16 May 2026 11:19:36 +0530 Subject: [PATCH 26/56] pep440/version: fix dead "already trimmed" fast-path in `only_release_trimmed` (#19425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `only_release_trimmed` is intended to short-circuit with a cheap `.clone()` when the release segment already has no trailing zeros. The guard condition was: ```rust if last_non_zero == self.release().len() { ``` `rposition` returns a zero-based index, so its maximum possible value is `len - 1`. The value `len` is unreachable, making the "Already trimmed" branch permanently dead code. Every call — including the common case of already-trimmed versions like `1.2.3` or `3.11` — fell through to the `else` branch and performed a redundant `Self::new(...)` allocation. Fix: change the condition to `last_non_zero + 1 == self.release().len()`, which is true exactly when the last non-zero index is the final element of the slice. `only_release_trimmed` is called on hot paths in version specifier simplification, version range normalization, and marker algebra. ## Test Plan Verified by reading `rposition`'s contract in the standard library docs — it returns `Option` where the index is always in `0..len`. The corrected condition can be confirmed by tracing through representative inputs: - `[1, 2, 3]` → `last_non_zero = 2`, `2 + 1 == 3` ✓ fast-path clone - `[1, 2, 0]` → `last_non_zero = 1`, `1 + 1 != 3` → trims to `[1, 2]` ✓ - `[0, 0, 0]` → `rposition` returns `None` → `Self::new([0])` ✓ --------- Co-authored-by: Charlie Marsh --- crates/uv-pep440/src/version.rs | 41 +++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/uv-pep440/src/version.rs b/crates/uv-pep440/src/version.rs index 4faa803b7dbc9..0a78ed0ffb8d9 100644 --- a/crates/uv-pep440/src/version.rs +++ b/crates/uv-pep440/src/version.rs @@ -628,8 +628,16 @@ impl Version { #[must_use] pub fn only_release_trimmed(&self) -> Self { if let Some(last_non_zero) = self.release().iter().rposition(|segment| *segment != 0) { - if last_non_zero == self.release().len() { - // Already trimmed. + if last_non_zero + 1 == self.release().len() + && self.epoch() == 0 + && self.pre().is_none() + && self.post().is_none() + && self.dev().is_none() + && self.local().is_empty() + && self.min().is_none() + && self.max().is_none() + { + // Already a trimmed release-only version. self.clone() } else { Self::new(self.release().iter().take(last_non_zero + 1).copied()) @@ -4258,6 +4266,35 @@ mod tests { assert_eq!(v2.to_string(), "1.2"); } + #[test] + fn only_release_trimmed_discards_non_release_segments() { + for version in ["1.2a1", "1.2.post1", "1!1.2", "1.2+local", "1.2.dev1"] { + let version = version.parse::().unwrap(); + assert_eq!(version.only_release_trimmed(), Version::new([1, 2])); + } + + assert_eq!( + Version::new([1, 2]) + .with_min(Some(0)) + .only_release_trimmed(), + Version::new([1, 2]) + ); + assert_eq!( + Version::new([1, 2]) + .with_max(Some(0)) + .only_release_trimmed(), + Version::new([1, 2]) + ); + assert_eq!( + Version::new([1, 2, 0]).only_release_trimmed(), + Version::new([1, 2]) + ); + assert_eq!( + Version::new([1, 2]).only_release_trimmed(), + Version::new([1, 2]) + ); + } + #[test] fn type_size() { assert_eq!(size_of::(), size_of::() * 2); From d19f1cd498202e04da70224573bbd5b79b94a726 Mon Sep 17 00:00:00 2001 From: Gyeongjae Choi Date: Sun, 17 May 2026 14:06:28 +0900 Subject: [PATCH 27/56] Fix Pyodide fetch script to handle old releases (#19434) ## Summary Fixes old Pyodide metadata going away from the download-metadata.json (See: https://github.com/astral-sh/uv/pull/19396#discussion_r3239032504). ## Test Plan `uv run -- fetch-download-metadata.py` --- crates/uv-python/fetch-download-metadata.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/uv-python/fetch-download-metadata.py b/crates/uv-python/fetch-download-metadata.py index 822869c5a5959..1800050bbd6cc 100755 --- a/crates/uv-python/fetch-download-metadata.py +++ b/crates/uv-python/fetch-download-metadata.py @@ -494,7 +494,7 @@ async def _fetch_checksums(self, downloads: list[PythonDownload]) -> None: class PyodideFinder(Finder): implementation = ImplementationName.CPYTHON - RELEASE_URL = "https://api.github.com/repos/pyodide/pyodide/releases" + RELEASE_URL = "https://api.github.com/repos/pyodide/pyodide/releases?per_page=100" METADATA_URL = ( "https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json" ) @@ -514,15 +514,20 @@ async def find(self) -> list[PythonDownload]: return downloads async def _fetch_downloads(self) -> list[PythonDownload]: - # This will only download the first page, i.e., ~30 releases - [release_resp, meta_resp] = await asyncio.gather( - self.client.get(self.RELEASE_URL), self.client.get(self.METADATA_URL) - ) - release_resp.raise_for_status() + meta_resp = await self.client.get(self.METADATA_URL) meta_resp.raise_for_status() - releases = release_resp.json() metadata = meta_resp.json()["releases"] + # Paginate through all pages of releases via the Link header + releases = [] + release_url: str | None = self.RELEASE_URL + while release_url is not None: + resp = await self.client.get(release_url) + resp.raise_for_status() + releases.extend(resp.json()) + next_link = resp.links.get("next", {}) + release_url = next_link.get("url") + results = {} for release in releases: # Skip prereleases From f8fdfe3b44a71e7cb0b4c07b32d64dac8f55f693 Mon Sep 17 00:00:00 2001 From: Dev-X25874 <283057883+Dev-X25874@users.noreply.github.com> Date: Sun, 17 May 2026 21:52:57 +0530 Subject: [PATCH 28/56] Reject empty string as an invalid package name (#19435) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Both `normalize` and `is_normalized` in `crates/uv-normalize/src/lib.rs` work by iterating over bytes. When given an empty string, the loop never executes, `last` stays `None`, and both functions return success — `normalize` returns `Ok("")` and `is_normalized` returns `Ok(true)`. As a result, `validate_and_normalize_ref("")` short-circuits and returns `Ok(SmallString::from(""))`, meaning `PackageName::from_str("")`, `ExtraName::from_str("")`, and `GroupName::from_str("")` all succeed silently. The [PyPA name-normalization spec](https://packaging.python.org/en/latest/specifications/name-normalization/) requires at least one alphanumeric character, and the existing `InvalidNameError` display message already says *"Names must start and end with a letter or digit"* — which an empty string does not satisfy. The fix adds a single early-return guard at the top of `validate_and_normalize_ref`, the one shared entry point for all three name types, avoiding any duplication in the private helpers. ## Test Plan Added `""` to the `failures` test in `crates/uv-normalize/src/lib.rs` and verified that `validate_and_normalize_ref("")` now returns `Err(InvalidNameError(...))`. The assertion on `is_normalized` was intentionally omitted for the empty-string case: `is_normalized` is a private implementation detail never called from outside this module, and the guard in `validate_and_normalize_ref` is sufficient to enforce the public contract. --------- Co-authored-by: Charlie Marsh --- crates/uv-normalize/src/lib.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/uv-normalize/src/lib.rs b/crates/uv-normalize/src/lib.rs index 2c00e3ef65e14..d665d026dcbde 100644 --- a/crates/uv-normalize/src/lib.rs +++ b/crates/uv-normalize/src/lib.rs @@ -27,6 +27,11 @@ pub(crate) fn validate_and_normalize_ref( /// Normalize an unowned package or extra name. fn normalize(name: &str) -> Result { + // An empty string is not a valid package, extra, or group name. + if name.is_empty() { + return Err(InvalidNameError(name.to_string())); + } + let mut normalized = String::with_capacity(name.len()); let mut last = None; @@ -61,6 +66,11 @@ fn normalize(name: &str) -> Result { /// Returns `true` if the name is already normalized. fn is_normalized(name: impl AsRef) -> Result { + // An empty string is not a valid package, extra, or group name. + if name.as_ref().is_empty() { + return Err(InvalidNameError(name.as_ref().to_string())); + } + let mut last = None; for char in name.as_ref().bytes() { match char { @@ -227,6 +237,7 @@ mod tests { #[test] fn failures() { let failures = [ + "", " starts-with-space", "-starts-with-dash", "ends-with-dash-", From 8587aec33216d1a1a6873296fe652aca96dea292 Mon Sep 17 00:00:00 2001 From: Sean Kenneth Doherty Date: Sun, 17 May 2026 11:59:58 -0500 Subject: [PATCH 29/56] Skip empty directories in uv build outputs (#19437) ## Summary This updates the uv build backend so wheels and source distributions no longer emit standalone empty directory entries. Directory entries are still written when they are ancestors of included files, preserving the existing deterministic archive structure for real package content. ## Root cause The build walkers wrote directory entries as they encountered matching directories. That meant empty directories, and directories whose contents were excluded later, could appear in build outputs even though they contained no installable or source content. ## Changes - Collect included file paths and their ancestor directories before writing archive entries. - Skip raw directory entries that do not have included descendants. - Preserve deterministic output order with `BTreeMap` collection before writing. - Extend the build-backend snapshot fixture with an empty directory and an excluded `__pycache__` directory so the regression is covered for both sdists and wheels. ## Validation - `cargo test -p uv-build-backend built_by_uv_building` - `cargo test -p uv-build-backend` - `cargo fmt --check --package uv-build-backend` - `cargo clippy -p uv-build-backend --all-targets --all-features -- -D warnings` - `git diff --check origin/main...HEAD && git diff --check` Generated with assistance from OpenAI Codex. --------- Co-authored-by: Charlie Marsh --- crates/uv-build-backend/src/lib.rs | 52 +++++++++++++++++----- crates/uv-build-backend/src/source_dist.rs | 22 ++++++--- crates/uv-build-backend/src/wheel.rs | 50 ++++++++++++--------- 3 files changed, 85 insertions(+), 39 deletions(-) diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index 5fa26f37ea974..bde8ef4c0bcb9 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -11,6 +11,7 @@ pub use source_dist::{build_source_dist, list_source_dist}; use uv_warnings::warn_user_once; pub use wheel::{build_editable, build_wheel, list_wheel, metadata}; +use rustc_hash::FxHashSet; use std::collections::HashSet; use std::ffi::OsStr; use std::io; @@ -18,7 +19,6 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use thiserror::Error; use tracing::debug; -use walkdir::DirEntry; use uv_fs::{Simplified, normalize_path}; use uv_globfilter::PortableGlobError; @@ -98,16 +98,6 @@ trait DirectoryWriter { /// Files added through the method are considered generated when listing included files. fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error>; - /// Add the file or directory to the path. - fn write_dir_entry(&mut self, entry: &DirEntry, target_path: &str) -> Result<(), Error> { - if entry.file_type().is_dir() { - self.write_directory(target_path)?; - } else { - self.write_file(target_path, entry.path())?; - } - Ok(()) - } - /// Add a local file. fn write_file(&mut self, path: &str, file: &Path) -> Result<(), Error>; @@ -118,6 +108,37 @@ trait DirectoryWriter { fn close(self, dist_info_dir: &str) -> Result<(), Error>; } +fn write_directory_once( + writer: &mut impl DirectoryWriter, + directories: &mut FxHashSet, + directory: &Path, +) -> Result<(), Error> { + if directories.insert(directory.to_path_buf()) { + debug!("Adding directory: {}", directory.user_display()); + writer.write_directory(&directory.portable_display().to_string())?; + } + Ok(()) +} + +fn write_file_with_directories( + writer: &mut impl DirectoryWriter, + directories: &mut FxHashSet, + prefix: &Path, + relative: &Path, + source: &Path, +) -> Result<(), Error> { + if let Some(parent) = relative.parent() { + let mut directory = PathBuf::new(); + for component in parent.components() { + directory.push(component); + write_directory_once(writer, directories, &prefix.join(&directory))?; + } + } + + let entry_path = prefix.join(relative).portable_display().to_string(); + writer.write_file(&entry_path, source) +} + /// Name of the file in the archive and path outside, if it wasn't generated. pub(crate) type FileList = Vec<(String, Option)>; @@ -729,6 +750,15 @@ mod tests { fs_err::create_dir_all(module_root.join("__pycache__")).unwrap(); File::create(module_root.join("__pycache__").join("compiled.pyc")).unwrap(); File::create(module_root.join("arithmetic").join("circle.pyc")).unwrap(); + fs_err::create_dir_all(module_root.join("empty")).unwrap(); + fs_err::create_dir_all(module_root.join("stale").join("__pycache__")).unwrap(); + File::create( + module_root + .join("stale") + .join("__pycache__") + .join("compiled.pyc"), + ) + .unwrap(); // Perform both the direct and the indirect build. let dist = TempDir::new().unwrap(); diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index d0a34a92bc825..79d925ff6256c 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -2,13 +2,14 @@ use crate::metadata::DEFAULT_EXCLUDES; use crate::wheel::build_exclude_matcher; use crate::{ BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, - error_on_venv, find_roots, + error_on_venv, find_roots, write_directory_once, write_file_with_directories, }; use flate2::Compression; use flate2::write::GzEncoder; use fs_err::File; use futures_lite::future::block_on; use globset::{Glob, GlobSet}; +use rustc_hash::FxHashSet; use std::io; use std::io::{BufReader, Cursor, Read, Write}; use std::path::{Component, Path, PathBuf}; @@ -234,6 +235,9 @@ fn write_source_dist( source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?; let mut files_visited = 0; + let mut written_directories = FxHashSet::::default(); + let top_level_directory = PathBuf::from(&top_level).join(""); + write_directory_once(&mut writer, &mut written_directories, &top_level_directory)?; for entry in WalkDir::new(source_tree) .sort_by_file_name() .into_iter() @@ -278,12 +282,18 @@ fn write_source_dist( error_on_venv(entry.file_name(), entry.path())?; - let entry_path = Path::new(&top_level) - .join(relative) - .portable_display() - .to_string(); + if entry.file_type().is_dir() { + continue; + } + debug!("Adding to sdist: {}", relative.user_display()); - writer.write_dir_entry(&entry, &entry_path)?; + write_file_with_directories( + &mut writer, + &mut written_directories, + Path::new(&top_level), + relative, + entry.path(), + )?; } debug!("Visited {files_visited} files for source dist build"); diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index fbeff5af7c7a0..4550a9373ec07 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -26,7 +26,7 @@ use uv_warnings::warn_user_once; use crate::metadata::DEFAULT_EXCLUDES; use crate::{ BuildBackendSettings, DirectoryWriter, Error, FileList, ListWriter, PyProjectToml, - error_on_venv, find_roots, + error_on_venv, find_roots, write_directory_once, write_file_with_directories, }; // Files at or below this size are buffered and written with `write_entry_whole`, @@ -171,19 +171,8 @@ fn write_wheel( )?; let mut files_visited = 0; - let mut prefix_directories = FxHashSet::default(); + let mut written_directories = FxHashSet::::default(); for module_relative in module_relative { - // For convenience, have directories for the whole tree in the wheel - for ancestor in module_relative.ancestors().skip(1) { - if ancestor == Path::new("") { - continue; - } - // Avoid duplicate directories in the zip. - if prefix_directories.insert(ancestor.to_path_buf()) { - wheel_writer.write_directory(&ancestor.portable_display().to_string())?; - } - } - for entry in WalkDir::new(src_root.join(module_relative)) .sort_by_file_name() .into_iter() @@ -219,9 +208,18 @@ fn write_wheel( error_on_venv(entry.file_name(), entry.path())?; - let entry_path = entry_path.portable_display().to_string(); - debug!("Adding to wheel: {entry_path}"); - wheel_writer.write_dir_entry(&entry, &entry_path)?; + if entry.file_type().is_dir() { + continue; + } + + debug!("Adding to wheel: {}", entry_path.user_display()); + write_file_with_directories( + &mut wheel_writer, + &mut written_directories, + Path::new(""), + entry_path, + entry.path(), + )?; } } debug!("Visited {files_visited} files for wheel build"); @@ -561,7 +559,8 @@ fn wheel_subdir_from_globs( source: err, })?; - wheel_writer.write_directory(target)?; + let mut written_directories = FxHashSet::::default(); + let target = Path::new(target); for entry in WalkDir::new(src) .sort_by_file_name() @@ -602,12 +601,19 @@ fn wheel_subdir_from_globs( error_on_venv(entry.file_name(), entry.path())?; - let license_path = Path::new(target) - .join(relative) - .portable_display() - .to_string(); + if entry.file_type().is_dir() { + continue; + } + debug!("Adding for {}: {}", globs_field, relative.user_display()); - wheel_writer.write_dir_entry(&entry, &license_path)?; + write_directory_once(wheel_writer, &mut written_directories, target)?; + write_file_with_directories( + wheel_writer, + &mut written_directories, + target, + relative, + entry.path(), + )?; } Ok(()) } From 9f68be16919ee45bafe168d5565f92ad655f1d6e Mon Sep 17 00:00:00 2001 From: Helio Machado <0x2b3bfa0+git@googlemail.com> Date: Sun, 17 May 2026 21:01:40 +0400 Subject: [PATCH 30/56] Apply workspace-member `[tool.uv.sources]` credentials under `uv sync --frozen` (#19423) When running `uv sync --frozen`, project discovery uses `MemberDiscovery::None`, so workspace members aren't loaded into `workspace.packages()` and their `[tool.uv.sources]` entries are invisible to `store_credentials_from_target`. For private Git dependencies this fails with e.g. `fatal: could not read Username for 'https://github.com': terminal prompts disabled`. Walk `lock.packages()` for workspace-member entries (`Source::Editable` or `Source::Virtual`) not already loaded, read each `pyproject.toml` off disk, and feed `[tool.uv.sources]` `Git` or `Url` entries into the existing credential stores. Closes #19419. --------- Co-authored-by: Charlie Marsh --- crates/uv-workspace/src/workspace.rs | 28 ++++++++++- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/tests/it/sync.rs | 65 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index a0de6b9b1a500..9cc9ad8fb7626 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -92,6 +92,8 @@ pub enum MemberDiscovery { /// Discover all workspace members. #[default] All, + /// Discover workspace members that are present, but ignore missing members. + Existing, /// Don't discover any workspace members. None, /// Discover workspace members, but ignore the given paths. @@ -1005,7 +1007,7 @@ impl Workspace { // If the directory is explicitly ignored, skip it. let skip = match &options.members { - MemberDiscovery::All => false, + MemberDiscovery::All | MemberDiscovery::Existing => false, MemberDiscovery::None => true, MemberDiscovery::Ignore(ignore) => ignore.contains(member_root.as_path()), }; @@ -1036,7 +1038,21 @@ impl Workspace { let contents = match fs_err::tokio::read_to_string(&pyproject_path).await { Ok(contents) => contents, Err(err) => { - if !fs_err::metadata(&member_root)?.is_dir() { + let metadata = match fs_err::metadata(&member_root) { + Ok(metadata) => metadata, + Err(err) + if matches!(options.members, MemberDiscovery::Existing) + && err.kind() == std::io::ErrorKind::NotFound => + { + debug!( + "Ignoring missing workspace member: `{}`", + member_root.simplified_display() + ); + continue; + } + Err(err) => return Err(err.into()), + }; + if !metadata.is_dir() { warn!( "Ignoring non-directory workspace member: `{}`", member_root.simplified_display() @@ -1069,6 +1085,14 @@ impl Workspace { continue; } + if matches!(options.members, MemberDiscovery::Existing) { + debug!( + "Ignoring missing workspace member: `{}`", + member_root.simplified_display() + ); + continue; + } + return Err(WorkspaceError::MissingPyprojectTomlMember( member_root, member_glob.to_string(), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 85cc3002edfbf..1bcaa064a5e3c 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -101,7 +101,7 @@ pub(crate) async fn sync( VirtualProject::discover( project_dir, &DiscoveryOptions { - members: MemberDiscovery::None, + members: MemberDiscovery::Existing, ..DiscoveryOptions::default() }, workspace_cache, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index a9bbc519bffd7..6cf1d9c17aed3 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -16477,3 +16477,68 @@ fn sync_reinstalls_on_version_change() -> Result<()> { Ok(()) } + +/// Regression test for #19419: `uv sync --frozen` must honor credentials +/// declared in a workspace member's `[tool.uv.sources]`. +#[test] +#[cfg(feature = "test-git")] +fn sync_frozen_workspace_member_git_credentials() -> Result<()> { + use uv_test::{READ_ONLY_GITHUB_TOKEN, decode_token}; + + let context = uv_test::test_context!("3.12").with_filtered_link_mode_warning(); + let token = decode_token(READ_ONLY_GITHUB_TOKEN); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str(indoc::indoc! {r#" + [project] + name = "root" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["uv-private-pypackage"] + + [tool.uv.workspace] + members = ["nested"] + "#})?; + + let nested = context.temp_dir.child("nested"); + nested.create_dir_all()?; + nested.child("pyproject.toml").write_str(&formatdoc! { + r#" + [project] + name = "nested" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["uv-private-pypackage"] + + [tool.uv.sources] + uv-private-pypackage = {{ git = "https://{token}@github.com/astral-test/uv-private-pypackage" }} + "#, + token = token, + })?; + + // `uv lock` reads the member's `[tool.uv.sources]` and resolves successfully. + uv_snapshot!(&context.filters(), context.lock(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "); + + // `uv sync --frozen` must propagate credentials from the member's + // `[tool.uv.sources]` to `GIT_STORE`. Use `--reinstall --no-cache` so the + // git fetch actually runs. + uv_snapshot!(&context.filters(), context.sync().arg("--frozen").arg("--reinstall").arg("--no-cache"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + uv-private-pypackage==0.1.0 (from git+https://github.com/astral-test/uv-private-pypackage@d780faf0ac91257d4d5a4f0c5a0e4509608c0071) + "); + + Ok(()) +} From a0b7b3b0237fcb884edb8485f70ace69f829681c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:11:48 +0100 Subject: [PATCH 31/56] Update Rust crate hashbrown to v0.17.1 (#19448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [hashbrown](https://redirect.github.com/rust-lang/hashbrown) | workspace.dependencies | patch | `0.17.0` → `0.17.1` | --- ### Release Notes
rust-lang/hashbrown (hashbrown) ### [`v0.17.1`](https://redirect.github.com/rust-lang/hashbrown/blob/HEAD/CHANGELOG.md#0171---2026-04-20) [Compare Source](https://redirect.github.com/rust-lang/hashbrown/compare/v0.17.0...v0.17.1) ##### Added - Added `HashMap::rustc_try_insert` ([#​722](https://redirect.github.com/rust-lang/hashbrown/issues/722))
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21bd2fb4e4150..5ee9764809328 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1921,9 +1921,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -2260,7 +2260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -4030,7 +4030,7 @@ checksum = "73389e0c99e664f919275ab5b5b0471391fe9a8de61e1dff9b1eaf56a90f16e3" dependencies = [ "bytecheck", "bytes", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap", "munge", "ptr_meta", @@ -6882,7 +6882,7 @@ name = "uv-pypi-types" version = "0.0.47" dependencies = [ "anyhow", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap", "insta", "itertools 0.14.0", @@ -7062,7 +7062,7 @@ dependencies = [ "either", "fs-err", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap", "insta", "itertools 0.14.0", From 46711bba30c33aca40ceefe65565c8c27bdb190a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:12:30 +0100 Subject: [PATCH 32/56] Update Rust crate tokio to v1.52.3 (#19449) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tokio](https://tokio.rs) ([source](https://redirect.github.com/tokio-rs/tokio)) | workspace.dependencies | patch | `1.52.1` → `1.52.3` | --- ### Release Notes
tokio-rs/tokio (tokio) ### [`v1.52.3`](https://redirect.github.com/tokio-rs/tokio/releases/tag/tokio-1.52.3): Tokio v1.52.3 [Compare Source](https://redirect.github.com/tokio-rs/tokio/compare/tokio-1.52.2...tokio-1.52.3) ### 1.52.3 (May 8th, 2026) ##### Fixed - sync: fix underflow in mpsc channel `len()` ([#​8062]) - sync: notify receivers in mpsc `OwnedPermit::release()` method ([#​8075]) - sync: require that an `RwLock` has `max_readers != 0` ([#​8076]) - sync: return `Empty` from `try_recv()` when mpsc is closed with outstanding permits ([#​8074]) [#​8062]: https://redirect.github.com/tokio-rs/tokio/pull/8062 [#​8074]: https://redirect.github.com/tokio-rs/tokio/pull/8074 [#​8075]: https://redirect.github.com/tokio-rs/tokio/pull/8075 [#​8076]: https://redirect.github.com/tokio-rs/tokio/pull/8076 ### [`v1.52.2`](https://redirect.github.com/tokio-rs/tokio/releases/tag/tokio-1.52.2): Tokio v1.52.2 [Compare Source](https://redirect.github.com/tokio-rs/tokio/compare/tokio-1.52.1...tokio-1.52.2) ### 1.52.2 (May 4th, 2026) This release reverts the LIFO slot stealing change introduced in 1.51.0 ([#​7431]), due to [its performance impact][#​8065]. ([#​8100]) [#​7431]: https://redirect.github.com/tokio-rs/tokio/pull/7431 [#​8065]: https://redirect.github.com/tokio-rs/tokio/pull/8065 [#​8100]: https://redirect.github.com/tokio-rs/tokio/pull/8100
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ee9764809328..eb3d2ab5574bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -90,7 +90,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -101,7 +101,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1297,7 +1297,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1476,7 +1476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2346,7 +2346,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2407,7 +2407,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -2529,7 +2529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "160f2eade097f30263b548aae5deb12ad349c909baa710fa24b92c9090b2e006" dependencies = [ "scopeguard", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2784,7 +2784,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2863,7 +2863,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3568,7 +3568,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4152,7 +4152,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4209,7 +4209,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4707,7 +4707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4914,7 +4914,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -5160,9 +5160,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5503,7 +5503,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7684,7 +7684,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] From 2ebd8410ceac280c385dfd335e912821577b3d50 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:16:18 +0100 Subject: [PATCH 33/56] Update Rust crate filetime to v0.2.28 (#19446) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | Pending | |---|---|---|---|---| | [filetime](https://redirect.github.com/alexcrichton/filetime) | workspace.dependencies | patch | `0.2.27` → `0.2.28` | `0.2.29` | --- ### Release Notes
alexcrichton/filetime (filetime) ### [`v0.2.28`](https://redirect.github.com/alexcrichton/filetime/compare/0.2.27...0.2.28) [Compare Source](https://redirect.github.com/alexcrichton/filetime/compare/0.2.27...0.2.28)
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb3d2ab5574bd..4600cb6613c26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1536,13 +1536,12 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "2d5b2eef6fafbf69f877e55509ce5b11a760690ac9700a2921be067aa6afaef6" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -2585,10 +2584,7 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags", "libc", - "plain", - "redox_syscall 0.7.0", ] [[package]] @@ -3105,7 +3101,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.15", + "redox_syscall", "smallvec", "windows-link 0.2.1", ] @@ -3702,15 +3698,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f3fe0889e69e2ae9e41f4d6c4c0181701d00e4697b356fb1f74173a5e0ee27" -dependencies = [ - "bitflags", -] - [[package]] name = "redox_users" version = "0.5.2" From be92e79ecaf301b856ae9fc5db635c85f24e1f1d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 06:16:53 +0100 Subject: [PATCH 34/56] Update CodSpeedHQ/action action to v4.15.1 (#19442) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [CodSpeedHQ/action](https://redirect.github.com/CodSpeedHQ/action) | action | patch | `v4.15.0` → `v4.15.1` | --- ### Release Notes
CodSpeedHQ/action (CodSpeedHQ/action) ### [`v4.15.1`](https://redirect.github.com/CodSpeedHQ/action/releases/tag/v4.15.1) [Compare Source](https://redirect.github.com/CodSpeedHQ/action/compare/v4.15.0...v4.15.1) #### Release Notes ##### 🚀 Features - Add a way to disable perf compression through env var by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) in [#​334](https://redirect.github.com/CodSpeedHQ/runner/pull/334) - Do not display the comparison nudge when not in an interactive terminal by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) in [#​300](https://redirect.github.com/CodSpeedHQ/runner/pull/300) - Display a warning when profiling generation failed by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) - Detect and abort on ring buffer overflow by [@​not-matthias](https://redirect.github.com/not-matthias) in [#​321](https://redirect.github.com/CodSpeedHQ/runner/pull/321) - Grow ring buffer to 16 MiB by [@​not-matthias](https://redirect.github.com/not-matthias) ##### ⚙️ Internals - chore: bump runner version to 4.15.1 by [@​github-actions](https://redirect.github.com/github-actions)\[bot] in [#​205](https://redirect.github.com/CodSpeedHQ/action/pull/205) - Increase stack sampling for python by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) in [#​337](https://redirect.github.com/CodSpeedHQ/runner/pull/337) - Replace hard coded lint job for a dedicated check action by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) in [#​320](https://redirect.github.com/CodSpeedHQ/runner/pull/320) - Bump cpp-linter-hooks to support darwin by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) - Swap pre-commit action for prek by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) - Fix unused clippy errors in test targets on macos by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) - Run pre-commit hooks on macos and ubuntu-latest by [@​GuillaumeLagrange](https://redirect.github.com/GuillaumeLagrange) #### Install codspeed-runner 4.15.1 ##### Install prebuilt binaries via shell script ```sh curl --proto '=https' --tlsv1.2 -LsSf https://github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-installer.sh | sh ``` #### Download codspeed-runner 4.15.1 | File | Platform | Checksum | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | [codspeed-runner-aarch64-apple-darwin.tar.gz](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-aarch64-apple-darwin.tar.gz) | Apple Silicon macOS | [checksum](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-aarch64-apple-darwin.tar.gz.sha256) | | [codspeed-runner-aarch64-unknown-linux-musl.tar.gz](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-aarch64-unknown-linux-musl.tar.gz) | ARM64 MUSL Linux | [checksum](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-aarch64-unknown-linux-musl.tar.gz.sha256) | | [codspeed-runner-x86\_64-unknown-linux-musl.tar.gz](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-x86_64-unknown-linux-musl.tar.gz) | x64 MUSL Linux | [checksum](https://redirect.github.com/CodSpeedHQ/codspeed/releases/download/v4.15.1/codspeed-runner-x86_64-unknown-linux-musl.tar.gz.sha256) | **Full Runner Changelog**: **Full Changelog**:
--- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - Between 12:00 AM and 03:59 AM, only on Monday (`* 0-3 * * 1`) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/astral-sh/uv). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/bench.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 3a8bcdc286192..059161a758da6 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -88,7 +88,7 @@ jobs: run: ./target/debug/uv venv --cache-dir .cache - name: "Run benchmarks" - uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0 + uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 with: run: cargo codspeed run mode: walltime @@ -129,7 +129,7 @@ jobs: run: cargo codspeed build --profile profiling -p uv-bench - name: "Run benchmarks" - uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0 + uses: CodSpeedHQ/action@3194d9a39c4d46684cb44bf7207fc56626aad8fd # v4.15.1 with: run: cargo codspeed run mode: simulation From 8e85f007013e262fb2a7da13b178d4a943289cea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:22:18 +0100 Subject: [PATCH 35/56] Update maturin to v1.13.2 (#19445) --- .github/workflows/build-release-binaries.yml | 44 ++++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 7ebc949fa772b..79164ac75385e 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -48,7 +48,7 @@ jobs: - name: "Build sdist" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 command: sdist args: --out dist - name: "Test sdist" @@ -69,7 +69,7 @@ jobs: - name: "Build sdist uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 command: sdist args: --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Fix Cargo.lock in sdist uv-build" @@ -107,7 +107,7 @@ jobs: - name: "Build wheels - x86_64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: x86_64 args: --release --locked --out dist --features self-update --compatibility pypi env: @@ -140,7 +140,7 @@ jobs: - name: "Build wheels uv-build - x86_64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: x86_64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -173,7 +173,7 @@ jobs: - name: "Build wheels - aarch64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: aarch64 manylinux: 2_17 args: --release --locked --out dist --features self-update --compatibility pypi @@ -213,7 +213,7 @@ jobs: - name: "Build wheels uv-build - aarch64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: aarch64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -270,7 +270,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} args: --release --locked --out dist --features self-update,windows-gui-bin --compatibility pypi env: @@ -312,7 +312,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -353,7 +353,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.target }} # Generally, we try to build in a target docker container. In this case however, a # 32-bit compiler runs out of memory (4GB memory limit for 32-bit), so we cross compile @@ -424,7 +424,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.target }} manylinux: 2_17 docker-options: -e CARGO @@ -483,7 +483,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -541,7 +541,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -600,7 +600,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -663,7 +663,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -723,7 +723,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -784,7 +784,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -832,7 +832,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_31 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -891,7 +891,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: 2_31 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -949,7 +949,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.target }} manylinux: musllinux_1_1 docker-options: -e CARGO @@ -1001,7 +1001,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.target }} manylinux: musllinux_1_1 docker-options: -e CARGO @@ -1060,7 +1060,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 # Tag the musl builds as manylinux 2_17 fallback cause the aarch64 build only support 2_28 @@ -1140,7 +1140,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.1 + maturin-version: v1.13.2 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 args: --profile minimal-size --locked ${{ matrix.platform.arch == 'aarch64' && '--compatibility 2_17' || ''}} --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi From c8c58da3c32fe81ea8c84f7440492e691b1afed3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:22:23 +0100 Subject: [PATCH 36/56] Update dependency astral-sh/uv to v0.11.14 (#19444) --- .github/workflows/check-docs.yml | 2 +- .github/workflows/check-fmt.yml | 2 +- .github/workflows/check-lint.yml | 6 +++--- .github/workflows/publish-docs.yml | 2 +- .github/workflows/test.yml | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/check-docs.yml b/.github/workflows/check-docs.yml index 412cf55ada2f1..ef257c1138c43 100644 --- a/.github/workflows/check-docs.yml +++ b/.github/workflows/check-docs.yml @@ -15,7 +15,7 @@ jobs: persist-credentials: false - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 diff --git a/.github/workflows/check-fmt.yml b/.github/workflows/check-fmt.yml index 0a26b42b33c5d..bdecd74f7b37e 100644 --- a/.github/workflows/check-fmt.yml +++ b/.github/workflows/check-fmt.yml @@ -34,5 +34,5 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - run: uvx ruff format --diff . diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index c8dd7cb180d89..5843d2ed842fb 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -28,7 +28,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - run: uvx ruff check . ty: @@ -41,7 +41,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - run: | uvx ty check python/uv uvx \ @@ -75,7 +75,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - run: uvx --from 'validate-pyproject[all,store]' validate-pyproject pyproject.toml readme: diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index e3e882f2a7990..80b7a85f6a671 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -75,7 +75,7 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - name: "Build docs" run: uv run --only-group docs mkdocs build --strict -f mkdocs.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index afb1e2574bc1e..7a8b52ad0ea79 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,7 +45,7 @@ jobs: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - name: "Install required Python versions" run: uv python install @@ -153,7 +153,7 @@ jobs: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - name: "Install required Python versions" run: uv python install @@ -226,7 +226,7 @@ jobs: - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - version: "0.11.13" + version: "0.11.14" - name: "Install required Python versions" run: uv python install From 2e8487e018a29cce34fd3de8ced33cad9478c735 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:22:32 +0100 Subject: [PATCH 37/56] Update aws-actions/configure-aws-credentials action to v6.1.1 (#19441) --- .github/workflows/test-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 4c9ca06f2481e..b2675874870c5 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -968,7 +968,7 @@ jobs: run: chmod +x ./uv - name: "Configure AWS credentials" - uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} From 1bcfa24190afd69320356e1a5600b22907670d2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:22:34 +0100 Subject: [PATCH 38/56] Update crate-ci/typos action to v1.46.1 (#19443) --- .github/workflows/check-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 5843d2ed842fb..1d88e98b071db 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -162,4 +162,4 @@ jobs: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - - uses: crate-ci/typos@bbaefadf97b0ec5fdc942684b647f1a6ab250274 # v1.46.0 + - uses: crate-ci/typos@5374cbf686e897b15713110e233094e2874de7ef # v1.46.1 From a5f06e55c29aaeb291f835109a4240986d876e03 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:22:59 +0100 Subject: [PATCH 39/56] Update Rust crate h2 to v0.4.14 (#19447) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4600cb6613c26..4e2172c2270aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1875,9 +1875,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", From de7b9b5b75b57f296d2135d215cee9b23320b9ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:23:10 +0100 Subject: [PATCH 40/56] Update taiki-e/install-action action to v2.77.5 (#19451) --- .github/workflows/bench.yml | 6 +++--- .github/workflows/check-lint.yml | 2 +- .github/workflows/test-windows-trampolines.yml | 2 +- .github/workflows/test.yml | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 059161a758da6..b44a60332e022 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -35,7 +35,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-codspeed @@ -72,7 +72,7 @@ jobs: persist-credentials: false - name: "Install codspeed" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-codspeed @@ -113,7 +113,7 @@ jobs: run: rustup show - name: "Install codspeed" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-codspeed diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 1d88e98b071db..694016b970755 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -151,7 +151,7 @@ jobs: with: persist-credentials: false - name: "Install cargo shear" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-shear@1.11.2 - run: cargo shear --deny-warnings diff --git a/.github/workflows/test-windows-trampolines.yml b/.github/workflows/test-windows-trampolines.yml index e7e8814104e51..6ec3d76d409b4 100644 --- a/.github/workflows/test-windows-trampolines.yml +++ b/.github/workflows/test-windows-trampolines.yml @@ -81,7 +81,7 @@ jobs: rustup component add rust-src --target ${{ matrix.target-arch }}-pc-windows-msvc - name: "Install cargo-bloat" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-bloat diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a8b52ad0ea79..6548ce9702565 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,7 +85,7 @@ jobs: sudo chown "$(id -u):$(id -g)" /minix - name: "Install cargo nextest" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-nextest @@ -159,7 +159,7 @@ jobs: run: uv python install - name: "Install cargo nextest" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-nextest @@ -246,7 +246,7 @@ jobs: run: New-Item -Path "C:\uv" -ItemType Directory -Force - name: "Install cargo nextest" - uses: taiki-e/install-action@db5fb34fa772531a3ece57ca434f579eb334e0fb # v2.75.30 + uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: tool: cargo-nextest From 99a032a48a7704cdea6fc024dfe14675a4fc5158 Mon Sep 17 00:00:00 2001 From: Dev-X25874 <283057883+Dev-X25874@users.noreply.github.com> Date: Mon, 18 May 2026 15:58:34 +0530 Subject: [PATCH 41/56] netrc/lex: fix `lineno` not incremented in `read_line` (#19452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Lex::read_line` iterates directly over `self.instream` instead of going through `self.read_char`, which is the only place that increments `self.lineno`. As a result, every newline consumed by `read_line` is invisible to the line counter, causing parse errors reported after any `macdef` block or bare-`#` comment to show an incorrect (too-low) line number. The fix is a one-line change: replace the `for ch in &mut self.instream` loop with `while let Some(ch) = self.read_char()`. The parsed output is identical; only the line-number bookkeeping is corrected. The three call sites affected are: - Discarding a bare `#` comment line in the top-level token loop - Reading each line of a `macdef` macro body - Discarding a bare `#` comment line in the follower-token loop ## Test Plan Existing tests in `crates/uv-netrc/src/netrc.rs` continue to pass. The bug manifests only in error-message line numbers, so no behavioural test was previously catching it. A targeted regression test can be added if the maintainers prefer — happy to include one on request. --- crates/uv-netrc/src/lex.rs | 2 +- crates/uv-netrc/src/netrc.rs | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/uv-netrc/src/lex.rs b/crates/uv-netrc/src/lex.rs index bd4cc7edcf7ed..10ce629dbd77a 100644 --- a/crates/uv-netrc/src/lex.rs +++ b/crates/uv-netrc/src/lex.rs @@ -26,7 +26,7 @@ impl<'a> Lex<'a> { pub(crate) fn read_line(&mut self) -> String { let mut s = String::new(); - for ch in &mut self.instream { + while let Some(ch) = self.read_char() { if ch == '\n' { return s; } diff --git a/crates/uv-netrc/src/netrc.rs b/crates/uv-netrc/src/netrc.rs index 905e0700ea5d8..48eb67bc4853e 100644 --- a/crates/uv-netrc/src/netrc.rs +++ b/crates/uv-netrc/src/netrc.rs @@ -609,4 +609,23 @@ mod tests { Authenticator::new("foo", "", "pass") ); } + + #[test] + fn test_lineno_after_macdef() { + let nrc = Netrc::from_str("macdef mymacro\nline1\nline2\n\nbad_token foo"); + let err = nrc.unwrap_err(); + assert_eq!( + err.to_string(), + "parsing error: bad toplevel token 'bad_token' (line 5)" + ); + } + #[test] + fn test_lineno_after_comment() { + let nrc = Netrc::from_str("# comment\n# comment\nbad_token foo"); + let err = nrc.unwrap_err(); + assert_eq!( + err.to_string(), + "parsing error: bad toplevel token 'bad_token' (line 3)" + ); + } } From c9873e0060b35b0e66ba82fa4315aa2bba43581f Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 18 May 2026 13:08:37 +0200 Subject: [PATCH 42/56] Add TOML v1.1 -> v1.0 backwards compatibility for source distributions (#18741) [TOML 1.1](https://github.com/toml-lang/toml/releases/tag/1.1.0) introduces support for new syntax that older tools with only TOML 1.0 support don't understand. Usually, the user is either in control of which tools need to read the TOML files or the TOML gets converted before publishing (for wheels: `pyproject.toml` -> `METADATA`). The specific case where this doesn't work is when a package manager that only support TOML 1.0 tries to build the source distribution of a dependency. Build tools need to parse `pyproject.toml` in source distributions to extract the `[build-system]` table, and if any other part of the file contains TOML 1.1 syntax, they fail to build. This generally doesn't trigger backtracking, so the user is left with a failure when any (transitive) dependency in their dependency tree has started using a single instance of TOML 1.1. Most package managers, including pip, are implemented in Python and use stdlib's tomllib, which only support TOML 1.0 up to including Python 3.14. To work around this, we do a best-effort rewrite of `pyproject.toml` to TOML 1.0 during source distribution builds. This approach is inspired by Cargo, which has been successfully rewriting published `Cargo.toml`s for many versions. While the `toml` crate doesn't guarantee this downgrade always works (https://github.com/toml-rs/toml/issues/1088), this crate is also used by Cargo, and this best effort rewrite handles the biggest failure case: Newlines and trailing commas in inline tables. Similarly following Cargo, we also add a `pyproject.toml.orig` to the source distribution. https://discuss.python.org/t/adopting-toml-1-1/105624 was inconclusive, but a best-in-class tool should do this transformation. --------- Co-authored-by: Tomasz (Tom) Kramkowski --- Cargo.lock | 85 ++-- Cargo.toml | 7 +- crates/uv-build-backend/Cargo.toml | 1 + crates/uv-build-backend/src/lib.rs | 398 ++++++++++++++---- crates/uv-build-backend/src/source_dist.rs | 90 ++++ crates/uv-preview/src/lib.rs | 7 + crates/uv-resolver/Cargo.toml | 1 + .../src/lock/export/pylock_toml.rs | 24 +- crates/uv-toml/Cargo.toml | 20 + crates/uv-toml/README.md | 13 + crates/uv-toml/src/lib.rs | 312 ++++++++++++++ crates/uv/README.md | 1 + crates/uv/tests/it/build.rs | 14 +- crates/uv/tests/it/build_backend.rs | 52 +++ crates/uv/tests/it/show_settings.rs | 2 + docs/concepts/build-backend.md | 6 +- 16 files changed, 891 insertions(+), 142 deletions(-) create mode 100644 crates/uv-toml/Cargo.toml create mode 100644 crates/uv-toml/README.md create mode 100644 crates/uv-toml/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 4e2172c2270aa..b4e3fd05dfc2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,7 +1476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2345,7 +2345,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2406,7 +2406,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3564,7 +3564,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4139,7 +4139,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4196,7 +4196,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4490,9 +4490,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4901,7 +4901,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5212,67 +5212,58 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.10+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ "foldhash 0.2.0", "indexmap", "serde_core", "serde_spanned", - "toml_datetime 0.7.5+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_datetime" -version = "1.0.0+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.4+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow", + "winnow 1.0.3", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -5978,6 +5969,7 @@ dependencies = [ "uv-platform-tags", "uv-preview", "uv-pypi-types", + "uv-toml", "uv-version", "uv-warnings", "walkdir", @@ -7239,6 +7231,14 @@ dependencies = [ "uv-version", ] +[[package]] +name = "uv-toml" +version = "0.0.47" +dependencies = [ + "toml_datetime", + "toml_parser", +] + [[package]] name = "uv-tool" version = "0.0.47" @@ -7671,7 +7671,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7976,6 +7976,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + [[package]] name = "wiremock" version = "0.6.5" @@ -8204,7 +8213,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -8232,7 +8241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "winnow", + "winnow 0.7.15", "zvariant", ] @@ -8380,7 +8389,7 @@ dependencies = [ "endi", "enumflags2", "serde", - "winnow", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] @@ -8408,5 +8417,5 @@ dependencies = [ "quote", "serde", "syn", - "winnow", + "winnow 0.7.15", ] diff --git a/Cargo.toml b/Cargo.toml index 088318e92f5e6..5334ee680a739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ uv-small-str = { version = "0.0.47", path = "crates/uv-small-str" } uv-state = { version = "0.0.47", path = "crates/uv-state" } uv-static = { version = "0.0.47", path = "crates/uv-static" } uv-test = { version = "0.0.47", path = "crates/uv-test" } +uv-toml = { version = "0.0.47", path = "crates/uv-toml" } uv-tool = { version = "0.0.47", path = "crates/uv-tool" } uv-torch = { version = "0.0.47", path = "crates/uv-torch" } uv-trampoline-builder = { version = "0.0.47", path = "crates/uv-trampoline-builder" } @@ -274,8 +275,10 @@ tokio = { version = "1.40.0", features = [ ] } tokio-stream = { version = "0.1.16" } tokio-util = { version = "0.7.12", features = ["compat", "io"] } -toml = { version = "0.9.2", features = ["fast_hash"] } -toml_edit = { version = "0.25.4", features = ["serde"] } +toml = { version = "1.1.0", features = ["fast_hash"] } +toml_datetime = { version = "1.1.0" } +toml_edit = { version = "0.25.8", features = ["serde"] } +toml_parser = { version = "1.1.0" } tracing = { version = "0.1.40" } tracing-durations-export = { version = "0.3.0", features = ["plot"] } tracing-subscriber = { version = "0.3.18" } # Default feature set for uv_build, uv activates extra features diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index 3354bbae6fbd4..dcee21debf312 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -25,6 +25,7 @@ uv-platform-tags = { workspace = true } uv-preview = { workspace = true } uv-pypi-types = { workspace = true } uv-version = { workspace = true } +uv-toml = { workspace = true } uv-warnings = { workspace = true } astral-tokio-tar = { workspace = true } diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index bde8ef4c0bcb9..75c708c7b6d58 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -36,6 +36,8 @@ pub enum Error { Persist(PathBuf, #[source] io::Error), #[error("Invalid metadata format in: {}", _0.user_display())] Toml(PathBuf, #[source] toml::de::Error), + #[error("Failed to serialize pyproject.toml")] + TomlSerialize(#[source] toml::ser::Error), #[error("Invalid project metadata")] Validation(#[from] ValidationError), #[error("Invalid module name: {0}")] @@ -504,21 +506,11 @@ mod tests { /// Run both a direct wheel build and an indirect wheel build through a source distribution, /// while checking that directly built wheel and indirectly built wheel are the same. - fn build( - source_root: &Path, - dist: &Path, - preview_features: &[PreviewFeature], - ) -> Result { + fn build(source_root: &Path, dist: &Path) -> Result { // Build a direct wheel, capture all its properties to compare it with the indirect wheel // latest and remove it since it has the same filename as the indirect wheel. - let (_name, direct_wheel_list_files) = { - let _preview = uv_preview::test::with_features(preview_features); - list_wheel(source_root, MOCK_UV_VERSION, false)? - }; - let direct_wheel_filename = { - let _preview = uv_preview::test::with_features(preview_features); - build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)? - }; + let (_name, direct_wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; + let direct_wheel_filename = build_wheel(source_root, dist, None, MOCK_UV_VERSION, false)?; let direct_wheel_path = dist.join(direct_wheel_filename.to_string()); let direct_wheel_contents = wheel_contents(&direct_wheel_path); let direct_wheel_hash = sha2::Sha256::digest(fs_err::read(&direct_wheel_path)?); @@ -529,10 +521,7 @@ mod tests { list_source_dist(source_root, MOCK_UV_VERSION, false)?; // TODO(konsti): This should run in the unpacked source dist tempdir, but we need to // normalize the path. - let (_name, wheel_list_files) = { - let _preview = uv_preview::test::with_features(preview_features); - list_wheel(source_root, MOCK_UV_VERSION, false)? - }; + let (_name, wheel_list_files) = list_wheel(source_root, MOCK_UV_VERSION, false)?; let source_dist_filename = build_source_dist(source_root, dist, MOCK_UV_VERSION, false)?; let source_dist_path = dist.join(source_dist_filename.to_string()); let source_dist_contents = sdist_contents(&source_dist_path); @@ -545,16 +534,13 @@ mod tests { source_dist_filename.name.as_dist_info_name(), source_dist_filename.version )); - let wheel_filename = { - let _preview = uv_preview::test::with_features(preview_features); - build_wheel( - &sdist_top_level_directory, - dist, - None, - MOCK_UV_VERSION, - false, - )? - }; + let wheel_filename = build_wheel( + &sdist_top_level_directory, + dist, + None, + MOCK_UV_VERSION, + false, + )?; let wheel_contents = wheel_contents(&dist.join(wheel_filename.to_string())); // Check that direct and indirect wheels are identical. @@ -578,7 +564,7 @@ mod tests { fn build_err(source_root: &Path) -> String { let dist = TempDir::new().unwrap(); - let build_err = build(source_root, dist.path(), &[]).unwrap_err(); + let build_err = build(source_root, dist.path()).unwrap_err(); let err_message: String = format_err(&build_err) .replace(&source_root.user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -700,6 +686,7 @@ mod tests { /// platform-independent deterministic builds. #[test] fn built_by_uv_building() { + let _preview = uv_preview::test::with_features(&[]); let built_by_uv = Path::new("../../test/packages/built-by-uv"); let src = TempDir::new().unwrap(); for dir in [ @@ -762,7 +749,7 @@ mod tests { // Perform both the direct and the indirect build. let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path(), &[]).unwrap(); + let build = build(src.path(), dist.path()).unwrap(); let source_dist_path = dist.path().join(build.source_dist_filename.to_string()); assert_eq!( @@ -893,6 +880,7 @@ mod tests { /// Test that `license = { file = "LICENSE" }` is supported. #[test] fn license_file_pre_pep639() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), @@ -929,17 +917,14 @@ mod tests { let sdist_tree = TempDir::new().unwrap(); let source_dist_path = output_dir.path().join("pep_pep639_license-1.0.0.tar.gz"); unpack_sdist(&source_dist_path, sdist_tree.path()).unwrap(); - { - let _preview = uv_preview::test::with_features(&[]); - build_wheel( - &sdist_tree.path().join("pep_pep639_license-1.0.0"), - output_dir.path(), - None, - "0.5.15", - false, - ) - .unwrap(); - } + build_wheel( + &sdist_tree.path().join("pep_pep639_license-1.0.0"), + output_dir.path(), + None, + "0.5.15", + false, + ) + .unwrap(); let wheel = output_dir .path() .join("pep_pep639_license-1.0.0-py3-none-any.whl"); @@ -957,6 +942,7 @@ mod tests { /// Test that `build_wheel` works after the `prepare_metadata_for_build_wheel` hook. #[test] fn prepare_metadata_then_build_wheel() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), @@ -982,31 +968,22 @@ mod tests { .unwrap(); // Prepare the metadata. - let metadata_dir = { - let _preview = uv_preview::test::with_features(&[]); - TempDir::new().unwrap() - }; - let dist_info_dir = { - let _preview = uv_preview::test::with_features(&[]); - metadata(src.path(), metadata_dir.path(), "0.5.15").unwrap() - }; + let metadata_dir = TempDir::new().unwrap(); + let dist_info_dir = metadata(src.path(), metadata_dir.path(), "0.5.15").unwrap(); let metadata_prepared = fs_err::read_to_string(metadata_dir.path().join(&dist_info_dir).join("METADATA")) .unwrap(); // Build the wheel, using the prepared metadata directory. let output_dir = TempDir::new().unwrap(); - { - let _preview = uv_preview::test::with_features(&[]); - build_wheel( - src.path(), - output_dir.path(), - Some(&metadata_dir.path().join(&dist_info_dir)), - "0.5.15", - false, - ) - .unwrap(); - } + build_wheel( + src.path(), + output_dir.path(), + Some(&metadata_dir.path().join(&dist_info_dir)), + "0.5.15", + false, + ) + .unwrap(); let wheel = output_dir .path() .join("two_step_build-1.0.0-py3-none-any.whl"); @@ -1024,6 +1001,7 @@ mod tests { /// Check that non-normalized paths for `module-root` work with the glob inclusions. #[test] fn test_glob_path_normalization() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), @@ -1047,7 +1025,7 @@ mod tests { File::create(src.path().join("two_step_build").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path(), &[]).unwrap(); + let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @" two_step_build-1.0.0/ @@ -1086,13 +1064,14 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build2 = build(src.path(), dist.path(), &[]).unwrap(); + let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// Check that upper case letters in module names work. #[test] fn test_camel_case() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1113,7 +1092,7 @@ mod tests { File::create(src.path().join("src").join("camelCase").join("__init__.py")).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path(), &[]).unwrap(); + let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @" camelCase/ @@ -1130,7 +1109,7 @@ mod tests { pyproject_toml.replace("camelCase", "camel_case"), ) .unwrap(); - let build_err = build(src.path(), dist.path(), &[]).unwrap_err(); + let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -1143,6 +1122,7 @@ mod tests { /// Test that no partial files are left in dist directory when build fails. #[test] fn no_partial_files_on_build_failure() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); // Create a minimal pyproject.toml without __init__.py (will fail) @@ -1167,10 +1147,7 @@ mod tests { assert!(sdist_result.is_err()); // Wheel build should fail - let wheel_result = { - let _preview = uv_preview::test::with_features(&[]); - build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false) - }; + let wheel_result = build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false); assert!(wheel_result.is_err()); // dist directory should be empty (no partial files) @@ -1184,6 +1161,7 @@ mod tests { /// Test that pre-existing files in the dist directory are deleted before build starts. #[test] fn existing_files_deleted_on_build_failure() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); // Create a minimal pyproject.toml without __init__.py (will fail) @@ -1214,10 +1192,7 @@ mod tests { let sdist_result = build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false); assert!(sdist_result.is_err()); - let wheel_result = { - let _preview = uv_preview::test::with_features(&[]); - build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false) - }; + let wheel_result = build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false); assert!(wheel_result.is_err()); // Verify pre-existing files were deleted @@ -1234,6 +1209,7 @@ mod tests { /// Test that existing files are overwritten on successful build. #[test] fn existing_files_overwritten_on_success() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); // Create a valid project @@ -1270,10 +1246,7 @@ mod tests { // Build should succeed and overwrite existing files build_source_dist(src.path(), dist.path(), MOCK_UV_VERSION, false).unwrap(); - { - let _preview = uv_preview::test::with_features(&[]); - build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false).unwrap(); - } + build_wheel(src.path(), dist.path(), None, MOCK_UV_VERSION, false).unwrap(); // Verify files were overwritten (content should be different) assert_ne!( @@ -1300,6 +1273,7 @@ mod tests { #[test] fn invalid_stubs_name() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1317,7 +1291,7 @@ mod tests { fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); let dist = TempDir::new().unwrap(); - let build_err = build(src.path(), dist.path(), &[]).unwrap_err(); + let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err); assert_snapshot!( err_message, @@ -1331,6 +1305,7 @@ mod tests { /// Stubs packages use a special name and `__init__.pyi`. #[test] fn stubs_package() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1353,7 +1328,7 @@ mod tests { File::create(®ular_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build_err = build(src.path(), dist.path(), &[]).unwrap_err(); + let build_err = build(src.path(), dist.path()).unwrap_err(); let err_message = format_err(&build_err) .replace(&src.path().user_display().to_string(), "[TEMP_PATH]") .replace('\\', "/"); @@ -1372,7 +1347,7 @@ mod tests { ) .unwrap(); - let build1 = build(src.path(), dist.path(), &[]).unwrap(); + let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @" stuffed_bird-stubs/ stuffed_bird-stubs/__init__.pyi @@ -1398,13 +1373,14 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path(), &[]).unwrap(); + let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1.wheel_contents, build2.wheel_contents); } /// A simple namespace package with a single root `__init__.py`. #[test] fn simple_namespace_package() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1452,7 +1428,7 @@ mod tests { fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path(), &[]).unwrap(); + let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.source_dist_contents.join("\n"), @" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO @@ -1489,13 +1465,14 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path(), &[]).unwrap(); + let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// A complex namespace package with a multiple root `__init__.py`. #[test] fn complex_namespace_package() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1543,7 +1520,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build1 = build(src.path(), dist.path(), &[]).unwrap(); + let build1 = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build1.wheel_contents.join("\n"), @" complex_namespace-1.0.0.dist-info/ complex_namespace-1.0.0.dist-info/METADATA @@ -1573,13 +1550,14 @@ mod tests { }; fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); - let build2 = build(src.path(), dist.path(), &[]).unwrap(); + let build2 = build(src.path(), dist.path()).unwrap(); assert_eq!(build1, build2); } /// Stubs for a namespace package. #[test] fn stubs_namespace() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1614,7 +1592,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path(), &[]).unwrap(); + let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.wheel_contents.join("\n"), @" cloud-stubs/ cloud-stubs/db/ @@ -1630,6 +1608,7 @@ mod tests { /// A package with multiple modules, one a regular module and two namespace modules. #[test] fn multiple_module_names() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1711,7 +1690,7 @@ mod tests { fs_err::remove_file(bogus_init_py).unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path(), &[]).unwrap(); + let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @" simple_namespace_part-1.0.0/ simple_namespace_part-1.0.0/PKG-INFO @@ -1797,6 +1776,7 @@ mod tests { /// A package with duplicate module names. #[test] fn duplicate_module_names() { + let _preview = uv_preview::test::with_features(&[]); let src = TempDir::new().unwrap(); let pyproject_toml = indoc! {r#" [project] @@ -1825,7 +1805,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path(), &[]).unwrap(); + let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.source_dist_contents.join("\n"), @" duplicate-1.0.0/ duplicate-1.0.0/PKG-INFO @@ -1853,6 +1833,7 @@ mod tests { /// Check that JSON metadata files are present. #[test] fn metadata_json_preview() { + let _preview = uv_preview::test::with_features(&[PreviewFeature::MetadataJson]); let src = TempDir::new().unwrap(); fs_err::write( src.path().join("pyproject.toml"), @@ -1878,7 +1859,7 @@ mod tests { .unwrap(); let dist = TempDir::new().unwrap(); - let build = build(src.path(), dist.path(), &[PreviewFeature::MetadataJson]).unwrap(); + let build = build(src.path(), dist.path()).unwrap(); assert_snapshot!(build.wheel_contents.join("\n"), @" metadata_json_preview-1.0.0.dist-info/ @@ -1891,4 +1872,247 @@ mod tests { metadata_json_preview/__init__.py "); } + + /// Test that nested `pyproject.toml` files in subdirectories are preserved in the sdist and + /// not accidentally skipped by the root-level TOML rewriting logic. + #[test] + fn nested_pyproject_toml_preserved() { + let _preview = + uv_preview::test::with_features(&[PreviewFeature::TomlBackwardsCompatibility]); + let tmp_dir = TempDir::new().unwrap(); + + fs_err::write( + tmp_dir.path().join("pyproject.toml"), + indoc! {r#" + [project] + name = "nested-pyproject" + version = "1.0.0" + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + + [tool.uv.build-backend] + source-include = ["subproject/**"] + "#}, + ) + .unwrap(); + + let nested_pyproject = tmp_dir.path().join("src").join("nested_pyproject"); + fs_err::create_dir_all(&nested_pyproject).unwrap(); + File::create(nested_pyproject.join("__init__.py")).unwrap(); + + // Create a nested pyproject.toml in a subdirectory + fs_err::write( + nested_pyproject.join("pyproject.toml"), + indoc! {r#" + [project] + name = "subproject" + version = "0.1.0" + "#}, + ) + .unwrap(); + + let dist = TempDir::new().unwrap(); + let source_dist_filename = + build_source_dist(tmp_dir.path(), dist.path(), MOCK_UV_VERSION, false).unwrap(); + let source_dist_path = dist.path().join(source_dist_filename.to_string()); + let contents = sdist_contents(&source_dist_path); + + // The nested `pyproject.toml` should be present, and not being rewritten + // with a `pyproject.toml.orig`. + assert!( + contents + .iter() + .any(|f| f.contains("nested_pyproject/pyproject.toml")), + ); + assert!( + !contents + .iter() + .any(|f| f.contains("nested_pyproject/pyproject.toml.orig")), + ); + } + + /// Test that TOML 1.1 features in pyproject.toml are rewritten to TOML 1.0 for backward + /// compatibility with older tools. The original file is preserved as pyproject.toml.orig. + #[test] + fn toml_1_1_backward_compatibility() { + let _preview = + uv_preview::test::with_features(&[PreviewFeature::TomlBackwardsCompatibility]); + let src = TempDir::new().unwrap(); + + // A `pyproject.toml` with a TOML 1.1 feature, trailing commas in inline tables. + let pyproject_toml = indoc! {r#" + [project] + name = "toml11-project" + version = "0.1.0" + description = "A test package using TOML 1.1 features" + requires-python = ">=3.12" + # TOML 1.1 feature: Trailing comma in inline table + authors = [ + { name = "Ferris", email = "ferris@example.com", }, + { name = "Platypus", email = "platypus@example.com", }, + ] + + [tool.foo] + when = 1969-06-20T20:17Z + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + "#}; + + fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); + fs_err::create_dir_all(src.path().join("src").join("toml11_project")).unwrap(); + File::create( + src.path() + .join("src") + .join("toml11_project") + .join("__init__.py"), + ) + .unwrap(); + + let dist = TempDir::new().unwrap(); + let build = build(src.path(), dist.path()).unwrap(); + + // Check that both `pyproject.toml` and `pyproject.toml.orig` are in the sdist. + assert_snapshot!(build.source_dist_contents.join("\n"), @" + toml11_project-0.1.0/ + toml11_project-0.1.0/PKG-INFO + toml11_project-0.1.0/pyproject.toml + toml11_project-0.1.0/pyproject.toml.orig + toml11_project-0.1.0/src + toml11_project-0.1.0/src/toml11_project + toml11_project-0.1.0/src/toml11_project/__init__.py + "); + + // Extract the sdist to verify the contents of both files. + let source_dist_path = dist.path().join(build.source_dist_filename.to_string()); + let sdist_tree = TempDir::new().unwrap(); + unpack_sdist(&source_dist_path, sdist_tree.path()).unwrap(); + let sdist_top_level_directory = sdist_tree.path().join(format!( + "{}-{}", + build.source_dist_filename.name.as_dist_info_name(), + build.source_dist_filename.version + )); + let pyproject_toml_content = + fs_err::read_to_string(sdist_top_level_directory.join("pyproject.toml")).unwrap(); + let pyproject_toml_orig_content = + fs_err::read_to_string(sdist_top_level_directory.join("pyproject.toml.orig")).unwrap(); + + assert_eq!(pyproject_toml_orig_content, pyproject_toml); + assert_snapshot!(pyproject_toml_content, @r#" + [project] + name = "toml11-project" + version = "0.1.0" + description = "A test package using TOML 1.1 features" + requires-python = ">=3.12" + + [[project.authors]] + name = "Ferris" + email = "ferris@example.com" + + [[project.authors]] + name = "Platypus" + email = "platypus@example.com" + + [tool.foo] + when = 1969-06-20T20:17:00Z + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + "#); + } + + /// Test that TOML 1.1 features in pyproject.toml trigger auto-detection and rewrite to TOML + /// 1.0, even without explicitly enabling `PreviewFeature::TomlBackwardsCompatibility`. + #[test] + fn toml_1_1_backward_compatibility_auto_detection() { + let _preview = uv_preview::test::with_features(&[]); + let src = TempDir::new().unwrap(); + + // A `pyproject.toml` with a TOML 1.1 feature, trailing commas in inline tables. + let pyproject_toml = indoc! {r#" + [project] + name = "toml11-project" + version = "0.1.0" + description = "A test package using TOML 1.1 features" + requires-python = ">=3.12" + # TOML 1.1 feature: Trailing comma in inline table + authors = [ + { name = "Ferris", email = "ferris@example.com", }, + { name = "Platypus", email = "platypus@example.com", }, + ] + + [tool.foo] + when = 1969-06-20T20:17Z + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + "#}; + + fs_err::write(src.path().join("pyproject.toml"), pyproject_toml).unwrap(); + fs_err::create_dir_all(src.path().join("src").join("toml11_project")).unwrap(); + File::create( + src.path() + .join("src") + .join("toml11_project") + .join("__init__.py"), + ) + .unwrap(); + + let dist = TempDir::new().unwrap(); + let build = build(src.path(), dist.path()).unwrap(); + + // Check that both `pyproject.toml` and `pyproject.toml.orig` are in the sdist. + assert_snapshot!(build.source_dist_contents.join("\n"), @" + toml11_project-0.1.0/ + toml11_project-0.1.0/PKG-INFO + toml11_project-0.1.0/pyproject.toml + toml11_project-0.1.0/pyproject.toml.orig + toml11_project-0.1.0/src + toml11_project-0.1.0/src/toml11_project + toml11_project-0.1.0/src/toml11_project/__init__.py + "); + + // Extract the sdist to verify the contents of both files. + let source_dist_path = dist.path().join(build.source_dist_filename.to_string()); + let sdist_tree = TempDir::new().unwrap(); + unpack_sdist(&source_dist_path, sdist_tree.path()).unwrap(); + let sdist_top_level_directory = sdist_tree.path().join(format!( + "{}-{}", + build.source_dist_filename.name.as_dist_info_name(), + build.source_dist_filename.version + )); + let pyproject_toml_content = + fs_err::read_to_string(sdist_top_level_directory.join("pyproject.toml")).unwrap(); + let pyproject_toml_orig_content = + fs_err::read_to_string(sdist_top_level_directory.join("pyproject.toml.orig")).unwrap(); + + assert_eq!(pyproject_toml_orig_content, pyproject_toml); + assert_snapshot!(pyproject_toml_content, @r#" + [project] + name = "toml11-project" + version = "0.1.0" + description = "A test package using TOML 1.1 features" + requires-python = ">=3.12" + + [[project.authors]] + name = "Ferris" + email = "ferris@example.com" + + [[project.authors]] + name = "Platypus" + email = "platypus@example.com" + + [tool.foo] + when = 1969-06-20T20:17:00Z + + [build-system] + requires = ["uv_build>=0.5.15,<0.6.0"] + build-backend = "uv_build" + "#); + } } diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 79d925ff6256c..e83f1b75f4fab 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -21,6 +21,8 @@ use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; use uv_fs::{Simplified, normalize_path}; use uv_globfilter::{GlobDirFilter, PortableGlobParser}; +use uv_preview::PreviewFeature; +use uv_toml::has_toml11_features; use uv_warnings::warn_user_once; use walkdir::WalkDir; @@ -231,6 +233,57 @@ fn write_source_dist( metadata_email.as_bytes(), )?; + // Build tools need to parse `pyproject.toml` in source distributions to extract the + // `[build-system]` table, and if any other part of the file contains too new TOML syntax, they + // fail to build. This generally doesn't trigger backtracking, so the user is left if a failure + // when any (transitive) dependency in their dependency tree has started using a single instance + // of TOML 1.1. Most package managers, including pip, are implemented in Python and use stdlib's + // tomllib, which only support TOML 1.0 up to including Python 3.14. + // + // To work around this, we do a best-effort rewrite of `pyproject.toml` to TOML 1.0. We also + // add the original `pyproject.toml` as `pyproject.toml.orig` for reference. + // + // The feature is enabled either explicitly via the preview flag, or automatically when the + // `pyproject.toml` is detected to contain TOML 1.1-only syntax. + let pyproject_path = source_tree.join("pyproject.toml"); + let pyproject_contents = fs_err::read_to_string(&pyproject_path)?; + let toml_backwards_compatibility = + if uv_preview::is_enabled(PreviewFeature::TomlBackwardsCompatibility) { + true + } else if has_toml11_features(&pyproject_contents) { + warn_user_once!( + "`pyproject.toml` uses TOML 1.1 features; rewriting to TOML 1.0 for \ + compatibility with older build tools. Use `--preview-feature \ + {feature}` to suppress this warning.", + feature = PreviewFeature::TomlBackwardsCompatibility + ); + true + } else { + false + }; + if toml_backwards_compatibility { + let mut pyproject_value: toml::Value = toml::from_str(&pyproject_contents) + .map_err(|err| Error::Toml(pyproject_path.clone(), err))?; + // See https://github.com/toml-rs/toml/issues/1088 for `to_string_pretty`. + normalize_toml10_datetimes(&mut pyproject_value); + let pyproject_rewritten = + toml::to_string_pretty(&pyproject_value).map_err(Error::TomlSerialize)?; + writer.write_bytes( + &Path::new(&top_level) + .join("pyproject.toml") + .portable_display() + .to_string(), + pyproject_rewritten.as_bytes(), + )?; + writer.write_file( + &Path::new(&top_level) + .join("pyproject.toml.orig") + .portable_display() + .to_string(), + &pyproject_path, + )?; + } + let (include_matcher, exclude_matcher) = source_dist_matcher(source_tree, &pyproject_toml, settings, show_warnings)?; @@ -280,6 +333,17 @@ fn write_source_dist( continue; } + if toml_backwards_compatibility { + // `pyproject.toml` is handled separately. + if relative == "pyproject.toml" { + continue; + } + if relative == "pyproject.toml.orig" { + debug!("Ignoring existing `pyproject.toml.orig`"); + continue; + } + } + error_on_venv(entry.file_name(), entry.path())?; if entry.file_type().is_dir() { @@ -373,6 +437,32 @@ impl TarGzWriter { } } +fn normalize_toml10_datetimes(value: &mut toml::Value) { + match value { + toml::Value::Datetime(datetime) => { + if let Some(time) = datetime.time.as_mut() + && time.second.is_none() + { + time.second = Some(0); + } + } + toml::Value::Array(values) => { + for value in values { + normalize_toml10_datetimes(value); + } + } + toml::Value::Table(values) => { + for (_, value) in values.iter_mut() { + normalize_toml10_datetimes(value); + } + } + toml::Value::String(_) + | toml::Value::Integer(_) + | toml::Value::Float(_) + | toml::Value::Boolean(_) => {} + } +} + impl DirectoryWriter for TarGzWriter { fn write_bytes(&mut self, path: &str, bytes: &[u8]) -> Result<(), Error> { let mut header = Header::new_gnu(); diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index b599687c3ea26..3d283a8b9cd5e 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -257,6 +257,7 @@ pub enum PreviewFeature { ProjectDirectoryMustExist = 1 << 27, IndexExcludeNewer = 1 << 28, AzureEndpoint = 1 << 29, + TomlBackwardsCompatibility = 1 << 30, } impl PreviewFeature { @@ -293,6 +294,7 @@ impl PreviewFeature { Self::ProjectDirectoryMustExist => "project-directory-must-exist", Self::IndexExcludeNewer => "index-exclude-newer", Self::AzureEndpoint => "azure-endpoint", + Self::TomlBackwardsCompatibility => "toml-backwards-compatibility", } } } @@ -342,6 +344,7 @@ impl FromStr for PreviewFeature { "project-directory-must-exist" => Self::ProjectDirectoryMustExist, "index-exclude-newer" => Self::IndexExcludeNewer, "azure-endpoint" => Self::AzureEndpoint, + "toml-backwards-compatibility" => Self::TomlBackwardsCompatibility, _ => return Err(PreviewFeatureParseError), }) } @@ -596,6 +599,10 @@ mod tests { "index-exclude-newer" ); assert_eq!(PreviewFeature::AzureEndpoint.as_str(), "azure-endpoint"); + assert_eq!( + PreviewFeature::TomlBackwardsCompatibility.as_str(), + "toml-backwards-compatibility" + ); } #[test] diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 6bf5259dc03fd..8435fdc4c6f24 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -82,4 +82,5 @@ insta = { workspace = true } toml = { workspace = true } [features] +schemars = ["dep:schemars", "uv-distribution-types/schemars"] tracing-durations-export = [] diff --git a/crates/uv-resolver/src/lock/export/pylock_toml.rs b/crates/uv-resolver/src/lock/export/pylock_toml.rs index 313b605139c35..a61d1d0d9cb59 100644 --- a/crates/uv-resolver/src/lock/export/pylock_toml.rs +++ b/crates/uv-resolver/src/lock/export/pylock_toml.rs @@ -1711,13 +1711,9 @@ where minute: u8::try_from(timestamp.minute()).map_err(serde::ser::Error::custom)?, second: Some(u8::try_from(timestamp.second()).map_err(serde::ser::Error::custom)?), nanosecond: { - let nanosecond = + let nanos = u32::try_from(timestamp.nanosecond()).map_err(serde::ser::Error::custom)?; - if nanosecond == 0 { - None - } else { - Some(nanosecond) - } + if nanos == 0 { None } else { Some(nanos) } }, }), offset: Some(toml_edit::Offset::Z), @@ -1761,10 +1757,18 @@ where let time = if let Some(time) = datetime.time { let hour = i8::try_from(time.hour).map_err(serde::de::Error::custom)?; let minute = i8::try_from(time.minute).map_err(serde::de::Error::custom)?; - let second = - i8::try_from(time.second.unwrap_or_default()).map_err(serde::de::Error::custom)?; - let nanosecond = - i32::try_from(time.nanosecond.unwrap_or_default()).map_err(serde::de::Error::custom)?; + let second = time + .second + .map(i8::try_from) + .transpose() + .map_err(serde::de::Error::custom)? + .unwrap_or_default(); + let nanosecond = time + .nanosecond + .map(i32::try_from) + .transpose() + .map_err(serde::de::Error::custom)? + .unwrap_or_default(); Time::new(hour, minute, second, nanosecond).map_err(serde::de::Error::custom)? } else { Time::midnight() diff --git a/crates/uv-toml/Cargo.toml b/crates/uv-toml/Cargo.toml new file mode 100644 index 0000000000000..abbcae9ad4369 --- /dev/null +++ b/crates/uv-toml/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "uv-toml" +version = "0.0.47" +description = "This is an internal component crate of uv" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lib] +doctest = false + +[lints] +workspace = true + +[dependencies] +toml_datetime = { workspace = true } +toml_parser = { workspace = true } diff --git a/crates/uv-toml/README.md b/crates/uv-toml/README.md new file mode 100644 index 0000000000000..3c6b445e3f8e6 --- /dev/null +++ b/crates/uv-toml/README.md @@ -0,0 +1,13 @@ + + +# uv-toml + +This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here +is unstable and will have frequent breaking changes. + +This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-toml). + +See uv's +[crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) +for details on versioning. diff --git a/crates/uv-toml/src/lib.rs b/crates/uv-toml/src/lib.rs new file mode 100644 index 0000000000000..d7ebe90d537f3 --- /dev/null +++ b/crates/uv-toml/src/lib.rs @@ -0,0 +1,312 @@ +use toml_datetime::Datetime; +use toml_parser::decoder::Encoding; +use toml_parser::lexer::Token; +use toml_parser::parser::{EventReceiver, parse_document}; +use toml_parser::{ErrorSink, Source, Span}; + +/// Detect TOML 1.1 specific features in a TOML document. +/// +/// Note: This function does _not_ perform any validation. +pub fn has_toml11_features(source: &str) -> bool { + let tokens: Box<[Token]> = Source::new(source).lex().collect(); + let mut checker = DetectToml11::new(source); + let mut errors = None; + parse_document(&tokens, &mut checker, &mut errors); + checker.is_11() +} + +/// Structure state in a TOML document +#[derive(Debug, Copy, Clone)] +enum State { + /// Regular table (e.g. `[foo]`) + StdTable, + /// Array table (e.g. `[[foo]]`) + ArrayTable, + /// Inline table (e.g. `{ k = "v" }` + InlineTable { trailing_sep: bool }, + /// Array (e.g. `[1, 2, 3]`) + Array, +} + +/// Detect TOML 1.1 specific features. +pub struct DetectToml11<'s> { + /// The underlying TOML source + source: &'s str, + /// Current nesting state + state: Vec, + /// Set to true when a TOML 1.1 specific feature is seen + toml11: bool, +} + +impl<'s> DetectToml11<'s> { + fn new(source: &'s str) -> Self { + Self { + source, + state: Vec::new(), + toml11: false, + } + } + + fn raw_at(&self, span: Span) -> &'s str { + &self.source[span.start()..span.end()] + } + + fn flag_11(&mut self) { + self.toml11 = true; + } + + fn set_sep(&mut self, sep: bool) { + if let Some(State::InlineTable { trailing_sep }) = self.state.last_mut() { + *trailing_sep = sep; + } + } + + pub fn is_11(&self) -> bool { + self.toml11 + } +} + +impl EventReceiver for DetectToml11<'_> { + fn std_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.state.push(State::StdTable); + } + + fn std_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.state.pop(); + } + + fn array_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.state.push(State::ArrayTable); + } + + fn array_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.state.pop(); + } + + fn inline_table_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool { + self.state.push(State::InlineTable { + trailing_sep: false, + }); + true + } + + fn inline_table_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + if matches!( + self.state.last(), + Some(State::InlineTable { trailing_sep: true }) + ) { + // TOML 1.1 introduces trailing commas in inline tables + self.flag_11(); + } + self.state.pop(); + } + + fn array_open(&mut self, _span: Span, _error: &mut dyn ErrorSink) -> bool { + self.state.push(State::Array); + true + } + + fn array_close(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.state.pop(); + } + + fn simple_key(&mut self, span: Span, kind: Option, _error: &mut dyn ErrorSink) { + self.set_sep(false); + + if matches!(kind, Some(Encoding::BasicString | Encoding::MlBasicString)) + && has_toml11_escapes(self.raw_at(span)) + { + // TOML 1.1 introduces new escape sequences + self.flag_11(); + } + } + + fn scalar(&mut self, span: Span, kind: Option, _error: &mut dyn ErrorSink) { + self.set_sep(false); + + if matches!(kind, Some(Encoding::BasicString | Encoding::MlBasicString)) { + if has_toml11_escapes(self.raw_at(span)) { + // TOML 1.1 introduces new escape sequences + self.flag_11(); + } + } else if has_toml11_optional_second_time(self.raw_at(span)) { + // TOML 1.1 makes seconds optional in times and datetimes. + self.flag_11(); + } + } + + fn value_sep(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + self.set_sep(true); + } + + fn newline(&mut self, _span: Span, _error: &mut dyn ErrorSink) { + if matches!(self.state.last(), Some(State::InlineTable { .. })) { + // TOML 1.1 introduces newlines in inline tables + self.flag_11(); + } + } +} + +/// Scan the characters of a snippet of TOML representing a basic string for the TOML 1.1 exclusive +/// escape sequences: `\xHH` and `\e` +fn has_toml11_escapes(raw: &str) -> bool { + let mut chars = raw.chars(); + while let Some(c) = chars.next() { + if c == '\\' + && let Some(c) = chars.next() + && matches!(c, 'x' | 'e') + { + return true; + } + } + false +} + +/// Scan for the TOML 1.1 optional-second time syntax, such as `12:34` and `1969-06-20T20:17Z`. +fn has_toml11_optional_second_time(raw: &str) -> bool { + // Non-datetime scalars, such as booleans and integers, fail to parse as date and/or time. + let Ok(datetime) = raw.parse::() else { + return false; + }; + + datetime + .time + .as_ref() + .is_some_and(|time| time.second.is_none()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn escapes_plain_string() { + assert!(!has_toml11_escapes(r#""hello world""#)); + } + + #[test] + fn escapes_toml10_escape_n() { + assert!(!has_toml11_escapes(r#""hello\nworld""#)); + } + + #[test] + fn escapes_toml10_escape_u() { + assert!(!has_toml11_escapes(r#""r\u00E9sum\u00E9""#)); + } + + #[test] + fn escapes_toml11_hex() { + assert!(has_toml11_escapes(r#""val \x41""#)); + } + + #[test] + fn escapes_toml11_esc() { + assert!(has_toml11_escapes(r#""val \e""#)); + } + + #[test] + fn escapes_double_backslash_e() { + assert!(!has_toml11_escapes(r#""\\e""#)); + } + + #[test] + fn escapes_double_backslash_x() { + assert!(!has_toml11_escapes(r#""\\x41""#)); + } + + #[test] + fn features_plain_toml10() { + assert!(!has_toml11_features("x = 1\ny = \"hello\"\nz = true\n")); + } + + #[test] + fn features_std_table() { + assert!(!has_toml11_features( + "[server]\nhost = \"localhost\"\nport = 8080\n" + )); + } + + #[test] + fn features_array_of_tables() { + assert!(!has_toml11_features( + "[[items]]\nname = \"a\"\n[[items]]\nname = \"b\"\n" + )); + } + + #[test] + fn features_inline_table_no_trailing_comma() { + assert!(!has_toml11_features("x = {a = 1, b = 2}\n")); + } + + #[test] + fn features_trailing_comma_in_inline_table() { + assert!(has_toml11_features("x = {a = 1, b = 2,}\n")); + } + + #[test] + fn features_multiline_inline_table() { + assert!(has_toml11_features("x = {\n a = 1\n}\n")); + } + + #[test] + fn features_multiline_inline_table_with_trailing_comma() { + assert!(has_toml11_features("x = {\n a = 1,\n}\n")); + } + + #[test] + fn features_hex_escape() { + assert!(has_toml11_features("x = \"val \\x41\"\n")); + } + + #[test] + fn features_hex_escape_in_quoted_key() { + assert!(has_toml11_features("\"\\x62ar\" = \"baz\"\n")); + } + + #[test] + fn features_hex_escape_in_dotted_quoted_key() { + assert!(has_toml11_features("foo.\"\\x62ar\" = \"baz\"\n")); + } + + #[test] + fn features_esc_escape() { + assert!(has_toml11_features("x = \"val \\e\"\n")); + } + + #[test] + fn features_double_backslash_not_escape() { + assert!(!has_toml11_features("x = \"\\\\e\"\n")); + } + + #[test] + fn features_toml10_escape_in_value() { + assert!(!has_toml11_features("x = \"tab\\there\"\n")); + } + + #[test] + fn features_escape_in_nested_structure() { + assert!(has_toml11_features("[t]\na = {b = \"\\x20\",}\n")); + } + + #[test] + fn features_trailing_comma_in_array_is_not_11() { + assert!(!has_toml11_features("x = [1, 2, 3,]\n")); + } + + #[test] + fn features_optional_second_time_values() { + assert!(has_toml11_features("x = 20:17\n")); + assert!(has_toml11_features("x = 1969-06-20T20:17\n")); + assert!(has_toml11_features("x = 1969-06-20 20:17\n")); + assert!(has_toml11_features("x = 1969-06-20T20:17Z\n")); + assert!(has_toml11_features("x = 1969-06-20T20:17z\n")); + assert!(has_toml11_features("x = 1969-06-20T20:17-07:00\n")); + } + + #[test] + fn features_toml10_time_values_are_not_11() { + assert!(!has_toml11_features("x = 20:17:00\n")); + assert!(!has_toml11_features("x = 1969-06-20T20:17:00Z\n")); + assert!(!has_toml11_features("x = 1969-06-20\n")); + } +} diff --git a/crates/uv/README.md b/crates/uv/README.md index 378557859e352..574b7e3e3a830 100644 --- a/crates/uv/README.md +++ b/crates/uv/README.md @@ -69,6 +69,7 @@ The following uv workspace members are also available: - [uv-state](https://crates.io/crates/uv-state) - [uv-static](https://crates.io/crates/uv-static) - [uv-test](https://crates.io/crates/uv-test) +- [uv-toml](https://crates.io/crates/uv-toml) - [uv-tool](https://crates.io/crates/uv-tool) - [uv-torch](https://crates.io/crates/uv-torch) - [uv-trampoline-builder](https://crates.io/crates/uv-trampoline-builder) diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index ceab878114d1c..e28a64d47efed 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -2078,18 +2078,21 @@ fn build_list_files() -> Result<()> { .arg(&built_by_uv) .arg("--out-dir") .arg(context.temp_dir.join("output1")) - .arg("--list"), @" + .arg("--list") + .arg("--preview-features") + .arg("toml-backwards-compatibility"), @" success: true exit_code: 0 ----- stdout ----- Building built_by_uv-0.1.0.tar.gz will include the following files: built_by_uv-0.1.0/PKG-INFO (generated) + built_by_uv-0.1.0/pyproject.toml (generated) + built_by_uv-0.1.0/pyproject.toml.orig (pyproject.toml) built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE) built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT) built_by_uv-0.1.0/README.md (README.md) built_by_uv-0.1.0/assets/data.csv (assets/data.csv) built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h) - built_by_uv-0.1.0/pyproject.toml (pyproject.toml) built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh) built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py) @@ -2135,18 +2138,21 @@ fn build_list_files() -> Result<()> { .arg(context.temp_dir.join("output2")) .arg("--list") .arg("--sdist") - .arg("--wheel"), @" + .arg("--wheel") + .arg("--preview-features") + .arg("toml-backwards-compatibility"), @" success: true exit_code: 0 ----- stdout ----- Building built_by_uv-0.1.0.tar.gz will include the following files: built_by_uv-0.1.0/PKG-INFO (generated) + built_by_uv-0.1.0/pyproject.toml (generated) + built_by_uv-0.1.0/pyproject.toml.orig (pyproject.toml) built_by_uv-0.1.0/LICENSE-APACHE (LICENSE-APACHE) built_by_uv-0.1.0/LICENSE-MIT (LICENSE-MIT) built_by_uv-0.1.0/README.md (README.md) built_by_uv-0.1.0/assets/data.csv (assets/data.csv) built_by_uv-0.1.0/header/built_by_uv.h (header/built_by_uv.h) - built_by_uv-0.1.0/pyproject.toml (pyproject.toml) built_by_uv-0.1.0/scripts/whoami.sh (scripts/whoami.sh) built_by_uv-0.1.0/src/built_by_uv/__init__.py (src/built_by_uv/__init__.py) built_by_uv-0.1.0/src/built_by_uv/arithmetic/__init__.py (src/built_by_uv/arithmetic/__init__.py) diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index e897347c437ba..5ea5aa105a7d6 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -1613,3 +1613,55 @@ fn warn_on_license_classifier() -> Result<()> { Ok(()) } + +/// Auto-detect TOML 1.1 features in `pyproject.toml` and warn the user. +#[test] +fn warn_on_toml_1_1_auto_detected() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + context + .temp_dir + .child("pyproject.toml") + .write_str(indoc! {r#" + [project] + name = "foo" + version = "1.0.0" + requires-python = ">=3.12" + # TOML 1.1 feature: trailing comma in inline table + authors = [{ name = "Ferris", email = "ferris@example.com", }] + + [build-system] + requires = ["uv_build>=0.7,<10000"] + build-backend = "uv_build" + "#})?; + context.temp_dir.child("src/foo/__init__.py").touch()?; + + // Without the preview flag: auto-detection fires and a warning is shown. + uv_snapshot!(context.filters(), context.build(), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + warning: `pyproject.toml` uses TOML 1.1 features; rewriting to TOML 1.0 for compatibility with older build tools. Use `--preview-feature toml-backwards-compatibility` to suppress this warning. + Building wheel from source distribution (uv build backend)... + Successfully built dist/foo-1.0.0.tar.gz + Successfully built dist/foo-1.0.0-py3-none-any.whl + "); + + // With the preview flag set explicitly: rewrite still happens, but no warning. + uv_snapshot!(context.filters(), context.build().arg("--preview-feature").arg("toml-backwards-compatibility"), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + Building wheel from source distribution (uv build backend)... + Successfully built dist/foo-1.0.0.tar.gz + Successfully built dist/foo-1.0.0-py3-none-any.whl + "); + + Ok(()) +} diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 744ec6f935230..80983526ff1e5 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -8259,6 +8259,7 @@ fn preview_features() { ProjectDirectoryMustExist, IndexExcludeNewer, AzureEndpoint, + TomlBackwardsCompatibility, ], }, python_preference: Managed, @@ -8532,6 +8533,7 @@ fn preview_features() { ProjectDirectoryMustExist, IndexExcludeNewer, AzureEndpoint, + TomlBackwardsCompatibility, ], }, python_preference: Managed, diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 2547c25087110..10b8129b45a29 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -205,7 +205,11 @@ By default, uv excludes `__pycache__`, `*.pyc`, and `*.pyo`. When building a source distribution, the following files and directories are included: -- The `pyproject.toml` +- The `pyproject.toml`. If uv detects TOML 1.1-only syntax, it issues a warning and automatically + enables the `toml-backwards-compatibility` preview feature: the `pyproject.toml` is reformatted + for backwards compatibility, and the original file is preserved as `pyproject.toml.orig`. Pass + `--preview-feature toml-backwards-compatibility` to enable the feature explicitly and suppress the + warning. - The [module](#modules) under [`tool.uv.build-backend.module-root`](../reference/settings.md#build-backend_module-root). - The files referenced by `project.license-files` and `project.readme`. From 8bc4427d464cf2e0ecbb8b5a46de191fbe7bf75e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 18 May 2026 12:26:37 +0100 Subject: [PATCH 43/56] Remove duplicate winnow dependency (#19455) After the TOML 1.1 compatibility work landed, `winnow 0.7.15` remained in the dependency graph through the zbus family. This updates zbus and its companion crates to versions that use `winnow 1.0.3`, leaving the workspace with a single `winnow` version. --- Cargo.lock | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b4e3fd05dfc2d..3b86462074b39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5223,7 +5223,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow", ] [[package]] @@ -5247,7 +5247,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow 1.0.3", + "winnow", ] [[package]] @@ -5256,7 +5256,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.3", + "winnow", ] [[package]] @@ -7967,15 +7967,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - [[package]] name = "winnow" version = "1.0.3" @@ -8191,9 +8182,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" dependencies = [ "async-broadcast", "async-recursion", @@ -8213,7 +8204,7 @@ dependencies = [ "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.15", + "winnow", "zbus_macros", "zbus_names", "zvariant", @@ -8221,9 +8212,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.14.0" +version = "5.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -8236,12 +8227,12 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.3.1" +version = "4.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" dependencies = [ "serde", - "winnow 0.7.15", + "winnow", "zvariant", ] @@ -8382,23 +8373,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.15", + "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -8409,13 +8400,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.3.0" +version = "3.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" dependencies = [ "proc-macro2", "quote", "serde", "syn", - "winnow 0.7.15", + "winnow", ] From 0fd3f030628506770ac612297908d7724c755e78 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 11:39:54 +0000 Subject: [PATCH 44/56] Update Rust crate uuid to v1.23.1 (#19170) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b86462074b39..19ee2c877550a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5653,9 +5653,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.1", "js-sys", From ebdda47b98880993f30e10e5ab8183a4bc675280 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 18 May 2026 10:15:14 -0700 Subject: [PATCH 45/56] Bump astral-tokio-tar from 0.6.1 to 0.6.2 (#19463) Signed-off-by: William Woodruff --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 19ee2c877550a..92c9446e2adf3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -277,9 +277,9 @@ dependencies = [ [[package]] name = "astral-tokio-tar" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ce73b17c62717c4b6a9af10b43e87c578b0cac27e00666d48304d3b7d2c0693" +checksum = "cb50a7aae84a03bf55b067832bc376f4961b790c97e64d3eacee97d389b90277" dependencies = [ "filetime", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 5334ee680a739..2042f58f9f3b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ anstream = { version = "1.0.0" } anyhow = { version = "1.0.89" } arcstr = { version = "1.2.0" } arrayvec = { version = "0.7.6" } -astral-tokio-tar = { version = "0.6.1" } +astral-tokio-tar = { version = "0.6.2" } async-channel = { version = "2.3.1" } async-compression = { version = "0.4.12", features = [ "bzip2", From ba86bb9f454a5fea57460b433f0144f4a9952897 Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Mon, 18 May 2026 10:16:25 -0700 Subject: [PATCH 46/56] Pin cargo-shear to v1.12.4 (#19462) See https://github.com/astral-sh/ruff/pull/25218 --- .github/workflows/check-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-lint.yml b/.github/workflows/check-lint.yml index 694016b970755..398dc7951926b 100644 --- a/.github/workflows/check-lint.yml +++ b/.github/workflows/check-lint.yml @@ -153,7 +153,7 @@ jobs: - name: "Install cargo shear" uses: taiki-e/install-action@fa0dd4cd0a40696e6f9766370614a5ce482e6aa8 # v2.77.5 with: - tool: cargo-shear@1.11.2 + tool: cargo-shear@1.12.4 - run: cargo shear --deny-warnings typos: From 078480dc40fdc6d2e56145730dacfb2744da5940 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 10:26:07 -0700 Subject: [PATCH 47/56] Configure maturin and uv so `uv run` can be used to work on uv itself (#19461) The default is release builds otherwise! which is why it was so slow --- pyproject.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a299e87be4945..c94e7ca19f1b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ Discord = "https://discord.gg/astral-sh" [tool.maturin] bindings = "bin" +editable-profile = "dev" manifest-path = "crates/uv/Cargo.toml" module-name = "uv" python-source = "python" @@ -110,6 +111,15 @@ docker = [ "cargo-zigbuild>=0.19.8", ] +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "Cargo.toml" }, + { file = "Cargo.lock" }, + { file = "crates/**/*.rs" }, + { file = "crates/**/Cargo.toml" }, +] + [tool.uv.dependency-groups] docs = { requires-python = ">=3.12" } docker = { requires-python = ">=3.12" } From fb8d3d46ac08ab5fc88f7a96c89f55860f343012 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:33:29 -0400 Subject: [PATCH 48/56] Update Rust crate rustls-pki-types to v1.14.1 (#19251) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92c9446e2adf3..f39e8e51fefba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4170,9 +4170,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", From 5373e96b15609291995a630704ef08cea31b4b08 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 18 May 2026 13:34:05 -0400 Subject: [PATCH 49/56] Update Rust crate rustls to v0.23.40 (#19250) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f39e8e51fefba..dac756d3bf6aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4144,9 +4144,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", From d77d849816de6278f61d3f385b70b2735ca2f0fd Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 10:38:47 -0700 Subject: [PATCH 50/56] Revert "Update maturin to v1.13.2 (#19445)" (#19465) This reverts https://github.com/astral-sh/uv/pull/19445 / 8e85f007013e262fb2a7da13b178d4a943289cea. --- .github/workflows/build-release-binaries.yml | 44 ++++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build-release-binaries.yml b/.github/workflows/build-release-binaries.yml index 79164ac75385e..7ebc949fa772b 100644 --- a/.github/workflows/build-release-binaries.yml +++ b/.github/workflows/build-release-binaries.yml @@ -48,7 +48,7 @@ jobs: - name: "Build sdist" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 command: sdist args: --out dist - name: "Test sdist" @@ -69,7 +69,7 @@ jobs: - name: "Build sdist uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 command: sdist args: --out crates/uv-build/dist -m crates/uv-build/Cargo.toml - name: "Fix Cargo.lock in sdist uv-build" @@ -107,7 +107,7 @@ jobs: - name: "Build wheels - x86_64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: x86_64 args: --release --locked --out dist --features self-update --compatibility pypi env: @@ -140,7 +140,7 @@ jobs: - name: "Build wheels uv-build - x86_64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: x86_64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -173,7 +173,7 @@ jobs: - name: "Build wheels - aarch64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: aarch64 manylinux: 2_17 args: --release --locked --out dist --features self-update --compatibility pypi @@ -213,7 +213,7 @@ jobs: - name: "Build wheels uv-build - aarch64" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: aarch64 args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -270,7 +270,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} args: --release --locked --out dist --features self-update,windows-gui-bin --compatibility pypi env: @@ -312,7 +312,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} args: --profile minimal-size --locked --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi env: @@ -353,7 +353,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.target }} # Generally, we try to build in a target docker container. In this case however, a # 32-bit compiler runs out of memory (4GB memory limit for 32-bit), so we cross compile @@ -424,7 +424,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.target }} manylinux: 2_17 docker-options: -e CARGO @@ -483,7 +483,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -541,7 +541,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux }} docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -600,7 +600,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -663,7 +663,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -723,7 +723,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -784,7 +784,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_17 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -832,7 +832,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_31 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -891,7 +891,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: 2_31 docker-options: -e CARGO ${{ matrix.platform.maturin_docker_options }} @@ -949,7 +949,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.target }} manylinux: musllinux_1_1 docker-options: -e CARGO @@ -1001,7 +1001,7 @@ jobs: - name: "Build wheels uv-build" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.target }} manylinux: musllinux_1_1 docker-options: -e CARGO @@ -1060,7 +1060,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 # Tag the musl builds as manylinux 2_17 fallback cause the aarch64 build only support 2_28 @@ -1140,7 +1140,7 @@ jobs: - name: "Build wheels" uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0 with: - maturin-version: v1.13.2 + maturin-version: v1.13.1 target: ${{ matrix.platform.target }} manylinux: musllinux_1_1 args: --profile minimal-size --locked ${{ matrix.platform.arch == 'aarch64' && '--compatibility 2_17' || ''}} --out crates/uv-build/dist -m crates/uv-build/Cargo.toml --compatibility pypi From 9a65753e9a8d0e99e7079114d78001311444c718 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 10:49:26 -0700 Subject: [PATCH 51/56] Enforce that entry points cannot escape in the scripts directory (#19464) For [GHSA-4gg8-gxpx-9rph](https://github.com/astral-sh/uv/security/advisories/GHSA-4gg8-gxpx-9rph) --- crates/uv-fs/src/path.rs | 41 +++++ crates/uv-install-wheel/src/lib.rs | 4 +- crates/uv-install-wheel/src/wheel.rs | 135 ++++++++++------ crates/uv/tests/it/pip_install.rs | 221 ++++++++++++++++++++++++++- 4 files changed, 350 insertions(+), 51 deletions(-) diff --git a/crates/uv-fs/src/path.rs b/crates/uv-fs/src/path.rs index 99c6b9db882e9..2e58930e6ab71 100644 --- a/crates/uv-fs/src/path.rs +++ b/crates/uv-fs/src/path.rs @@ -263,6 +263,21 @@ pub fn normalize_path<'path>(path: impl Into>) -> Cow<'path, Pa path } +/// Normalize `path`, returning it when it remains strictly under a non-empty `root`. +/// +/// This comparison is lexical and does not resolve symlinks. The returned path is normalized, and +/// equality with `root` is rejected. +pub fn normalize_path_under(path: impl AsRef, root: impl AsRef) -> Option { + let path = normalize_path(path.as_ref()).into_owned(); + let root = normalize_path(root.as_ref()); + + if root.as_os_str().is_empty() || path.as_path() == root.as_ref() { + None + } else { + path.starts_with(root.as_ref()).then_some(path) + } +} + /// Normalize a [`Path`]. /// /// Unlike [`normalize_absolute_path`], this works with relative paths and does never error. @@ -727,6 +742,32 @@ mod tests { } } + #[test] + fn test_normalize_path_under() { + assert_eq!( + normalize_path_under("scripts/script", "scripts"), + Some(PathBuf::from("scripts/script")) + ); + assert_eq!( + normalize_path_under("/scripts/script", "/scripts"), + Some(PathBuf::from("/scripts/script")) + ); + assert_eq!( + normalize_path_under("scripts/nested/../script", "scripts"), + Some(PathBuf::from("scripts/script")) + ); + assert_eq!( + normalize_path_under("/scripts/nested/../script", "/scripts"), + Some(PathBuf::from("/scripts/script")) + ); + assert_eq!(normalize_path_under("scripts/.", "scripts"), None); + assert_eq!(normalize_path_under("/scripts/.", "/scripts"), None); + assert_eq!(normalize_path_under("scripts/../script", "scripts"), None); + assert_eq!(normalize_path_under("/scripts/../script", "/scripts"), None); + assert_eq!(normalize_path_under("scripts/script", "."), None); + assert_eq!(normalize_path_under("scripts/script", ""), None); + } + #[test] fn test_normalize_trailing_path_separator() { let cases = [ diff --git a/crates/uv-install-wheel/src/lib.rs b/crates/uv-install-wheel/src/lib.rs index 0632d1d5fcf21..bb498503e2c22 100644 --- a/crates/uv-install-wheel/src/lib.rs +++ b/crates/uv-install-wheel/src/lib.rs @@ -86,8 +86,8 @@ pub enum Error { InvalidEggLink(PathBuf), #[error(transparent)] LauncherError(#[from] uv_trampoline_builder::Error), - #[error("Scripts must not use the reserved name {0}")] - ReservedScriptName(String), + #[error("Scripts must not use the reserved name `{reserved}`, got: `{declared}`")] + ReservedScriptName { reserved: String, declared: String }, #[error(transparent)] Copy(#[from] uv_fs::link::LinkError), } diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index ed008340e8edc..29babaca2a8f5 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -14,7 +14,7 @@ use sha2::{Digest, Sha256}; use tracing::{debug, instrument, trace, warn}; use walkdir::WalkDir; -use uv_fs::{PortablePath, Simplified, persist_with_retry_sync, relative_to}; +use uv_fs::{PortablePath, Simplified, normalize_path_under, persist_with_retry_sync, relative_to}; use uv_normalize::PackageName; use uv_pypi_types::DirectUrl; use uv_shell::escape_posix_for_single_quotes; @@ -160,69 +160,108 @@ fn get_script_executable(python_executable: &Path, is_gui: bool) -> PathBuf { } } -/// Determine the absolute path to an entrypoint script. -fn entrypoint_path(entrypoint: &Script, layout: &Layout) -> PathBuf { - if cfg!(windows) { - // On windows we actually build an .exe wrapper - let script_name = entrypoint - .name - // FIXME: What are the in-reality rules here for names? - .strip_suffix(".py") - .unwrap_or(&entrypoint.name) - .to_string() - + ".exe"; - - layout.scheme.scripts.join(script_name) - } else { - layout.scheme.scripts.join(&entrypoint.name) - } +const RESERVED_SCRIPT_NAMES_ERROR: &[&str; 3] = &["python", "pythonw", "python3"]; +const RESERVED_SCRIPT_NAMES_WARN: &[&str; 2] = &["activate", "activate_this.py"]; + +/// A form of [`Script`] guaranteed by [`ValidatedScript::try_from_script`] to be constrained to +/// the scripts directory. +struct ValidatedScript<'script> { + path: PathBuf, + script: &'script Script, } -/// Create the wrapper scripts in the bin folder of the venv for launching console scripts. -pub(crate) fn write_script_entrypoints( - layout: &Layout, - relocatable: bool, - site_packages: &Path, - entrypoints: &[Script], - record: &mut Vec, - is_gui: bool, -) -> Result<(), Error> { - for entrypoint in entrypoints { - let warn_names = ["activate", "activate_this.py"]; - if warn_names.contains(&entrypoint.name.as_str()) - || entrypoint.name.starts_with("activate.") - { +impl<'script> ValidatedScript<'script> { + fn try_from_script(script: &'script Script, layout: &Layout) -> Result { + let Some(path) = normalize_path_under( + layout.scheme.scripts.join(&script.name), + &layout.scheme.scripts, + ) else { + return Err(Error::InvalidWheel(format!( + "Script path must resolve to a file within the scripts directory: `{}`", + script.name + ))); + }; + + let name = relative_to(&path, &layout.scheme.scripts)? + .to_string_lossy() + .into_owned(); + + if RESERVED_SCRIPT_NAMES_WARN.contains(&name.as_str()) || name.starts_with("activate.") { warn_user_once!( "The script name `{}` is reserved for virtual environment activation scripts.", - entrypoint.name + name ); } - let reserved_names = ["python", "pythonw", "python3"]; - if reserved_names.contains(&entrypoint.name.as_str()) - || entrypoint - .name + + if RESERVED_SCRIPT_NAMES_ERROR.contains(&name.as_str()) + || name .strip_prefix("python3.") .is_some_and(|suffix| suffix.parse::().is_ok()) { - return Err(Error::ReservedScriptName(entrypoint.name.clone())); + return Err(Error::ReservedScriptName { + reserved: name, + declared: script.name.clone(), + }); } - let entrypoint_absolute = entrypoint_path(entrypoint, layout); + let path = if cfg!(windows) { + // On Windows we actually build an `.exe` wrapper. + let name = name + // FIXME: What are the in-reality rules here for names? + .strip_suffix(".py") + .unwrap_or(&name) + .to_string() + + std::env::consts::EXE_SUFFIX; - let entrypoint_relative = pathdiff::diff_paths(&entrypoint_absolute, site_packages) - .ok_or_else(|| { - Error::Io(io::Error::other(format!( - "Could not find relative path for: {}", - entrypoint_absolute.simplified_display() - ))) - })?; + layout.scheme.scripts.join(name) + } else { + layout.scheme.scripts.join(name) + }; + + Ok(Self { path, script }) + } + + fn as_path(&self) -> &Path { + &self.path + } + + fn inner(&self) -> &Script { + self.script + } + + /// Return the script destination relative to `site_packages` for use in `RECORD`. + /// + /// Entry points are installed in the scripts directory, which may sit outside + /// `site_packages`, so we use a lexical diff rather than stripping a prefix. + fn relative_to_site_package(&self, site_packages: &Path) -> Result { + pathdiff::diff_paths(self.as_path(), site_packages).ok_or_else(|| { + Error::Io(io::Error::other(format!( + "Could not find relative path for: {}", + self.as_path().simplified_display() + ))) + }) + } +} + +/// Create the wrapper scripts in the bin folder of the venv for launching console scripts. +pub(crate) fn write_script_entrypoints( + layout: &Layout, + relocatable: bool, + site_packages: &Path, + entrypoints: &[Script], + record: &mut Vec, + is_gui: bool, +) -> Result<(), Error> { + for script in entrypoints { + let script = ValidatedScript::try_from_script(script, layout)?; + let entrypoint_relative = script.relative_to_site_package(site_packages)?; // Generate the launcher script. let launcher_executable = get_script_executable(&layout.sys_executable, is_gui); let launcher_executable = get_relocatable_executable(launcher_executable, layout, relocatable)?; let launcher_python_script = get_script_launcher( - entrypoint, + script.inner(), &format_shebang(&launcher_executable, &layout.os_name, relocatable), ); @@ -248,8 +287,8 @@ pub(crate) fn write_script_entrypoints( use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - let path = site_packages.join(entrypoint_relative); - let permissions = fs::metadata(&path)?.permissions(); + let path = script.as_path(); + let permissions = fs::metadata(path)?.permissions(); if permissions.mode() & 0o111 != 0o111 { fs::set_permissions(path, Permissions::from_mode(permissions.mode() | 0o111))?; } diff --git a/crates/uv/tests/it/pip_install.rs b/crates/uv/tests/it/pip_install.rs index a07d70e423285..38265f87ceaa3 100644 --- a/crates/uv/tests/it/pip_install.rs +++ b/crates/uv/tests/it/pip_install.rs @@ -13282,13 +13282,232 @@ fn reserved_script_name() -> Result<()> { Prepared 1 package in [TIME] Uninstalled 1 package in [TIME] error: Failed to install: project-0.1.0-py3-none-any.whl (project==0.1.0 (from file://[TEMP_DIR]/)) - Caused by: Scripts must not use the reserved name python + Caused by: Scripts must not use the reserved name `python`, got: `python` " ); Ok(()) } +fn repacked_wheel_with_entrypoint( + context: &TestContext, + section: &str, + entrypoint_name: &str, +) -> Result { + context.init().arg("--lib").arg("foo").assert().success(); + context.build().arg("--wheel").arg("foo").assert().success(); + + let built_wheel = context.temp_dir.join("foo/dist/foo-0.1.0-py3-none-any.whl"); + let unpacked = context.temp_dir.join("foo-unpacked"); + uv_extract::unzip(File::open(&built_wheel)?, &unpacked)?; + + fs::write( + unpacked.join("foo-0.1.0.dist-info/entry_points.txt"), + formatdoc! {" + [{section}] + {entrypoint_name} = foo:main + ", + }, + )?; + + let repacked_wheel = context.temp_dir.join("foo-0.1.0-py3-none-any.whl"); + let mut writer = ZipFileWriter::new(Vec::new()); + for entry in WalkDir::new(&unpacked) { + let entry = entry?; + let path = entry.path(); + let name = path.strip_prefix(&unpacked)?; + if name.as_os_str().is_empty() { + continue; + } + // Zip entries must use forward slashes, even on Windows. + let mut name = PortablePath::from(name).to_string(); + if path.is_dir() { + name.push('/'); + let entry = ZipEntryBuilder::new(name.into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, &[]))?; + } else { + let entry = ZipEntryBuilder::new(name.into(), Compression::Stored); + block_on(writer.write_entry_whole(entry, &fs_err::read(path)?))?; + } + } + fs_err::write(&repacked_wheel, block_on(writer.close())?)?; + + Ok(repacked_wheel) +} + +#[test] +fn reject_wheel_entrypoint_paths() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + // Build a normal wheel, then rewrite its entry-point metadata to exercise the installer + // directly, rather than relying on backend-side validation. + let escaped_entrypoint = context.temp_dir.child("escaped-entrypoint"); + let repacked_wheel = repacked_wheel_with_entrypoint( + &context, + "console_scripts", + &escaped_entrypoint.path().portable_display().to_string(), + )?; + + #[cfg(unix)] + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: The wheel is invalid: Script path must resolve to a file within the scripts directory: `[TEMP_DIR]/escaped-entrypoint` + " + ); + + #[cfg(windows)] + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: The wheel is invalid: invalid console script: '/uv-tmp/uv/tests/[TMP]/escaped-entrypoint = foo:main' + " + ); + + escaped_entrypoint.assert(predicates::path::missing()); + + Ok(()) +} + +#[test] +fn reject_normalized_reserved_wheel_entrypoint_name() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = + repacked_wheel_with_entrypoint(&context, "console_scripts", "nested/../python")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: Scripts must not use the reserved name `python`, got: `nested/../python` + " + ); + + Ok(()) +} + +#[test] +fn reject_normalized_reserved_gui_wheel_entrypoint_name() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = + repacked_wheel_with_entrypoint(&context, "gui_scripts", "nested/../python")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + error: Failed to install: foo-0.1.0-py3-none-any.whl (foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl)) + Caused by: Scripts must not use the reserved name `python`, got: `nested/../python` + " + ); + + Ok(()) +} + +#[test] +fn warn_normalized_activation_wheel_entrypoint_name() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = + repacked_wheel_with_entrypoint(&context, "console_scripts", "nested/../activate.bash")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + warning: The script name `activate.bash` is reserved for virtual environment activation scripts. + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl) + " + ); + + Ok(()) +} + +#[test] +fn accept_normalized_gui_wheel_entrypoint_paths() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = + repacked_wheel_with_entrypoint(&context, "gui_scripts", "nested/../normalized-gui")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl) + " + ); + + let script_name = if cfg!(windows) { + "normalized-gui.exe" + } else { + "normalized-gui" + }; + let normalized_script = venv_bin_path(&context.venv).join(script_name); + assert!(normalized_script.exists()); + + Ok(()) +} + +#[test] +fn accept_normalized_wheel_entrypoint_paths() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let repacked_wheel = + repacked_wheel_with_entrypoint(&context, "console_scripts", "nested/../normalized-script")?; + + uv_snapshot!(context.filters(), context.pip_install().arg(&repacked_wheel), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + foo==0.1.0 (from file://[TEMP_DIR]/foo-0.1.0-py3-none-any.whl) + " + ); + + let script_name = if cfg!(windows) { + "normalized-script.exe" + } else { + "normalized-script" + }; + let normalized_script = venv_bin_path(&context.venv).join(script_name); + assert!(normalized_script.exists()); + + Ok(()) +} + #[test] fn pep_751_dependency() -> Result<()> { let context = uv_test::test_context!("3.12"); From 0588b8fb3505235ed97fc511a874a3ce0cbd9b72 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 10:49:39 -0700 Subject: [PATCH 52/56] Run release builds on maturin version bumps in CI (#19466) Avoid future cases of https://github.com/astral-sh/uv/pull/19465 --- .github/renovate.json5 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index b62949e0c8e83..566dcf26c6423 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -138,6 +138,9 @@ matchManagers: ["custom.regex"], matchDepNames: ["maturin"], commitMessageTopic: "maturin", + // Maturin upgrades can change release wheel contents, so keep release + // binary validation enabled. + labels: ["internal", "build:skip-docker"], }, ], customManagers: [ From 2d566bced5152ba9da3aedbc02f1dd087bfeb8e9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 10:56:16 -0700 Subject: [PATCH 53/56] Allow retry of `custom-publish-crates` separately from `announce` (#19470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adjusts `announce` to no longer wait for `custom-publish-crates` so we can retry `custom-publish-crates` without re-running `announce`. This may be temporary — once we can get crates publishing working consistently, we can roll back. --- .github/workflows/release.yml | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 718ed13e9715e..c3671d9b0d1d7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -307,12 +307,10 @@ jobs: - host - release-gate - custom-publish-pypi - - custom-publish-crates - # use "always() && ..." to allow us to wait for all publish jobs while - # still allowing individual publish jobs to skip themselves (for prereleases). + # use "always() && ..." to allow publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! - # `custom-publish-crates` is intentionally not gated on its result: it's - # not critical to the release and isn't idempotent on retry. + # `custom-publish-crates` is intentionally not a dependency: it's not + # critical to the release and isn't idempotent on retry. if: ${{ always() && needs.host.result == 'success' && needs.release-gate.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }} runs-on: "depot-ubuntu-latest-4" environment: @@ -364,9 +362,8 @@ jobs: needs: - plan - announce - # Use `always() && ...` so this job is not skipped when an unrelated - # upstream job (e.g., `custom-publish-crates`) fails. `announce` itself - # tolerates a `custom-publish-crates` failure, so we mirror that here. + # Use `always() && ...` so this job gates only on whether `announce` + # completed successfully. if: ${{ always() && needs.announce.result == 'success' }} uses: ./.github/workflows/publish-docs.yml with: From cf826cc4e0feeafb23e4e52b85929848ab2d16a7 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 11:07:07 -0700 Subject: [PATCH 54/56] Disable `test_simultaneous_create_set_then_move` on Linux (#19469) It races and flakes --- crates/uv-keyring/tests/threading.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-keyring/tests/threading.rs b/crates/uv-keyring/tests/threading.rs index 1ab3e7f40787c..82df4cd461466 100644 --- a/crates/uv-keyring/tests/threading.rs +++ b/crates/uv-keyring/tests/threading.rs @@ -127,7 +127,7 @@ async fn test_create_set_then_move() { } #[tokio::test] -#[cfg(not(target_os = "windows"))] +#[cfg(not(any(target_os = "windows", target_os = "linux")))] async fn test_simultaneous_create_set_then_move() { init_logger(); From de16a7b1a30c54e3137db0d414ff250a1c0ae427 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 12:10:13 -0700 Subject: [PATCH 55/56] Bump version to 0.11.15 (#19472) Co-authored-by: Codex --- .known-crates | 2 + CHANGELOG.md | 43 ++ Cargo.lock | 138 ++--- Cargo.toml | 130 ++--- crates/uv-audit/Cargo.toml | 2 +- crates/uv-audit/README.md | 4 +- crates/uv-auth/Cargo.toml | 2 +- crates/uv-auth/README.md | 4 +- crates/uv-bench/Cargo.toml | 2 +- crates/uv-bench/README.md | 4 +- crates/uv-bin-install/Cargo.toml | 2 +- crates/uv-bin-install/README.md | 4 +- crates/uv-build-backend/Cargo.toml | 2 +- crates/uv-build-backend/README.md | 4 +- crates/uv-build-frontend/Cargo.toml | 2 +- crates/uv-build-frontend/README.md | 4 +- crates/uv-build/Cargo.toml | 2 +- crates/uv-build/pyproject.toml | 2 +- crates/uv-cache-info/Cargo.toml | 2 +- crates/uv-cache-info/README.md | 4 +- crates/uv-cache-key/Cargo.toml | 2 +- crates/uv-cache-key/README.md | 4 +- crates/uv-cache/Cargo.toml | 2 +- crates/uv-cache/README.md | 4 +- crates/uv-cli/Cargo.toml | 2 +- crates/uv-cli/README.md | 4 +- crates/uv-client/Cargo.toml | 2 +- crates/uv-client/README.md | 4 +- crates/uv-configuration/Cargo.toml | 2 +- crates/uv-configuration/README.md | 4 +- crates/uv-console/Cargo.toml | 2 +- crates/uv-console/README.md | 4 +- crates/uv-dev/Cargo.toml | 2 +- crates/uv-dev/README.md | 4 +- crates/uv-dirs/Cargo.toml | 2 +- crates/uv-dirs/README.md | 4 +- crates/uv-dispatch/Cargo.toml | 2 +- crates/uv-dispatch/README.md | 4 +- crates/uv-distribution-filename/Cargo.toml | 2 +- crates/uv-distribution-filename/README.md | 4 +- crates/uv-distribution-types/Cargo.toml | 2 +- crates/uv-distribution-types/README.md | 4 +- crates/uv-distribution/Cargo.toml | 2 +- crates/uv-distribution/README.md | 4 +- crates/uv-extract/Cargo.toml | 2 +- crates/uv-extract/README.md | 4 +- crates/uv-fastid/Cargo.toml | 2 +- crates/uv-fastid/README.md | 4 +- crates/uv-flags/Cargo.toml | 2 +- crates/uv-flags/README.md | 4 +- crates/uv-fs/Cargo.toml | 2 +- crates/uv-fs/README.md | 4 +- crates/uv-git-types/Cargo.toml | 2 +- crates/uv-git-types/README.md | 4 +- crates/uv-git/Cargo.toml | 2 +- crates/uv-git/README.md | 4 +- crates/uv-globfilter/Cargo.toml | 2 +- crates/uv-install-wheel/Cargo.toml | 2 +- crates/uv-install-wheel/README.md | 4 +- crates/uv-installer/Cargo.toml | 2 +- crates/uv-installer/README.md | 4 +- crates/uv-keyring/Cargo.toml | 2 +- crates/uv-logging/Cargo.toml | 2 +- crates/uv-logging/README.md | 4 +- crates/uv-macros/Cargo.toml | 2 +- crates/uv-macros/README.md | 4 +- crates/uv-metadata/Cargo.toml | 2 +- crates/uv-metadata/README.md | 4 +- crates/uv-netrc/Cargo.toml | 2 +- crates/uv-normalize/Cargo.toml | 2 +- crates/uv-normalize/README.md | 4 +- crates/uv-once-map/Cargo.toml | 2 +- crates/uv-once-map/README.md | 4 +- crates/uv-options-metadata/Cargo.toml | 2 +- crates/uv-options-metadata/README.md | 4 +- crates/uv-pep440/Cargo.toml | 2 +- crates/uv-pep440/README.md | 4 +- crates/uv-pep508/Cargo.toml | 2 +- crates/uv-pep508/README.md | 4 +- .../Cargo.toml | 2 +- .../uv-performance-memory-allocator/README.md | 4 +- crates/uv-platform-tags/Cargo.toml | 2 +- crates/uv-platform-tags/README.md | 4 +- crates/uv-platform/Cargo.toml | 2 +- crates/uv-platform/README.md | 4 +- crates/uv-preview/Cargo.toml | 2 +- crates/uv-preview/README.md | 4 +- crates/uv-publish/Cargo.toml | 2 +- crates/uv-publish/README.md | 4 +- crates/uv-pypi-types/Cargo.toml | 2 +- crates/uv-pypi-types/README.md | 4 +- crates/uv-python/Cargo.toml | 2 +- crates/uv-python/README.md | 4 +- crates/uv-redacted/Cargo.toml | 2 +- crates/uv-redacted/README.md | 4 +- crates/uv-requirements-txt/Cargo.toml | 2 +- crates/uv-requirements-txt/README.md | 4 +- crates/uv-requirements/Cargo.toml | 2 +- crates/uv-requirements/README.md | 4 +- crates/uv-resolver/Cargo.toml | 2 +- crates/uv-resolver/README.md | 4 +- crates/uv-scripts/Cargo.toml | 2 +- crates/uv-scripts/README.md | 4 +- crates/uv-settings/Cargo.toml | 2 +- crates/uv-settings/README.md | 4 +- crates/uv-shell/Cargo.toml | 2 +- crates/uv-shell/README.md | 4 +- crates/uv-small-str/Cargo.toml | 2 +- crates/uv-small-str/README.md | 4 +- crates/uv-state/Cargo.toml | 2 +- crates/uv-state/README.md | 4 +- crates/uv-static/Cargo.toml | 2 +- crates/uv-static/README.md | 4 +- crates/uv-test/Cargo.toml | 2 +- crates/uv-test/README.md | 4 +- crates/uv-toml/Cargo.toml | 2 +- crates/uv-toml/README.md | 4 +- crates/uv-tool/Cargo.toml | 2 +- crates/uv-tool/README.md | 4 +- crates/uv-torch/Cargo.toml | 2 +- crates/uv-torch/README.md | 4 +- crates/uv-trampoline-builder/Cargo.toml | 2 +- crates/uv-trampoline-builder/README.md | 4 +- crates/uv-trampoline/Cargo.lock | 6 +- crates/uv-types/Cargo.toml | 2 +- crates/uv-types/README.md | 4 +- crates/uv-unix/Cargo.toml | 2 +- crates/uv-unix/README.md | 4 +- crates/uv-version/Cargo.toml | 2 +- crates/uv-version/README.md | 4 +- crates/uv-virtualenv/Cargo.toml | 2 +- crates/uv-warnings/Cargo.toml | 2 +- crates/uv-warnings/README.md | 4 +- crates/uv-windows/Cargo.toml | 2 +- crates/uv-windows/README.md | 4 +- crates/uv-workspace/Cargo.toml | 2 +- crates/uv-workspace/README.md | 4 +- crates/uv/Cargo.toml | 2 +- crates/uv/README.md | 5 +- docs/concepts/build-backend.md | 2 +- docs/concepts/projects/init.md | 6 +- docs/concepts/projects/workspaces.md | 6 +- docs/getting-started/installation.md | 4 +- docs/guides/integration/aws-lambda.md | 4 +- docs/guides/integration/docker.md | 10 +- docs/guides/integration/github.md | 2 +- docs/guides/integration/gitlab.md | 2 +- docs/guides/integration/pre-commit.md | 10 +- pyproject.toml | 2 +- scripts/setup-crates-io-publish.py.lock | 39 +- uv.lock | 510 ++++++++++-------- 151 files changed, 700 insertions(+), 613 deletions(-) diff --git a/.known-crates b/.known-crates index fcce583a3bb6b..929af6adca0a2 100644 --- a/.known-crates +++ b/.known-crates @@ -31,6 +31,7 @@ uv-keyring uv-logging uv-macros uv-metadata +uv-netrc uv-normalize uv-once-map uv-options-metadata @@ -54,6 +55,7 @@ uv-small-str uv-state uv-static uv-test +uv-toml uv-tool uv-torch uv-trampoline-builder diff --git a/CHANGELOG.md b/CHANGELOG.md index 204770206f648..1ebe45829e8d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,49 @@ +## 0.11.15 + +Released on 2026-05-18. + +### Security + +- Fix a TAR partial differential, see [GHSA-3cv2-h65g-fgmm](https://github.com/astral-sh/tokio-tar/security/advisories/GHSA-3cv2-h65g-fgmm) ([#19463](https://github.com/astral-sh/uv/pull/19463)) +- Enforce that entry points cannot escape in the scripts directory, see [GHSA-4gg8-gxpx-9rph](https://github.com/astral-sh/tokio-tar/security/advisories/GHSA-4gg8-gxpx-9rph) ([#19464](https://github.com/astral-sh/uv/pull/19464)) + +### Enhancements + +- Add TOML v1.1 -> v1.0 backwards compatibility for source distributions ([#18741](https://github.com/astral-sh/uv/pull/18741)) +- Add support for Azure request signing ([#19421](https://github.com/astral-sh/uv/pull/19421)) +- Apply stricter validation to all wheel filename segments ([#19364](https://github.com/astral-sh/uv/pull/19364)) +- Reject empty strings as an invalid package name ([#19435](https://github.com/astral-sh/uv/pull/19435)) +- Use structured errors for signing authentication failures ([#19422](https://github.com/astral-sh/uv/pull/19422)) + +### Preview + +- uv audit: Add JSON output ([#19305](https://github.com/astral-sh/uv/pull/19305)) + +### Configuration + +- Respect `required-environments` in `uv pip compile` ([#19378](https://github.com/astral-sh/uv/pull/19378)) + +### Performance + +- Avoid parsing JSON manifest when local Python is available ([#19398](https://github.com/astral-sh/uv/pull/19398)) +- Avoid walking nested directories in linker conflict registration ([#19382](https://github.com/astral-sh/uv/pull/19382)) +- Optimize async wheel ZIP writing ([#19383](https://github.com/astral-sh/uv/pull/19383)) +- Fix dead "already trimmed" fast-path in `Version::only_release_trimmed` ([#19425](https://github.com/astral-sh/uv/pull/19425)) + +### Bug fixes + +- Apply workspace-member `[tool.uv.sources]` credentials under `uv sync --frozen` ([#19423](https://github.com/astral-sh/uv/pull/19423)) +- Skip empty directories in uv build outputs ([#19437](https://github.com/astral-sh/uv/pull/19437)) +- Fix Git submodule handling when using relative paths ([#12156](https://github.com/astral-sh/uv/pull/12156)) +- Fix line number reporting in netrc parsing ([#19452](https://github.com/astral-sh/uv/pull/19452)) + +### Documentation + +- Move Bazel auth helper setup into integration guide ([#19392](https://github.com/astral-sh/uv/pull/19392)) + ## 0.11.14 Released on 2026-05-12. diff --git a/Cargo.lock b/Cargo.lock index dac756d3bf6aa..20632bea57c92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5665,7 +5665,7 @@ dependencies = [ [[package]] name = "uv" -version = "0.11.14" +version = "0.11.15" dependencies = [ "anstream", "anyhow", @@ -5790,7 +5790,7 @@ dependencies = [ [[package]] name = "uv-audit" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral-reqwest-middleware", "clap", @@ -5818,7 +5818,7 @@ dependencies = [ [[package]] name = "uv-auth" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "arcstr", @@ -5861,7 +5861,7 @@ dependencies = [ [[package]] name = "uv-bench" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "codspeed-criterion-compat", @@ -5888,7 +5888,7 @@ dependencies = [ [[package]] name = "uv-bin-install" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral-reqwest-middleware", "astral-reqwest-retry", @@ -5916,7 +5916,7 @@ dependencies = [ [[package]] name = "uv-build" -version = "0.11.14" +version = "0.11.15" dependencies = [ "anstream", "anyhow", @@ -5931,7 +5931,7 @@ dependencies = [ [[package]] name = "uv-build-backend" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral-tokio-tar", "astral-version-ranges", @@ -5977,7 +5977,7 @@ dependencies = [ [[package]] name = "uv-build-frontend" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "fs-err", @@ -6014,7 +6014,7 @@ dependencies = [ [[package]] name = "uv-cache" -version = "0.0.47" +version = "0.0.48" dependencies = [ "clap", "fs-err", @@ -6040,7 +6040,7 @@ dependencies = [ [[package]] name = "uv-cache-info" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "fs-err", @@ -6057,7 +6057,7 @@ dependencies = [ [[package]] name = "uv-cache-key" -version = "0.0.47" +version = "0.0.48" dependencies = [ "hex", "memchr", @@ -6069,7 +6069,7 @@ dependencies = [ [[package]] name = "uv-cli" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "anyhow", @@ -6102,7 +6102,7 @@ dependencies = [ [[package]] name = "uv-client" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6172,7 +6172,7 @@ dependencies = [ [[package]] name = "uv-configuration" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "clap", @@ -6206,14 +6206,14 @@ dependencies = [ [[package]] name = "uv-console" -version = "0.0.47" +version = "0.0.48" dependencies = [ "console", ] [[package]] name = "uv-dev" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "anyhow", @@ -6262,7 +6262,7 @@ dependencies = [ [[package]] name = "uv-dirs" -version = "0.0.47" +version = "0.0.48" dependencies = [ "assert_fs", "etcetera", @@ -6274,7 +6274,7 @@ dependencies = [ [[package]] name = "uv-dispatch" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "futures", @@ -6307,7 +6307,7 @@ dependencies = [ [[package]] name = "uv-distribution" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6358,7 +6358,7 @@ dependencies = [ [[package]] name = "uv-distribution-filename" -version = "0.0.47" +version = "0.0.48" dependencies = [ "insta", "memchr", @@ -6375,7 +6375,7 @@ dependencies = [ [[package]] name = "uv-distribution-types" -version = "0.0.47" +version = "0.0.48" dependencies = [ "arcstr", "astral-version-ranges", @@ -6415,7 +6415,7 @@ dependencies = [ [[package]] name = "uv-extract" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral-tokio-tar", "astral_async_zip", @@ -6444,7 +6444,7 @@ dependencies = [ [[package]] name = "uv-fastid" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fastrand", "rand 0.9.4", @@ -6454,14 +6454,14 @@ dependencies = [ [[package]] name = "uv-flags" -version = "0.0.47" +version = "0.0.48" dependencies = [ "bitflags", ] [[package]] name = "uv-fs" -version = "0.0.47" +version = "0.0.48" dependencies = [ "backon", "clap", @@ -6492,7 +6492,7 @@ dependencies = [ [[package]] name = "uv-git" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "astral-reqwest-middleware", @@ -6518,7 +6518,7 @@ dependencies = [ [[package]] name = "uv-git-types" -version = "0.0.47" +version = "0.0.48" dependencies = [ "percent-encoding", "serde", @@ -6532,7 +6532,7 @@ dependencies = [ [[package]] name = "uv-globfilter" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "fs-err", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "uv-install-wheel" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_fs", @@ -6586,7 +6586,7 @@ dependencies = [ [[package]] name = "uv-installer" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "anyhow", @@ -6629,7 +6629,7 @@ dependencies = [ [[package]] name = "uv-keyring" -version = "0.0.47" +version = "0.0.48" dependencies = [ "async-trait", "byteorder", @@ -6645,7 +6645,7 @@ dependencies = [ [[package]] name = "uv-logging" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "jiff", @@ -6656,7 +6656,7 @@ dependencies = [ [[package]] name = "uv-macros" -version = "0.0.47" +version = "0.0.48" dependencies = [ "proc-macro2", "quote", @@ -6666,7 +6666,7 @@ dependencies = [ [[package]] name = "uv-metadata" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral_async_zip", "fs-err", @@ -6682,7 +6682,7 @@ dependencies = [ [[package]] name = "uv-netrc" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fs-err", "shellexpand", @@ -6692,7 +6692,7 @@ dependencies = [ [[package]] name = "uv-normalize" -version = "0.0.47" +version = "0.0.48" dependencies = [ "rkyv", "schemars", @@ -6702,7 +6702,7 @@ dependencies = [ [[package]] name = "uv-once-map" -version = "0.0.47" +version = "0.0.48" dependencies = [ "dashmap", "futures", @@ -6711,14 +6711,14 @@ dependencies = [ [[package]] name = "uv-options-metadata" -version = "0.0.47" +version = "0.0.48" dependencies = [ "serde", ] [[package]] name = "uv-pep440" -version = "0.0.47" +version = "0.0.48" dependencies = [ "astral-version-ranges", "indoc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "uv-pep508" -version = "0.0.47" +version = "0.0.48" dependencies = [ "arcstr", "astral-version-ranges", @@ -6762,7 +6762,7 @@ dependencies = [ [[package]] name = "uv-performance-memory-allocator" -version = "0.0.47" +version = "0.0.48" dependencies = [ "mimalloc", "tikv-jemallocator", @@ -6770,7 +6770,7 @@ dependencies = [ [[package]] name = "uv-platform" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fs-err", "goblin", @@ -6791,7 +6791,7 @@ dependencies = [ [[package]] name = "uv-platform-tags" -version = "0.0.47" +version = "0.0.48" dependencies = [ "bitflags", "insta", @@ -6805,7 +6805,7 @@ dependencies = [ [[package]] name = "uv-preview" -version = "0.0.47" +version = "0.0.48" dependencies = [ "enumflags2", "itertools 0.14.0", @@ -6816,7 +6816,7 @@ dependencies = [ [[package]] name = "uv-publish" -version = "0.0.47" +version = "0.0.48" dependencies = [ "ambient-id", "anstream", @@ -6858,7 +6858,7 @@ dependencies = [ [[package]] name = "uv-pypi-types" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -6891,7 +6891,7 @@ dependencies = [ [[package]] name = "uv-python" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_fs", @@ -6953,7 +6953,7 @@ dependencies = [ [[package]] name = "uv-redacted" -version = "0.0.47" +version = "0.0.48" dependencies = [ "ref-cast", "schemars", @@ -6964,7 +6964,7 @@ dependencies = [ [[package]] name = "uv-requirements" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "configparser", @@ -6998,7 +6998,7 @@ dependencies = [ [[package]] name = "uv-requirements-txt" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_fs", @@ -7031,7 +7031,7 @@ dependencies = [ [[package]] name = "uv-resolver" -version = "0.0.47" +version = "0.0.48" dependencies = [ "arcstr", "astral-pubgrub", @@ -7097,7 +7097,7 @@ dependencies = [ [[package]] name = "uv-scripts" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fs-err", "indoc", @@ -7122,7 +7122,7 @@ dependencies = [ [[package]] name = "uv-settings" -version = "0.0.47" +version = "0.0.48" dependencies = [ "clap", "fs-err", @@ -7159,7 +7159,7 @@ dependencies = [ [[package]] name = "uv-shell" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "fs-err", @@ -7176,7 +7176,7 @@ dependencies = [ [[package]] name = "uv-small-str" -version = "0.0.47" +version = "0.0.48" dependencies = [ "arcstr", "rkyv", @@ -7186,7 +7186,7 @@ dependencies = [ [[package]] name = "uv-state" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fs-err", "tempfile", @@ -7195,7 +7195,7 @@ dependencies = [ [[package]] name = "uv-static" -version = "0.0.47" +version = "0.0.48" dependencies = [ "thiserror", "uv-macros", @@ -7203,7 +7203,7 @@ dependencies = [ [[package]] name = "uv-test" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_cmd", @@ -7233,7 +7233,7 @@ dependencies = [ [[package]] name = "uv-toml" -version = "0.0.47" +version = "0.0.48" dependencies = [ "toml_datetime", "toml_parser", @@ -7241,7 +7241,7 @@ dependencies = [ [[package]] name = "uv-tool" -version = "0.0.47" +version = "0.0.48" dependencies = [ "fs-err", "owo-colors", @@ -7270,7 +7270,7 @@ dependencies = [ [[package]] name = "uv-torch" -version = "0.0.47" +version = "0.0.48" dependencies = [ "clap", "either", @@ -7290,7 +7290,7 @@ dependencies = [ [[package]] name = "uv-trampoline-builder" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_cmd", @@ -7309,7 +7309,7 @@ dependencies = [ [[package]] name = "uv-types" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "dashmap", @@ -7331,7 +7331,7 @@ dependencies = [ [[package]] name = "uv-unix" -version = "0.0.47" +version = "0.0.48" dependencies = [ "nix 0.31.2", "thiserror", @@ -7339,11 +7339,11 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.11.14" +version = "0.11.15" [[package]] name = "uv-virtualenv" -version = "0.0.47" +version = "0.0.48" dependencies = [ "console", "fs-err", @@ -7363,7 +7363,7 @@ dependencies = [ [[package]] name = "uv-warnings" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anstream", "anyhow", @@ -7375,7 +7375,7 @@ dependencies = [ [[package]] name = "uv-windows" -version = "0.0.47" +version = "0.0.48" dependencies = [ "arrayvec", "windows", @@ -7383,7 +7383,7 @@ dependencies = [ [[package]] name = "uv-workspace" -version = "0.0.47" +version = "0.0.48" dependencies = [ "anyhow", "assert_fs", diff --git a/Cargo.toml b/Cargo.toml index 2042f58f9f3b5..727fa139bac1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,80 +16,80 @@ authors = ["uv"] license = "MIT OR Apache-2.0" [workspace.dependencies] -uv-audit = { version = "0.0.47", path = "crates/uv-audit" } -uv-auth = { version = "0.0.47", path = "crates/uv-auth" } -uv-bin-install = { version = "0.0.47", path = "crates/uv-bin-install" } -uv-build-backend = { version = "0.0.47", path = "crates/uv-build-backend" } -uv-build-frontend = { version = "0.0.47", path = "crates/uv-build-frontend" } -uv-cache = { version = "0.0.47", path = "crates/uv-cache" } -uv-cache-info = { version = "0.0.47", path = "crates/uv-cache-info" } -uv-cache-key = { version = "0.0.47", path = "crates/uv-cache-key" } -uv-cli = { version = "0.0.47", path = "crates/uv-cli" } -uv-client = { version = "0.0.47", path = "crates/uv-client" } -uv-configuration = { version = "0.0.47", path = "crates/uv-configuration" } -uv-console = { version = "0.0.47", path = "crates/uv-console" } -uv-dirs = { version = "0.0.47", path = "crates/uv-dirs" } -uv-dispatch = { version = "0.0.47", path = "crates/uv-dispatch" } -uv-distribution = { version = "0.0.47", path = "crates/uv-distribution" } -uv-distribution-filename = { version = "0.0.47", path = "crates/uv-distribution-filename" } -uv-distribution-types = { version = "0.0.47", path = "crates/uv-distribution-types" } -uv-extract = { version = "0.0.47", path = "crates/uv-extract" } -uv-fastid = { version = "0.0.47", path = "crates/uv-fastid" } -uv-flags = { version = "0.0.47", path = "crates/uv-flags" } -uv-fs = { version = "0.0.47", path = "crates/uv-fs", features = [ +uv-audit = { version = "0.0.48", path = "crates/uv-audit" } +uv-auth = { version = "0.0.48", path = "crates/uv-auth" } +uv-bin-install = { version = "0.0.48", path = "crates/uv-bin-install" } +uv-build-backend = { version = "0.0.48", path = "crates/uv-build-backend" } +uv-build-frontend = { version = "0.0.48", path = "crates/uv-build-frontend" } +uv-cache = { version = "0.0.48", path = "crates/uv-cache" } +uv-cache-info = { version = "0.0.48", path = "crates/uv-cache-info" } +uv-cache-key = { version = "0.0.48", path = "crates/uv-cache-key" } +uv-cli = { version = "0.0.48", path = "crates/uv-cli" } +uv-client = { version = "0.0.48", path = "crates/uv-client" } +uv-configuration = { version = "0.0.48", path = "crates/uv-configuration" } +uv-console = { version = "0.0.48", path = "crates/uv-console" } +uv-dirs = { version = "0.0.48", path = "crates/uv-dirs" } +uv-dispatch = { version = "0.0.48", path = "crates/uv-dispatch" } +uv-distribution = { version = "0.0.48", path = "crates/uv-distribution" } +uv-distribution-filename = { version = "0.0.48", path = "crates/uv-distribution-filename" } +uv-distribution-types = { version = "0.0.48", path = "crates/uv-distribution-types" } +uv-extract = { version = "0.0.48", path = "crates/uv-extract" } +uv-fastid = { version = "0.0.48", path = "crates/uv-fastid" } +uv-flags = { version = "0.0.48", path = "crates/uv-flags" } +uv-fs = { version = "0.0.48", path = "crates/uv-fs", features = [ "serde", "tokio", ] } -uv-git = { version = "0.0.47", path = "crates/uv-git" } -uv-git-types = { version = "0.0.47", path = "crates/uv-git-types" } -uv-globfilter = { version = "0.0.47", path = "crates/uv-globfilter" } -uv-install-wheel = { version = "0.0.47", path = "crates/uv-install-wheel", default-features = false } -uv-installer = { version = "0.0.47", path = "crates/uv-installer" } -uv-keyring = { version = "0.0.47", path = "crates/uv-keyring" } -uv-logging = { version = "0.0.47", path = "crates/uv-logging" } -uv-macros = { version = "0.0.47", path = "crates/uv-macros" } -uv-metadata = { version = "0.0.47", path = "crates/uv-metadata" } -uv-netrc = { version = "0.0.47", path = "crates/uv-netrc" } -uv-normalize = { version = "0.0.47", path = "crates/uv-normalize" } -uv-once-map = { version = "0.0.47", path = "crates/uv-once-map" } -uv-options-metadata = { version = "0.0.47", path = "crates/uv-options-metadata" } -uv-performance-memory-allocator = { version = "0.0.47", path = "crates/uv-performance-memory-allocator" } -uv-pep440 = { version = "0.0.47", path = "crates/uv-pep440", features = [ +uv-git = { version = "0.0.48", path = "crates/uv-git" } +uv-git-types = { version = "0.0.48", path = "crates/uv-git-types" } +uv-globfilter = { version = "0.0.48", path = "crates/uv-globfilter" } +uv-install-wheel = { version = "0.0.48", path = "crates/uv-install-wheel", default-features = false } +uv-installer = { version = "0.0.48", path = "crates/uv-installer" } +uv-keyring = { version = "0.0.48", path = "crates/uv-keyring" } +uv-logging = { version = "0.0.48", path = "crates/uv-logging" } +uv-macros = { version = "0.0.48", path = "crates/uv-macros" } +uv-metadata = { version = "0.0.48", path = "crates/uv-metadata" } +uv-netrc = { version = "0.0.48", path = "crates/uv-netrc" } +uv-normalize = { version = "0.0.48", path = "crates/uv-normalize" } +uv-once-map = { version = "0.0.48", path = "crates/uv-once-map" } +uv-options-metadata = { version = "0.0.48", path = "crates/uv-options-metadata" } +uv-performance-memory-allocator = { version = "0.0.48", path = "crates/uv-performance-memory-allocator" } +uv-pep440 = { version = "0.0.48", path = "crates/uv-pep440", features = [ "tracing", "rkyv", "version-ranges", ] } -uv-pep508 = { version = "0.0.47", path = "crates/uv-pep508", features = [ +uv-pep508 = { version = "0.0.48", path = "crates/uv-pep508", features = [ "non-pep508-extensions", ] } -uv-platform = { version = "0.0.47", path = "crates/uv-platform" } -uv-platform-tags = { version = "0.0.47", path = "crates/uv-platform-tags" } -uv-preview = { version = "0.0.47", path = "crates/uv-preview" } -uv-publish = { version = "0.0.47", path = "crates/uv-publish" } -uv-pypi-types = { version = "0.0.47", path = "crates/uv-pypi-types" } -uv-python = { version = "0.0.47", path = "crates/uv-python" } -uv-redacted = { version = "0.0.47", path = "crates/uv-redacted" } -uv-requirements = { version = "0.0.47", path = "crates/uv-requirements" } -uv-requirements-txt = { version = "0.0.47", path = "crates/uv-requirements-txt" } -uv-resolver = { version = "0.0.47", path = "crates/uv-resolver" } -uv-scripts = { version = "0.0.47", path = "crates/uv-scripts" } -uv-settings = { version = "0.0.47", path = "crates/uv-settings" } -uv-shell = { version = "0.0.47", path = "crates/uv-shell" } -uv-small-str = { version = "0.0.47", path = "crates/uv-small-str" } -uv-state = { version = "0.0.47", path = "crates/uv-state" } -uv-static = { version = "0.0.47", path = "crates/uv-static" } -uv-test = { version = "0.0.47", path = "crates/uv-test" } -uv-toml = { version = "0.0.47", path = "crates/uv-toml" } -uv-tool = { version = "0.0.47", path = "crates/uv-tool" } -uv-torch = { version = "0.0.47", path = "crates/uv-torch" } -uv-trampoline-builder = { version = "0.0.47", path = "crates/uv-trampoline-builder" } -uv-types = { version = "0.0.47", path = "crates/uv-types" } -uv-unix = { version = "0.0.47", path = "crates/uv-unix" } -uv-version = { version = "0.11.14", path = "crates/uv-version" } -uv-virtualenv = { version = "0.0.47", path = "crates/uv-virtualenv" } -uv-warnings = { version = "0.0.47", path = "crates/uv-warnings" } -uv-windows = { version = "0.0.47", path = "crates/uv-windows" } -uv-workspace = { version = "0.0.47", path = "crates/uv-workspace" } +uv-platform = { version = "0.0.48", path = "crates/uv-platform" } +uv-platform-tags = { version = "0.0.48", path = "crates/uv-platform-tags" } +uv-preview = { version = "0.0.48", path = "crates/uv-preview" } +uv-publish = { version = "0.0.48", path = "crates/uv-publish" } +uv-pypi-types = { version = "0.0.48", path = "crates/uv-pypi-types" } +uv-python = { version = "0.0.48", path = "crates/uv-python" } +uv-redacted = { version = "0.0.48", path = "crates/uv-redacted" } +uv-requirements = { version = "0.0.48", path = "crates/uv-requirements" } +uv-requirements-txt = { version = "0.0.48", path = "crates/uv-requirements-txt" } +uv-resolver = { version = "0.0.48", path = "crates/uv-resolver" } +uv-scripts = { version = "0.0.48", path = "crates/uv-scripts" } +uv-settings = { version = "0.0.48", path = "crates/uv-settings" } +uv-shell = { version = "0.0.48", path = "crates/uv-shell" } +uv-small-str = { version = "0.0.48", path = "crates/uv-small-str" } +uv-state = { version = "0.0.48", path = "crates/uv-state" } +uv-static = { version = "0.0.48", path = "crates/uv-static" } +uv-test = { version = "0.0.48", path = "crates/uv-test" } +uv-toml = { version = "0.0.48", path = "crates/uv-toml" } +uv-tool = { version = "0.0.48", path = "crates/uv-tool" } +uv-torch = { version = "0.0.48", path = "crates/uv-torch" } +uv-trampoline-builder = { version = "0.0.48", path = "crates/uv-trampoline-builder" } +uv-types = { version = "0.0.48", path = "crates/uv-types" } +uv-unix = { version = "0.0.48", path = "crates/uv-unix" } +uv-version = { version = "0.11.15", path = "crates/uv-version" } +uv-virtualenv = { version = "0.0.48", path = "crates/uv-virtualenv" } +uv-warnings = { version = "0.0.48", path = "crates/uv-warnings" } +uv-windows = { version = "0.0.48", path = "crates/uv-windows" } +uv-workspace = { version = "0.0.48", path = "crates/uv-workspace" } ambient-id = { version = "0.0.11", default-features = false, features = [ "reqwest-middleware", diff --git a/crates/uv-audit/Cargo.toml b/crates/uv-audit/Cargo.toml index e4691f5b69883..f48b9f53e3577 100644 --- a/crates/uv-audit/Cargo.toml +++ b/crates/uv-audit/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-audit" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition.workspace = true rust-version.workspace = true diff --git a/crates/uv-audit/README.md b/crates/uv-audit/README.md index 53c6c8492e237..53147829bf363 100644 --- a/crates/uv-audit/README.md +++ b/crates/uv-audit/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-audit). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-audit). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index e7c46e39860b7..30dc88740ff61 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-auth" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-auth/README.md b/crates/uv-auth/README.md index 6f50dc1deeb9a..1154637fe8857 100644 --- a/crates/uv-auth/README.md +++ b/crates/uv-auth/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-auth). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-auth). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-bench/Cargo.toml b/crates/uv-bench/Cargo.toml index ed23cae288d9d..dbf95898bda77 100644 --- a/crates/uv-bench/Cargo.toml +++ b/crates/uv-bench/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-bench" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" publish = false authors = { workspace = true } diff --git a/crates/uv-bench/README.md b/crates/uv-bench/README.md index dabed125e5c86..25d2e929e93a8 100644 --- a/crates/uv-bench/README.md +++ b/crates/uv-bench/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-bench). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-bench). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-bin-install/Cargo.toml b/crates/uv-bin-install/Cargo.toml index be8afea45a9a1..70ad414257f89 100644 --- a/crates/uv-bin-install/Cargo.toml +++ b/crates/uv-bin-install/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-bin-install" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-bin-install/README.md b/crates/uv-bin-install/README.md index f974e640a1916..07a4c3d085c9d 100644 --- a/crates/uv-bin-install/README.md +++ b/crates/uv-bin-install/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-bin-install). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-bin-install). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build-backend/Cargo.toml b/crates/uv-build-backend/Cargo.toml index dcee21debf312..73726c4a7acea 100644 --- a/crates/uv-build-backend/Cargo.toml +++ b/crates/uv-build-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build-backend" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build-backend/README.md b/crates/uv-build-backend/README.md index db38d7fb69669..0a116b2d11c55 100644 --- a/crates/uv-build-backend/README.md +++ b/crates/uv-build-backend/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-build-backend). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-build-backend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index 0097898a35e56..f0452f075dc19 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build-frontend" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build-frontend/README.md b/crates/uv-build-frontend/README.md index 84c7f7d6eb44b..948c0f325dd5d 100644 --- a/crates/uv-build-frontend/README.md +++ b/crates/uv-build-frontend/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-build-frontend). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-build-frontend). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 7e0dba0cb3c23..2983c70f4bdee 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-build" -version = "0.11.14" +version = "0.11.15" description = "A Python build backend" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-build/pyproject.toml b/crates/uv-build/pyproject.toml index 7da9f56d78c8e..a9bc27128e3c2 100644 --- a/crates/uv-build/pyproject.toml +++ b/crates/uv-build/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uv-build" -version = "0.11.14" +version = "0.11.15" description = "The uv build backend" authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/crates/uv-cache-info/Cargo.toml b/crates/uv-cache-info/Cargo.toml index 15746c2d93d24..780cca4a053b5 100644 --- a/crates/uv-cache-info/Cargo.toml +++ b/crates/uv-cache-info/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache-info" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache-info/README.md b/crates/uv-cache-info/README.md index 06d54251772d6..eab63a7d45e9f 100644 --- a/crates/uv-cache-info/README.md +++ b/crates/uv-cache-info/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-cache-info). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-cache-info). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cache-key/Cargo.toml b/crates/uv-cache-key/Cargo.toml index 8d2df75036e09..b627688ea6a69 100644 --- a/crates/uv-cache-key/Cargo.toml +++ b/crates/uv-cache-key/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache-key" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache-key/README.md b/crates/uv-cache-key/README.md index 47699c6277c08..2dd19d1ef0042 100644 --- a/crates/uv-cache-key/README.md +++ b/crates/uv-cache-key/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-cache-key). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-cache-key). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cache/Cargo.toml b/crates/uv-cache/Cargo.toml index 837b96005c099..9cd60a68bb6e9 100644 --- a/crates/uv-cache/Cargo.toml +++ b/crates/uv-cache/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cache" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cache/README.md b/crates/uv-cache/README.md index 6f0801f2d351e..736b0afa13e55 100644 --- a/crates/uv-cache/README.md +++ b/crates/uv-cache/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-cache). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-cache). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-cli/Cargo.toml b/crates/uv-cli/Cargo.toml index ceeb27719fa14..0360f3075363b 100644 --- a/crates/uv-cli/Cargo.toml +++ b/crates/uv-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-cli" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-cli/README.md b/crates/uv-cli/README.md index 6253f38e24fbe..cf3e80f85a568 100644 --- a/crates/uv-cli/README.md +++ b/crates/uv-cli/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-cli). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-cli). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 6c37f0b16a31e..7ac63346c2429 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-client" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-client/README.md b/crates/uv-client/README.md index d687060b5cd56..06ab170beecd9 100644 --- a/crates/uv-client/README.md +++ b/crates/uv-client/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-client). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-client). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 16ea2582d8bfd..d0f29d217aa24 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-configuration" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-configuration/README.md b/crates/uv-configuration/README.md index 8fa375038e0c3..322c9e992ef50 100644 --- a/crates/uv-configuration/README.md +++ b/crates/uv-configuration/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-configuration). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-configuration). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-console/Cargo.toml b/crates/uv-console/Cargo.toml index d30743468a653..2604b784ea50c 100644 --- a/crates/uv-console/Cargo.toml +++ b/crates/uv-console/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-console" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-console/README.md b/crates/uv-console/README.md index e8ca22f35847a..03a9f60a55069 100644 --- a/crates/uv-console/README.md +++ b/crates/uv-console/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-console). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-console). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index f937d13f1287a..ae38811d181af 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dev" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" publish = false diff --git a/crates/uv-dev/README.md b/crates/uv-dev/README.md index f4159e420f432..fd48128ab8d4a 100644 --- a/crates/uv-dev/README.md +++ b/crates/uv-dev/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-dev). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-dev). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dirs/Cargo.toml b/crates/uv-dirs/Cargo.toml index 5bc5cc54f138e..f41c8a05bd935 100644 --- a/crates/uv-dirs/Cargo.toml +++ b/crates/uv-dirs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dirs" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-dirs/README.md b/crates/uv-dirs/README.md index 9ff9697f3a157..a89a26da98864 100644 --- a/crates/uv-dirs/README.md +++ b/crates/uv-dirs/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-dirs). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-dirs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index b811145ab93a6..d7afe7d0cd4a0 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-dispatch" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-dispatch/README.md b/crates/uv-dispatch/README.md index 61b2377e0010c..2aaf629fab811 100644 --- a/crates/uv-dispatch/README.md +++ b/crates/uv-dispatch/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-dispatch). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-dispatch). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution-filename/Cargo.toml b/crates/uv-distribution-filename/Cargo.toml index 22745b0c7e6bb..929bd73b10420 100644 --- a/crates/uv-distribution-filename/Cargo.toml +++ b/crates/uv-distribution-filename/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution-filename" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-distribution-filename/README.md b/crates/uv-distribution-filename/README.md index 7beca5b03db82..de1b8581275c0 100644 --- a/crates/uv-distribution-filename/README.md +++ b/crates/uv-distribution-filename/README.md @@ -5,9 +5,9 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-distribution-filename). +[here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-distribution-filename). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution-types/Cargo.toml b/crates/uv-distribution-types/Cargo.toml index 479935861ccf9..cf84a18c204ee 100644 --- a/crates/uv-distribution-types/Cargo.toml +++ b/crates/uv-distribution-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution-types" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-distribution-types/README.md b/crates/uv-distribution-types/README.md index dcca5f7b0fbad..ccdc34e9e11fb 100644 --- a/crates/uv-distribution-types/README.md +++ b/crates/uv-distribution-types/README.md @@ -5,9 +5,9 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-distribution-types). +[here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-distribution-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 202d9fbb50193..0572252f01929 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-distribution" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-distribution/README.md b/crates/uv-distribution/README.md index a211fca660531..79a7197052051 100644 --- a/crates/uv-distribution/README.md +++ b/crates/uv-distribution/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-distribution). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-distribution). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index 99ad315dd2ca3..7d902a086e874 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-extract" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-extract/README.md b/crates/uv-extract/README.md index 8a3271b977aad..a1b7cd0cc4b2c 100644 --- a/crates/uv-extract/README.md +++ b/crates/uv-extract/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-extract). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-extract). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-fastid/Cargo.toml b/crates/uv-fastid/Cargo.toml index 54ca554df6162..80315dc7f2c36 100644 --- a/crates/uv-fastid/Cargo.toml +++ b/crates/uv-fastid/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "uv-fastid" description = "This is an internal component crate of uv" -version = "0.0.47" +version = "0.0.48" edition.workspace = true rust-version.workspace = true homepage.workspace = true diff --git a/crates/uv-fastid/README.md b/crates/uv-fastid/README.md index c68037bb52e72..c9867e8428f8c 100644 --- a/crates/uv-fastid/README.md +++ b/crates/uv-fastid/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-fastid). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-fastid). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-flags/Cargo.toml b/crates/uv-flags/Cargo.toml index 4bb1844c4a799..dec75a044e2c2 100644 --- a/crates/uv-flags/Cargo.toml +++ b/crates/uv-flags/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-flags" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-flags/README.md b/crates/uv-flags/README.md index a643db9b5295c..08e7fc4dc6c7d 100644 --- a/crates/uv-flags/README.md +++ b/crates/uv-flags/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-flags). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-flags). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-fs/Cargo.toml b/crates/uv-fs/Cargo.toml index 93f276deeecda..5593ffae0e77d 100644 --- a/crates/uv-fs/Cargo.toml +++ b/crates/uv-fs/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-fs" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-fs/README.md b/crates/uv-fs/README.md index 41e08b024948e..2736d6d6a4e36 100644 --- a/crates/uv-fs/README.md +++ b/crates/uv-fs/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-fs). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-fs). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-git-types/Cargo.toml b/crates/uv-git-types/Cargo.toml index fc9483688be5f..72d82cb52d051 100644 --- a/crates/uv-git-types/Cargo.toml +++ b/crates/uv-git-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-git-types" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-git-types/README.md b/crates/uv-git-types/README.md index 6b27833068dda..e8bedd4d91309 100644 --- a/crates/uv-git-types/README.md +++ b/crates/uv-git-types/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-git-types). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-git-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-git/Cargo.toml b/crates/uv-git/Cargo.toml index b4ef3893e9a05..6c10865c3e67f 100644 --- a/crates/uv-git/Cargo.toml +++ b/crates/uv-git/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-git" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-git/README.md b/crates/uv-git/README.md index 640fe1d4a04e2..7c70eae668011 100644 --- a/crates/uv-git/README.md +++ b/crates/uv-git/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-git). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-git). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-globfilter/Cargo.toml b/crates/uv-globfilter/Cargo.toml index cba40aed4867b..85e9bdad4dc59 100644 --- a/crates/uv-globfilter/Cargo.toml +++ b/crates/uv-globfilter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-globfilter" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" readme = "README.md" edition = { workspace = true } diff --git a/crates/uv-install-wheel/Cargo.toml b/crates/uv-install-wheel/Cargo.toml index d3cab10e6e1ae..462c0b5e283e0 100644 --- a/crates/uv-install-wheel/Cargo.toml +++ b/crates/uv-install-wheel/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-install-wheel" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" keywords = ["wheel", "python"] diff --git a/crates/uv-install-wheel/README.md b/crates/uv-install-wheel/README.md index 020f68cf0c9a1..d3b6daeaf2749 100644 --- a/crates/uv-install-wheel/README.md +++ b/crates/uv-install-wheel/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-install-wheel). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-install-wheel). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index 57f8fb8fc5881..608bf8b293b7a 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-installer" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-installer/README.md b/crates/uv-installer/README.md index c6b03d37b32d9..007020217831b 100644 --- a/crates/uv-installer/README.md +++ b/crates/uv-installer/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-installer). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-installer). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-keyring/Cargo.toml b/crates/uv-keyring/Cargo.toml index 01e7a32bbe52c..1691006d7e7ed 100644 --- a/crates/uv-keyring/Cargo.toml +++ b/crates/uv-keyring/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-keyring" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-logging/Cargo.toml b/crates/uv-logging/Cargo.toml index 90820f48fc1bf..6c4c49fd41e92 100644 --- a/crates/uv-logging/Cargo.toml +++ b/crates/uv-logging/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-logging" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-logging/README.md b/crates/uv-logging/README.md index 1c86615c85347..0babe50d930a8 100644 --- a/crates/uv-logging/README.md +++ b/crates/uv-logging/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-logging). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-logging). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-macros/Cargo.toml b/crates/uv-macros/Cargo.toml index ca878cf6cad26..dc901daaf2cc4 100644 --- a/crates/uv-macros/Cargo.toml +++ b/crates/uv-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-macros" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-macros/README.md b/crates/uv-macros/README.md index 3f8f5db0a321c..96723b7eb47c8 100644 --- a/crates/uv-macros/README.md +++ b/crates/uv-macros/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-macros). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-macros). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-metadata/Cargo.toml b/crates/uv-metadata/Cargo.toml index f007955f37c55..1e026c253ba04 100644 --- a/crates/uv-metadata/Cargo.toml +++ b/crates/uv-metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-metadata" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-metadata/README.md b/crates/uv-metadata/README.md index 445fd325c13db..7671d123634dd 100644 --- a/crates/uv-metadata/README.md +++ b/crates/uv-metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-metadata). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-metadata). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-netrc/Cargo.toml b/crates/uv-netrc/Cargo.toml index bcebe15e24180..1fce937d26301 100644 --- a/crates/uv-netrc/Cargo.toml +++ b/crates/uv-netrc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-netrc" -version = "0.0.47" +version = "0.0.48" description = "A vendored netrc parser for uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-normalize/Cargo.toml b/crates/uv-normalize/Cargo.toml index 3f762d5993853..6435740a76faf 100644 --- a/crates/uv-normalize/Cargo.toml +++ b/crates/uv-normalize/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-normalize" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-normalize/README.md b/crates/uv-normalize/README.md index 5c11268357616..c182cb23e9043 100644 --- a/crates/uv-normalize/README.md +++ b/crates/uv-normalize/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-normalize). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-normalize). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-once-map/Cargo.toml b/crates/uv-once-map/Cargo.toml index fd503dc10a5fc..16483589a4842 100644 --- a/crates/uv-once-map/Cargo.toml +++ b/crates/uv-once-map/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-once-map" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-once-map/README.md b/crates/uv-once-map/README.md index 43027329b6cdb..8a1fb46e113c9 100644 --- a/crates/uv-once-map/README.md +++ b/crates/uv-once-map/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-once-map). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-once-map). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-options-metadata/Cargo.toml b/crates/uv-options-metadata/Cargo.toml index fd32f9a33bdbc..5942d66554fe1 100644 --- a/crates/uv-options-metadata/Cargo.toml +++ b/crates/uv-options-metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-options-metadata" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-options-metadata/README.md b/crates/uv-options-metadata/README.md index e4e212aeef2e2..5fb5b42ee60ba 100644 --- a/crates/uv-options-metadata/README.md +++ b/crates/uv-options-metadata/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-options-metadata). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-options-metadata). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pep440/Cargo.toml b/crates/uv-pep440/Cargo.toml index 09073094e6f66..bb9f4e75602b5 100644 --- a/crates/uv-pep440/Cargo.toml +++ b/crates/uv-pep440/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pep440" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" license = "Apache-2.0 OR BSD-2-Clause" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] diff --git a/crates/uv-pep440/README.md b/crates/uv-pep440/README.md index 4ffcbebebce03..46e6d4d417717 100644 --- a/crates/uv-pep440/README.md +++ b/crates/uv-pep440/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-pep440). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-pep440). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pep508/Cargo.toml b/crates/uv-pep508/Cargo.toml index 9bdf6c74d042f..a41794c91958d 100644 --- a/crates/uv-pep508/Cargo.toml +++ b/crates/uv-pep508/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pep508" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" include = ["/src", "Changelog.md", "License-Apache", "License-BSD", "Readme.md", "pyproject.toml"] license = "Apache-2.0 OR BSD-2-Clause" diff --git a/crates/uv-pep508/README.md b/crates/uv-pep508/README.md index 4aa3a87a34de6..a313149cae195 100644 --- a/crates/uv-pep508/README.md +++ b/crates/uv-pep508/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-pep508). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-pep508). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-performance-memory-allocator/Cargo.toml b/crates/uv-performance-memory-allocator/Cargo.toml index cf802bfa2c701..ff4ceb6374296 100644 --- a/crates/uv-performance-memory-allocator/Cargo.toml +++ b/crates/uv-performance-memory-allocator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-performance-memory-allocator" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-performance-memory-allocator/README.md b/crates/uv-performance-memory-allocator/README.md index f50b7ebb0eb5c..20ab25125b659 100644 --- a/crates/uv-performance-memory-allocator/README.md +++ b/crates/uv-performance-memory-allocator/README.md @@ -5,9 +5,9 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-performance-memory-allocator). +[here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-performance-memory-allocator). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-platform-tags/Cargo.toml b/crates/uv-platform-tags/Cargo.toml index 0dddef195f746..24faa14e9dc89 100644 --- a/crates/uv-platform-tags/Cargo.toml +++ b/crates/uv-platform-tags/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-platform-tags" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-platform-tags/README.md b/crates/uv-platform-tags/README.md index da189a410edd6..195ecf04f9fe6 100644 --- a/crates/uv-platform-tags/README.md +++ b/crates/uv-platform-tags/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-platform-tags). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-platform-tags). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-platform/Cargo.toml b/crates/uv-platform/Cargo.toml index 97835b70733de..b1277f0828033 100644 --- a/crates/uv-platform/Cargo.toml +++ b/crates/uv-platform/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-platform" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-platform/README.md b/crates/uv-platform/README.md index 4f59359de3a20..ee1d3eef79a84 100644 --- a/crates/uv-platform/README.md +++ b/crates/uv-platform/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-platform). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-platform). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-preview/Cargo.toml b/crates/uv-preview/Cargo.toml index 0010259c6dea0..2f13fb47f4d5c 100644 --- a/crates/uv-preview/Cargo.toml +++ b/crates/uv-preview/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-preview" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-preview/README.md b/crates/uv-preview/README.md index 5458d78ff3055..8a22d7aeee13f 100644 --- a/crates/uv-preview/README.md +++ b/crates/uv-preview/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-preview). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-preview). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-publish/Cargo.toml b/crates/uv-publish/Cargo.toml index 429e251fe608e..82e0bbf3f3f4a 100644 --- a/crates/uv-publish/Cargo.toml +++ b/crates/uv-publish/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-publish" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-publish/README.md b/crates/uv-publish/README.md index 5cebfa6808ad0..175c8336a9cc9 100644 --- a/crates/uv-publish/README.md +++ b/crates/uv-publish/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-publish). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-publish). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-pypi-types/Cargo.toml b/crates/uv-pypi-types/Cargo.toml index c7fe39296aede..849a627552128 100644 --- a/crates/uv-pypi-types/Cargo.toml +++ b/crates/uv-pypi-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-pypi-types" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-pypi-types/README.md b/crates/uv-pypi-types/README.md index a648780fa31e3..8a57cb23b5cb1 100644 --- a/crates/uv-pypi-types/README.md +++ b/crates/uv-pypi-types/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-pypi-types). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-pypi-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index 392ff09750948..472a5c1706dc2 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-python" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-python/README.md b/crates/uv-python/README.md index b0c6062ddfd5a..85516c541be02 100644 --- a/crates/uv-python/README.md +++ b/crates/uv-python/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-python). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-python). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-redacted/Cargo.toml b/crates/uv-redacted/Cargo.toml index a91d67097aeb0..9e56cffab6e9e 100644 --- a/crates/uv-redacted/Cargo.toml +++ b/crates/uv-redacted/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-redacted" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-redacted/README.md b/crates/uv-redacted/README.md index 10623c92b626b..a32765e6d7791 100644 --- a/crates/uv-redacted/README.md +++ b/crates/uv-redacted/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-redacted). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-redacted). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-requirements-txt/Cargo.toml b/crates/uv-requirements-txt/Cargo.toml index 801b1824263cf..1ad3b94775677 100644 --- a/crates/uv-requirements-txt/Cargo.toml +++ b/crates/uv-requirements-txt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-requirements-txt" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-requirements-txt/README.md b/crates/uv-requirements-txt/README.md index f9c48835190e4..18dbc086a62d9 100644 --- a/crates/uv-requirements-txt/README.md +++ b/crates/uv-requirements-txt/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-requirements-txt). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-requirements-txt). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index 01f3450fa299c..059130f8a4bbf 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-requirements" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-requirements/README.md b/crates/uv-requirements/README.md index ec13c52f8d27e..49c8e0a67170c 100644 --- a/crates/uv-requirements/README.md +++ b/crates/uv-requirements/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-requirements). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-requirements). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 8435fdc4c6f24..cb6d8035d570f 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-resolver" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-resolver/README.md b/crates/uv-resolver/README.md index b3915f9409e7d..1fbfb80704b7b 100644 --- a/crates/uv-resolver/README.md +++ b/crates/uv-resolver/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-resolver). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-resolver). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index d368bb67f4f63..7b8c0b59c0d23 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-scripts" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-scripts/README.md b/crates/uv-scripts/README.md index b0f7ea6a3b4dd..7247dff75f29b 100644 --- a/crates/uv-scripts/README.md +++ b/crates/uv-scripts/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-scripts). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-scripts). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-settings/Cargo.toml b/crates/uv-settings/Cargo.toml index 16980df9b4522..2c93960124694 100644 --- a/crates/uv-settings/Cargo.toml +++ b/crates/uv-settings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-settings" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-settings/README.md b/crates/uv-settings/README.md index e8e94c88435cf..c3eceb89191a2 100644 --- a/crates/uv-settings/README.md +++ b/crates/uv-settings/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-settings). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-settings). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-shell/Cargo.toml b/crates/uv-shell/Cargo.toml index ff07135725788..2b700a9628532 100644 --- a/crates/uv-shell/Cargo.toml +++ b/crates/uv-shell/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-shell" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-shell/README.md b/crates/uv-shell/README.md index 9a5c15dcdc991..0737f1dc0d398 100644 --- a/crates/uv-shell/README.md +++ b/crates/uv-shell/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-shell). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-shell). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-small-str/Cargo.toml b/crates/uv-small-str/Cargo.toml index 88ab05a0ca0f6..87a4ae66fdb87 100644 --- a/crates/uv-small-str/Cargo.toml +++ b/crates/uv-small-str/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-small-str" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-small-str/README.md b/crates/uv-small-str/README.md index fa193f463ee84..f3532c671abc9 100644 --- a/crates/uv-small-str/README.md +++ b/crates/uv-small-str/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-small-str). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-small-str). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-state/Cargo.toml b/crates/uv-state/Cargo.toml index 18b77ba029d6f..01d3e59786604 100644 --- a/crates/uv-state/Cargo.toml +++ b/crates/uv-state/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-state" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-state/README.md b/crates/uv-state/README.md index ebaeee0a4066d..6d362dae5f1b0 100644 --- a/crates/uv-state/README.md +++ b/crates/uv-state/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-state). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-state). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-static/Cargo.toml b/crates/uv-static/Cargo.toml index 0885f94f6621b..622d87ded5535 100644 --- a/crates/uv-static/Cargo.toml +++ b/crates/uv-static/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-static" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-static/README.md b/crates/uv-static/README.md index bf81631b92948..b7c77ae0104ba 100644 --- a/crates/uv-static/README.md +++ b/crates/uv-static/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-static). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-static). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-test/Cargo.toml b/crates/uv-test/Cargo.toml index bd2e9066b564e..d971c4f70fe45 100644 --- a/crates/uv-test/Cargo.toml +++ b/crates/uv-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-test" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-test/README.md b/crates/uv-test/README.md index ee878705187c9..500a57d876520 100644 --- a/crates/uv-test/README.md +++ b/crates/uv-test/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-test). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-test). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-toml/Cargo.toml b/crates/uv-toml/Cargo.toml index abbcae9ad4369..82ff77de33057 100644 --- a/crates/uv-toml/Cargo.toml +++ b/crates/uv-toml/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-toml" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-toml/README.md b/crates/uv-toml/README.md index 3c6b445e3f8e6..4b46c68a2c09f 100644 --- a/crates/uv-toml/README.md +++ b/crates/uv-toml/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-toml). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-toml). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-tool/Cargo.toml b/crates/uv-tool/Cargo.toml index 9a45843a73588..3cf8072a542c4 100644 --- a/crates/uv-tool/Cargo.toml +++ b/crates/uv-tool/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-tool" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-tool/README.md b/crates/uv-tool/README.md index 5375384048d57..46dc6d9c317ed 100644 --- a/crates/uv-tool/README.md +++ b/crates/uv-tool/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-tool). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-tool). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-torch/Cargo.toml b/crates/uv-torch/Cargo.toml index dc81f5680b7ee..bc534d4c4a81a 100644 --- a/crates/uv-torch/Cargo.toml +++ b/crates/uv-torch/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-torch" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-torch/README.md b/crates/uv-torch/README.md index 67c8128d28d1f..8ad57ca437eb8 100644 --- a/crates/uv-torch/README.md +++ b/crates/uv-torch/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-torch). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-torch). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-trampoline-builder/Cargo.toml b/crates/uv-trampoline-builder/Cargo.toml index 43b446bc38fea..05e6a5e8656b8 100644 --- a/crates/uv-trampoline-builder/Cargo.toml +++ b/crates/uv-trampoline-builder/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-trampoline-builder" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } diff --git a/crates/uv-trampoline-builder/README.md b/crates/uv-trampoline-builder/README.md index 11e4a6cfe6318..ea6cea42cfc40 100644 --- a/crates/uv-trampoline-builder/README.md +++ b/crates/uv-trampoline-builder/README.md @@ -5,9 +5,9 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-trampoline-builder). +[here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-trampoline-builder). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-trampoline/Cargo.lock b/crates/uv-trampoline/Cargo.lock index fc7daf4c84c5e..14f237c2dada2 100644 --- a/crates/uv-trampoline/Cargo.lock +++ b/crates/uv-trampoline/Cargo.lock @@ -138,7 +138,7 @@ checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "uv-macros" -version = "0.0.47" +version = "0.0.48" dependencies = [ "proc-macro2", "quote", @@ -148,7 +148,7 @@ dependencies = [ [[package]] name = "uv-static" -version = "0.0.47" +version = "0.0.48" dependencies = [ "thiserror", "uv-macros", @@ -169,7 +169,7 @@ dependencies = [ [[package]] name = "uv-windows" -version = "0.0.47" +version = "0.0.48" dependencies = [ "windows", ] diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 86a836276b7b9..d040b2de71dc9 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-types" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-types/README.md b/crates/uv-types/README.md index 906734ad75c1b..80d739fd86c4d 100644 --- a/crates/uv-types/README.md +++ b/crates/uv-types/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-types). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-types). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-unix/Cargo.toml b/crates/uv-unix/Cargo.toml index 48c054a89e4db..07cb123d3e4e0 100644 --- a/crates/uv-unix/Cargo.toml +++ b/crates/uv-unix/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-unix" -version = "0.0.47" +version = "0.0.48" description = "Unix-specific functionality for uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-unix/README.md b/crates/uv-unix/README.md index 35cc05c3d9134..fca1b8a1e1924 100644 --- a/crates/uv-unix/README.md +++ b/crates/uv-unix/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-unix). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-unix). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 21ca77db23948..e069fff70d20c 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.11.14" +version = "0.11.15" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-version/README.md b/crates/uv-version/README.md index 97915fc04b4c0..65561d7f8c493 100644 --- a/crates/uv-version/README.md +++ b/crates/uv-version/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.11.14) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-version). +This version (0.11.15) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-version). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-virtualenv/Cargo.toml b/crates/uv-virtualenv/Cargo.toml index d0776574efc36..70c3b4f9b45cb 100644 --- a/crates/uv-virtualenv/Cargo.toml +++ b/crates/uv-virtualenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-virtualenv" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" keywords = ["virtualenv", "venv", "python"] diff --git a/crates/uv-warnings/Cargo.toml b/crates/uv-warnings/Cargo.toml index 7a19513cd4b47..f1efb84d0f007 100644 --- a/crates/uv-warnings/Cargo.toml +++ b/crates/uv-warnings/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-warnings" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-warnings/README.md b/crates/uv-warnings/README.md index 1504e5d7395ee..6bf0f3d76ddcd 100644 --- a/crates/uv-warnings/README.md +++ b/crates/uv-warnings/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-warnings). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-warnings). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-windows/Cargo.toml b/crates/uv-windows/Cargo.toml index a2a525685ca5a..fc5c4af6517ed 100644 --- a/crates/uv-windows/Cargo.toml +++ b/crates/uv-windows/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-windows" -version = "0.0.47" +version = "0.0.48" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv-windows/README.md b/crates/uv-windows/README.md index 26e149411eaaf..fc70222d4a19f 100644 --- a/crates/uv-windows/README.md +++ b/crates/uv-windows/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-windows). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-windows). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index caa8adcc392c6..4f406dd0d3e5e 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-workspace" -version = "0.0.47" +version = "0.0.48" description = "This is an internal component crate of uv" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv-workspace/README.md b/crates/uv-workspace/README.md index 172b0ecd5c583..d04edb611920d 100644 --- a/crates/uv-workspace/README.md +++ b/crates/uv-workspace/README.md @@ -5,8 +5,8 @@ This crate is an internal component of [uv](https://crates.io/crates/uv). The Rust API exposed here is unstable and will have frequent breaking changes. -This version (0.0.47) is a component of [uv 0.11.14](https://crates.io/crates/uv/0.11.14). The -source can be found [here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv-workspace). +This version (0.0.48) is a component of [uv 0.11.15](https://crates.io/crates/uv/0.11.15). The +source can be found [here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv-workspace). See uv's [crate versioning policy](https://docs.astral.sh/uv/reference/policies/versioning/#crate-versioning) diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index a7526be510d33..2eed80566cf37 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.11.14" +version = "0.11.15" description = "A Python package and project manager" edition = { workspace = true } rust-version = { workspace = true } diff --git a/crates/uv/README.md b/crates/uv/README.md index 574b7e3e3a830..da6d0062c5f0d 100644 --- a/crates/uv/README.md +++ b/crates/uv/README.md @@ -10,8 +10,8 @@ for more information. This crate is the entry point to the uv command-line interface. The Rust API exposed here is not considered public interface. -This is version 0.11.14. The source can be found -[here](https://github.com/astral-sh/uv/blob/0.11.14/crates/uv). +This is version 0.11.15. The source can be found +[here](https://github.com/astral-sh/uv/blob/0.11.15/crates/uv). The following uv workspace members are also available: @@ -46,6 +46,7 @@ The following uv workspace members are also available: - [uv-logging](https://crates.io/crates/uv-logging) - [uv-macros](https://crates.io/crates/uv-macros) - [uv-metadata](https://crates.io/crates/uv-metadata) +- [uv-netrc](https://crates.io/crates/uv-netrc) - [uv-normalize](https://crates.io/crates/uv-normalize) - [uv-once-map](https://crates.io/crates/uv-once-map) - [uv-options-metadata](https://crates.io/crates/uv-options-metadata) diff --git a/docs/concepts/build-backend.md b/docs/concepts/build-backend.md index 10b8129b45a29..78d0dde0af1bf 100644 --- a/docs/concepts/build-backend.md +++ b/docs/concepts/build-backend.md @@ -31,7 +31,7 @@ To use uv as a build backend in an existing project, add `uv_build` to the ```toml title="pyproject.toml" [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/concepts/projects/init.md b/docs/concepts/projects/init.md index 2a939b5279943..31c4d8492f921 100644 --- a/docs/concepts/projects/init.md +++ b/docs/concepts/projects/init.md @@ -113,7 +113,7 @@ dependencies = [] example-pkg = "example_pkg:main" [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` @@ -136,7 +136,7 @@ dependencies = [] example-pkg = "example_pkg:main" [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` @@ -197,7 +197,7 @@ requires-python = ">=3.11" dependencies = [] [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/concepts/projects/workspaces.md b/docs/concepts/projects/workspaces.md index 008b92f5d27e0..99a5560226186 100644 --- a/docs/concepts/projects/workspaces.md +++ b/docs/concepts/projects/workspaces.md @@ -75,7 +75,7 @@ bird-feeder = { workspace = true } members = ["packages/*"] [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` @@ -106,7 +106,7 @@ tqdm = { git = "https://github.com/tqdm/tqdm" } members = ["packages/*"] [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` @@ -188,7 +188,7 @@ dependencies = ["bird-feeder", "tqdm>=4,<5"] bird-feeder = { path = "packages/bird-feeder" } [build-system] -requires = ["uv_build>=0.11.14,<0.12"] +requires = ["uv_build>=0.11.15,<0.12"] build-backend = "uv_build" ``` diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 8abf83325eb8c..6826a55d1dfbb 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -25,7 +25,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```console - $ curl -LsSf https://astral.sh/uv/0.11.14/install.sh | sh + $ curl -LsSf https://astral.sh/uv/0.11.15/install.sh | sh ``` === "Windows" @@ -41,7 +41,7 @@ uv provides a standalone installer to download and install uv: Request a specific version by including it in the URL: ```pwsh-session - PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.11.14/install.ps1 | iex" + PS> powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/0.11.15/install.ps1 | iex" ``` !!! tip diff --git a/docs/guides/integration/aws-lambda.md b/docs/guides/integration/aws-lambda.md index 197fb8cbc4e8f..c2f855cb66b75 100644 --- a/docs/guides/integration/aws-lambda.md +++ b/docs/guides/integration/aws-lambda.md @@ -92,7 +92,7 @@ the second stage, we'll copy this directory over to the final image, omitting th other unnecessary files. ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.11.14 AS uv +FROM ghcr.io/astral-sh/uv:0.11.15 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder @@ -334,7 +334,7 @@ And confirm that opening http://127.0.0.1:8000/ in a web browser displays, "Hell Finally, we'll update the Dockerfile to include the local library in the deployment package: ```dockerfile title="Dockerfile" -FROM ghcr.io/astral-sh/uv:0.11.14 AS uv +FROM ghcr.io/astral-sh/uv:0.11.15 AS uv # First, bundle the dependencies into the task root. FROM public.ecr.aws/lambda/python:3.13 AS builder diff --git a/docs/guides/integration/docker.md b/docs/guides/integration/docker.md index d9a0734d87e68..cfd698ba3ab82 100644 --- a/docs/guides/integration/docker.md +++ b/docs/guides/integration/docker.md @@ -31,7 +31,7 @@ $ docker run --rm -it ghcr.io/astral-sh/uv:debian uv --help The following distroless images are available: - `ghcr.io/astral-sh/uv:latest` -- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.11.14` +- `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}`, e.g., `ghcr.io/astral-sh/uv:0.11.15` - `ghcr.io/astral-sh/uv:{major}.{minor}`, e.g., `ghcr.io/astral-sh/uv:0.8` (the latest patch version) @@ -92,7 +92,7 @@ And the following derived images are available: As with the distroless image, each derived image is published with uv version tags as `ghcr.io/astral-sh/uv:{major}.{minor}.{patch}-{base}` and -`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.11.14-alpine`. +`ghcr.io/astral-sh/uv:{major}.{minor}-{base}`, e.g., `ghcr.io/astral-sh/uv:0.11.15-alpine`. In addition, starting with `0.8` each derived image also sets `UV_TOOL_BIN_DIR` to `/usr/local/bin` to allow `uv tool install` to work as expected with the default user. @@ -133,7 +133,7 @@ Note this requires `curl` to be available. In either case, it is best practice to pin to a specific uv version, e.g., with: ```dockerfile -COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:0.11.15 /uv /uvx /bin/ ``` !!! tip @@ -151,7 +151,7 @@ COPY --from=ghcr.io/astral-sh/uv:0.11.14 /uv /uvx /bin/ Or, with the installer: ```dockerfile -ADD https://astral.sh/uv/0.11.14/install.sh /uv-installer.sh +ADD https://astral.sh/uv/0.11.15/install.sh /uv-installer.sh ``` ### Installing a project @@ -619,5 +619,5 @@ Verified OK !!! tip These examples use `latest`, but best practice is to verify the attestation for a specific - version tag, e.g., `ghcr.io/astral-sh/uv:0.11.14`, or (even better) the specific image digest, + version tag, e.g., `ghcr.io/astral-sh/uv:0.11.15`, or (even better) the specific image digest, such as `ghcr.io/astral-sh/uv:0.5.27@sha256:5adf09a5a526f380237408032a9308000d14d5947eafa687ad6c6a2476787b4f`. diff --git a/docs/guides/integration/github.md b/docs/guides/integration/github.md index bb761d8c1dd3f..131c74fde184f 100644 --- a/docs/guides/integration/github.md +++ b/docs/guides/integration/github.md @@ -47,7 +47,7 @@ jobs: uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: # Install a specific version of uv. - version: "0.11.14" + version: "0.11.15" ``` ## Setting up Python diff --git a/docs/guides/integration/gitlab.md b/docs/guides/integration/gitlab.md index 62bbaf037babc..130427fef1bb7 100644 --- a/docs/guides/integration/gitlab.md +++ b/docs/guides/integration/gitlab.md @@ -13,7 +13,7 @@ Select a variant that is suitable for your workflow. ```yaml title=".gitlab-ci.yml" variables: - UV_VERSION: "0.11.14" + UV_VERSION: "0.11.15" PYTHON_VERSION: "3.12" BASE_LAYER: trixie-slim # GitLab CI creates a separate mountpoint for the build directory, diff --git a/docs/guides/integration/pre-commit.md b/docs/guides/integration/pre-commit.md index 4df3dbbf98a15..37e64ec6652f3 100644 --- a/docs/guides/integration/pre-commit.md +++ b/docs/guides/integration/pre-commit.md @@ -19,7 +19,7 @@ To make sure your `uv.lock` file is up to date even if your `pyproject.toml` fil repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.14 + rev: 0.11.15 hooks: - id: uv-lock ``` @@ -30,7 +30,7 @@ To keep a `requirements.txt` file in sync with your `uv.lock` file: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.14 + rev: 0.11.15 hooks: - id: uv-export ``` @@ -41,7 +41,7 @@ To compile requirements files: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.14 + rev: 0.11.15 hooks: # Compile requirements - id: pip-compile @@ -54,7 +54,7 @@ To compile alternative requirements files, modify `args` and `files`: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.14 + rev: 0.11.15 hooks: # Compile requirements - id: pip-compile @@ -68,7 +68,7 @@ To run the hook over multiple files at the same time, add additional entries: repos: - repo: https://github.com/astral-sh/uv-pre-commit # uv version. - rev: 0.11.14 + rev: 0.11.15 hooks: # Compile requirements - id: pip-compile diff --git a/pyproject.toml b/pyproject.toml index c94e7ca19f1b3..30f808b49d8c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.11.14" +version = "0.11.15" description = "An extremely fast Python package and project manager, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" diff --git a/scripts/setup-crates-io-publish.py.lock b/scripts/setup-crates-io-publish.py.lock index c2c2cdc7d4aac..31407d8091a62 100644 --- a/scripts/setup-crates-io-publish.py.lock +++ b/scripts/setup-crates-io-publish.py.lock @@ -2,72 +2,75 @@ version = 1 revision = 3 requires-python = ">=3.13" +[options] +exclude-newer = "2026-05-11T05:42:05Z" + [manifest] requirements = [{ name = "httpx" }] [[package]] name = "anyio" version = "4.13.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "certifi" version = "2026.2.25" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "idna" version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] diff --git a/uv.lock b/uv.lock index 2decd091be45a..573cff0e34b9b 100644 --- a/uv.lock +++ b/uv.lock @@ -17,16 +17,15 @@ wheels = [ [[package]] name = "backrefs" -version = "6.2" +version = "7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, - { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, - { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, ] [[package]] @@ -44,7 +43,7 @@ wheels = [ [[package]] name = "black" -version = "26.1.0" +version = "26.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click", marker = "python_full_version >= '3.12'" }, @@ -54,193 +53,208 @@ dependencies = [ { name = "platformdirs", marker = "python_full_version >= '3.12'" }, { name = "pytokens", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, - { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, - { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, - { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, + { url = "https://files.pythonhosted.org/packages/be/84/b3f55026206a9e8820a91503308075ca48eadc515e436731ca01dbe043b3/black-26.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9942db8888e06943c5dde66ca0037dcff82a2a4ec1ad0ada9e0d2ee9d9823893", size = 1987719, upload-time = "2026-05-18T17:05:02.757Z" }, + { url = "https://files.pythonhosted.org/packages/c6/34/7db312c5e5783d6e76cffd9d5ac8972a32badae4c6e3288dac0eed8d3bed/black-26.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:89c93167a74d3a75dfaa38a5c7cca015537d5820dd7f17d63267d674a61cae90", size = 1810083, upload-time = "2026-05-18T17:05:04.302Z" }, + { url = "https://files.pythonhosted.org/packages/33/e2/e0101e73c2c8727634e2efcb35e2b34bd23ad70dfa673789f5773a591b21/black-26.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f2cd76d069cc54c71f10360744ba8983fbb616903b4304a85b734915c8e1b4", size = 1860633, upload-time = "2026-05-18T17:05:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/e15c0c5b23cf3651035fe5addcce90e283af3548a3f91bb03d81b83106ab/black-26.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:87ed5c6f450580a2f6790bc7cbfb016dfc73bc750249762268a3695361315eef", size = 1477886, upload-time = "2026-05-18T17:05:07.96Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3f/59d43ade98d2ce5c8dc34a4e46cbecd177e6d55d7d4092969c6003ccc655/black-26.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:58b4bd92cf88aacf83d88479c8f9caee044b1ec55f2451a337354a7ea2590a22", size = 1277111, upload-time = "2026-05-18T17:05:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/4b/96/3c3e09f09f44a37aac36b178a279cd19aa7001bd796187a7b162a294c81f/black-26.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:96ae2c733b2aabdd9986e2c5df628ff3473676cd1c5faded1ff496cf6d74083c", size = 1970639, upload-time = "2026-05-18T17:05:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/83/ea/5ad117b9ee3ecd933c712bcbae610006e5b7cc9f41c526cd7ed3b6c4124c/black-26.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e48b87e03bf109288e55cfceadcfa15ff5470aca2851a851950ed2926f450d7", size = 1792130, upload-time = "2026-05-18T17:05:12.983Z" }, + { url = "https://files.pythonhosted.org/packages/06/3a/7c448bc623fcdfa96672531beb5a616ea5e64f6975955254d7731ffb0ad9/black-26.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5119fa92ae61f786e8c3662fd60aece1d0a2dd5cca5d0c79417a95e7a4272a59", size = 1846134, upload-time = "2026-05-18T17:05:14.506Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5b/0b39b3a5917f0657ac014ad2edb58c139553a478adfe7f817abf1622ff6e/black-26.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:30d3c14661f2792e9142cce3eeeb1cbc175b3eb5f733be0c8eeb99651e52b0c3", size = 1478883, upload-time = "2026-05-18T17:05:16.542Z" }, + { url = "https://files.pythonhosted.org/packages/4c/48/dc222692e0f95030db1bbfb6c857e76858bad09058221ea7aae815255327/black-26.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:1ef92b76f7733f282fd096ea406200b5a286c42947412b0eaff3a74e3616cefe", size = 1277776, upload-time = "2026-05-18T17:05:18.029Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/7744b906703228264ef73bdd534df88ec1ef3de45c4e78f6d31b9e32d0c9/black-26.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4ad6fa01f941920f54f2bbb35f3df7673428a0ef98a0b0840c2eaef3b110efa8", size = 2012518, upload-time = "2026-05-18T17:05:20.108Z" }, + { url = "https://files.pythonhosted.org/packages/b7/c0/c5a3b1636dfd09c42534f2b3cf33506814f6d3e066fb0879ffa16c1ae860/black-26.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3915f256e75a2d7cf88d8953d37f780455dc586cc72dee059c528fe77f581217", size = 1816016, upload-time = "2026-05-18T17:05:21.84Z" }, + { url = "https://files.pythonhosted.org/packages/1f/0e/36044316b65ca471d3bb6d3703fd06fb50c6b727c3562f6a5a3153634f88/black-26.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d98d4137277c75dfb898ec8d846c4fd68ba1e9cf77f95e2865c203dc18f4c3d", size = 1884150, upload-time = "2026-05-18T17:05:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/b3/33/dafc5808c2af43672912111d7c3354af1615f7e2be3bed7a878461abbe4d/black-26.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:a1dca32d9f1784af512a13410ec204c6f7f0aa9797a111c42e1c03449821c264", size = 1486825, upload-time = "2026-05-18T17:05:25.004Z" }, + { url = "https://files.pythonhosted.org/packages/82/14/b965ee6ad2a311f28bdbf692def3ee9848d2ae289dab28b27657fcee3e78/black-26.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:1037d5ac7b7b310b2632ad867ec8d0e4c4819dcdb0b820f63135da746a24e418", size = 1288646, upload-time = "2026-05-18T17:05:26.477Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" }, + { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" }, + { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" }, + { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" }, ] [[package]] name = "cargo-zigbuild" -version = "0.22.1" +version = "0.22.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ziglang", marker = "python_full_version >= '3.12'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/f4/3c88a06f50d00c0bb24d61b379a819c2ac143e851d8e574e30e3159926d6/cargo_zigbuild-0.22.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9cf70fe0e9edad06aef81c8fd044223d29c8ceab0a5624bb605a784e77578d6a", size = 2932111, upload-time = "2026-02-18T09:07:31.778Z" }, - { url = "https://files.pythonhosted.org/packages/b2/24/c956fccd07f89d423f2c7925820c5a7a246910324446eaa3d0f3026fb1b3/cargo_zigbuild-0.22.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22fb45a099398e4f79bd8fbe1a3a23d51ee2b870c1d9c6b04b120e1db421f91b", size = 1484362, upload-time = "2026-02-18T09:07:37.224Z" }, - { url = "https://files.pythonhosted.org/packages/bb/4a/c3c5d328604be32bbe4fb41d3427d42a13f9045c0c0daf37837cb5f8c411/cargo_zigbuild-0.22.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d98bce6424d4f22c5d2cc11ac1264243a333e309091336e5c38cb933f7e26f2", size = 1428603, upload-time = "2026-02-18T09:07:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f6/52/9e1d1f08330b203be1ea4d5bca14e19193ce284540a4729b0f9daba757d7/cargo_zigbuild-0.22.1-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c0010fc7889e1dda1f8a2bc6d59b398cd8d83d2558ee25e1564b0006e7bb123b", size = 1616665, upload-time = "2026-02-18T09:07:29.116Z" }, - { url = "https://files.pythonhosted.org/packages/c6/71/a0b0193db7258652de1c955637683bc999d42988f0a84b54c5b64e026fd4/cargo_zigbuild-0.22.1-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d24545d1595738c7be21be37b61122814216893b3df79db41476477f2141f438", size = 1604629, upload-time = "2026-02-18T09:07:32.88Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/ca4758f65eb1ed1879d468f2f6d79684d232a0f06f5ba0dbf231628da8c6/cargo_zigbuild-0.22.1-py3-none-win32.whl", hash = "sha256:d5c7a8e3859968cfba6e714f9f4ca23d4d53492ad71d2a956e48fb133cc6d6ba", size = 1337484, upload-time = "2026-02-18T09:07:36.074Z" }, - { url = "https://files.pythonhosted.org/packages/75/03/47059e3e4ad6309669fc04b834c9c5a047ea5371642b3dbc31068277449a/cargo_zigbuild-0.22.1-py3-none-win_amd64.whl", hash = "sha256:0549be58f9d0aac3383b3afb26170b69ebf50374bcb0ab53778375c91d38ba9c", size = 1478294, upload-time = "2026-02-18T09:07:30.48Z" }, - { url = "https://files.pythonhosted.org/packages/c2/49/35ef5905db32cd3432dd11949f8d7234c9eaa5efee54b101ceab5d630ef8/cargo_zigbuild-0.22.1-py3-none-win_arm64.whl", hash = "sha256:0577795e982b894ac0b8727127c6533e1d3f172a68bec228e42078c87786032f", size = 1407888, upload-time = "2026-02-18T09:07:38.709Z" }, + { url = "https://files.pythonhosted.org/packages/a0/94/7a351597a84d173ca259c57ecda6ef86152d875774003585e0d7b7504668/cargo_zigbuild-0.22.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:429bd3c789f4e1771fa1e8fc1fed4efdcdd0d540110b4a04cc27ab9600ead887", size = 2913539, upload-time = "2026-04-26T03:15:42.548Z" }, + { url = "https://files.pythonhosted.org/packages/ef/8e/5e5953118fe87790ef18a6d1ac6369bf49d9a634cc0593add1e8a8ca98d8/cargo_zigbuild-0.22.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ec32ef02a87bf4b836a043ebc70839ec63654c53f43ea16ef0d8eacef4f7f82", size = 1478132, upload-time = "2026-04-26T03:15:41.274Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ed/c9f020dc8b658b07c2f6ff3e266cc9b4f2e538522d400da81974386ce4cd/cargo_zigbuild-0.22.3-py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d567df9f55c6eba1d6edfe0fa7240ced5cbcb0e6222b5ab7ea7c7932e8f708c4", size = 1607159, upload-time = "2026-04-26T03:15:39.854Z" }, + { url = "https://files.pythonhosted.org/packages/23/34/7dc4ed9aed1069cad8be5b1d7b4669a8fe96a219ad8d0c2b17f64fdd85fd/cargo_zigbuild-0.22.3-py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1684d9405a3c505f97a652dddcccf4cddf6e99e1520c93b694f22c4b061d3f73", size = 1595191, upload-time = "2026-04-26T03:15:35.571Z" }, + { url = "https://files.pythonhosted.org/packages/5e/23/5f84d7b49d3dfcde6506cff6c59bced1ad996c3833bdc6c13eb0a7365b34/cargo_zigbuild-0.22.3-py3-none-win32.whl", hash = "sha256:f4d657a0d54ab543166549e1b9515f48ea983696b92fe3a805abfbf7d0c48273", size = 1326098, upload-time = "2026-04-26T03:15:37.345Z" }, + { url = "https://files.pythonhosted.org/packages/69/59/b1027f9c7710e303338a642f6efdffe87ddc0bbabdd20a557ee9a6faacb8/cargo_zigbuild-0.22.3-py3-none-win_amd64.whl", hash = "sha256:b8d093235ff94cb0a05612598689027b2cb5388c21984e29dbb44e6cc0b9937f", size = 1476196, upload-time = "2026-04-26T03:15:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/a0/79/76312a591ff6cddcfa405d5ca46b88bdc99dcebfaadd6fd5f4d29a031e8c/cargo_zigbuild-0.22.3-py3-none-win_arm64.whl", hash = "sha256:f0667e9f181368e1e0ec74cd3a7f5e1162522a5bd292d9d93fcadc20f0fc50c4", size = 1405317, upload-time = "2026-04-26T03:15:38.784Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.4.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4e/3926a1c11f0433791985727965263f788af00db3482d89a7545ca5ecc921/charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84", size = 198599, upload-time = "2025-10-14T04:41:53.213Z" }, - { url = "https://files.pythonhosted.org/packages/ec/7c/b92d1d1dcffc34592e71ea19c882b6709e43d20fa498042dea8b815638d7/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3", size = 143090, upload-time = "2025-10-14T04:41:54.385Z" }, - { url = "https://files.pythonhosted.org/packages/84/ce/61a28d3bb77281eb24107b937a497f3c43089326d27832a63dcedaab0478/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac", size = 139490, upload-time = "2025-10-14T04:41:55.551Z" }, - { url = "https://files.pythonhosted.org/packages/c0/bd/c9e59a91b2061c6f8bb98a150670cb16d4cd7c4ba7d11ad0cdf789155f41/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af", size = 155334, upload-time = "2025-10-14T04:41:56.724Z" }, - { url = "https://files.pythonhosted.org/packages/bf/37/f17ae176a80f22ff823456af91ba3bc59df308154ff53aef0d39eb3d3419/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2", size = 152823, upload-time = "2025-10-14T04:41:58.236Z" }, - { url = "https://files.pythonhosted.org/packages/bf/fa/cf5bb2409a385f78750e78c8d2e24780964976acdaaed65dbd6083ae5b40/charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d", size = 147618, upload-time = "2025-10-14T04:41:59.409Z" }, - { url = "https://files.pythonhosted.org/packages/9b/63/579784a65bc7de2d4518d40bb8f1870900163e86f17f21fd1384318c459d/charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3", size = 145516, upload-time = "2025-10-14T04:42:00.579Z" }, - { url = "https://files.pythonhosted.org/packages/a3/a9/94ec6266cd394e8f93a4d69cca651d61bf6ac58d2a0422163b30c698f2c7/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63", size = 145266, upload-time = "2025-10-14T04:42:01.684Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/d6626eb97764b58c2779fa7928fa7d1a49adb8ce687c2dbba4db003c1939/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7", size = 139559, upload-time = "2025-10-14T04:42:02.902Z" }, - { url = "https://files.pythonhosted.org/packages/09/01/ddbe6b01313ba191dbb0a43c7563bc770f2448c18127f9ea4b119c44dff0/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4", size = 156653, upload-time = "2025-10-14T04:42:04.005Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/d05543378bea89296e9af4510b44c704626e191da447235c8fdedfc5b7b2/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf", size = 145644, upload-time = "2025-10-14T04:42:05.211Z" }, - { url = "https://files.pythonhosted.org/packages/72/01/2866c4377998ef8a1f6802f6431e774a4c8ebe75b0a6e569ceec55c9cbfb/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074", size = 153964, upload-time = "2025-10-14T04:42:06.341Z" }, - { url = "https://files.pythonhosted.org/packages/4a/66/66c72468a737b4cbd7851ba2c522fe35c600575fbeac944460b4fd4a06fe/charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a", size = 148777, upload-time = "2025-10-14T04:42:07.535Z" }, - { url = "https://files.pythonhosted.org/packages/50/94/d0d56677fdddbffa8ca00ec411f67bb8c947f9876374ddc9d160d4f2c4b3/charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa", size = 98687, upload-time = "2025-10-14T04:42:08.678Z" }, - { url = "https://files.pythonhosted.org/packages/00/64/c3bc303d1b586480b1c8e6e1e2191a6d6dd40255244e5cf16763dcec52e6/charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576", size = 106115, upload-time = "2025-10-14T04:42:09.793Z" }, - { url = "https://files.pythonhosted.org/packages/46/7c/0c4760bccf082737ca7ab84a4c2034fcc06b1f21cf3032ea98bd6feb1725/charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9", size = 209609, upload-time = "2025-10-14T04:42:10.922Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/69719daef2f3d7f1819de60c9a6be981b8eeead7542d5ec4440f3c80e111/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d", size = 149029, upload-time = "2025-10-14T04:42:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/e6/21/8d4e1d6c1e6070d3672908b8e4533a71b5b53e71d16828cc24d0efec564c/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608", size = 144580, upload-time = "2025-10-14T04:42:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/a7/0a/a616d001b3f25647a9068e0b9199f697ce507ec898cacb06a0d5a1617c99/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc", size = 162340, upload-time = "2025-10-14T04:42:14.892Z" }, - { url = "https://files.pythonhosted.org/packages/85/93/060b52deb249a5450460e0585c88a904a83aec474ab8e7aba787f45e79f2/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e", size = 159619, upload-time = "2025-10-14T04:42:16.676Z" }, - { url = "https://files.pythonhosted.org/packages/dd/21/0274deb1cc0632cd587a9a0ec6b4674d9108e461cb4cd40d457adaeb0564/charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1", size = 153980, upload-time = "2025-10-14T04:42:17.917Z" }, - { url = "https://files.pythonhosted.org/packages/28/2b/e3d7d982858dccc11b31906976323d790dded2017a0572f093ff982d692f/charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3", size = 152174, upload-time = "2025-10-14T04:42:19.018Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ff/4a269f8e35f1e58b2df52c131a1fa019acb7ef3f8697b7d464b07e9b492d/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6", size = 151666, upload-time = "2025-10-14T04:42:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/da/c9/ec39870f0b330d58486001dd8e532c6b9a905f5765f58a6f8204926b4a93/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88", size = 145550, upload-time = "2025-10-14T04:42:21.324Z" }, - { url = "https://files.pythonhosted.org/packages/75/8f/d186ab99e40e0ed9f82f033d6e49001701c81244d01905dd4a6924191a30/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1", size = 163721, upload-time = "2025-10-14T04:42:22.46Z" }, - { url = "https://files.pythonhosted.org/packages/96/b1/6047663b9744df26a7e479ac1e77af7134b1fcf9026243bb48ee2d18810f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf", size = 152127, upload-time = "2025-10-14T04:42:23.712Z" }, - { url = "https://files.pythonhosted.org/packages/59/78/e5a6eac9179f24f704d1be67d08704c3c6ab9f00963963524be27c18ed87/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318", size = 161175, upload-time = "2025-10-14T04:42:24.87Z" }, - { url = "https://files.pythonhosted.org/packages/e5/43/0e626e42d54dd2f8dd6fc5e1c5ff00f05fbca17cb699bedead2cae69c62f/charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c", size = 155375, upload-time = "2025-10-14T04:42:27.246Z" }, - { url = "https://files.pythonhosted.org/packages/e9/91/d9615bf2e06f35e4997616ff31248c3657ed649c5ab9d35ea12fce54e380/charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505", size = 99692, upload-time = "2025-10-14T04:42:28.425Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/6c040053909d9d1ef4fcab45fddec083aedc9052c10078339b47c8573ea8/charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966", size = 107192, upload-time = "2025-10-14T04:42:29.482Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c6/4fa536b2c0cd3edfb7ccf8469fa0f363ea67b7213a842b90909ca33dd851/charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50", size = 100220, upload-time = "2025-10-14T04:42:30.632Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/fce169ad09419b8e8a5a81db61e08cd7b9fd31332221b84bd176fe0a3136/charset_normalizer-3.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259", size = 283148, upload-time = "2026-04-02T09:27:52.419Z" }, + { url = "https://files.pythonhosted.org/packages/81/76/14ab25789e14f83124c4318f0edbbf15a6ed535bd3d88720c42001a954df/charset_normalizer-3.4.7-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01", size = 192457, upload-time = "2026-04-02T09:27:53.681Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0e/0f722c41d983dd204b3142606fbfcdbb0a33c34b9b031ef3c1fe9e8187ad/charset_normalizer-3.4.7-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3", size = 209614, upload-time = "2026-04-02T09:27:55.01Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ec/e7961eea9977a4d5ac920627e78938784272cb9b752cf1209da91e93d006/charset_normalizer-3.4.7-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30", size = 205833, upload-time = "2026-04-02T09:27:56.648Z" }, + { url = "https://files.pythonhosted.org/packages/17/85/cacf6d45cff52be431468ee4cfa6f625eb622ab8f23a892218af8c77094d/charset_normalizer-3.4.7-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734", size = 199240, upload-time = "2026-04-02T09:27:57.95Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f1/40a59aae52edc5275e85813cbc49621c10758f481deeb27f71c97406cda0/charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60", size = 188301, upload-time = "2026-04-02T09:27:59.351Z" }, + { url = "https://files.pythonhosted.org/packages/96/53/6ea2906da0fd3773d57398e7cee5628d004d844b0c4903ea3038ae8488cd/charset_normalizer-3.4.7-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0", size = 197431, upload-time = "2026-04-02T09:28:00.634Z" }, + { url = "https://files.pythonhosted.org/packages/52/f9/47a52cbcce0140f612ef7a37797b2929244bcaaf2f83ade3775429457252/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545", size = 195156, upload-time = "2026-04-02T09:28:02.312Z" }, + { url = "https://files.pythonhosted.org/packages/76/79/0e09d2169b7ba38a04e9660669d124ea688605f66189030e4c2be44d8e27/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5", size = 188708, upload-time = "2026-04-02T09:28:03.949Z" }, + { url = "https://files.pythonhosted.org/packages/75/20/8b3cefb78df39d40272d7831dda07b51875d89af1f390f97a801eaedec78/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f", size = 211495, upload-time = "2026-04-02T09:28:05.301Z" }, + { url = "https://files.pythonhosted.org/packages/47/3f/9bb0864a92b4abf0ec0d1f40546297f45afd73851795e3216c899b360aa0/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686", size = 197309, upload-time = "2026-04-02T09:28:07.03Z" }, + { url = "https://files.pythonhosted.org/packages/ec/5e/0739d2975ae6fd42505fdb80881ab5e99b4edfbff1d581f4cd5aa94f2d94/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706", size = 207388, upload-time = "2026-04-02T09:28:08.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/5e/8161a7bbf4a7f88d0409091ab5a5762c014913c9ef80a48b50f806140918/charset_normalizer-3.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7", size = 201139, upload-time = "2026-04-02T09:28:09.73Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/c1f1f791467d865b48b749842c895668229e553dd79b71ad80498a0b646f/charset_normalizer-3.4.7-cp38-cp38-win32.whl", hash = "sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207", size = 142063, upload-time = "2026-04-02T09:28:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/9e74560659e3f8a7650e09dac978749d408917c8e9764af13f5f81ceec53/charset_normalizer-3.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83", size = 151922, upload-time = "2026-04-02T09:28:12.77Z" }, + { url = "https://files.pythonhosted.org/packages/01/1b/ef725f8eb19b5a261b30f78efa9252ef9d017985cb499102f6f49834cd12/charset_normalizer-3.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217", size = 299121, upload-time = "2026-04-02T09:28:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2f12878fbc680fbbb52386cd39a379801f62eaca74fc8b323381325f0f04/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5", size = 200612, upload-time = "2026-04-02T09:28:16.162Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b6/10c84e789126ca97d4a7228863a30481e786980a8b8cfcbf4f30658ca63c/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9", size = 221041, upload-time = "2026-04-02T09:28:17.554Z" }, + { url = "https://files.pythonhosted.org/packages/21/7b/c414866a138400b2e81973d006da7f694cfeaf895ef07d2cba9a8743841a/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a", size = 216323, upload-time = "2026-04-02T09:28:18.863Z" }, + { url = "https://files.pythonhosted.org/packages/2e/92/bdcf94997e06b223d826df3abed45a5ad6e17f609b7df9d25cd23b5bde30/charset_normalizer-3.4.7-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc", size = 208419, upload-time = "2026-04-02T09:28:20.332Z" }, + { url = "https://files.pythonhosted.org/packages/1a/64/3f9142293c88b1b10e199649ed1330f070c2a68e305335a5819fa7f25fa7/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00", size = 195016, upload-time = "2026-04-02T09:28:21.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d1/d8a6b7dd5c5636b76ce0d080bc57d8e56c7bbd6bc2ac941529a35e41d84a/charset_normalizer-3.4.7-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776", size = 206115, upload-time = "2026-04-02T09:28:23.259Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8c/60ebe912379627d023eb96995b40bc50308729f210f43d66109ca0a7bbd2/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319", size = 204022, upload-time = "2026-04-02T09:28:24.779Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2a/41816ceda78a551cbfdfbeab6f3891152b0e3f758ce6580c2c18c829f774/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24", size = 195914, upload-time = "2026-04-02T09:28:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9b/7c7f4b7f11525fcbdfba752455314ac60646bae91cdd671d531c1f7a97c6/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42", size = 222159, upload-time = "2026-04-02T09:28:27.504Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/301682e7469bdbfa2ce219a804f0668b2266ab8520570d85d3b3ef483ea3/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4", size = 206154, upload-time = "2026-04-02T09:28:28.848Z" }, + { url = "https://files.pythonhosted.org/packages/20/ec/90339ff5cdc598b265748c1f231c7d7fbd9123a92cee10f757e0b1448de4/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67", size = 217423, upload-time = "2026-04-02T09:28:30.248Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e7/a7a6147f8e3375676309cf584b25c72a3bab784ea4085b0011fa07b23aeb/charset_normalizer-3.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274", size = 210604, upload-time = "2026-04-02T09:28:31.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/62/d9340c7a79c393e57807d7fb6c57e82060687891f81b74d3201958b919c1/charset_normalizer-3.4.7-cp39-cp39-win32.whl", hash = "sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366", size = 144631, upload-time = "2026-04-02T09:28:33.158Z" }, + { url = "https://files.pythonhosted.org/packages/21/e7/92901117e2ddc8facfe8235a3ecd4eb482185b2ad5d5b6606b37c1afea06/charset_normalizer-3.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444", size = 154710, upload-time = "2026-04-02T09:28:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4f/e1fb138201ad9a32499dd9a98aa4a5a5441fbf7f56b52b619a54b7ee8777/charset_normalizer-3.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c", size = 143716, upload-time = "2026-04-02T09:28:35.908Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] [[package]] name = "click" -version = "8.3.1" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e4/796662cd90cf80e3a363c99db2b88e0e394b988a575f60a17e16440cd011/click-8.4.0.tar.gz", hash = "sha256:638f1338fe1235c8f4e008e4a8a254fb5c5fbdcbb40ece3c9142ebb78e792973", size = 350843, upload-time = "2026-05-17T00:47:58.425Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ae/8e92f8058baf87f6c7d86ee7e457668690195cc77efedb8d3797a06e3940/click-8.4.0-py3-none-any.whl", hash = "sha256:40c50b7c6c6adac2823d411041ec84f3f103f1b280d5e9ce0d7f998995832f81", size = 116147, upload-time = "2026-05-17T00:47:56.842Z" }, ] [[package]] @@ -278,23 +292,23 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.46" +version = "3.1.50" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/f6/354ae6491228b5eb40e10d89c4d13c651fe1cf7556e35ebdded50cff57ce/gitpython-3.1.50.tar.gz", hash = "sha256:80da2d12504d52e1f998772dc5baf6e553f8d2fcfe1fcc226c9d9a2ee3372dcc", size = 219798, upload-time = "2026-05-06T04:01:26.571Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] [[package]] name = "idna" -version = "3.11" +version = "3.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ] [[package]] @@ -320,14 +334,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -496,14 +510,14 @@ wheels = [ [[package]] name = "mdit-py-plugins" -version = "0.5.0" +version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, + { url = "https://files.pythonhosted.org/packages/a5/69/6da5581c6a7fede7dc261bf4e67d6adca4196f176b43288b55b3db395b6e/mdit_py_plugins-0.6.1-py3-none-any.whl", hash = "sha256:214c82fb2ac524472ab6a5bcab1de80f73b50443e187f401bfd77efbc7c6481d", size = 66663, upload-time = "2026-05-13T09:03:37.76Z" }, ] [[package]] @@ -550,21 +564,21 @@ wheels = [ [[package]] name = "mkdocs-get-deps" -version = "0.2.0" +version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mergedeep", marker = "python_full_version >= '3.12'" }, { name = "platformdirs", marker = "python_full_version >= '3.12'" }, { name = "pyyaml", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, ] [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "1.5.1" +version = "1.5.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel", marker = "python_full_version >= '3.12'" }, @@ -572,9 +586,9 @@ dependencies = [ { name = "mkdocs", marker = "python_full_version >= '3.12'" }, { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/16/25d7b1b930a802bf8b0c6ee64a9b34ea6e7d0a34c6bc69adbbb59b9d2f4b/mkdocs_git_revision_date_localized_plugin-1.5.1.tar.gz", hash = "sha256:2b0239455cd84784dd87ac8dfc9253fe4b2dd35e102696f21b5d34e2175981c6", size = 449557, upload-time = "2026-01-26T13:34:30.912Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/5c/098e733fc8aa9371d21ff56aa97b587ffb5e28803cb9cb58ae1e7da7e3c6/mkdocs_git_revision_date_localized_plugin-1.5.2.tar.gz", hash = "sha256:0b3d65abdb8b82591d724c1d5ece6af7bb3cd305bdd47e2fadd430886a9a2513", size = 451991, upload-time = "2026-05-13T07:13:51.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/3f/4f663fb7e889fbb2fabef7a67ddd96f8355edca917aa724c6c6cda352d01/mkdocs_git_revision_date_localized_plugin-1.5.1-py3-none-any.whl", hash = "sha256:b00fd36ed0f9b2326b1488fd8fa31bf2ce64e68c4aa60a9ce857f10719571903", size = 26150, upload-time = "2026-01-26T13:34:28.768Z" }, + { url = "https://files.pythonhosted.org/packages/03/c8/64c36918723be26d866cd2cc3e8a38a353f8966734ffde268f8490626e96/mkdocs_git_revision_date_localized_plugin-1.5.2-py3-none-any.whl", hash = "sha256:4f3175e039bc4fe0055cac97d295ce8d0d233a13d65986d42fd5324e4985acc2", size = 26151, upload-time = "2026-05-13T07:13:49.841Z" }, ] [[package]] @@ -593,7 +607,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.3" +version = "9.7.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel", marker = "python_full_version >= '3.12'" }, @@ -608,9 +622,9 @@ dependencies = [ { name = "pymdown-extensions", marker = "python_full_version >= '3.12'" }, { name = "requests", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8a/b4/f900fcb8e6f510241e334ca401eddcb61ed880fb6572f7f32e4228472ca1/mkdocs_material-9.7.3.tar.gz", hash = "sha256:e5f0a18319699da7e78c35e4a8df7e93537a888660f61a86bd773a7134798f22", size = 4097748, upload-time = "2026-02-24T12:06:22.646Z" } +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/1b/16ad0193079bb8a15aa1d2620813a9cd15b18de150a4ea1b2c607fb4c74d/mkdocs_material-9.7.3-py3-none-any.whl", hash = "sha256:37ebf7b4788c992203faf2e71900be3c197c70a4be9b0d72aed537b08a91dd9d", size = 9305078, upload-time = "2026-02-24T12:06:19.155Z" }, + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, ] [[package]] @@ -624,23 +638,24 @@ wheels = [ [[package]] name = "mkdocs-redirects" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mkdocs", marker = "python_full_version >= '3.12'" }, + { name = "properdocs", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/a8/6d44a6cf07e969c7420cb36ab287b0669da636a2044de38a7d2208d5a758/mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095", size = 7162, upload-time = "2024-11-07T14:57:21.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/25/49725f78ca5d3026b09973f7a2b3a8b179cc2e8c15e43d5a13bc79f6b274/mkdocs_redirects-1.2.3.tar.gz", hash = "sha256:5e980330999299729a2d6a125347d1af78023d68a23681a4de3053ce7dfe2e51", size = 7712, upload-time = "2026-03-28T13:57:41.766Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ec/38443b1f2a3821bbcb24e46cd8ba979154417794d54baf949fefde1c2146/mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5", size = 6142, upload-time = "2024-11-07T14:57:19.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/90/871b1cddc01d2ba1637b858eeeabc2e3013dc8df591306b5567b98ef0870/mkdocs_redirects-1.2.3-py3-none-any.whl", hash = "sha256:ec7312fff462d03ec16395d0c001006a418f8d0c21cdf2b47ff11cf839dc3ce0", size = 6245, upload-time = "2026-03-28T13:57:40.466Z" }, ] [[package]] name = "more-itertools" -version = "10.8.0" +version = "11.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/f7/139d22fef48ac78127d18e01d80cf1be40236ae489769d17f35c3d425293/more_itertools-11.0.2.tar.gz", hash = "sha256:392a9e1e362cbc106a2457d37cabf9b36e5e12efd4ebff1654630e76597df804", size = 144659, upload-time = "2026-04-09T15:01:33.297Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, + { url = "https://files.pythonhosted.org/packages/cb/98/6af411189d9413534c3eb691182bff1f5c6d44ed2f93f2edfe52a1bbceb8/more_itertools-11.0.2-py3-none-any.whl", hash = "sha256:6e35b35f818b01f691643c6c611bc0902f2e92b46c18fffa77ae1e7c46e912e4", size = 71939, upload-time = "2026-04-09T15:01:32.21Z" }, ] [[package]] @@ -654,11 +669,11 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] [[package]] @@ -672,42 +687,65 @@ wheels = [ [[package]] name = "pathspec" -version = "1.0.4" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] name = "platformdirs" -version = "4.9.2" +version = "4.9.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "properdocs" +version = "1.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", marker = "python_full_version >= '3.12'" }, + { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "ghp-import", marker = "python_full_version >= '3.12'" }, + { name = "jinja2", marker = "python_full_version >= '3.12'" }, + { name = "markdown", marker = "python_full_version >= '3.12'" }, + { name = "markupsafe", marker = "python_full_version >= '3.12'" }, + { name = "packaging", marker = "python_full_version >= '3.12'" }, + { name = "pathspec", marker = "python_full_version >= '3.12'" }, + { name = "platformdirs", marker = "python_full_version >= '3.12'" }, + { name = "pyyaml", marker = "python_full_version >= '3.12'" }, + { name = "pyyaml-env-tag", marker = "python_full_version >= '3.12'" }, + { name = "watchdog", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/29/f27a4e1eddf72ed3db6e47818fbafe6debbf09fd7051f9c1a007239b46ef/properdocs-1.6.7.tar.gz", hash = "sha256:adc7b16e562890af0e098a7e5b02e3a81c20894a87d6a28d345c9300de73c26e", size = 276141, upload-time = "2026-03-20T20:07:48.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4d/fc923f5c85318ee8cc903566dc4e0ebe41b2dfc1d2ecf5546db232397ed6/properdocs-1.6.7-py3-none-any.whl", hash = "sha256:6fa0cfa2e01bf338f684892c8a506cf70ea88ae7f3479c933b6fa20168101cbd", size = 225406, upload-time = "2026-03-20T20:07:46.875Z" }, ] [[package]] name = "pygments" -version = "2.19.2" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] name = "pymdown-extensions" -version = "10.21" +version = "10.21.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown", marker = "python_full_version >= '3.12'" }, { name = "pyyaml", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/26/d1015444da4d952a1ca487a236b522eb979766f0295a0bd0c5fc089989a9/pymdown_extensions-10.21.3.tar.gz", hash = "sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354", size = 854140, upload-time = "2026-05-13T12:57:32.267Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" }, + { url = "https://files.pythonhosted.org/packages/7e/85/545a951eecc270fcd688288c600017e2050a1aacb56c711d208586d3e470/pymdown_extensions-10.21.3-py3-none-any.whl", hash = "sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6", size = 269002, upload-time = "2026-05-13T12:57:30.296Z" }, ] [[package]] @@ -865,7 +903,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi", marker = "python_full_version >= '3.12'" }, @@ -873,9 +911,9 @@ dependencies = [ { name = "idna", marker = "python_full_version >= '3.12'" }, { name = "urllib3", marker = "python_full_version >= '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -889,11 +927,11 @@ wheels = [ [[package]] name = "smmap" -version = "5.0.2" +version = "5.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, ] [[package]] @@ -916,25 +954,25 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.3" +version = "2026.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uv" -version = "0.11.14" +version = "0.11.15" source = { editable = "." } [package.dev-dependencies] @@ -1008,28 +1046,28 @@ wheels = [ [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/ee/afaf0f85a9a18fe47a67f1e4422ed6cf1fe642f0ae0a2f81166231303c52/wcwidth-0.7.0.tar.gz", hash = "sha256:90e3a7ea092341c44b99562e75d09e4d5160fe7a3974c6fb842a101a95e7eed0", size = 182132, upload-time = "2026-05-02T16:04:12.653Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/e465037f5375f43533d1a80b6923955201596a99142ed524d77b571a1418/wcwidth-0.7.0-py3-none-any.whl", hash = "sha256:5d69154c429a82910e241c738cd0e2976fac8a2dd47a1a805f4afed1c0f136f2", size = 110825, upload-time = "2026-05-02T16:04:11.033Z" }, ] [[package]] name = "ziglang" -version = "0.15.2" +version = "0.16.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/2e/42c49a5b539a595ff5f2293de0bd339636875e44b4c69f51470bdf1d599d/ziglang-0.15.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:acae2a9ce67ed249ad68d42c21bc73b5f9d51a33ba5ea42f609f78683e8d9bb7", size = 92993702, upload-time = "2025-11-10T06:23:24.805Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c7/151df628321266a38da133d481c5e929bd36d9d0424e8c2b7a9c4964c172/ziglang-0.15.2-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:47af4f85e3e59bd672126cc026d3a01b516cad24b9a9ea075abfe8532f4c6bc1", size = 97049112, upload-time = "2026-01-29T09:44:04.612Z" }, - { url = "https://files.pythonhosted.org/packages/0a/13/006c26f2dc7510385dc549eb44983402e4539519d48567d9a6b0ae71adc7/ziglang-0.15.2-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:04eb9c4135ef431487b920c73312cd3a5e754019839024eeed3521772d1585a2", size = 97299386, upload-time = "2026-01-29T09:44:21.973Z" }, - { url = "https://files.pythonhosted.org/packages/db/81/f9f5c7fd68d80b8edff2a641a1c95ac4125356387d298374227040b8e1e8/ziglang-0.15.2-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:806316453d1ede7174ed3fefb8e5348154629089c3bb5fe812dfd0e172fac130", size = 93508126, upload-time = "2026-01-29T09:44:41.936Z" }, - { url = "https://files.pythonhosted.org/packages/53/7d/8c277208250ffa72f12a10f52dfc1d45850f08244093065b40c5f4628260/ziglang-0.15.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:edc0aa60ec964a4cf462d40f68d7de242ddf37fd9a80f2afaee6397059463230", size = 90542084, upload-time = "2026-01-29T09:45:00.806Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ba/b85879ff883f152e2278852d3f8af37924badefaabb38d89e62a5d066b40/ziglang-0.15.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ad43a4692311512ab55cc918596c814ff44fb811b1299fa9a57cc4e2bf315c6b", size = 91550707, upload-time = "2026-01-29T09:45:22.725Z" }, - { url = "https://files.pythonhosted.org/packages/57/ef/09d2baf722521d3878b8e17d3ee6271f02c7ef7ec398bbd059fa6466a5e6/ziglang-0.15.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:85faefbd4cfe420de92aaa8f8300bec2b9afe8d1f1d8c5abf71bf92754682536", size = 99943068, upload-time = "2026-01-29T09:45:42.651Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5d/5690870969ed52501deaa4c722c476279a00d2c034ef7f82cf778507c3fd/ziglang-0.15.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:978ebcc3c6c9624cbc999dd8dcb65427e7474650d9709a889aace8e325705ce2", size = 99561757, upload-time = "2026-01-29T09:46:03.149Z" }, - { url = "https://files.pythonhosted.org/packages/b5/da/2e6655300a3174bd6299c06da577b0cb14685058d9b00b61042cf9d9e38b/ziglang-0.15.2-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d3fe40825e18238e986b305cbf3a4d93a50461b893339e31c81b0d6c73f33937", size = 94112157, upload-time = "2026-01-29T09:46:24.268Z" }, - { url = "https://files.pythonhosted.org/packages/57/04/e54964be82dce7b9a5708a8a5f345d373bc78c558c0712e6e356593292ec/ziglang-0.15.2-py3-none-win32.whl", hash = "sha256:b174f7ebb9d1f2210d32d1420333120f6117ebee3de8b3e221e6ebb0c3beec4b", size = 96136271, upload-time = "2026-01-29T09:46:47.975Z" }, - { url = "https://files.pythonhosted.org/packages/b9/6b/8ab0853a312108b4089747486366b824b5da89773cb56f661f9994de17db/ziglang-0.15.2-py3-none-win_amd64.whl", hash = "sha256:ca80dc9c70dbdfdd9ee51d000de3501a95d33d9782ab9ab9b56f700484ebcd83", size = 94052409, upload-time = "2026-01-29T09:47:02.419Z" }, - { url = "https://files.pythonhosted.org/packages/1e/b4/dec7e1b867ce3640f9bba9204f8ea658f6ca5161350e92b76bf17aeffb9b/ziglang-0.15.2-py3-none-win_arm64.whl", hash = "sha256:a81ebfc61e1b7aa43a33add23f93b98954d757434f894eb21bd40b176b6688aa", size = 89688080, upload-time = "2026-01-29T09:47:20.903Z" }, + { url = "https://files.pythonhosted.org/packages/cd/59/012f0c2800f7428b87bb16c5c78db7ef806efed274491998155955c02558/ziglang-0.16.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:b61e5413c49508d9d62e5dcea543e3af491594154d74a00ff52f84ed508260cc", size = 97252633, upload-time = "2026-04-15T03:55:02.488Z" }, + { url = "https://files.pythonhosted.org/packages/75/60/f924aa24b95a1ad347e845acc7ab6b5d062ae5b0b540d494654cd40d4e0b/ziglang-0.16.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:18e14f6b25678d7b7c65708c82501ee6090fe39ed5c783477d56267af1fa5629", size = 101228024, upload-time = "2026-04-15T03:55:12.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/aa/7966d158d768fb0e40bf5fef6e7ffe230dfee502382782ec39be1331ee4a/ziglang-0.16.0-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:8b83661247430aa335b3cbc29569084900412dde5633b02c80a6d2ff734de3d2", size = 101810276, upload-time = "2026-04-15T03:55:17.843Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ed/7b79023aa27ceb5d461ecf761181e7c33c57bbc1a6256a39535d1c7083d2/ziglang-0.16.0-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:9fcda73f62b851dd72a54b710ad40a209896db14cfb13649e62191243556342b", size = 97941847, upload-time = "2026-04-15T03:55:23.246Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ed/d6663a5e52c504944d578b9e0bfcb7857f292803bcd09ebe0d10fe2b293d/ziglang-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:e27d409812b11e0fb89ed0200cf2e55b6464d43f9461553104e4a4f9a94a1fd5", size = 95008112, upload-time = "2026-04-15T03:55:32.025Z" }, + { url = "https://files.pythonhosted.org/packages/07/e4/beae76926e3070978d06fa156b243642d5f75ffd784f7cd64e783520e456/ziglang-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:7249779f0f916cfd2d1a9d54eb7600f3901384dc8dcbe1eea226bd32a8237fb5", size = 95860236, upload-time = "2026-04-15T03:55:37.069Z" }, + { url = "https://files.pythonhosted.org/packages/b6/fc/5cb1555281d2a998355ee7081f05f45d2f6a2789ca0fcb02015cfcb4b900/ziglang-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:0fd671c599f6961638cc302cf1f9461486696385311d4c0276171dcf7d2b67f5", size = 104100995, upload-time = "2026-04-15T03:55:42.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/e0138d89ec47da986ac08009ae8802af052c1e335163d983c074d9eaa1c3/ziglang-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:df0e6599e41d087c912b0d3d7cedef6c9cb1f0032465c8fc820e9986772d11e4", size = 103517876, upload-time = "2026-04-15T03:55:48.695Z" }, + { url = "https://files.pythonhosted.org/packages/fe/29/d281591fa0be47aefcb23ac379cdbff5b34a1fdaafa139a5f8703ca9559c/ziglang-0.16.0-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:d4bd8197344fffe276e1d6446e4ef7bc38637d821b4685fe96a936503d4b0619", size = 98388042, upload-time = "2026-04-15T03:55:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f0/10a5203071af21bd649a0f97e29f70b710d6ad97b1ac1995a0bb30f51a71/ziglang-0.16.0-py3-none-win32.whl", hash = "sha256:f978c12b5337cf418034964de00023b7ec5ec484383dc97c6a6585f4a9105840", size = 100655765, upload-time = "2026-04-15T03:55:59.7Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3c/baff40b3fc8ab4e83530246a52e8f3a5186c3606562712dfb58483b04f79/ziglang-0.16.0-py3-none-win_amd64.whl", hash = "sha256:089a16a4eb5a2f45151993342f8fabad24ff1a0723dc146642895c29208a3939", size = 98676957, upload-time = "2026-04-15T03:56:05.934Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/bff28a4acc437cb7da705bffafd81fafbbd37a23144363cecc72becaef93/ziglang-0.16.0-py3-none-win_arm64.whl", hash = "sha256:e44a5271f7b72f7980017bb615823ed52e765f96b6482d93a2fd338cd53101bf", size = 94347646, upload-time = "2026-04-15T03:56:11.85Z" }, ] From 3cffe97c2e48c9e49422c738da3af95919dd0bf5 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 18 May 2026 12:20:44 -0700 Subject: [PATCH 56/56] Fix crates.io publish script lockfile (#19473) Regenerate the script lock without leaked socket-firewall registry and global exclude-newer state. Co-authored-by: Codex Co-authored-by: Codex --- scripts/setup-crates-io-publish.py.lock | 43 ++++++++++++------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/scripts/setup-crates-io-publish.py.lock b/scripts/setup-crates-io-publish.py.lock index 31407d8091a62..64434d5ecf4c6 100644 --- a/scripts/setup-crates-io-publish.py.lock +++ b/scripts/setup-crates-io-publish.py.lock @@ -2,75 +2,72 @@ version = 1 revision = 3 requires-python = ">=3.13" -[options] -exclude-newer = "2026-05-11T05:42:05Z" - [manifest] requirements = [{ name = "httpx" }] [[package]] name = "anyio" version = "4.13.0" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, ] -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] [[package]] name = "certifi" -version = "2026.2.25" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, ] [[package]] name = "h11" version = "0.16.0" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] [[package]] name = "httpcore" version = "1.0.9" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] [[package]] name = "httpx" version = "0.28.1" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "certifi" }, { name = "httpcore" }, { name = "idna" }, ] -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] [[package]] name = "idna" -version = "3.11" -source = { registry = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/simple/" } -sdist = { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +version = "3.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } wheels = [ - { url = "https://socket-firewall-registry.gateway.unified-30.internal.api.openai.org/pypi/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, ]