mirror of https://github.com/astral-sh/uv
Allow pinning managed Python versions to specific build versions (#15314)
Allows pinning the Python build version via environment variables, e.g., `UV_PYTHON_CPYTHON_BUILD=...`. Each variable is implementation specific, because they use different versioning schemes. Updates the Python download metadata to include a `build` string, so we can filter downloads by the pin. Writes the build version to a file in the managed install, e.g., `cpython-3.10.18-macos-aarch64-none/BUILD`, so we can filter installed versions by the pin. Some important follow-up here: - Include the build version in not found errors (when pinned) - Automatically use a remote list of Python downloads to satisfy build versions not present in the latest embedded download metadata Some less important follow-ups to consider: - Allow using ranges for build version pins
This commit is contained in:
parent
b6f1fb7d3f
commit
9b8d6989d4
File diff suppressed because it is too large
Load Diff
|
|
@ -153,6 +153,7 @@ class PythonDownload:
|
|||
implementation: ImplementationName
|
||||
filename: str
|
||||
url: str
|
||||
build: str
|
||||
sha256: str | None = None
|
||||
build_options: list[str] = field(default_factory=list)
|
||||
variant: Variant | None = None
|
||||
|
|
@ -397,6 +398,7 @@ class CPythonFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=filename,
|
||||
url=url,
|
||||
build=str(release),
|
||||
build_options=build_options,
|
||||
variant=variant,
|
||||
sha256=sha256,
|
||||
|
|
@ -507,6 +509,7 @@ class PyPyFinder(Finder):
|
|||
python_version = Version.from_str(version["python_version"])
|
||||
if python_version < (3, 7, 0):
|
||||
continue
|
||||
pypy_version = version["pypy_version"]
|
||||
for file in version["files"]:
|
||||
arch = self._normalize_arch(file["arch"])
|
||||
platform = self._normalize_os(file["platform"])
|
||||
|
|
@ -523,6 +526,7 @@ class PyPyFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=file["filename"],
|
||||
url=file["download_url"],
|
||||
build=pypy_version,
|
||||
)
|
||||
# Only keep the latest pypy version of each arch/platform
|
||||
if (python_version, arch, platform) not in results:
|
||||
|
|
@ -612,6 +616,7 @@ class PyodideFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=asset["name"],
|
||||
url=url,
|
||||
build=pyodide_version,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -708,6 +713,7 @@ class GraalPyFinder(Finder):
|
|||
implementation=self.implementation,
|
||||
filename=asset["name"],
|
||||
url=url,
|
||||
build=graalpy_version,
|
||||
sha256=sha256,
|
||||
)
|
||||
# Only keep the latest GraalPy version of each arch/platform
|
||||
|
|
@ -811,6 +817,7 @@ def render(downloads: list[PythonDownload]) -> None:
|
|||
"url": download.url,
|
||||
"sha256": download.sha256,
|
||||
"variant": download.variant if download.variant else None,
|
||||
"build": download.build,
|
||||
}
|
||||
|
||||
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
|
|||
use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
|
||||
#[cfg(windows)]
|
||||
use crate::microsoft_store::find_microsoft_store_pythons;
|
||||
use crate::python_version::python_build_versions_from_env;
|
||||
use crate::virtualenv::Error as VirtualEnvError;
|
||||
use crate::virtualenv::{
|
||||
CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
|
||||
|
|
@ -263,6 +264,9 @@ pub enum Error {
|
|||
// TODO(zanieb): Is this error case necessary still? We should probably drop it.
|
||||
#[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
|
||||
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
|
||||
|
||||
#[error(transparent)]
|
||||
BuildVersion(#[from] crate::python_version::BuildVersionError),
|
||||
}
|
||||
|
||||
/// Lazily iterate over Python executables in mutable virtual environments.
|
||||
|
|
@ -342,6 +346,9 @@ fn python_executables_from_installed<'a>(
|
|||
installed_installations.root().user_display()
|
||||
);
|
||||
let installations = installed_installations.find_matching_current_platform()?;
|
||||
|
||||
let build_versions = python_build_versions_from_env()?;
|
||||
|
||||
// Check that the Python version and platform satisfy the request to avoid
|
||||
// unnecessary interpreter queries later
|
||||
Ok(installations
|
||||
|
|
@ -355,6 +362,22 @@ fn python_executables_from_installed<'a>(
|
|||
debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(requested_build) = build_versions.get(&installation.implementation()) {
|
||||
let Some(installation_build) = installation.build() else {
|
||||
debug!(
|
||||
"Skipping managed installation `{installation}`: a build version was requested but is not recorded for this installation"
|
||||
);
|
||||
return false;
|
||||
};
|
||||
if installation_build != requested_build {
|
||||
debug!(
|
||||
"Skipping managed installation `{installation}`: requested build version `{requested_build}` does not match installation build version `{installation_build}`"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.inspect(|installation| debug!("Found managed installation `{installation}`"))
|
||||
|
|
@ -1218,6 +1241,7 @@ pub fn find_python_installations<'a>(
|
|||
return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
|
||||
}
|
||||
}
|
||||
|
||||
Box::new({
|
||||
debug!("Searching for {request} in {sources}");
|
||||
python_interpreters(
|
||||
|
|
@ -1229,7 +1253,9 @@ pub fn find_python_installations<'a>(
|
|||
cache,
|
||||
preview,
|
||||
)
|
||||
.filter_ok(|(_source, interpreter)| request.satisfied_by_interpreter(interpreter))
|
||||
.filter_ok(move |(_source, interpreter)| {
|
||||
request.satisfied_by_interpreter(interpreter)
|
||||
})
|
||||
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
||||
})
|
||||
}
|
||||
|
|
@ -3186,6 +3212,7 @@ mod tests {
|
|||
arch: None,
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
|
@ -3205,6 +3232,7 @@ mod tests {
|
|||
))),
|
||||
os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
|
||||
libc: Some(Libc::None),
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
|
@ -3221,6 +3249,7 @@ mod tests {
|
|||
arch: None,
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
|
@ -3240,6 +3269,7 @@ mod tests {
|
|||
))),
|
||||
os: None,
|
||||
libc: None,
|
||||
build: None,
|
||||
prereleases: None
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ use crate::implementation::{
|
|||
};
|
||||
use crate::installation::PythonInstallationKey;
|
||||
use crate::managed::ManagedPythonInstallation;
|
||||
use crate::python_version::{BuildVersionError, python_build_version_from_env};
|
||||
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
|
|
@ -110,6 +111,8 @@ pub enum Error {
|
|||
url: Box<Url>,
|
||||
python_builds_dir: PathBuf,
|
||||
},
|
||||
#[error(transparent)]
|
||||
BuildVersion(#[from] BuildVersionError),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
|
@ -144,6 +147,7 @@ pub struct ManagedPythonDownload {
|
|||
key: PythonInstallationKey,
|
||||
url: Cow<'static, str>,
|
||||
sha256: Option<Cow<'static, str>>,
|
||||
build: Option<&'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
|
||||
|
|
@ -153,6 +157,7 @@ pub struct PythonDownloadRequest {
|
|||
pub(crate) arch: Option<ArchRequest>,
|
||||
pub(crate) os: Option<Os>,
|
||||
pub(crate) libc: Option<Libc>,
|
||||
pub(crate) build: Option<String>,
|
||||
|
||||
/// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is
|
||||
/// not None, and false otherwise.
|
||||
|
|
@ -255,6 +260,7 @@ impl PythonDownloadRequest {
|
|||
arch,
|
||||
os,
|
||||
libc,
|
||||
build: None,
|
||||
prereleases,
|
||||
}
|
||||
}
|
||||
|
|
@ -311,6 +317,12 @@ impl PythonDownloadRequest {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_build(mut self, build: String) -> Self {
|
||||
self.build = Some(build);
|
||||
self
|
||||
}
|
||||
|
||||
/// Construct a new [`PythonDownloadRequest`] from a [`PythonRequest`] if possible.
|
||||
///
|
||||
/// Returns [`None`] if the request kind is not compatible with a download, e.g., it is
|
||||
|
|
@ -356,11 +368,25 @@ impl PythonDownloadRequest {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Fill the build field from the environment variable relevant for the [`ImplementationName`].
|
||||
pub fn fill_build_from_env(mut self) -> Result<Self, Error> {
|
||||
if self.build.is_some() {
|
||||
return Ok(self);
|
||||
}
|
||||
let Some(implementation) = self.implementation else {
|
||||
return Ok(self);
|
||||
};
|
||||
|
||||
self.build = python_build_version_from_env(implementation)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn fill(mut self) -> Result<Self, Error> {
|
||||
if self.implementation.is_none() {
|
||||
self.implementation = Some(ImplementationName::CPython);
|
||||
}
|
||||
self = self.fill_platform()?;
|
||||
self = self.fill_build_from_env()?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
|
|
@ -434,7 +460,31 @@ impl PythonDownloadRequest {
|
|||
|
||||
/// Whether this request is satisfied by a Python download.
|
||||
pub fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
|
||||
self.satisfied_by_key(download.key())
|
||||
// First check the key
|
||||
if !self.satisfied_by_key(download.key()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Then check the build if specified
|
||||
if let Some(ref requested_build) = self.build {
|
||||
let Some(download_build) = download.build() else {
|
||||
debug!(
|
||||
"Skipping download `{}`: a build version was requested but is not available for this download",
|
||||
download
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
if download_build != requested_build {
|
||||
debug!(
|
||||
"Skipping download `{}`: requested build version `{}` does not match download build version `{}`",
|
||||
download, requested_build, download_build
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether this download request opts-in to pre-release Python versions.
|
||||
|
|
@ -753,6 +803,7 @@ struct JsonPythonDownload {
|
|||
url: String,
|
||||
sha256: Option<String>,
|
||||
variant: Option<String>,
|
||||
build: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
@ -767,7 +818,25 @@ pub enum DownloadResult {
|
|||
Fetched(PathBuf),
|
||||
}
|
||||
|
||||
/// A wrapper type to display a `ManagedPythonDownload` with its build information.
|
||||
pub struct ManagedPythonDownloadWithBuild<'a>(&'a ManagedPythonDownload);
|
||||
|
||||
impl Display for ManagedPythonDownloadWithBuild<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(build) = self.0.build {
|
||||
write!(f, "{}+{}", self.0.key, build)
|
||||
} else {
|
||||
write!(f, "{}", self.0.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManagedPythonDownload {
|
||||
/// Return a display type that includes the build information.
|
||||
pub fn to_display_with_build(&self) -> ManagedPythonDownloadWithBuild<'_> {
|
||||
ManagedPythonDownloadWithBuild(self)
|
||||
}
|
||||
|
||||
/// Return the first [`ManagedPythonDownload`] matching a request, if any.
|
||||
///
|
||||
/// If there is no stable version matching the request, a compatible pre-release version will
|
||||
|
|
@ -852,6 +921,10 @@ impl ManagedPythonDownload {
|
|||
self.sha256.as_ref()
|
||||
}
|
||||
|
||||
pub fn build(&self) -> Option<&'static str> {
|
||||
self.build
|
||||
}
|
||||
|
||||
/// Download and extract a Python distribution, retrying on failure.
|
||||
#[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
|
||||
pub async fn fetch_with_retry(
|
||||
|
|
@ -1345,6 +1418,9 @@ fn parse_json_downloads(
|
|||
|
||||
let url = Cow::Owned(entry.url);
|
||||
let sha256 = entry.sha256.map(Cow::Owned);
|
||||
let build = entry
|
||||
.build
|
||||
.map(|s| Box::leak(s.into_boxed_str()) as &'static str);
|
||||
|
||||
Some(ManagedPythonDownload {
|
||||
key: PythonInstallationKey::new_from_version(
|
||||
|
|
@ -1355,6 +1431,7 @@ fn parse_json_downloads(
|
|||
),
|
||||
url,
|
||||
sha256,
|
||||
build,
|
||||
})
|
||||
})
|
||||
.sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
|
||||
|
|
@ -1513,6 +1590,10 @@ async fn read_url(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::implementation::LenientImplementationName;
|
||||
use crate::installation::PythonInstallationKey;
|
||||
use uv_platform::{Arch, Libc, Os, Platform};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Parse a request with all of its fields.
|
||||
|
|
@ -1726,4 +1807,90 @@ mod tests {
|
|||
|
||||
assert!(matches!(result, Err(Error::TooManyParts(_))));
|
||||
}
|
||||
|
||||
/// Test that build filtering works correctly
|
||||
#[test]
|
||||
fn test_python_download_request_build_filtering() {
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_version(VersionRequest::from_str("3.12").unwrap())
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_build("20240814".to_string());
|
||||
|
||||
let downloads: Vec<_> = ManagedPythonDownload::iter_all(None)
|
||||
.unwrap()
|
||||
.filter(|d| request.satisfied_by_download(d))
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!downloads.is_empty(),
|
||||
"Should find at least one matching download"
|
||||
);
|
||||
for download in downloads {
|
||||
assert_eq!(download.build(), Some("20240814"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test that an invalid build results in no matches
|
||||
#[test]
|
||||
fn test_python_download_request_invalid_build() {
|
||||
// Create a request with a non-existent build
|
||||
let request = PythonDownloadRequest::default()
|
||||
.with_version(VersionRequest::from_str("3.12").unwrap())
|
||||
.with_implementation(ImplementationName::CPython)
|
||||
.with_build("99999999".to_string());
|
||||
|
||||
// Should find no matching downloads
|
||||
let downloads: Vec<_> = ManagedPythonDownload::iter_all(None)
|
||||
.unwrap()
|
||||
.filter(|d| request.satisfied_by_download(d))
|
||||
.collect();
|
||||
|
||||
assert_eq!(downloads.len(), 0);
|
||||
}
|
||||
|
||||
/// Test build display
|
||||
#[test]
|
||||
fn test_managed_python_download_build_display() {
|
||||
// Create a test download with a build
|
||||
let key = PythonInstallationKey::new(
|
||||
LenientImplementationName::Known(crate::implementation::ImplementationName::CPython),
|
||||
3,
|
||||
12,
|
||||
0,
|
||||
None,
|
||||
Platform::new(
|
||||
Os::from_str("linux").unwrap(),
|
||||
Arch::from_str("x86_64").unwrap(),
|
||||
Libc::from_str("gnu").unwrap(),
|
||||
),
|
||||
crate::PythonVariant::default(),
|
||||
);
|
||||
|
||||
let download_with_build = ManagedPythonDownload {
|
||||
key,
|
||||
url: Cow::Borrowed("https://example.com/python.tar.gz"),
|
||||
sha256: Some(Cow::Borrowed("abc123")),
|
||||
build: Some("20240101"),
|
||||
};
|
||||
|
||||
// Test display with build
|
||||
assert_eq!(
|
||||
download_with_build.to_display_with_build().to_string(),
|
||||
"cpython-3.12.0-linux-x86_64-gnu+20240101"
|
||||
);
|
||||
|
||||
// Test download without build
|
||||
let download_without_build = ManagedPythonDownload {
|
||||
key: download_with_build.key.clone(),
|
||||
url: Cow::Borrowed("https://example.com/python.tar.gz"),
|
||||
sha256: Some(Cow::Borrowed("abc123")),
|
||||
build: None,
|
||||
};
|
||||
|
||||
// Test display without build
|
||||
assert_eq!(
|
||||
download_without_build.to_display_with_build().to_string(),
|
||||
"cpython-3.12.0-linux-x86_64-gnu"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ impl PythonInstallation {
|
|||
installed.ensure_externally_managed()?;
|
||||
installed.ensure_sysconfig_patched()?;
|
||||
installed.ensure_canonical_executables()?;
|
||||
installed.ensure_build_file()?;
|
||||
|
||||
let minor_version = installed.minor_version_key();
|
||||
let highest_patch = installations
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ pub use crate::interpreter::{
|
|||
};
|
||||
pub use crate::pointer_size::PointerSize;
|
||||
pub use crate::prefix::Prefix;
|
||||
pub use crate::python_version::PythonVersion;
|
||||
pub use crate::python_version::{BuildVersionError, PythonVersion};
|
||||
pub use crate::target::Target;
|
||||
pub use crate::version_files::{
|
||||
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
||||
|
|
|
|||
|
|
@ -320,6 +320,10 @@ pub struct ManagedPythonInstallation {
|
|||
///
|
||||
/// Empty when self was constructed from a path.
|
||||
sha256: Option<Cow<'static, str>>,
|
||||
/// The build version of the Python installation.
|
||||
///
|
||||
/// Empty when self was constructed from a path without a BUILD file.
|
||||
build: Option<Cow<'static, str>>,
|
||||
}
|
||||
|
||||
impl ManagedPythonInstallation {
|
||||
|
|
@ -329,6 +333,7 @@ impl ManagedPythonInstallation {
|
|||
key: download.key().clone(),
|
||||
url: Some(download.url().clone()),
|
||||
sha256: download.sha256().cloned(),
|
||||
build: download.build().map(Cow::Borrowed),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -342,11 +347,19 @@ impl ManagedPythonInstallation {
|
|||
|
||||
let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
|
||||
|
||||
// Try to read the BUILD file if it exists
|
||||
let build = match fs::read_to_string(path.join("BUILD")) {
|
||||
Ok(content) => Some(Cow::Owned(content.trim().to_string())),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => None,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
path,
|
||||
key,
|
||||
url: None,
|
||||
sha256: None,
|
||||
build,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -455,6 +468,11 @@ impl ManagedPythonInstallation {
|
|||
self.key.platform()
|
||||
}
|
||||
|
||||
/// The build version of this installation, if available.
|
||||
pub fn build(&self) -> Option<&str> {
|
||||
self.build.as_deref()
|
||||
}
|
||||
|
||||
pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
|
||||
PythonInstallationMinorVersionKey::ref_cast(&self.key)
|
||||
}
|
||||
|
|
@ -618,6 +636,15 @@ impl ManagedPythonInstallation {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the build version is written to a BUILD file in the installation directory.
|
||||
pub fn ensure_build_file(&self) -> Result<(), Error> {
|
||||
if let Some(ref build) = self.build {
|
||||
let build_file = self.path.join("BUILD");
|
||||
fs::write(&build_file, build.as_ref())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a link to this installation's binary, e.g., as created by
|
||||
/// [`create_bin_link`].
|
||||
pub fn is_bin_link(&self, path: &Path) -> bool {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,24 @@
|
|||
#[cfg(feature = "schemars")]
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
|
||||
use thiserror::Error;
|
||||
use uv_pep440::Version;
|
||||
use uv_pep508::{MarkerEnvironment, StringVersion};
|
||||
use uv_static::EnvVars;
|
||||
|
||||
use crate::implementation::ImplementationName;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BuildVersionError {
|
||||
#[error("`{0}` is not valid unicode: {1:?}")]
|
||||
NotUnicode(&'static str, OsString),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PythonVersion(StringVersion);
|
||||
|
|
@ -206,6 +219,51 @@ impl PythonVersion {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the environment variable name for the build constraint for a given implementation.
|
||||
pub(crate) fn python_build_version_variable(implementation: ImplementationName) -> &'static str {
|
||||
match implementation {
|
||||
ImplementationName::CPython => EnvVars::UV_PYTHON_CPYTHON_BUILD,
|
||||
ImplementationName::PyPy => EnvVars::UV_PYTHON_PYPY_BUILD,
|
||||
ImplementationName::GraalPy => EnvVars::UV_PYTHON_GRAALPY_BUILD,
|
||||
ImplementationName::Pyodide => EnvVars::UV_PYTHON_PYODIDE_BUILD,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the build version number from the environment variable for a given implementation.
|
||||
pub(crate) fn python_build_version_from_env(
|
||||
implementation: ImplementationName,
|
||||
) -> Result<Option<String>, BuildVersionError> {
|
||||
let variable = python_build_version_variable(implementation);
|
||||
|
||||
let Some(build_os) = env::var_os(variable) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let build = build_os
|
||||
.into_string()
|
||||
.map_err(|raw| BuildVersionError::NotUnicode(variable, raw))?;
|
||||
|
||||
let trimmed = build.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(trimmed.to_string()))
|
||||
}
|
||||
|
||||
/// Get the build version numbers for all Python implementations.
|
||||
pub(crate) fn python_build_versions_from_env()
|
||||
-> Result<BTreeMap<ImplementationName, String>, BuildVersionError> {
|
||||
let mut versions = BTreeMap::new();
|
||||
for implementation in ImplementationName::iter_all() {
|
||||
let Some(build) = python_build_version_from_env(implementation)? else {
|
||||
continue;
|
||||
};
|
||||
versions.insert(implementation, build);
|
||||
}
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
|
|
|||
|
|
@ -332,6 +332,26 @@ impl EnvVars {
|
|||
/// Distributions can be read from a local directory by using the `file://` URL scheme.
|
||||
pub const UV_PYPY_INSTALL_MIRROR: &'static str = "UV_PYPY_INSTALL_MIRROR";
|
||||
|
||||
/// Pin managed CPython versions to a specific build version.
|
||||
///
|
||||
/// For CPython, this should be the build date (e.g., "20250814").
|
||||
pub const UV_PYTHON_CPYTHON_BUILD: &'static str = "UV_PYTHON_CPYTHON_BUILD";
|
||||
|
||||
/// Pin managed PyPy versions to a specific build version.
|
||||
///
|
||||
/// For PyPy, this should be the PyPy version (e.g., "7.3.20").
|
||||
pub const UV_PYTHON_PYPY_BUILD: &'static str = "UV_PYTHON_PYPY_BUILD";
|
||||
|
||||
/// Pin managed GraalPy versions to a specific build version.
|
||||
///
|
||||
/// For GraalPy, this should be the GraalPy version (e.g., "24.2.2").
|
||||
pub const UV_PYTHON_GRAALPY_BUILD: &'static str = "UV_PYTHON_GRAALPY_BUILD";
|
||||
|
||||
/// Pin managed Pyodide versions to a specific build version.
|
||||
///
|
||||
/// For Pyodide, this should be the Pyodide version (e.g., "0.28.1").
|
||||
pub const UV_PYTHON_PYODIDE_BUILD: &'static str = "UV_PYTHON_PYODIDE_BUILD";
|
||||
|
||||
/// Equivalent to the `--clear` command-line argument. If set, uv will remove any
|
||||
/// existing files or directories at the target path.
|
||||
pub const UV_VENV_CLEAR: &'static str = "UV_VENV_CLEAR";
|
||||
|
|
|
|||
|
|
@ -483,6 +483,7 @@ pub(crate) async fn install(
|
|||
installation.ensure_externally_managed()?;
|
||||
installation.ensure_sysconfig_patched()?;
|
||||
installation.ensure_canonical_executables()?;
|
||||
installation.ensure_build_file()?;
|
||||
if let Err(e) = installation.ensure_dylib_patched() {
|
||||
e.warn_user(installation);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,28 +220,32 @@ impl TestContext {
|
|||
/// and `.exe` suffixes.
|
||||
#[must_use]
|
||||
pub fn with_filtered_python_names(mut self) -> Self {
|
||||
use env::consts::EXE_SUFFIX;
|
||||
let exe_suffix = regex::escape(EXE_SUFFIX);
|
||||
for name in ["python", "pypy"] {
|
||||
// Note we strip version numbers from the executable names because, e.g., on Windows
|
||||
// `python.exe` is the equivalent to a Unix `python3.12`.`
|
||||
let suffix = if cfg!(windows) {
|
||||
// On Windows, we'll require a `.exe` suffix for disambiguation
|
||||
// We'll also strip version numbers if present, which is not common for `python.exe`
|
||||
// but can occur for, e.g., `pypy3.12.exe`
|
||||
let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
|
||||
format!(r"(\d\.\d+|\d)?{exe_suffix}")
|
||||
} else {
|
||||
// On Unix, we'll strip version numbers
|
||||
if name == "python" {
|
||||
// We can't require them in this case since `/python` is common
|
||||
r"(\d\.\d+|\d)?".to_string()
|
||||
} else {
|
||||
// However, for other names we'll require them to avoid over-matching
|
||||
r"(\d\.\d+|\d)".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
self.filters.push((
|
||||
format!(r"python\d.\d\d{exe_suffix}"),
|
||||
"[PYTHON]".to_string(),
|
||||
// We use a leading path separator to help disambiguate cases where the name is not
|
||||
// used in a path.
|
||||
format!(r"[\\/]{name}{suffix}"),
|
||||
format!("/[{}]", name.to_uppercase()),
|
||||
));
|
||||
self.filters
|
||||
.push((format!(r"python\d{exe_suffix}"), "[PYTHON]".to_string()));
|
||||
|
||||
if cfg!(windows) {
|
||||
// On Windows, we want to filter out all `python.exe` instances
|
||||
self.filters
|
||||
.push((format!(r"python{exe_suffix}"), "[PYTHON]".to_string()));
|
||||
// Including ones where we'd already stripped the `.exe` in another filter
|
||||
self.filters
|
||||
.push((r"[\\/]python".to_string(), "/[PYTHON]".to_string()));
|
||||
} else {
|
||||
// On Unix, it's a little trickier — we don't want to clobber use of `python` in the
|
||||
// middle of something else, e.g., `cpython`. For this reason, we require a leading `/`.
|
||||
self.filters
|
||||
.push((format!(r"/python{exe_suffix}"), "/[PYTHON]".to_string()));
|
||||
}
|
||||
|
||||
self
|
||||
|
|
@ -270,15 +274,34 @@ impl TestContext {
|
|||
/// the virtual environment equivalent.
|
||||
#[must_use]
|
||||
pub fn with_filtered_python_install_bin(mut self) -> Self {
|
||||
// We don't want to eagerly match paths that aren't actually Python executables, so we
|
||||
// do our best to detect that case
|
||||
let suffix = if cfg!(windows) {
|
||||
let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
|
||||
// On Windows, we usually don't have a version attached but we might, e.g., for pypy3.12
|
||||
format!(r"(\d\.\d+|\d)?{exe_suffix}")
|
||||
} else {
|
||||
// On Unix, we'll require a version to be attached to avoid over-matching
|
||||
r"\d\.\d+|\d".to_string()
|
||||
};
|
||||
|
||||
if cfg!(unix) {
|
||||
self.filters.push((
|
||||
r"[\\/]bin/python".to_string(),
|
||||
"/[INSTALL-BIN]/python".to_string(),
|
||||
format!(r"[\\/]bin/python({suffix})"),
|
||||
"/[INSTALL-BIN]/python$1".to_string(),
|
||||
));
|
||||
self.filters.push((
|
||||
format!(r"[\\/]bin/pypy({suffix})"),
|
||||
"/[INSTALL-BIN]/pypy$1".to_string(),
|
||||
));
|
||||
} else {
|
||||
self.filters.push((
|
||||
r"[\\/]python".to_string(),
|
||||
"/[INSTALL-BIN]/python".to_string(),
|
||||
format!(r"[\\/]python({suffix})"),
|
||||
"/[INSTALL-BIN]/python$1".to_string(),
|
||||
));
|
||||
self.filters.push((
|
||||
format!(r"[\\/]pypy({suffix})"),
|
||||
"/[INSTALL-BIN]/pypy$1".to_string(),
|
||||
));
|
||||
}
|
||||
self
|
||||
|
|
|
|||
|
|
@ -56,14 +56,14 @@ fn missing_venv() -> Result<()> {
|
|||
assert!(predicates::path::missing().eval(&context.venv));
|
||||
|
||||
// If not "active", we hint to create one
|
||||
uv_snapshot!(context.filters(), context.pip_sync().arg("requirements.txt").env_remove(EnvVars::VIRTUAL_ENV), @r###"
|
||||
uv_snapshot!(context.filters(), context.pip_sync().arg("requirements.txt").env_remove(EnvVars::VIRTUAL_ENV), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment
|
||||
"###);
|
||||
");
|
||||
|
||||
assert!(predicates::path::missing().eval(&context.venv));
|
||||
|
||||
|
|
@ -5342,7 +5342,7 @@ fn target_no_build_isolation() -> Result<()> {
|
|||
requirements_in.write_str("flit_core")?;
|
||||
|
||||
uv_snapshot!(context.filters(), context.pip_sync()
|
||||
.arg("requirements.in"), @r###"
|
||||
.arg("requirements.in"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -5352,7 +5352,7 @@ fn target_no_build_isolation() -> Result<()> {
|
|||
Prepared 1 package in [TIME]
|
||||
Installed 1 package in [TIME]
|
||||
+ flit-core==3.9.0
|
||||
"###);
|
||||
");
|
||||
|
||||
// Install `iniconfig` to the target directory.
|
||||
let requirements_in = context.temp_dir.child("requirements.in");
|
||||
|
|
|
|||
|
|
@ -412,13 +412,13 @@ fn python_find_venv() {
|
|||
.with_filtered_virtualenv_bin();
|
||||
|
||||
// Create a virtual environment
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.12").arg("-q"), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.12").arg("-q"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
// We should find it first
|
||||
// TODO(zanieb): On Windows, this has in a different display path for virtual environments which
|
||||
|
|
@ -449,28 +449,28 @@ fn python_find_venv() {
|
|||
child_dir.create_dir_all().unwrap();
|
||||
|
||||
// Unless the system flag is passed
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--system"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--system"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.11]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
// Or, `UV_SYSTEM_PYTHON` is set
|
||||
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_SYSTEM_PYTHON, "1"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_SYSTEM_PYTHON, "1"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.11]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
// Unless, `--no-system` is included
|
||||
// TODO(zanieb): Report this as a bug upstream — this should be allowed.
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-system").env(EnvVars::UV_SYSTEM_PYTHON, "1"), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-system").env(EnvVars::UV_SYSTEM_PYTHON, "1"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
|
@ -481,7 +481,7 @@ fn python_find_venv() {
|
|||
Usage: uv python find --cache-dir [CACHE_DIR] [REQUEST]
|
||||
|
||||
For more information, try '--help'.
|
||||
"###);
|
||||
");
|
||||
|
||||
// We should find virtual environments from a child directory
|
||||
#[cfg(not(windows))]
|
||||
|
|
@ -495,13 +495,13 @@ fn python_find_venv() {
|
|||
");
|
||||
|
||||
// A virtual environment in the child directory takes precedence over the parent
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11").arg("-q").current_dir(&child_dir), @r###"
|
||||
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("3.11").arg("-q").current_dir(&child_dir), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
#[cfg(not(windows))]
|
||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir).env_remove(EnvVars::VIRTUAL_ENV), @r"
|
||||
|
|
@ -706,26 +706,26 @@ fn python_find_venv_invalid() {
|
|||
");
|
||||
|
||||
// Unless the virtual environment is not active
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find(), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.12]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
// If there's not a `pyvenv.cfg` file, it's also non-fatal, we ignore the environment
|
||||
fs_err::remove_file(context.venv.join("pyvenv.cfg")).unwrap();
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()), @r###"
|
||||
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::VIRTUAL_ENV, context.venv.as_os_str()), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[PYTHON-3.12]
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -857,14 +857,14 @@ fn python_find_script() {
|
|||
.with_filtered_python_names()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
uv_snapshot!(context.filters(), context.init().arg("--script").arg("foo.py"), @r###"
|
||||
uv_snapshot!(context.filters(), context.init().arg("--script").arg("foo.py"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Initialized script at `foo.py`
|
||||
"###);
|
||||
");
|
||||
|
||||
uv_snapshot!(context.filters(), context.sync().arg("--script").arg("foo.py"), @r"
|
||||
success: true
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use std::path::PathBuf;
|
|||
use std::{env, path::Path, process::Command};
|
||||
|
||||
use crate::common::{TestContext, uv_snapshot};
|
||||
use assert_cmd::assert::OutputAssertExt;
|
||||
use assert_fs::{
|
||||
assert::PathAssert,
|
||||
prelude::{FileTouch, FileWriteStr, PathChild, PathCreateDir},
|
||||
|
|
@ -2062,8 +2063,9 @@ fn python_install_314() {
|
|||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
.with_filtered_python_keys()
|
||||
.with_managed_python_dirs()
|
||||
.with_filtered_exe_suffix()
|
||||
.with_python_download_cache();
|
||||
.with_python_download_cache()
|
||||
.with_filtered_python_install_bin()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
// Install 3.14
|
||||
// For now, this provides test coverage of pre-release handling
|
||||
|
|
@ -2087,17 +2089,21 @@ fn python_install_314() {
|
|||
Installed Python 3.14.0a4 in [TIME]
|
||||
+ cpython-3.14.0a4-[PLATFORM]
|
||||
");
|
||||
}
|
||||
|
||||
// Add name filtering for the `find` tests, we avoid it in `install` tests because it clobbers
|
||||
// the version suffixes which matter in the install logs
|
||||
let filters = context
|
||||
.filters()
|
||||
.iter()
|
||||
.map(|(a, b)| ((*a).to_string(), (*b).to_string()))
|
||||
.collect::<Vec<_>>();
|
||||
let context = context
|
||||
#[test]
|
||||
fn python_find_314() {
|
||||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
.with_filtered_python_keys()
|
||||
.with_managed_python_dirs()
|
||||
.with_python_download_cache()
|
||||
.with_filtered_python_install_bin()
|
||||
.with_filtered_python_names();
|
||||
.with_filtered_python_names()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
// See [`python_install_314`] coverage of these.
|
||||
context.python_install().arg("3.14").assert().success();
|
||||
context.python_install().arg("3.14.0a4").assert().success();
|
||||
|
||||
// We should be able to find this version without opt-in, because there is no stable release
|
||||
// installed
|
||||
|
|
@ -2130,7 +2136,7 @@ fn python_install_314() {
|
|||
");
|
||||
|
||||
// If we install a stable version, that should be preferred though
|
||||
uv_snapshot!(filters, context.python_install().arg("3.13"), @r"
|
||||
uv_snapshot!(context.filters(), context.python_install().arg("3.13"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -3158,3 +3164,157 @@ fn python_install_pyodide() {
|
|||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_install_build_version() {
|
||||
use uv_python::managed::platform_key_from_env;
|
||||
|
||||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
.with_filtered_python_keys()
|
||||
.with_managed_python_dirs()
|
||||
.with_python_download_cache()
|
||||
.with_filtered_python_sources()
|
||||
.with_filtered_python_install_bin()
|
||||
.with_filtered_python_names()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_install()
|
||||
.arg("3.12")
|
||||
.env(EnvVars::UV_PYTHON_CPYTHON_BUILD, "20240814"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Installed Python 3.12.5 in [TIME]
|
||||
+ cpython-3.12.5-[PLATFORM] (python3.12)
|
||||
");
|
||||
|
||||
// A BUILD file should be present with the version
|
||||
let cpython_dir = context.temp_dir.child("managed").child(format!(
|
||||
"cpython-3.12.5-{}",
|
||||
platform_key_from_env().unwrap()
|
||||
));
|
||||
let build_file_path = cpython_dir.join("BUILD");
|
||||
let build_content = fs_err::read_to_string(&build_file_path).unwrap();
|
||||
assert_eq!(build_content, "20240814");
|
||||
|
||||
// We should find the build
|
||||
uv_snapshot!(context.filters(), context.python_find()
|
||||
.arg("3.12")
|
||||
.env(EnvVars::UV_PYTHON_CPYTHON_BUILD, "20240814"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[TEMP_DIR]/managed/cpython-3.12.5-[PLATFORM]/[INSTALL-BIN]/[PYTHON]
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// If the build number does not match, we should ignore the installation
|
||||
uv_snapshot!(context.filters(), context.python_find()
|
||||
.arg("3.12")
|
||||
.env(EnvVars::UV_PYTHON_CPYTHON_BUILD, "99999999"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for Python 3.12 in [PYTHON SOURCES]
|
||||
");
|
||||
|
||||
// If there's no install for a build number, we should fail
|
||||
uv_snapshot!(context.filters(), context.python_install()
|
||||
.arg("3.12")
|
||||
.env(EnvVars::UV_PYTHON_CPYTHON_BUILD, "99999999"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No download found for request: cpython-3.12-[PLATFORM]
|
||||
");
|
||||
|
||||
// Requesting a specific patch version without a matching build number should fail
|
||||
uv_snapshot!(context.filters(), context.python_install()
|
||||
.arg("3.12.10")
|
||||
.env(EnvVars::UV_PYTHON_CPYTHON_BUILD, "20250814"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No download found for request: cpython-3.12.10-[PLATFORM]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_install_build_version_pypy() {
|
||||
use uv_python::managed::platform_key_from_env;
|
||||
|
||||
let context: TestContext = TestContext::new_with_versions(&[])
|
||||
.with_filtered_python_keys()
|
||||
.with_filtered_python_sources()
|
||||
.with_managed_python_dirs()
|
||||
.with_python_download_cache()
|
||||
.with_filtered_python_install_bin()
|
||||
.with_filtered_python_names()
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
uv_snapshot!(context.filters(), context.python_install()
|
||||
.arg("pypy3.10")
|
||||
.env(EnvVars::UV_PYTHON_PYPY_BUILD, "7.3.19"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Installed Python 3.10.16 in [TIME]
|
||||
+ pypy-3.10.16-[PLATFORM] (python3.10)
|
||||
");
|
||||
|
||||
// A BUILD file should be present with the version
|
||||
let pypy_dir = context
|
||||
.temp_dir
|
||||
.child("managed")
|
||||
.child(format!("pypy-3.10.16-{}", platform_key_from_env().unwrap()));
|
||||
let build_file_path = pypy_dir.join("BUILD");
|
||||
let build_content = fs_err::read_to_string(&build_file_path).unwrap();
|
||||
assert_eq!(build_content, "7.3.19");
|
||||
|
||||
// We should find the build
|
||||
uv_snapshot!(context.filters(), context.python_find()
|
||||
.arg("pypy3.10")
|
||||
.env(EnvVars::UV_PYTHON_PYPY_BUILD, "7.3.19"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
[TEMP_DIR]/managed/pypy-3.10.16-[PLATFORM]/[INSTALL-BIN]/[PYPY]
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// If the build number does not match, we should ignore the installation
|
||||
uv_snapshot!(context.filters(), context.python_find()
|
||||
.arg("pypy3.10")
|
||||
.env(EnvVars::UV_PYTHON_PYPY_BUILD, "99.99.99"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No interpreter found for PyPy 3.10 in [PYTHON SOURCES]
|
||||
");
|
||||
|
||||
// If there's no install for a build number, we should fail
|
||||
uv_snapshot!(context.filters(), context.python_install()
|
||||
.arg("pypy3.10")
|
||||
.env(EnvVars::UV_PYTHON_PYPY_BUILD, "99.99.99"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No download found for request: pypy-3.10-[PLATFORM]
|
||||
");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3332,14 +3332,14 @@ fn run_project_toml_error() -> Result<()> {
|
|||
init.touch()?;
|
||||
|
||||
// `run` should fail
|
||||
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r###"
|
||||
uv_snapshot!(context.filters(), context.run().arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: No `project` table found in: `[TEMP_DIR]/pyproject.toml`
|
||||
"###);
|
||||
");
|
||||
|
||||
// `run --no-project` should not
|
||||
uv_snapshot!(context.filters(), context.run().arg("--no-project").arg("python").arg("-c").arg("import sys; print(sys.executable)"), @r"
|
||||
|
|
@ -4086,7 +4086,7 @@ fn run_linked_environment_path() -> Result<()> {
|
|||
|
||||
// Running `uv sync` should use the environment at `target``
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r###"
|
||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
|
@ -4101,7 +4101,7 @@ fn run_linked_environment_path() -> Result<()> {
|
|||
+ packaging==24.0
|
||||
+ pathspec==0.12.1
|
||||
+ platformdirs==4.2.0
|
||||
"###);
|
||||
");
|
||||
|
||||
// `sys.prefix` and `sys.executable` should be from the `target` directory
|
||||
uv_snapshot!(context.filters(), context.run()
|
||||
|
|
|
|||
|
|
@ -422,7 +422,7 @@ fn sync_json() -> Result<()> {
|
|||
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
.arg("--locked")
|
||||
.arg("--output-format").arg("json"), @r###"
|
||||
.arg("--output-format").arg("json"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
|
@ -430,7 +430,7 @@ fn sync_json() -> Result<()> {
|
|||
----- stderr -----
|
||||
Resolved 2 packages in [TIME]
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. To update the lockfile, run `uv lock`.
|
||||
"###);
|
||||
");
|
||||
|
||||
// Test that JSON output is shown even with --quiet flag
|
||||
uv_snapshot!(context.filters(), context.sync()
|
||||
|
|
@ -6367,7 +6367,7 @@ fn sync_active_script_environment() -> Result<()> {
|
|||
fn sync_active_script_environment_json() -> Result<()> {
|
||||
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
||||
.with_filtered_virtualenv_bin()
|
||||
.with_filtered_python_names();
|
||||
.with_filtered_exe_suffix();
|
||||
|
||||
let script = context.temp_dir.child("script.py");
|
||||
script.write_str(indoc! { r#"
|
||||
|
|
@ -6402,7 +6402,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
|||
"environment": {
|
||||
"path": "[CACHE_DIR]/environments-v2/script-[HASH]",
|
||||
"python": {
|
||||
"path": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/[PYTHON]",
|
||||
"path": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python",
|
||||
"version": "3.11.[X]",
|
||||
"implementation": "cpython"
|
||||
}
|
||||
|
|
@ -6448,7 +6448,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
|||
"environment": {
|
||||
"path": "[TEMP_DIR]/foo",
|
||||
"python": {
|
||||
"path": "[TEMP_DIR]/foo/[BIN]/[PYTHON]",
|
||||
"path": "[TEMP_DIR]/foo/[BIN]/python",
|
||||
"version": "3.11.[X]",
|
||||
"implementation": "cpython"
|
||||
}
|
||||
|
|
@ -6507,7 +6507,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
|||
"environment": {
|
||||
"path": "[TEMP_DIR]/foo",
|
||||
"python": {
|
||||
"path": "[TEMP_DIR]/foo/[BIN]/[PYTHON]",
|
||||
"path": "[TEMP_DIR]/foo/[BIN]/python",
|
||||
"version": "3.12.[X]",
|
||||
"implementation": "cpython"
|
||||
}
|
||||
|
|
@ -8076,14 +8076,14 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
// If the directory already exists and is not a virtual environment we should fail with an error
|
||||
fs_err::create_dir(context.temp_dir.join(".venv"))?;
|
||||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found)
|
||||
"###);
|
||||
");
|
||||
|
||||
// But if it's just an incompatible virtual environment...
|
||||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
||||
|
|
@ -8178,7 +8178,7 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||||
|
||||
// We should never delete it
|
||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
||||
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
|
@ -8186,7 +8186,7 @@ fn sync_invalid_environment() -> Result<()> {
|
|||
----- stderr -----
|
||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
||||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a compatible environment but cannot be recreated because it is not a virtual environment
|
||||
"###);
|
||||
");
|
||||
|
||||
// Even if there's no Python executable
|
||||
fs_err::remove_dir_all(&bin)?;
|
||||
|
|
|
|||
|
|
@ -410,6 +410,12 @@ Specifies the directory to place links to installed, managed Python executables.
|
|||
Specifies the directory for caching the archives of managed Python installations before
|
||||
installation.
|
||||
|
||||
### `UV_PYTHON_CPYTHON_BUILD`
|
||||
|
||||
Pin managed CPython versions to a specific build version.
|
||||
|
||||
For CPython, this should be the build date (e.g., "20250814").
|
||||
|
||||
### `UV_PYTHON_DOWNLOADS`
|
||||
|
||||
Equivalent to the
|
||||
|
|
@ -425,6 +431,12 @@ This will allow for setting each property of the Python installation, mostly the
|
|||
|
||||
Note that currently, only local paths are supported.
|
||||
|
||||
### `UV_PYTHON_GRAALPY_BUILD`
|
||||
|
||||
Pin managed GraalPy versions to a specific build version.
|
||||
|
||||
For GraalPy, this should be the GraalPy version (e.g., "24.2.2").
|
||||
|
||||
### `UV_PYTHON_INSTALL_BIN`
|
||||
|
||||
Whether to install the Python executable into the `UV_PYTHON_BIN_DIR` directory.
|
||||
|
|
@ -451,6 +463,18 @@ Whether to install the Python executable into the Windows registry.
|
|||
|
||||
Whether uv should prefer system or managed Python versions.
|
||||
|
||||
### `UV_PYTHON_PYODIDE_BUILD`
|
||||
|
||||
Pin managed Pyodide versions to a specific build version.
|
||||
|
||||
For Pyodide, this should be the Pyodide version (e.g., "0.28.1").
|
||||
|
||||
### `UV_PYTHON_PYPY_BUILD`
|
||||
|
||||
Pin managed PyPy versions to a specific build version.
|
||||
|
||||
For PyPy, this should be the PyPy version (e.g., "7.3.20").
|
||||
|
||||
### `UV_REQUEST_TIMEOUT`
|
||||
|
||||
Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`.
|
||||
|
|
|
|||
Loading…
Reference in New Issue