Add support for wheel tag parsing (#15)

Closes https://github.com/astral-sh/puffin/issues/12.
This commit is contained in:
Charlie Marsh 2023-10-05 20:59:58 -04:00 committed by GitHub
parent 2d6266b167
commit 94895de46d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 720 additions and 181 deletions

View file

@ -10,6 +10,8 @@ authors.workspace = true
license.workspace = true
[dependencies]
puffin-platform = { path = "../puffin-platform" }
anyhow = { version = "1.0.75" }
pep508_rs = { version = "0.2.3", features = ["serde"] }
serde_json = { version = "1.0.107" }

View file

@ -2,11 +2,12 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use pep508_rs::MarkerEnvironment;
use puffin_platform::Platform;
use crate::platform::Platform;
use crate::python_platform::PythonPlatform;
mod markers;
mod platform;
mod python_platform;
mod virtual_env;
/// A Python executable and its associated platform markers.
@ -18,10 +19,10 @@ pub struct PythonExecutable {
impl PythonExecutable {
/// Detect the current Python executable from the host environment.
pub fn from_env() -> Result<Self> {
let target = Platform::from_host();
let venv = virtual_env::detect_virtual_env(&target)?;
let executable = target.get_venv_python(venv);
pub fn from_env(platform: &Platform) -> Result<Self> {
let platform = PythonPlatform::from(platform);
let venv = virtual_env::detect_virtual_env(&platform)?;
let executable = platform.venv_python(venv);
let markers = markers::detect_markers(&executable)?;
Ok(Self {
@ -30,11 +31,23 @@ impl PythonExecutable {
})
}
/// Returns the path to the Python executable.
pub fn executable(&self) -> &Path {
self.executable.as_path()
}
/// Returns the [`MarkerEnvironment`] for this Python executable.
pub fn markers(&self) -> &MarkerEnvironment {
&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"),
)
}
}

View file

@ -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"),
}
}
}

View 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)
}
}

View file

@ -4,10 +4,10 @@ use std::path::PathBuf;
use anyhow::{bail, Result};
use tracing::debug;
use crate::platform::Platform;
use crate::python_platform::PythonPlatform;
/// 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")) {
(Some(dir), None) => 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()
);
}
let python = target.get_venv_python(&dot_venv);
let python = target.venv_python(&dot_venv);
if !python.is_file() {
bail!(
"Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}",