mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Add bootstrapping and isolation of development Python versions (#1105)
Replaces https://github.com/astral-sh/puffin/pull/1068 and #1070 which were more complicated than I wanted. - Introduces a `.python-versions` file which defines the Python versions needed for development - Adds a Bash script at `scripts/bootstrap/install` which installs the required Python versions from `python-build-standalone` to `./bin` - Checks in a `versions.json` file with metadata about available versions on each platform and a `fetch-version` Python script derived from `rye` for updating the versions - Updates CI to use these Python builds instead of the `setup-python` action - Updates to the latest packse scenarios which require Python 3.8+ instead of 3.7+ since we cannot use 3.7 anymore and includes new test coverage of patch Python version requests - Adds a `PUFFIN_PYTHON_PATH` variable to prevent lookup of system Python versions for isolation during development Tested on Linux (via CI) and macOS (locally) — presumably it will be a bit more complicated to do proper Windows support.
This commit is contained in:
parent
cc0e211074
commit
21577ad002
16 changed files with 5789 additions and 444 deletions
|
@ -1,3 +1,4 @@
|
|||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
|
@ -136,6 +137,10 @@ impl Interpreter {
|
|||
/// - If a python version is given: `pythonx.y`
|
||||
/// - `python3` (unix) or `python.exe` (windows)
|
||||
///
|
||||
/// If `PUFFIN_PYTHON_PATH` is set, we will not check for Python versions in the
|
||||
/// global PATH, instead we will search using the provided path. Virtual environments
|
||||
/// will still be respected.
|
||||
///
|
||||
/// If a version is provided and an interpreter cannot be found with the given version,
|
||||
/// we will return [`None`].
|
||||
pub fn find_version(
|
||||
|
@ -170,7 +175,8 @@ impl Interpreter {
|
|||
python_version.major(),
|
||||
python_version.minor()
|
||||
);
|
||||
if let Ok(executable) = which::which(&requested) {
|
||||
|
||||
if let Ok(executable) = Interpreter::find_executable(&requested) {
|
||||
debug!("Resolved {requested} to {}", executable.display());
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
|
@ -179,7 +185,7 @@ impl Interpreter {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(executable) = which::which("python3") {
|
||||
if let Ok(executable) = Interpreter::find_executable("python3") {
|
||||
debug!("Resolved python3 to {}", executable.display());
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
|
@ -198,7 +204,7 @@ impl Interpreter {
|
|||
}
|
||||
}
|
||||
|
||||
if let Ok(executable) = which::which("python.exe") {
|
||||
if let Ok(executable) = Interpreter::find_executable("python.exe") {
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
|
@ -211,6 +217,23 @@ impl Interpreter {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn find_executable<R: AsRef<OsStr> + Into<OsString> + Copy>(
|
||||
requested: R,
|
||||
) -> Result<PathBuf, Error> {
|
||||
if let Some(isolated) = std::env::var_os("PUFFIN_PYTHON_PATH") {
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
which::which_in(requested, Some(isolated), cwd)
|
||||
.map_err(|err| Error::Which(requested.into(), err))
|
||||
} else {
|
||||
which::which_in_global(requested, Some(isolated))
|
||||
.map_err(|err| Error::Which(requested.into(), err))
|
||||
.and_then(|mut paths| paths.next().ok_or(Error::PythonNotFound))
|
||||
}
|
||||
} else {
|
||||
which::which(requested).map_err(|err| Error::Which(requested.into(), err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the Python virtual environment.
|
||||
#[inline]
|
||||
pub fn platform(&self) -> &Platform {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::ffi::OsString;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTimeError;
|
||||
|
@ -49,6 +50,8 @@ pub enum Error {
|
|||
NoPythonInstalledUnix,
|
||||
#[error("Could not find `python.exe` in PATH and `py --list-paths` did not list any Python versions. Do you need to install Python?")]
|
||||
NoPythonInstalledWindows,
|
||||
#[error("Patch versions cannot be requested on Windows")]
|
||||
PatchVersionRequestedWindows,
|
||||
#[error("{message}:\n--- stdout:\n{stdout}\n--- stderr:\n{stderr}\n---")]
|
||||
PythonSubcommandOutput {
|
||||
message: String,
|
||||
|
@ -63,6 +66,6 @@ pub enum Error {
|
|||
Encode(#[from] rmp_serde::encode::Error),
|
||||
#[error("Failed to parse pyvenv.cfg")]
|
||||
Cfg(#[from] cfg::Error),
|
||||
#[error("Couldn't find `{0}` in PATH")]
|
||||
Which(PathBuf, #[source] which::Error),
|
||||
#[error("Couldn't find `{}` in PATH", _0.to_string_lossy())]
|
||||
Which(OsString, #[source] which::Error),
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use once_cell::sync::Lazy;
|
|||
use regex::Regex;
|
||||
use tracing::{info_span, instrument};
|
||||
|
||||
use crate::Error;
|
||||
use crate::{Error, Interpreter};
|
||||
|
||||
/// ```text
|
||||
/// -V:3.12 C:\Users\Ferris\AppData\Local\Programs\Python\Python312\python.exe
|
||||
|
@ -27,23 +27,30 @@ static PY_LIST_PATHS: Lazy<Regex> = Lazy::new(|| {
|
|||
/// * `-p /home/ferris/.local/bin/python3.10` uses this exact Python.
|
||||
#[instrument]
|
||||
pub fn find_requested_python(request: &str) -> Result<PathBuf, Error> {
|
||||
let major_minor = request
|
||||
.split_once('.')
|
||||
.and_then(|(major, minor)| Some((major.parse::<u8>().ok()?, minor.parse::<u8>().ok()?)));
|
||||
if let Some((major, minor)) = major_minor {
|
||||
// `-p 3.10`
|
||||
let versions = request
|
||||
.splitn(3, '.')
|
||||
.map(str::parse::<u8>)
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
if let Ok(versions) = versions {
|
||||
// `-p 3.10` or `-p 3.10.1`
|
||||
if cfg!(unix) {
|
||||
let formatted = PathBuf::from(format!("python{major}.{minor}"));
|
||||
which::which(&formatted).map_err(|err| Error::Which(formatted, err))
|
||||
let formatted = PathBuf::from(format!("python{request}"));
|
||||
Interpreter::find_executable(&formatted)
|
||||
} else if cfg!(windows) {
|
||||
find_python_windows(major, minor)?.ok_or(Error::NoSuchPython { major, minor })
|
||||
if let [major, minor] = versions.as_slice() {
|
||||
find_python_windows(*major, *minor)?.ok_or(Error::NoSuchPython {
|
||||
major: *major,
|
||||
minor: *minor,
|
||||
})
|
||||
} else {
|
||||
Err(Error::PatchVersionRequestedWindows)
|
||||
}
|
||||
} else {
|
||||
unimplemented!("Only Windows and Unix are supported")
|
||||
}
|
||||
} else if !request.contains(std::path::MAIN_SEPARATOR) {
|
||||
// `-p python3.10`; Generally not used on windows because all Python are `python.exe`.
|
||||
let request = PathBuf::from(request);
|
||||
which::which(&request).map_err(|err| Error::Which(request, err))
|
||||
Interpreter::find_executable(request)
|
||||
} else {
|
||||
// `-p /home/ferris/.local/bin/python3.10`
|
||||
Ok(fs_err::canonicalize(request)?)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue