mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add support for wheel tag parsing (#15)
Closes https://github.com/astral-sh/puffin/issues/12.
This commit is contained in:
parent
2d6266b167
commit
94895de46d
10 changed files with 720 additions and 181 deletions
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
puffin-client = { path = "../puffin-client" }
|
puffin-client = { path = "../puffin-client" }
|
||||||
puffin-interpreter = { path = "../puffin-interpreter" }
|
puffin-interpreter = { path = "../puffin-interpreter" }
|
||||||
|
puffin-platform = { path = "../puffin-platform" }
|
||||||
puffin-requirements = { path = "../puffin-requirements" }
|
puffin-requirements = { path = "../puffin-requirements" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.75" }
|
anyhow = { version = "1.0.75" }
|
||||||
|
|
|
@ -11,9 +11,10 @@ use tracing::debug;
|
||||||
|
|
||||||
use puffin_client::{File, PypiClientBuilder, SimpleJson};
|
use puffin_client::{File, PypiClientBuilder, SimpleJson};
|
||||||
use puffin_interpreter::PythonExecutable;
|
use puffin_interpreter::PythonExecutable;
|
||||||
|
use puffin_platform::Platform;
|
||||||
use puffin_requirements::metadata::Metadata21;
|
use puffin_requirements::metadata::Metadata21;
|
||||||
use puffin_requirements::package_name::PackageName;
|
use puffin_requirements::package_name::PackageName;
|
||||||
use puffin_requirements::wheel::WheelName;
|
use puffin_requirements::wheel::WheelFilename;
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
|
|
||||||
|
@ -37,12 +38,16 @@ pub(crate) async fn install(src: &Path, cache: Option<&Path>) -> Result<ExitStat
|
||||||
let requirements = puffin_requirements::Requirements::from_str(&requirements_txt)?;
|
let requirements = puffin_requirements::Requirements::from_str(&requirements_txt)?;
|
||||||
|
|
||||||
// Detect the current Python interpreter.
|
// Detect the current Python interpreter.
|
||||||
let python = PythonExecutable::from_env()?;
|
let platform = Platform::current()?;
|
||||||
|
let python = PythonExecutable::from_env(&platform)?;
|
||||||
debug!(
|
debug!(
|
||||||
"Using Python interpreter: {}",
|
"Using Python interpreter: {}",
|
||||||
python.executable().display()
|
python.executable().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Determine the compatible platform tags.
|
||||||
|
let tags = platform.compatible_tags(python.version())?;
|
||||||
|
|
||||||
// Instantiate a client.
|
// Instantiate a client.
|
||||||
let pypi_client = {
|
let pypi_client = {
|
||||||
let mut pypi_client = PypiClientBuilder::default();
|
let mut pypi_client = PypiClientBuilder::default();
|
||||||
|
@ -102,13 +107,21 @@ pub(crate) async fn install(src: &Path, cache: Option<&Path>) -> Result<ExitStat
|
||||||
// Pick a version that satisfies the requirement.
|
// Pick a version that satisfies the requirement.
|
||||||
let Some(file) = metadata.files.iter().rev().find(|file| {
|
let Some(file) = metadata.files.iter().rev().find(|file| {
|
||||||
// We only support wheels for now.
|
// We only support wheels for now.
|
||||||
let Ok(name) = WheelName::from_str(file.filename.as_str()) else {
|
let Ok(name) = WheelFilename::from_str(file.filename.as_str()) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Ok(version) = Version::from_str(&name.version) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !name.is_compatible(&tags) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
specifiers
|
specifiers
|
||||||
.iter()
|
.iter()
|
||||||
.all(|specifier| specifier.contains(&name.version))
|
.all(|specifier| specifier.contains(&version))
|
||||||
}) else {
|
}) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,8 @@ authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
puffin-platform = { path = "../puffin-platform" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.75" }
|
anyhow = { version = "1.0.75" }
|
||||||
pep508_rs = { version = "0.2.3", features = ["serde"] }
|
pep508_rs = { version = "0.2.3", features = ["serde"] }
|
||||||
serde_json = { version = "1.0.107" }
|
serde_json = { version = "1.0.107" }
|
||||||
|
|
|
@ -2,11 +2,12 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use pep508_rs::MarkerEnvironment;
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use puffin_platform::Platform;
|
||||||
|
|
||||||
use crate::platform::Platform;
|
use crate::python_platform::PythonPlatform;
|
||||||
|
|
||||||
mod markers;
|
mod markers;
|
||||||
mod platform;
|
mod python_platform;
|
||||||
mod virtual_env;
|
mod virtual_env;
|
||||||
|
|
||||||
/// A Python executable and its associated platform markers.
|
/// A Python executable and its associated platform markers.
|
||||||
|
@ -18,10 +19,10 @@ pub struct PythonExecutable {
|
||||||
|
|
||||||
impl PythonExecutable {
|
impl PythonExecutable {
|
||||||
/// Detect the current Python executable from the host environment.
|
/// Detect the current Python executable from the host environment.
|
||||||
pub fn from_env() -> Result<Self> {
|
pub fn from_env(platform: &Platform) -> Result<Self> {
|
||||||
let target = Platform::from_host();
|
let platform = PythonPlatform::from(platform);
|
||||||
let venv = virtual_env::detect_virtual_env(&target)?;
|
let venv = virtual_env::detect_virtual_env(&platform)?;
|
||||||
let executable = target.get_venv_python(venv);
|
let executable = platform.venv_python(venv);
|
||||||
let markers = markers::detect_markers(&executable)?;
|
let markers = markers::detect_markers(&executable)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
@ -30,11 +31,23 @@ impl PythonExecutable {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the Python executable.
|
||||||
pub fn executable(&self) -> &Path {
|
pub fn executable(&self) -> &Path {
|
||||||
self.executable.as_path()
|
self.executable.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the [`MarkerEnvironment`] for this Python executable.
|
||||||
pub fn markers(&self) -> &MarkerEnvironment {
|
pub fn markers(&self) -> &MarkerEnvironment {
|
||||||
&self.markers
|
&self.markers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the Python version as a tuple of (major, minor).
|
||||||
|
pub fn version(&self) -> (u8, u8) {
|
||||||
|
// TODO(charlie): Use `Version`.
|
||||||
|
let python_version = &self.markers.python_version;
|
||||||
|
(
|
||||||
|
u8::try_from(python_version.release[0]).expect("Python major version is too large"),
|
||||||
|
u8::try_from(python_version.release[1]).expect("Python minor version is too large"),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,105 +0,0 @@
|
||||||
use std::env;
|
|
||||||
use std::fmt;
|
|
||||||
use std::path::Path;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
|
||||||
pub(crate) struct Platform {
|
|
||||||
os: Option<Os>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Platform {
|
|
||||||
/// Infer the target based on the current version used for compilation.
|
|
||||||
pub(crate) fn from_host() -> Self {
|
|
||||||
Self {
|
|
||||||
os: if cfg!(windows) {
|
|
||||||
Some(Os::Windows)
|
|
||||||
} else if cfg!(unix) {
|
|
||||||
Some(Os::Linux)
|
|
||||||
} else if cfg!(macos) {
|
|
||||||
Some(Os::Macos)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the current platform is Linux.
|
|
||||||
#[allow(unused)]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_linux(&self) -> bool {
|
|
||||||
self.os == Some(Os::Linux)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the current platform is macOS.
|
|
||||||
#[allow(unused)]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_macos(&self) -> bool {
|
|
||||||
self.os == Some(Os::Macos)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns `true` if the current platform is Windows.
|
|
||||||
#[allow(unused)]
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn is_windows(&self) -> bool {
|
|
||||||
self.os == Some(Os::Windows)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path to the `python` executable inside a virtual environment.
|
|
||||||
pub(crate) fn get_venv_python(&self, venv_base: impl AsRef<Path>) -> PathBuf {
|
|
||||||
self.get_venv_bin_dir(venv_base).join(self.get_python())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the directory in which the binaries are stored inside a virtual environment.
|
|
||||||
pub(crate) fn get_venv_bin_dir(&self, venv_base: impl AsRef<Path>) -> PathBuf {
|
|
||||||
let venv = venv_base.as_ref();
|
|
||||||
if self.is_windows() {
|
|
||||||
let bin_dir = venv.join("Scripts");
|
|
||||||
if bin_dir.join("python.exe").exists() {
|
|
||||||
return bin_dir;
|
|
||||||
}
|
|
||||||
// Python installed via msys2 on Windows might produce a POSIX-like venv
|
|
||||||
// See https://github.com/PyO3/maturin/issues/1108
|
|
||||||
let bin_dir = venv.join("bin");
|
|
||||||
if bin_dir.join("python.exe").exists() {
|
|
||||||
return bin_dir;
|
|
||||||
}
|
|
||||||
// for conda environment
|
|
||||||
venv.to_path_buf()
|
|
||||||
} else {
|
|
||||||
venv.join("bin")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the path to the `python` executable.
|
|
||||||
///
|
|
||||||
/// For Windows, it's always `python.exe`. For UNIX, it's the `python` in the virtual
|
|
||||||
/// environment; or, if there is no virtual environment, `python3`.
|
|
||||||
pub(crate) fn get_python(&self) -> PathBuf {
|
|
||||||
if self.is_windows() {
|
|
||||||
PathBuf::from("python.exe")
|
|
||||||
} else if env::var_os("VIRTUAL_ENV").is_some() {
|
|
||||||
PathBuf::from("python")
|
|
||||||
} else {
|
|
||||||
PathBuf::from("python3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// All supported operating systems.
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
enum Os {
|
|
||||||
Linux,
|
|
||||||
Windows,
|
|
||||||
Macos,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Os {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
Os::Linux => write!(f, "Linux"),
|
|
||||||
Os::Windows => write!(f, "Windows"),
|
|
||||||
Os::Macos => write!(f, "macOS"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
47
crates/puffin-interpreter/src/python_platform.rs
Normal file
47
crates/puffin-interpreter/src/python_platform.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use std::path::Path;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use puffin_platform::Platform;
|
||||||
|
|
||||||
|
/// A Python-aware wrapper around [`Platform`].
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub(crate) struct PythonPlatform<'a>(&'a Platform);
|
||||||
|
|
||||||
|
impl PythonPlatform<'_> {
|
||||||
|
/// Returns the path to the `python` executable inside a virtual environment.
|
||||||
|
pub(crate) fn venv_python(&self, venv_base: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let python = if self.0.is_windows() {
|
||||||
|
"python.exe"
|
||||||
|
} else {
|
||||||
|
"python"
|
||||||
|
};
|
||||||
|
self.venv_bin_dir(venv_base).join(python)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the directory in which the binaries are stored inside a virtual environment.
|
||||||
|
pub(crate) fn venv_bin_dir(&self, venv_base: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let venv = venv_base.as_ref();
|
||||||
|
if self.0.is_windows() {
|
||||||
|
let bin_dir = venv.join("Scripts");
|
||||||
|
if bin_dir.join("python.exe").exists() {
|
||||||
|
return bin_dir;
|
||||||
|
}
|
||||||
|
// Python installed via msys2 on Windows might produce a POSIX-like venv
|
||||||
|
// See https://github.com/PyO3/maturin/issues/1108
|
||||||
|
let bin_dir = venv.join("bin");
|
||||||
|
if bin_dir.join("python.exe").exists() {
|
||||||
|
return bin_dir;
|
||||||
|
}
|
||||||
|
// for conda environment
|
||||||
|
venv.to_path_buf()
|
||||||
|
} else {
|
||||||
|
venv.join("bin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Platform> for PythonPlatform<'a> {
|
||||||
|
fn from(platform: &'a Platform) -> Self {
|
||||||
|
Self(platform)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,10 +4,10 @@ use std::path::PathBuf;
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::platform::Platform;
|
use crate::python_platform::PythonPlatform;
|
||||||
|
|
||||||
/// Locate the current virtual environment.
|
/// Locate the current virtual environment.
|
||||||
pub(crate) fn detect_virtual_env(target: &Platform) -> Result<PathBuf> {
|
pub(crate) fn detect_virtual_env(target: &PythonPlatform) -> Result<PathBuf> {
|
||||||
match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) {
|
match (env::var_os("VIRTUAL_ENV"), env::var_os("CONDA_PREFIX")) {
|
||||||
(Some(dir), None) => return Ok(PathBuf::from(dir)),
|
(Some(dir), None) => return Ok(PathBuf::from(dir)),
|
||||||
(None, Some(dir)) => return Ok(PathBuf::from(dir)),
|
(None, Some(dir)) => return Ok(PathBuf::from(dir)),
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) fn detect_virtual_env(target: &Platform) -> Result<PathBuf> {
|
||||||
dot_venv.display()
|
dot_venv.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let python = target.get_venv_python(&dot_venv);
|
let python = target.venv_python(&dot_venv);
|
||||||
if !python.is_file() {
|
if !python.is_file() {
|
||||||
bail!(
|
bail!(
|
||||||
"Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}",
|
"Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}",
|
||||||
|
|
23
crates/puffin-platform/Cargo.toml
Normal file
23
crates/puffin-platform/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "puffin-platform"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
glibc_version = "0.1.2"
|
||||||
|
goblin = "0.6.0"
|
||||||
|
platform-info = "2.0.2"
|
||||||
|
plist = "1.5.0"
|
||||||
|
regex = "1.9.6"
|
||||||
|
serde = "1.0.188"
|
||||||
|
target-lexicon = "0.12.11"
|
||||||
|
thiserror = "1.0.49"
|
||||||
|
tracing = "0.1.37"
|
549
crates/puffin-platform/src/lib.rs
Normal file
549
crates/puffin-platform/src/lib.rs
Normal file
|
@ -0,0 +1,549 @@
|
||||||
|
//! Abstractions for understanding the current platform (operating system and architecture).
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::{fmt, fs, io};
|
||||||
|
|
||||||
|
use goblin::elf::Elf;
|
||||||
|
use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PlatformError {
|
||||||
|
#[error(transparent)]
|
||||||
|
IOError(#[from] io::Error),
|
||||||
|
#[error("Failed to detect the operating system version: {0}")]
|
||||||
|
OsVersionDetectionError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Platform {
|
||||||
|
os: Os,
|
||||||
|
arch: Arch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
pub fn current() -> Result<Self, PlatformError> {
|
||||||
|
let os = Os::current()?;
|
||||||
|
let arch = Arch::current()?;
|
||||||
|
Ok(Self { os, arch })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_windows(&self) -> bool {
|
||||||
|
matches!(self.os, Os::Windows)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compatible_tags(
|
||||||
|
&self,
|
||||||
|
python_version: (u8, u8),
|
||||||
|
) -> Result<Vec<(String, String, String)>, PlatformError> {
|
||||||
|
compatible_tags(python_version, &self.os, self.arch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All supported operating systems.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
pub enum Os {
|
||||||
|
Manylinux { major: u16, minor: u16 },
|
||||||
|
Musllinux { major: u16, minor: u16 },
|
||||||
|
Windows,
|
||||||
|
Macos { major: u16, minor: u16 },
|
||||||
|
FreeBsd { release: String },
|
||||||
|
NetBsd { release: String },
|
||||||
|
OpenBsd { release: String },
|
||||||
|
Dragonfly { release: String },
|
||||||
|
Illumos { release: String, arch: String },
|
||||||
|
Haiku { release: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Os {
|
||||||
|
pub fn current() -> Result<Self, PlatformError> {
|
||||||
|
let target_triple = target_lexicon::HOST;
|
||||||
|
|
||||||
|
let os = match target_triple.operating_system {
|
||||||
|
target_lexicon::OperatingSystem::Linux => Self::detect_linux_libc()?,
|
||||||
|
target_lexicon::OperatingSystem::Windows => Os::Windows,
|
||||||
|
target_lexicon::OperatingSystem::MacOSX { major, minor, .. } => {
|
||||||
|
Os::Macos { major, minor }
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Darwin => {
|
||||||
|
let (major, minor) = get_mac_os_version()?;
|
||||||
|
Os::Macos { major, minor }
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Netbsd => Os::NetBsd {
|
||||||
|
release: Os::platform_info()?.release().to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Freebsd => Os::FreeBsd {
|
||||||
|
release: Os::platform_info()?.release().to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Openbsd => Os::OpenBsd {
|
||||||
|
release: Os::platform_info()?.release().to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Dragonfly => Os::Dragonfly {
|
||||||
|
release: Os::platform_info()?.release().to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
target_lexicon::OperatingSystem::Illumos => {
|
||||||
|
let platform_info = Os::platform_info()?;
|
||||||
|
Os::Illumos {
|
||||||
|
release: platform_info.release().to_string_lossy().to_string(),
|
||||||
|
arch: platform_info.machine().to_string_lossy().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target_lexicon::OperatingSystem::Haiku => Os::Haiku {
|
||||||
|
release: Os::platform_info()?.release().to_string_lossy().to_string(),
|
||||||
|
},
|
||||||
|
unsupported => {
|
||||||
|
return Err(PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"The operating system {unsupported:?} is not supported"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(os)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn platform_info() -> Result<PlatformInfo, PlatformError> {
|
||||||
|
PlatformInfo::new().map_err(|err| PlatformError::OsVersionDetectionError(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detect_linux_libc() -> Result<Self, PlatformError> {
|
||||||
|
let libc = find_libc()?;
|
||||||
|
let linux = if let Ok(Some((major, minor))) = get_musl_version(&libc) {
|
||||||
|
Os::Musllinux { major, minor }
|
||||||
|
} else if let Ok(glibc_ld) = fs::read_link(&libc) {
|
||||||
|
// Try reading the link first as it's faster
|
||||||
|
let filename = glibc_ld
|
||||||
|
.file_name()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
PlatformError::OsVersionDetectionError(
|
||||||
|
"Expected the glibc ld to be a file".to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.to_string_lossy();
|
||||||
|
let expr = Regex::new(r"ld-(\d{1,3})\.(\d{1,3})\.so").unwrap();
|
||||||
|
|
||||||
|
if let Some(capture) = expr.captures(&filename) {
|
||||||
|
let major = capture.get(1).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
let minor = capture.get(2).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
Os::Manylinux { major, minor }
|
||||||
|
} else {
|
||||||
|
trace!("Couldn't use ld filename, using `ldd --version`");
|
||||||
|
// runs `ldd --version`
|
||||||
|
let version = glibc_version::get_version().map_err(|err| {
|
||||||
|
PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Failed to determine glibc version with `ldd --version`: {err}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Os::Manylinux {
|
||||||
|
major: u16::try_from(version.major).map_err(|_| {
|
||||||
|
PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Invalid glibc major version {}",
|
||||||
|
version.major
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
minor: u16::try_from(version.minor).map_err(|_| {
|
||||||
|
PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Invalid glibc minor version {}",
|
||||||
|
version.minor
|
||||||
|
))
|
||||||
|
})?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err(PlatformError::OsVersionDetectionError("Couldn't detect neither glibc version nor musl libc version, at least one of which is required".to_string()));
|
||||||
|
};
|
||||||
|
trace!("libc: {}", linux);
|
||||||
|
Ok(linux)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Os {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Os::Manylinux { .. } => write!(f, "Manylinux"),
|
||||||
|
Os::Musllinux { .. } => write!(f, "Musllinux"),
|
||||||
|
Os::Windows => write!(f, "Windows"),
|
||||||
|
Os::Macos { .. } => write!(f, "MacOS"),
|
||||||
|
Os::FreeBsd { .. } => write!(f, "FreeBSD"),
|
||||||
|
Os::NetBsd { .. } => write!(f, "NetBSD"),
|
||||||
|
Os::OpenBsd { .. } => write!(f, "OpenBSD"),
|
||||||
|
Os::Dragonfly { .. } => write!(f, "DragonFly"),
|
||||||
|
Os::Illumos { .. } => write!(f, "Illumos"),
|
||||||
|
Os::Haiku { .. } => write!(f, "Haiku"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// All supported CPU architectures
|
||||||
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
|
pub enum Arch {
|
||||||
|
Aarch64,
|
||||||
|
Armv7L,
|
||||||
|
Powerpc64Le,
|
||||||
|
Powerpc64,
|
||||||
|
X86,
|
||||||
|
X86_64,
|
||||||
|
S390X,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Arch {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Arch::Aarch64 => write!(f, "aarch64"),
|
||||||
|
Arch::Armv7L => write!(f, "armv7l"),
|
||||||
|
Arch::Powerpc64Le => write!(f, "ppc64le"),
|
||||||
|
Arch::Powerpc64 => write!(f, "ppc64"),
|
||||||
|
Arch::X86 => write!(f, "i686"),
|
||||||
|
Arch::X86_64 => write!(f, "x86_64"),
|
||||||
|
Arch::S390X => write!(f, "s390x"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Arch {
|
||||||
|
pub fn current() -> Result<Arch, PlatformError> {
|
||||||
|
let target_triple = target_lexicon::HOST;
|
||||||
|
let arch = match target_triple.architecture {
|
||||||
|
target_lexicon::Architecture::X86_64 => Arch::X86_64,
|
||||||
|
target_lexicon::Architecture::X86_32(_) => Arch::X86,
|
||||||
|
target_lexicon::Architecture::Arm(_) => Arch::Armv7L,
|
||||||
|
target_lexicon::Architecture::Aarch64(_) => Arch::Aarch64,
|
||||||
|
target_lexicon::Architecture::Powerpc64 => Arch::Powerpc64,
|
||||||
|
target_lexicon::Architecture::Powerpc64le => Arch::Powerpc64Le,
|
||||||
|
target_lexicon::Architecture::S390x => Arch::S390X,
|
||||||
|
unsupported => {
|
||||||
|
return Err(PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"The architecture {unsupported} is not supported"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the oldest possible Manylinux tag for this architecture
|
||||||
|
pub fn get_minimum_manylinux_minor(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
// manylinux 2014
|
||||||
|
Arch::Aarch64 | Arch::Armv7L | Arch::Powerpc64 | Arch::Powerpc64Le | Arch::S390X => 17,
|
||||||
|
// manylinux 1
|
||||||
|
Arch::X86 | Arch::X86_64 => 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mac_os_version() -> Result<(u16, u16), PlatformError> {
|
||||||
|
// This is actually what python does
|
||||||
|
// https://github.com/python/cpython/blob/cb2b3c8d3566ae46b3b8d0718019e1c98484589e/Lib/platform.py#L409-L428
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(rename_all = "PascalCase")]
|
||||||
|
struct SystemVersion {
|
||||||
|
product_version: String,
|
||||||
|
}
|
||||||
|
let system_version: SystemVersion =
|
||||||
|
plist::from_file("/System/Library/CoreServices/SystemVersion.plist")
|
||||||
|
.map_err(|err| PlatformError::OsVersionDetectionError(err.to_string()))?;
|
||||||
|
|
||||||
|
let invalid_mac_os_version = || {
|
||||||
|
PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Invalid macOS version {}",
|
||||||
|
system_version.product_version
|
||||||
|
))
|
||||||
|
};
|
||||||
|
match system_version
|
||||||
|
.product_version
|
||||||
|
.split('.')
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.as_slice()
|
||||||
|
{
|
||||||
|
[major, minor] | [major, minor, _] => {
|
||||||
|
let major = major.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||||
|
let minor = minor.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||||
|
Ok((major, minor))
|
||||||
|
}
|
||||||
|
_ => Err(invalid_mac_os_version()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the appropriate binary formats for a macOS version.
|
||||||
|
/// Source: <https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L314>
|
||||||
|
fn get_mac_binary_formats(major: u16, minor: u16, arch: Arch) -> Vec<String> {
|
||||||
|
let mut formats = vec![match arch {
|
||||||
|
Arch::Aarch64 => "arm64".to_string(),
|
||||||
|
_ => arch.to_string(),
|
||||||
|
}];
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64) {
|
||||||
|
if (major, minor) < (10, 4) {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
formats.extend([
|
||||||
|
"intel".to_string(),
|
||||||
|
"fat64".to_string(),
|
||||||
|
"fat32".to_string(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64 | Arch::Aarch64) {
|
||||||
|
formats.push("universal2".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(arch, Arch::X86_64) {
|
||||||
|
formats.push("universal".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
formats
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find musl libc path from executable's ELF header
|
||||||
|
fn find_libc() -> Result<PathBuf, PlatformError> {
|
||||||
|
let buffer = fs::read("/bin/ls")?;
|
||||||
|
let error_str = "Couldn't parse /bin/ls for detecting the ld version";
|
||||||
|
let elf = Elf::parse(&buffer)
|
||||||
|
.map_err(|err| PlatformError::OsVersionDetectionError(format!("{error_str}: {err}")))?;
|
||||||
|
if let Some(elf_interpreter) = elf.interpreter {
|
||||||
|
Ok(PathBuf::from(elf_interpreter))
|
||||||
|
} else {
|
||||||
|
Err(PlatformError::OsVersionDetectionError(
|
||||||
|
error_str.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the musl version from libc library's output. Taken from maturin
|
||||||
|
///
|
||||||
|
/// The libc library should output something like this to `stderr::`
|
||||||
|
///
|
||||||
|
/// musl libc (`x86_64`)
|
||||||
|
/// Version 1.2.2
|
||||||
|
/// Dynamic Program Loader
|
||||||
|
fn get_musl_version(ld_path: impl AsRef<Path>) -> std::io::Result<Option<(u16, u16)>> {
|
||||||
|
let output = Command::new(ld_path.as_ref())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()?;
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let expr = Regex::new(r"Version (\d{2,4})\.(\d{2,4})").unwrap();
|
||||||
|
if let Some(capture) = expr.captures(&stderr) {
|
||||||
|
let major = capture.get(1).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
let minor = capture.get(2).unwrap().as_str().parse::<u16>().unwrap();
|
||||||
|
return Ok(Some((major, minor)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compatible platform tags, e.g. `manylinux_2_17`, `macosx_11_0_arm64` or `win_amd64`
|
||||||
|
///
|
||||||
|
/// We have two cases: Actual platform specific tags (including "merged" tags such as universal2)
|
||||||
|
/// and "any".
|
||||||
|
///
|
||||||
|
/// Bit of a mess, needs to be cleaned up
|
||||||
|
pub fn compatible_platform_tags(os: &Os, arch: Arch) -> Result<Vec<String>, PlatformError> {
|
||||||
|
let platform_tags = match (&os, arch) {
|
||||||
|
(Os::Manylinux { major, minor }, _) => {
|
||||||
|
let mut platform_tags = vec![format!("linux_{}", arch)];
|
||||||
|
platform_tags.extend(
|
||||||
|
(arch.get_minimum_manylinux_minor()..=*minor)
|
||||||
|
.map(|minor| format!("manylinux_{major}_{minor}_{arch}")),
|
||||||
|
);
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=*minor).contains(&12) {
|
||||||
|
platform_tags.push(format!("manylinux2010_{arch}"));
|
||||||
|
}
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=*minor).contains(&17) {
|
||||||
|
platform_tags.push(format!("manylinux2014_{arch}"));
|
||||||
|
}
|
||||||
|
if (arch.get_minimum_manylinux_minor()..=*minor).contains(&5) {
|
||||||
|
platform_tags.push(format!("manylinux1_{arch}"));
|
||||||
|
}
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Musllinux { major, minor }, _) => {
|
||||||
|
let mut platform_tags = vec![format!("linux_{}", arch)];
|
||||||
|
// musl 1.1 is the lowest supported version in musllinux
|
||||||
|
platform_tags
|
||||||
|
.extend((1..=*minor).map(|minor| format!("musllinux_{major}_{minor}_{arch}")));
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Macos { major, minor }, Arch::X86_64) => {
|
||||||
|
// Source: https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L346
|
||||||
|
let mut platform_tags = vec![];
|
||||||
|
match major {
|
||||||
|
10 => {
|
||||||
|
// Prior to Mac OS 11, each yearly release of Mac OS bumped the "minor" version
|
||||||
|
// number. The major version was always 10.
|
||||||
|
for minor in (0..=*minor).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(*major, minor, arch) {
|
||||||
|
platform_tags.push(format!("macosx_{major}_{minor}_{binary_format}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
value if *value >= 11 => {
|
||||||
|
// Starting with Mac OS 11, each yearly release bumps the major version number.
|
||||||
|
// The minor versions are now the midyear updates.
|
||||||
|
for major in (10..=*major).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(major, 0, arch) {
|
||||||
|
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "universal2" binary format can have a macOS version earlier than 11.0
|
||||||
|
// when the x86_64 part of the binary supports that version of macOS.
|
||||||
|
for minor in (4..=16).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(10, minor, arch) {
|
||||||
|
platform_tags
|
||||||
|
.push(format!("macosx_{}_{}_{}", 10, minor, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Unsupported macOS version: {major}",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Macos { major, .. }, Arch::Aarch64) => {
|
||||||
|
// Source: https://github.com/pypa/packaging/blob/fd4f11139d1c884a637be8aa26bb60a31fbc9411/packaging/tags.py#L346
|
||||||
|
let mut platform_tags = vec![];
|
||||||
|
// Starting with Mac OS 11, each yearly release bumps the major version number.
|
||||||
|
// The minor versions are now the midyear updates.
|
||||||
|
for major in (10..=*major).rev() {
|
||||||
|
for binary_format in get_mac_binary_formats(major, 0, arch) {
|
||||||
|
platform_tags.push(format!("macosx_{}_{}_{}", major, 0, binary_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The "universal2" binary format can have a macOS version earlier than 11.0
|
||||||
|
// when the x86_64 part of the binary supports that version of macOS.
|
||||||
|
platform_tags.extend(
|
||||||
|
(4..=16)
|
||||||
|
.rev()
|
||||||
|
.map(|minor| format!("macosx_{}_{}_universal2", 10, minor)),
|
||||||
|
);
|
||||||
|
platform_tags
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::X86) => {
|
||||||
|
vec!["win32".to_string()]
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::X86_64) => {
|
||||||
|
vec!["win_amd64".to_string()]
|
||||||
|
}
|
||||||
|
(Os::Windows, Arch::Aarch64) => vec!["win_arm64".to_string()],
|
||||||
|
(
|
||||||
|
Os::FreeBsd { release }
|
||||||
|
| Os::NetBsd { release }
|
||||||
|
| Os::OpenBsd { release }
|
||||||
|
| Os::Dragonfly { release }
|
||||||
|
| Os::Haiku { release },
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
let release = release.replace(['.', '-'], "_");
|
||||||
|
vec![format!(
|
||||||
|
"{}_{}_{}",
|
||||||
|
os.to_string().to_lowercase(),
|
||||||
|
release,
|
||||||
|
arch
|
||||||
|
)]
|
||||||
|
}
|
||||||
|
(Os::Illumos { release, arch }, _) => {
|
||||||
|
// See https://github.com/python/cpython/blob/46c8d915715aa2bd4d697482aa051fe974d440e1/Lib/sysconfig.py#L722-L730
|
||||||
|
if let Some((major, other)) = release.split_once('_') {
|
||||||
|
let major_ver: u64 = major.parse().map_err(|err| {
|
||||||
|
PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"illumos major version is not a number: {err}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if major_ver >= 5 {
|
||||||
|
// SunOS 5 == Solaris 2
|
||||||
|
let os = "solaris".to_string();
|
||||||
|
let release = format!("{}_{}", major_ver - 3, other);
|
||||||
|
let arch = format!("{arch}_64bit");
|
||||||
|
return Ok(vec![format!("{}_{}_{}", os, release, arch)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let os = os.to_string().to_lowercase();
|
||||||
|
vec![format!("{}_{}_{}", os, release, arch)]
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(PlatformError::OsVersionDetectionError(format!(
|
||||||
|
"Unsupported operating system and architecture combination: {os} {arch}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(platform_tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the compatible tags in a (`python_tag`, `abi_tag`, `platform_tag`) format
|
||||||
|
pub fn compatible_tags(
|
||||||
|
python_version: (u8, u8),
|
||||||
|
os: &Os,
|
||||||
|
arch: Arch,
|
||||||
|
) -> Result<Vec<(String, String, String)>, PlatformError> {
|
||||||
|
assert_eq!(python_version.0, 3);
|
||||||
|
let mut tags = Vec::new();
|
||||||
|
let platform_tags = compatible_platform_tags(os, arch)?;
|
||||||
|
// 1. This exact c api version
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, python_version.1),
|
||||||
|
format!(
|
||||||
|
"cp{}{}{}",
|
||||||
|
python_version.0,
|
||||||
|
python_version.1,
|
||||||
|
// hacky but that's legacy anyways
|
||||||
|
if python_version.1 <= 7 { "m" } else { "" }
|
||||||
|
),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, python_version.1),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// 2. abi3 and no abi (e.g. executable binary)
|
||||||
|
// For some reason 3.2 is the minimum python for the cp abi
|
||||||
|
for minor in 2..=python_version.1 {
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("cp{}{}", python_version.0, minor),
|
||||||
|
"abi3".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. no abi (e.g. executable binary)
|
||||||
|
for minor in 0..=python_version.1 {
|
||||||
|
for platform_tag in &platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}{}", python_version.0, minor),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. major only
|
||||||
|
for platform_tag in platform_tags {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}", python_version.0),
|
||||||
|
"none".to_string(),
|
||||||
|
platform_tag,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// 5. no binary
|
||||||
|
for minor in 0..=python_version.1 {
|
||||||
|
tags.push((
|
||||||
|
format!("py{}{}", python_version.0, minor),
|
||||||
|
"none".to_string(),
|
||||||
|
"any".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
tags.push((
|
||||||
|
format!("py{}", python_version.0),
|
||||||
|
"none".to_string(),
|
||||||
|
"any".to_string(),
|
||||||
|
));
|
||||||
|
tags.sort();
|
||||||
|
Ok(tags)
|
||||||
|
}
|
|
@ -1,73 +1,69 @@
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail};
|
use thiserror::Error;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use pep440_rs::Version;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub struct WheelName {
|
pub struct WheelFilename {
|
||||||
// TODO(charlie): Normalized package name.
|
|
||||||
pub distribution: String,
|
pub distribution: String,
|
||||||
pub version: Version,
|
pub version: String,
|
||||||
pub build_number: Option<u32>,
|
pub python_tag: Vec<String>,
|
||||||
pub build_name: String,
|
pub abi_tag: Vec<String>,
|
||||||
pub py_tags: Vec<String>,
|
pub platform_tag: Vec<String>,
|
||||||
pub abi_tags: Vec<String>,
|
|
||||||
pub arch_tags: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static BUILD_TAG_SPLIT: Lazy<Regex> = Lazy::new(|| Regex::new(r"(^[0-9]*)(.*)$").unwrap());
|
impl FromStr for WheelFilename {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
impl FromStr for WheelName {
|
fn from_str(filename: &str) -> Result<Self, Self::Err> {
|
||||||
type Err = anyhow::Error;
|
let basename = filename.strip_suffix(".whl").ok_or_else(|| {
|
||||||
|
Error::InvalidWheelFileName(filename.to_string(), "Must end with .whl".to_string())
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
})?;
|
||||||
let suffix = ".whl";
|
// https://www.python.org/dev/peps/pep-0427/#file-name-convention
|
||||||
|
match basename.split('-').collect::<Vec<_>>().as_slice() {
|
||||||
let stem = s
|
// TODO(charlie): Build tag precedence
|
||||||
.strip_suffix(suffix)
|
&[distribution, version, _, python_tag, abi_tag, platform_tag]
|
||||||
.ok_or_else(|| anyhow!("expected wheel name to end with {:?}: {:?}", suffix, s))?;
|
| &[distribution, version, python_tag, abi_tag, platform_tag] => Ok(WheelFilename {
|
||||||
|
distribution: distribution.to_string(),
|
||||||
let mut pieces: Vec<&str> = stem.split('-').collect();
|
version: version.to_string(),
|
||||||
|
python_tag: python_tag.split('.').map(String::from).collect(),
|
||||||
let build_number: Option<u32>;
|
abi_tag: abi_tag.split('.').map(String::from).collect(),
|
||||||
let build_name: String;
|
platform_tag: platform_tag.split('.').map(String::from).collect(),
|
||||||
if pieces.len() == 6 {
|
}),
|
||||||
let build_tag = pieces.remove(2);
|
_ => Err(Error::InvalidWheelFileName(
|
||||||
if build_tag.is_empty() {
|
filename.to_string(),
|
||||||
bail!("found empty build tag: {s:?}");
|
"Expected four \"-\" in the filename".to_string(),
|
||||||
}
|
)),
|
||||||
// unwrap safe because: the regex cannot fail
|
|
||||||
let captures = BUILD_TAG_SPLIT.captures(build_tag).unwrap();
|
|
||||||
build_number = captures.get(1).and_then(|m| m.as_str().parse().ok());
|
|
||||||
// unwrap safe because: this group will always match something, even
|
|
||||||
// if only the empty string
|
|
||||||
build_name = captures.get(2).unwrap().as_str().into();
|
|
||||||
} else {
|
|
||||||
build_number = None;
|
|
||||||
build_name = String::new();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let [distribution, version, py_tags, abi_tags, arch_tags] = pieces.as_slice() else {
|
|
||||||
bail!("can't parse binary name {s:?}");
|
|
||||||
};
|
|
||||||
|
|
||||||
let distribution = (*distribution).to_string();
|
|
||||||
let version = Version::from_str(version)
|
|
||||||
.map_err(|e| anyhow!("failed to parse version {:?} from {:?}: {}", version, s, e))?;
|
|
||||||
let py_tags = py_tags.split('.').map(std::convert::Into::into).collect();
|
|
||||||
let abi_tags = abi_tags.split('.').map(std::convert::Into::into).collect();
|
|
||||||
let arch_tags = arch_tags.split('.').map(std::convert::Into::into).collect();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
distribution,
|
|
||||||
version,
|
|
||||||
build_number,
|
|
||||||
build_name,
|
|
||||||
py_tags,
|
|
||||||
abi_tags,
|
|
||||||
arch_tags,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WheelFilename {
|
||||||
|
/// Returns `true` if the wheel is compatible with the given tags.
|
||||||
|
pub fn is_compatible(&self, compatible_tags: &[(String, String, String)]) -> bool {
|
||||||
|
for tag in compatible_tags {
|
||||||
|
if self.python_tag.contains(&tag.0)
|
||||||
|
&& self.abi_tag.contains(&tag.1)
|
||||||
|
&& self.platform_tag.contains(&tag.2)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the tag for this wheel.
|
||||||
|
pub fn get_tag(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"{}-{}-{}",
|
||||||
|
self.python_tag.join("."),
|
||||||
|
self.abi_tag.join("."),
|
||||||
|
self.platform_tag.join(".")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("The wheel filename \"{0}\" is invalid: {1}")]
|
||||||
|
InvalidWheelFileName(String, String),
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue