From 6d874b1a25610cdb9850797f7cfdf3132e89c1da Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 24 Aug 2025 11:53:12 -0400 Subject: [PATCH] Move WHEEL file parsing into a struct (#15483) ## Summary No functional changes, but I need to add more behavior here for https://github.com/astral-sh/uv/issues/15035, so seems nice to do this separately. --- crates/uv-install-wheel/src/install.rs | 4 +- crates/uv-install-wheel/src/lib.rs | 2 +- crates/uv-install-wheel/src/wheel.rs | 131 ++++++++++++++----------- 3 files changed, 76 insertions(+), 61 deletions(-) diff --git a/crates/uv-install-wheel/src/install.rs b/crates/uv-install-wheel/src/install.rs index 42f0c5cfd..aab7d4229 100644 --- a/crates/uv-install-wheel/src/install.rs +++ b/crates/uv-install-wheel/src/install.rs @@ -14,7 +14,7 @@ use uv_pypi_types::{DirectUrl, Metadata10}; use crate::linker::{LinkMode, Locks}; use crate::wheel::{ - LibKind, dist_info_metadata, find_dist_info, install_data, parse_scripts, parse_wheel_file, + LibKind, WheelFile, dist_info_metadata, find_dist_info, install_data, parse_scripts, read_record_file, write_installer_metadata, write_script_entrypoints, }; use crate::{Error, Layout}; @@ -66,7 +66,7 @@ pub fn install_wheel( .as_ref() .join(format!("{dist_info_prefix}.dist-info/WHEEL")); let wheel_text = fs::read_to_string(wheel_file_path)?; - let lib_kind = parse_wheel_file(&wheel_text)?; + let lib_kind = WheelFile::parse(&wheel_text)?.lib_kind(); // > 1.c If Root-Is-Purelib == ‘true’, unpack archive into purelib (site-packages). // > 1.d Else unpack archive into platlib (site-packages). diff --git a/crates/uv-install-wheel/src/lib.rs b/crates/uv-install-wheel/src/lib.rs index f36a699f1..d79fd1ddd 100644 --- a/crates/uv-install-wheel/src/lib.rs +++ b/crates/uv-install-wheel/src/lib.rs @@ -13,7 +13,7 @@ use uv_pypi_types::Scheme; pub use install::install_wheel; pub use linker::{LinkMode, Locks}; pub use uninstall::{Uninstall, uninstall_egg, uninstall_legacy_editable, uninstall_wheel}; -pub use wheel::{LibKind, parse_wheel_file, read_record_file}; +pub use wheel::{LibKind, WheelFile, read_record_file}; mod install; mod linker; diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index f795fd329..869bb2d25 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -257,6 +257,76 @@ pub(crate) fn write_script_entrypoints( Ok(()) } +/// A parsed `WHEEL` file. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WheelFile(FxHashMap>); + +impl WheelFile { + /// Parse `WHEEL` file. + /// + /// > {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same + /// > email message format: + pub fn parse(wheel_text: &str) -> Result { + // {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same email message format: + let data = parse_email_message_file(&mut wheel_text.as_bytes(), "WHEEL")?; + + // mkl_fft-1.3.6-58-cp310-cp310-manylinux2014_x86_64.whl has multiple Wheel-Version entries, we have to ignore that + // like pip + let wheel_version = data + .get("Wheel-Version") + .and_then(|wheel_versions| wheel_versions.first()); + let wheel_version = wheel_version + .and_then(|wheel_version| wheel_version.split_once('.')) + .ok_or_else(|| { + Error::InvalidWheel(format!( + "Invalid Wheel-Version in WHEEL file: {wheel_version:?}" + )) + })?; + // pip has some test wheels that use that ancient version, + // and technically we only need to check that the version is not higher + if wheel_version == ("0", "1") { + warn!("Ancient wheel version 0.1 (expected is 1.0)"); + return Ok(Self(data)); + } + // Check that installer is compatible with Wheel-Version. Warn if minor version is greater, abort if major version is greater. + // Wheel-Version: 1.0 + if wheel_version.0 != "1" { + return Err(Error::InvalidWheel(format!( + "Unsupported wheel major version (expected {}, got {})", + 1, wheel_version.0 + ))); + } + if wheel_version.1 > "0" { + warn!( + "Warning: Unsupported wheel minor version (expected {}, got {})", + 0, wheel_version.1 + ); + } + Ok(Self(data)) + } + + /// Whether the wheel should be installed into the `purelib` or `platlib` directory. + pub fn lib_kind(&self) -> LibKind { + // Determine whether Root-Is-Purelib == ‘true’. + // If it is, the wheel is pure, and should be installed into purelib. + let root_is_purelib = self + .0 + .get("Root-Is-Purelib") + .and_then(|root_is_purelib| root_is_purelib.first()) + .is_some_and(|root_is_purelib| root_is_purelib == "true"); + if root_is_purelib { + LibKind::Pure + } else { + LibKind::Plat + } + } + + /// Return the list of wheel tags. + pub fn tags(&self) -> Option<&[String]> { + self.0.get("Tag").map(Vec::as_slice) + } +} + /// Whether the wheel should be installed into the `purelib` or `platlib` directory. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum LibKind { @@ -266,61 +336,6 @@ pub enum LibKind { Plat, } -/// Parse WHEEL file. -/// -/// > {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same -/// > email message format: -pub fn parse_wheel_file(wheel_text: &str) -> Result { - // {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same email message format: - let data = parse_email_message_file(&mut wheel_text.as_bytes(), "WHEEL")?; - - // Determine whether Root-Is-Purelib == ‘true’. - // If it is, the wheel is pure, and should be installed into purelib. - let root_is_purelib = data - .get("Root-Is-Purelib") - .and_then(|root_is_purelib| root_is_purelib.first()) - .is_some_and(|root_is_purelib| root_is_purelib == "true"); - let lib_kind = if root_is_purelib { - LibKind::Pure - } else { - LibKind::Plat - }; - - // mkl_fft-1.3.6-58-cp310-cp310-manylinux2014_x86_64.whl has multiple Wheel-Version entries, we have to ignore that - // like pip - let wheel_version = data - .get("Wheel-Version") - .and_then(|wheel_versions| wheel_versions.first()); - let wheel_version = wheel_version - .and_then(|wheel_version| wheel_version.split_once('.')) - .ok_or_else(|| { - Error::InvalidWheel(format!( - "Invalid Wheel-Version in WHEEL file: {wheel_version:?}" - )) - })?; - // pip has some test wheels that use that ancient version, - // and technically we only need to check that the version is not higher - if wheel_version == ("0", "1") { - warn!("Ancient wheel version 0.1 (expected is 1.0)"); - return Ok(lib_kind); - } - // Check that installer is compatible with Wheel-Version. Warn if minor version is greater, abort if major version is greater. - // Wheel-Version: 1.0 - if wheel_version.0 != "1" { - return Err(Error::InvalidWheel(format!( - "Unsupported wheel major version (expected {}, got {})", - 1, wheel_version.0 - ))); - } - if wheel_version.1 > "0" { - warn!( - "Warning: Unsupported wheel minor version (expected {}, got {})", - 0, wheel_version.1 - ); - } - Ok(lib_kind) -} - /// Moves the files and folders in src to dest, updating the RECORD in the process pub(crate) fn move_folder_recorded( src_dir: &Path, @@ -938,7 +953,7 @@ mod test { use crate::wheel::format_shebang; use super::{ - RecordEntry, Script, get_script_executable, parse_email_message_file, parse_wheel_file, + RecordEntry, Script, WheelFile, get_script_executable, parse_email_message_file, read_record_file, write_installer_metadata, }; @@ -1013,8 +1028,8 @@ mod test { version } } - parse_wheel_file(&wheel_with_version("1.0")).unwrap(); - parse_wheel_file(&wheel_with_version("2.0")).unwrap_err(); + WheelFile::parse(&wheel_with_version("1.0")).unwrap(); + WheelFile::parse(&wheel_with_version("2.0")).unwrap_err(); } #[test]