mirror of https://github.com/astral-sh/uv
Prefer target Python version over current version for builds (#1040)
Extends #1029 Closes https://github.com/astral-sh/puffin/issues/1038 Instead of always using the current Python version for builds when a target version is provided, we will do our best to use a compatible Python version for builds. Removes behavior where Python versions without patch versions were always assumed to be the latest known patch version (previously discussed in https://github.com/astral-sh/puffin/pull/534). While this was convenient for resolutions which include packages which require minimum patch versions e.g. `requires-python=">=3.7.4"`, it conflicts with the idea that the target Python version you provide is the _minimum_ compatible version. Additionally, it complicates interpreter lookup as we cannot tell if the user has asked for that specific patch version or not.
This commit is contained in:
parent
77dcb2421a
commit
ea4ab29bad
|
|
@ -33,7 +33,7 @@ fn run() -> Result<(), gourgeist::Error> {
|
|||
} else {
|
||||
Cache::from_path(".gourgeist_cache")?
|
||||
};
|
||||
let info = Interpreter::query(python.as_std_path(), platform, &cache).unwrap();
|
||||
let info = Interpreter::query(python.as_std_path(), &platform, &cache).unwrap();
|
||||
create_bare_venv(&location, &info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub struct Interpreter {
|
|||
|
||||
impl Interpreter {
|
||||
/// Detect the interpreter info for the given Python executable.
|
||||
pub fn query(executable: &Path, platform: Platform, cache: &Cache) -> Result<Self, Error> {
|
||||
pub fn query(executable: &Path, platform: &Platform, cache: &Cache) -> Result<Self, Error> {
|
||||
let info = InterpreterQueryResult::query_cached(executable, cache)?;
|
||||
debug_assert!(
|
||||
info.base_prefix == info.base_exec_prefix,
|
||||
|
|
@ -47,7 +47,7 @@ impl Interpreter {
|
|||
);
|
||||
|
||||
Ok(Self {
|
||||
platform: PythonPlatform(platform),
|
||||
platform: PythonPlatform(platform.to_owned()),
|
||||
markers: info.markers,
|
||||
base_exec_prefix: info.base_exec_prefix,
|
||||
base_prefix: info.base_prefix,
|
||||
|
|
@ -83,25 +83,80 @@ impl Interpreter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Detect the python interpreter to use.
|
||||
/// Find the best available Python interpreter to use.
|
||||
///
|
||||
/// Note that `python_version` is a preference here, not a requirement.
|
||||
/// If no Python version is provided, we will use the first available interpreter.
|
||||
///
|
||||
/// We check, in order:
|
||||
/// * `VIRTUAL_ENV` and `CONDA_PREFIX`
|
||||
/// * A `.venv` folder
|
||||
/// * If a python version is given: `pythonx.y`
|
||||
/// * `python3` (unix) or `python.exe` (windows)
|
||||
pub fn find(
|
||||
/// If a Python version is provided, we will first try to find an exact match. If
|
||||
/// that cannot be found and a patch version was requested, we will look for a match
|
||||
/// without comparing the patch version number. If that cannot be found, we fall back to
|
||||
/// the first available version.
|
||||
///
|
||||
/// See [`Self::find_strict`] for details on the precedence of Python lookup locations.
|
||||
pub fn find_best(
|
||||
python_version: Option<&PythonVersion>,
|
||||
platform: Platform,
|
||||
platform: &Platform,
|
||||
cache: &Cache,
|
||||
) -> Result<Self, Error> {
|
||||
let platform = PythonPlatform::from(platform);
|
||||
// First, check for an exact match (or the first available version if no Python version was provided)
|
||||
if let Some(interpreter) = Self::find_version(python_version, platform, cache)? {
|
||||
return Ok(interpreter);
|
||||
}
|
||||
|
||||
if let Some(python_version) = python_version {
|
||||
// If that fails, and a specific patch version was requested try again allowing a
|
||||
// different patch version
|
||||
if python_version.patch().is_some() {
|
||||
if let Some(interpreter) =
|
||||
Self::find_version(Some(&python_version.without_patch()), platform, cache)?
|
||||
{
|
||||
return Ok(interpreter);
|
||||
}
|
||||
}
|
||||
|
||||
// If a Python version was requested but cannot be fulfilled, just take any version
|
||||
if let Some(interpreter) = Self::find_version(None, platform, cache)? {
|
||||
return Ok(interpreter);
|
||||
}
|
||||
}
|
||||
|
||||
Err(Error::PythonNotFound)
|
||||
}
|
||||
|
||||
/// Find a Python interpreter.
|
||||
///
|
||||
/// We check, in order, the following locations:
|
||||
///
|
||||
/// - `VIRTUAL_ENV` and `CONDA_PREFIX`
|
||||
/// - A `.venv` folder
|
||||
/// - If a python version is given: `pythonx.y`
|
||||
/// - `python3` (unix) or `python.exe` (windows)
|
||||
///
|
||||
/// If a version is provided and an interpreter cannot be found with the given version,
|
||||
/// we will return [`None`].
|
||||
pub fn find_version(
|
||||
python_version: Option<&PythonVersion>,
|
||||
platform: &Platform,
|
||||
cache: &Cache,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let version_matches = |interpreter: &Self| -> bool {
|
||||
if let Some(python_version) = python_version {
|
||||
// If a patch version was provided, check for an exact match
|
||||
python_version.is_satisfied_by(interpreter)
|
||||
} else {
|
||||
// The version always matches if one was not provided
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
let platform = PythonPlatform::from(platform.to_owned());
|
||||
if let Some(venv) = detect_virtual_env(&platform)? {
|
||||
let executable = platform.venv_python(venv);
|
||||
let interpreter = Self::query(&executable, platform.0, cache)?;
|
||||
return Ok(interpreter);
|
||||
let interpreter = Self::query(&executable, &platform.0, cache)?;
|
||||
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
};
|
||||
|
||||
if cfg!(unix) {
|
||||
|
|
@ -113,32 +168,43 @@ impl Interpreter {
|
|||
);
|
||||
if let Ok(executable) = which::which(&requested) {
|
||||
debug!("Resolved {requested} to {}", executable.display());
|
||||
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
|
||||
return Ok(interpreter);
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let executable = which::which("python3")
|
||||
.map_err(|err| Error::WhichNotFound("python3".to_string(), err))?;
|
||||
debug!("Resolved python3 to {}", executable.display());
|
||||
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
|
||||
Ok(interpreter)
|
||||
if let Ok(executable) = which::which("python3") {
|
||||
debug!("Resolved python3 to {}", executable.display());
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
} else if cfg!(windows) {
|
||||
if let Some(python_version) = python_version {
|
||||
if let Some(path) =
|
||||
find_python_windows(python_version.major(), python_version.minor())?
|
||||
{
|
||||
return Interpreter::query(&path, platform.0, cache);
|
||||
let interpreter = Interpreter::query(&path, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let executable = which::which("python.exe")
|
||||
.map_err(|err| Error::WhichNotFound("python.exe".to_string(), err))?;
|
||||
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
|
||||
Ok(interpreter)
|
||||
if let Ok(executable) = which::which("python.exe") {
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
if version_matches(&interpreter) {
|
||||
return Ok(Some(interpreter));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unimplemented!("Only Windows and Unix are supported");
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the path to the Python virtual environment.
|
||||
|
|
@ -433,8 +499,7 @@ mod tests {
|
|||
std::os::unix::fs::PermissionsExt::from_mode(0o770),
|
||||
)
|
||||
.unwrap();
|
||||
let interpreter =
|
||||
Interpreter::query(&mocked_interpreter, platform.clone(), &cache).unwrap();
|
||||
let interpreter = Interpreter::query(&mocked_interpreter, &platform, &cache).unwrap();
|
||||
assert_eq!(
|
||||
interpreter.markers.python_version.version,
|
||||
Version::from_str("3.12").unwrap()
|
||||
|
|
@ -447,7 +512,7 @@ mod tests {
|
|||
"##, json.replace("3.12", "3.13")},
|
||||
)
|
||||
.unwrap();
|
||||
let interpreter = Interpreter::query(&mocked_interpreter, platform, &cache).unwrap();
|
||||
let interpreter = Interpreter::query(&mocked_interpreter, &platform, &cache).unwrap();
|
||||
assert_eq!(
|
||||
interpreter.markers.python_version.version,
|
||||
Version::from_str("3.13").unwrap()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ pub enum Error {
|
|||
BrokenVenv(PathBuf, PathBuf),
|
||||
#[error("Both VIRTUAL_ENV and CONDA_PREFIX are set. Please unset one of them.")]
|
||||
Conflict,
|
||||
#[error("No versions of Python could be found. Is Python installed?")]
|
||||
PythonNotFound,
|
||||
#[error("Could not find `{0}` in PATH")]
|
||||
WhichNotFound(String, #[source] which::Error),
|
||||
#[error("Failed to locate a virtualenv or Conda environment (checked: `VIRTUAL_ENV`, `CONDA_PREFIX`, and `.venv`). Run `puffin venv` to create a virtual environment.")]
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use tracing::debug;
|
||||
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::{MarkerEnvironment, StringVersion};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Interpreter;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PythonVersion(StringVersion);
|
||||
|
|
@ -29,34 +28,7 @@ impl FromStr for PythonVersion {
|
|||
return Err(format!("Python version {s} must be < 4.0"));
|
||||
}
|
||||
|
||||
// If the version lacks a patch, assume the most recent known patch for that minor version.
|
||||
match version.release() {
|
||||
[3, 7] => {
|
||||
debug!("Assuming Python 3.7.17");
|
||||
Ok(Self(StringVersion::from_str("3.7.17")?))
|
||||
}
|
||||
[3, 8] => {
|
||||
debug!("Assuming Python 3.8.18");
|
||||
Ok(Self(StringVersion::from_str("3.8.18")?))
|
||||
}
|
||||
[3, 9] => {
|
||||
debug!("Assuming Python 3.9.18");
|
||||
Ok(Self(StringVersion::from_str("3.9.18")?))
|
||||
}
|
||||
[3, 10] => {
|
||||
debug!("Assuming Python 3.10.13");
|
||||
Ok(Self(StringVersion::from_str("3.10.13")?))
|
||||
}
|
||||
[3, 11] => {
|
||||
debug!("Assuming Python 3.11.6");
|
||||
Ok(Self(StringVersion::from_str("3.11.6")?))
|
||||
}
|
||||
[3, 12] => {
|
||||
debug!("Assuming Python 3.12.0");
|
||||
Ok(Self(StringVersion::from_str("3.12.0")?))
|
||||
}
|
||||
_ => Ok(Self(version)),
|
||||
}
|
||||
Ok(Self(version))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -83,6 +55,11 @@ impl PythonVersion {
|
|||
markers
|
||||
}
|
||||
|
||||
/// Return the full parsed Python version.
|
||||
pub fn version(&self) -> &Version {
|
||||
&self.0.version
|
||||
}
|
||||
|
||||
/// Return the major version of this Python version.
|
||||
pub fn major(&self) -> u8 {
|
||||
u8::try_from(self.0.release()[0]).expect("invalid major version")
|
||||
|
|
@ -93,8 +70,31 @@ impl PythonVersion {
|
|||
u8::try_from(self.0.release()[1]).expect("invalid minor version")
|
||||
}
|
||||
|
||||
/// Returns the Python version as a simple tuple.
|
||||
pub fn simple_version(&self) -> (u8, u8) {
|
||||
(self.major(), self.minor())
|
||||
/// Check if this Python version is satisfied by the given interpreter.
|
||||
///
|
||||
/// If a patch version is present, we will require an exact match.
|
||||
/// Otherwise, just the major and minor version numbers need to match.
|
||||
pub fn is_satisfied_by(&self, interpreter: &Interpreter) -> bool {
|
||||
if self.patch().is_some() {
|
||||
self.version() == interpreter.python_version()
|
||||
} else {
|
||||
(self.major(), self.minor()) == interpreter.python_tuple()
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the patch version of this Python version, if set.
|
||||
pub fn patch(&self) -> Option<u8> {
|
||||
self.0
|
||||
.release()
|
||||
.get(2)
|
||||
.copied()
|
||||
.map(|patch| u8::try_from(patch).expect("invalid patch version"))
|
||||
}
|
||||
|
||||
/// Returns a copy of the Python version without the patch version
|
||||
#[must_use]
|
||||
pub fn without_patch(&self) -> Self {
|
||||
Self::from_str(format!("{}.{}", self.major(), self.minor()).as_str())
|
||||
.expect("dropping a patch should always be valid")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ impl Virtualenv {
|
|||
};
|
||||
let venv = fs_err::canonicalize(venv)?;
|
||||
let executable = platform.venv_python(&venv);
|
||||
let interpreter = Interpreter::query(&executable, platform.0, cache)?;
|
||||
let interpreter = Interpreter::query(&executable, &platform.0, cache)?;
|
||||
|
||||
Ok(Self {
|
||||
root: venv,
|
||||
|
|
|
|||
|
|
@ -122,9 +122,9 @@ pub(crate) async fn pip_compile(
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Detect the current Python interpreter.
|
||||
// Find an interpreter to use for building distributions
|
||||
let platform = Platform::current()?;
|
||||
let interpreter = Interpreter::find(python_version.as_ref(), platform, &cache)?;
|
||||
let interpreter = Interpreter::find_best(python_version.as_ref(), &platform, &cache)?;
|
||||
debug!(
|
||||
"Using Python {} interpreter at {} for builds",
|
||||
interpreter.python_version(),
|
||||
|
|
@ -147,7 +147,7 @@ pub(crate) async fn pip_compile(
|
|||
let tags = if let Some(python_version) = python_version.as_ref() {
|
||||
Cow::Owned(Tags::from_env(
|
||||
interpreter.platform(),
|
||||
python_version.simple_version(),
|
||||
(python_version.major(), python_version.minor()),
|
||||
interpreter.implementation_name(),
|
||||
interpreter.implementation_tuple(),
|
||||
)?)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ async fn venv_impl(
|
|||
|
||||
let platform = Platform::current().into_diagnostic()?;
|
||||
let interpreter =
|
||||
Interpreter::query(&base_python, platform, cache).map_err(VenvError::InterpreterError)?;
|
||||
Interpreter::query(&base_python, &platform, cache).map_err(VenvError::InterpreterError)?;
|
||||
|
||||
writeln!(
|
||||
printer,
|
||||
|
|
|
|||
|
|
@ -687,9 +687,9 @@ fn compile_python_37() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the requested Python version (3.7.17) does not satisfy
|
||||
Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude
|
||||
that black==23.10.1 cannot be used.
|
||||
╰─▶ Because the requested Python version (3.7) does not satisfy Python>=3.8
|
||||
and black==23.10.1 depends on Python>=3.8, we can conclude that
|
||||
black==23.10.1 cannot be used.
|
||||
And because you require black==23.10.1, we can conclude that the
|
||||
requirements are unsatisfiable.
|
||||
"###);
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ fn requires_compatible_python_version_incompatible_override() -> Result<()> {
|
|||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the requested Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used.
|
||||
╰─▶ Because the requested Python version (3.9) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used.
|
||||
And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable.
|
||||
"###);
|
||||
});
|
||||
|
|
@ -185,14 +185,15 @@ fn requires_incompatible_python_version_compatible_override_no_wheels() -> Resul
|
|||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.env("PUFFIN_NO_WRAP", "1")
|
||||
.current_dir(&temp_dir), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v[VERSION] via the following command:
|
||||
# puffin pip compile requirements.in --python-version=3.11 --extra-index-url https://test.pypi.org/simple --cache-dir [CACHE_DIR]
|
||||
albatross==1.0.0
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used.
|
||||
And because you require albatross==1.0.0, we can conclude that the requirements are unsatisfiable.
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
});
|
||||
|
||||
|
|
@ -314,22 +315,15 @@ fn requires_incompatible_python_version_compatible_override_other_wheel() -> Res
|
|||
.env("VIRTUAL_ENV", venv.as_os_str())
|
||||
.env("PUFFIN_NO_WRAP", "1")
|
||||
.current_dir(&temp_dir), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
# This file was autogenerated by Puffin v[VERSION] via the following command:
|
||||
# puffin pip compile requirements.in --python-version=3.11 --extra-index-url https://test.pypi.org/simple --cache-dir [CACHE_DIR]
|
||||
albatross==1.0.0
|
||||
|
||||
----- stderr -----
|
||||
× No solution found when resolving dependencies:
|
||||
╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and albatross==1.0.0 depends on Python>=3.10, we can conclude that albatross==1.0.0 cannot be used.
|
||||
And because there are no versions of albatross that satisfy any of:
|
||||
albatross<1.0.0
|
||||
albatross>1.0.0,<2.0.0
|
||||
albatross>2.0.0
|
||||
we can conclude that albatross<2.0.0 cannot be used. (1)
|
||||
|
||||
Because the requested Python version (3.11.6) does not satisfy Python>=3.12 and albatross==2.0.0 depends on Python>=3.12, we can conclude that albatross==2.0.0 cannot be used.
|
||||
And because we know from (1) that albatross<2.0.0 cannot be used, we can conclude that all versions of albatross cannot be used.
|
||||
And because you require albatross, we can conclude that the requirements are unsatisfiable.
|
||||
Resolved 1 package in [TIME]
|
||||
"###);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue