mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Add Python interpreter detection (#11)
Closes https://github.com/astral-sh/puffin/issues/2.
This commit is contained in:
parent
b059c590c4
commit
1063d8c150
9 changed files with 281 additions and 28 deletions
|
@ -3,10 +3,9 @@ name = "puffin-cli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
puffin-client = { path = "../puffin-client" }
|
puffin-client = { path = "../puffin-client" }
|
||||||
|
puffin-interpreter = { path = "../puffin-interpreter" }
|
||||||
puffin-requirements = { path = "../puffin-requirements" }
|
puffin-requirements = { path = "../puffin-requirements" }
|
||||||
|
|
||||||
anyhow = { version = "1.0.75" }
|
anyhow = { version = "1.0.75" }
|
||||||
|
|
|
@ -6,10 +6,11 @@ use anyhow::Result;
|
||||||
use futures::future::Either;
|
use futures::future::Either;
|
||||||
use futures::{StreamExt, TryFutureExt};
|
use futures::{StreamExt, TryFutureExt};
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
use pep508_rs::{MarkerEnvironment, Requirement, StringVersion, VersionOrUrl};
|
use pep508_rs::{Requirement, VersionOrUrl};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use puffin_client::{File, PypiClientBuilder, SimpleJson};
|
use puffin_client::{File, PypiClientBuilder, SimpleJson};
|
||||||
|
use puffin_interpreter::PythonExecutable;
|
||||||
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::WheelName;
|
||||||
|
@ -29,27 +30,19 @@ enum Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
|
pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
|
||||||
// TODO(charlie): Fetch from the environment.
|
|
||||||
let env = MarkerEnvironment {
|
|
||||||
implementation_name: String::new(),
|
|
||||||
implementation_version: StringVersion::from_str("3.10.0").unwrap(),
|
|
||||||
os_name: String::new(),
|
|
||||||
platform_machine: String::new(),
|
|
||||||
platform_python_implementation: String::new(),
|
|
||||||
platform_release: String::new(),
|
|
||||||
platform_system: String::new(),
|
|
||||||
platform_version: String::new(),
|
|
||||||
python_full_version: StringVersion::from_str("3.10.0").unwrap(),
|
|
||||||
python_version: StringVersion::from_str("3.10.0").unwrap(),
|
|
||||||
sys_platform: String::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read the `requirements.txt` from disk.
|
// Read the `requirements.txt` from disk.
|
||||||
let requirements_txt = std::fs::read_to_string(src)?;
|
let requirements_txt = std::fs::read_to_string(src)?;
|
||||||
|
|
||||||
// Parse the `requirements.txt` into a list of requirements.
|
// Parse the `requirements.txt` into a list of requirements.
|
||||||
let requirements = puffin_requirements::Requirements::from_str(&requirements_txt)?;
|
let requirements = puffin_requirements::Requirements::from_str(&requirements_txt)?;
|
||||||
|
|
||||||
|
// Detect the current Python interpreter.
|
||||||
|
let python = PythonExecutable::from_env()?;
|
||||||
|
debug!(
|
||||||
|
"Using Python interpreter: {}",
|
||||||
|
python.executable().display()
|
||||||
|
);
|
||||||
|
|
||||||
// Instantiate a client.
|
// Instantiate a client.
|
||||||
let pypi_client = PypiClientBuilder::default().build();
|
let pypi_client = PypiClientBuilder::default().build();
|
||||||
let proxy_client = PypiClientBuilder::default().build();
|
let proxy_client = PypiClientBuilder::default().build();
|
||||||
|
@ -130,9 +123,10 @@ pub(crate) async fn install(src: &Path) -> Result<ExitStatus> {
|
||||||
|
|
||||||
// Enqueue its dependencies.
|
// Enqueue its dependencies.
|
||||||
for dependency in metadata.requires_dist {
|
for dependency in metadata.requires_dist {
|
||||||
if !dependency
|
if !dependency.evaluate_markers(
|
||||||
.evaluate_markers(&env, requirement.extras.clone().unwrap_or_default())
|
python.markers(),
|
||||||
{
|
requirement.extras.clone().unwrap_or_default(),
|
||||||
|
) {
|
||||||
debug!("--> ignoring {dependency} due to environment mismatch");
|
debug!("--> ignoring {dependency} due to environment mismatch");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,6 @@ name = "puffin-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
puffin-requirements = { path = "../puffin-requirements" }
|
puffin-requirements = { path = "../puffin-requirements" }
|
||||||
|
|
||||||
|
|
16
crates/puffin-interpreter/Cargo.toml
Normal file
16
crates/puffin-interpreter/Cargo.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[package]
|
||||||
|
name = "puffin-interpreter"
|
||||||
|
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
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1.0.75" }
|
||||||
|
pep508_rs = { version = "0.2.3", features = ["serde"] }
|
||||||
|
serde_json = { version = "1.0.107" }
|
||||||
|
tracing = { version = "0.1.37" }
|
40
crates/puffin-interpreter/src/lib.rs
Normal file
40
crates/puffin-interpreter/src/lib.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
|
||||||
|
use crate::platform::Platform;
|
||||||
|
|
||||||
|
mod markers;
|
||||||
|
mod platform;
|
||||||
|
mod virtual_env;
|
||||||
|
|
||||||
|
/// A Python executable and its associated platform markers.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PythonExecutable {
|
||||||
|
executable: PathBuf,
|
||||||
|
markers: MarkerEnvironment,
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let markers = markers::detect_markers(&executable)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
executable,
|
||||||
|
markers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn executable(&self) -> &Path {
|
||||||
|
self.executable.as_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn markers(&self) -> &MarkerEnvironment {
|
||||||
|
&self.markers
|
||||||
|
}
|
||||||
|
}
|
59
crates/puffin-interpreter/src/markers.rs
Normal file
59
crates/puffin-interpreter/src/markers.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
|
||||||
|
/// Return the resolved [`MarkerEnvironment`] for the given Python executable.
|
||||||
|
pub(crate) fn detect_markers(python: impl AsRef<Path>) -> Result<MarkerEnvironment> {
|
||||||
|
let output = call_python(python.as_ref(), ["-c", CAPTURE_MARKERS_SCRIPT])?;
|
||||||
|
Ok(serde_json::from_slice::<MarkerEnvironment>(&output.stdout)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CAPTURE_MARKERS_SCRIPT: &str = "
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import platform
|
||||||
|
import json
|
||||||
|
def format_full_version(info):
|
||||||
|
version = '{0.major}.{0.minor}.{0.micro}'.format(info)
|
||||||
|
kind = info.releaselevel
|
||||||
|
if kind != 'final':
|
||||||
|
version += kind[0] + str(info.serial)
|
||||||
|
return version
|
||||||
|
|
||||||
|
if hasattr(sys, 'implementation'):
|
||||||
|
implementation_version = format_full_version(sys.implementation.version)
|
||||||
|
implementation_name = sys.implementation.name
|
||||||
|
else:
|
||||||
|
implementation_version = '0'
|
||||||
|
implementation_name = ''
|
||||||
|
bindings = {
|
||||||
|
'implementation_name': implementation_name,
|
||||||
|
'implementation_version': implementation_version,
|
||||||
|
'os_name': os.name,
|
||||||
|
'platform_machine': platform.machine(),
|
||||||
|
'platform_python_implementation': platform.python_implementation(),
|
||||||
|
'platform_release': platform.release(),
|
||||||
|
'platform_system': platform.system(),
|
||||||
|
'platform_version': platform.version(),
|
||||||
|
'python_full_version': platform.python_version(),
|
||||||
|
'python_version': '.'.join(platform.python_version_tuple()[:2]),
|
||||||
|
'sys_platform': sys.platform,
|
||||||
|
}
|
||||||
|
json.dump(bindings, sys.stdout)
|
||||||
|
sys.stdout.flush()
|
||||||
|
";
|
||||||
|
|
||||||
|
/// Run a Python script and return its output.
|
||||||
|
fn call_python<I, S>(python: &Path, args: I) -> Result<Output>
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: AsRef<OsStr>,
|
||||||
|
{
|
||||||
|
Command::new(python)
|
||||||
|
.args(args)
|
||||||
|
.output()
|
||||||
|
.context(format!("Failed to run `python` at: {:?}", &python))
|
||||||
|
}
|
105
crates/puffin-interpreter/src/platform.rs
Normal file
105
crates/puffin-interpreter/src/platform.rs
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
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/virtual_env.rs
Normal file
47
crates/puffin-interpreter/src/virtual_env.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use std::env;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use crate::platform::Platform;
|
||||||
|
|
||||||
|
/// Locate the current virtual environment.
|
||||||
|
pub(crate) fn detect_virtual_env(target: &Platform) -> 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)),
|
||||||
|
(Some(_), Some(_)) => {
|
||||||
|
bail!("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.")
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
// No environment variables set. Try to find a virtualenv in the current directory.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search for a `.venv` directory in the current or any parent directory.
|
||||||
|
let current_dir = env::current_dir().expect("Failed to detect current directory");
|
||||||
|
for dir in current_dir.ancestors() {
|
||||||
|
let dot_venv = dir.join(".venv");
|
||||||
|
if dot_venv.is_dir() {
|
||||||
|
if !dot_venv.join("pyvenv.cfg").is_file() {
|
||||||
|
bail!(
|
||||||
|
"Expected {} to be a virtual environment, but pyvenv.cfg is missing",
|
||||||
|
dot_venv.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let python = target.get_venv_python(&dot_venv);
|
||||||
|
if !python.is_file() {
|
||||||
|
bail!(
|
||||||
|
"Your virtualenv at {} is broken. It contains a pyvenv.cfg but no python at {}",
|
||||||
|
dot_venv.display(),
|
||||||
|
python.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
debug!("Found a virtualenv named .venv at {}", dot_venv.display());
|
||||||
|
return Ok(dot_venv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Couldn't find a virtualenv or conda environment.")
|
||||||
|
}
|
|
@ -3,13 +3,8 @@ name = "puffin-requirements"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { version = "1.0.75" }
|
anyhow = { version = "1.0.75" }
|
||||||
clap = { version = "4.4.6", features = ["derive"] }
|
|
||||||
colored = { version = "2.0.4" }
|
|
||||||
insta = { version = "1.33.0" }
|
|
||||||
mailparse = { version = "0.14.0" }
|
mailparse = { version = "0.14.0" }
|
||||||
memchr = { version = "2.6.4" }
|
memchr = { version = "2.6.4" }
|
||||||
once_cell = { version = "1.18.0" }
|
once_cell = { version = "1.18.0" }
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue