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.
This commit is contained in:
Charlie Marsh 2025-08-24 11:53:12 -04:00 committed by GitHub
parent f16760e10a
commit 6d874b1a25
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 76 additions and 61 deletions

View File

@ -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<Cache: serde::Serialize, Build: serde::Serialize>(
.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).

View File

@ -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;

View File

@ -257,6 +257,76 @@ pub(crate) fn write_script_entrypoints(
Ok(())
}
/// A parsed `WHEEL` file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WheelFile(FxHashMap<String, Vec<String>>);
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<Self, Error> {
// {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<LibKind, Error> {
// {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]