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
|
implementation: ImplementationName
|
||||||
filename: str
|
filename: str
|
||||||
url: str
|
url: str
|
||||||
|
build: str
|
||||||
sha256: str | None = None
|
sha256: str | None = None
|
||||||
build_options: list[str] = field(default_factory=list)
|
build_options: list[str] = field(default_factory=list)
|
||||||
variant: Variant | None = None
|
variant: Variant | None = None
|
||||||
|
|
@ -397,6 +398,7 @@ class CPythonFinder(Finder):
|
||||||
implementation=self.implementation,
|
implementation=self.implementation,
|
||||||
filename=filename,
|
filename=filename,
|
||||||
url=url,
|
url=url,
|
||||||
|
build=str(release),
|
||||||
build_options=build_options,
|
build_options=build_options,
|
||||||
variant=variant,
|
variant=variant,
|
||||||
sha256=sha256,
|
sha256=sha256,
|
||||||
|
|
@ -507,6 +509,7 @@ class PyPyFinder(Finder):
|
||||||
python_version = Version.from_str(version["python_version"])
|
python_version = Version.from_str(version["python_version"])
|
||||||
if python_version < (3, 7, 0):
|
if python_version < (3, 7, 0):
|
||||||
continue
|
continue
|
||||||
|
pypy_version = version["pypy_version"]
|
||||||
for file in version["files"]:
|
for file in version["files"]:
|
||||||
arch = self._normalize_arch(file["arch"])
|
arch = self._normalize_arch(file["arch"])
|
||||||
platform = self._normalize_os(file["platform"])
|
platform = self._normalize_os(file["platform"])
|
||||||
|
|
@ -523,6 +526,7 @@ class PyPyFinder(Finder):
|
||||||
implementation=self.implementation,
|
implementation=self.implementation,
|
||||||
filename=file["filename"],
|
filename=file["filename"],
|
||||||
url=file["download_url"],
|
url=file["download_url"],
|
||||||
|
build=pypy_version,
|
||||||
)
|
)
|
||||||
# Only keep the latest pypy version of each arch/platform
|
# Only keep the latest pypy version of each arch/platform
|
||||||
if (python_version, arch, platform) not in results:
|
if (python_version, arch, platform) not in results:
|
||||||
|
|
@ -612,6 +616,7 @@ class PyodideFinder(Finder):
|
||||||
implementation=self.implementation,
|
implementation=self.implementation,
|
||||||
filename=asset["name"],
|
filename=asset["name"],
|
||||||
url=url,
|
url=url,
|
||||||
|
build=pyodide_version,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -708,6 +713,7 @@ class GraalPyFinder(Finder):
|
||||||
implementation=self.implementation,
|
implementation=self.implementation,
|
||||||
filename=asset["name"],
|
filename=asset["name"],
|
||||||
url=url,
|
url=url,
|
||||||
|
build=graalpy_version,
|
||||||
sha256=sha256,
|
sha256=sha256,
|
||||||
)
|
)
|
||||||
# Only keep the latest GraalPy version of each arch/platform
|
# Only keep the latest GraalPy version of each arch/platform
|
||||||
|
|
@ -811,6 +817,7 @@ def render(downloads: list[PythonDownload]) -> None:
|
||||||
"url": download.url,
|
"url": download.url,
|
||||||
"sha256": download.sha256,
|
"sha256": download.sha256,
|
||||||
"variant": download.variant if download.variant else None,
|
"variant": download.variant if download.variant else None,
|
||||||
|
"build": download.build,
|
||||||
}
|
}
|
||||||
|
|
||||||
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
VERSIONS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
|
||||||
use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
|
use crate::managed::{ManagedPythonInstallations, PythonMinorVersionLink};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use crate::microsoft_store::find_microsoft_store_pythons;
|
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::Error as VirtualEnvError;
|
||||||
use crate::virtualenv::{
|
use crate::virtualenv::{
|
||||||
CondaEnvironmentKind, conda_environment_from_env, virtualenv_from_env,
|
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.
|
// 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")]
|
#[error("Interpreter discovery for `{0}` requires `{1}` but only `{2}` is allowed")]
|
||||||
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
|
SourceNotAllowed(PythonRequest, PythonSource, PythonPreference),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
BuildVersion(#[from] crate::python_version::BuildVersionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lazily iterate over Python executables in mutable virtual environments.
|
/// Lazily iterate over Python executables in mutable virtual environments.
|
||||||
|
|
@ -342,6 +346,9 @@ fn python_executables_from_installed<'a>(
|
||||||
installed_installations.root().user_display()
|
installed_installations.root().user_display()
|
||||||
);
|
);
|
||||||
let installations = installed_installations.find_matching_current_platform()?;
|
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
|
// Check that the Python version and platform satisfy the request to avoid
|
||||||
// unnecessary interpreter queries later
|
// unnecessary interpreter queries later
|
||||||
Ok(installations
|
Ok(installations
|
||||||
|
|
@ -355,6 +362,22 @@ fn python_executables_from_installed<'a>(
|
||||||
debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
|
debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
|
||||||
return false;
|
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
|
true
|
||||||
})
|
})
|
||||||
.inspect(|installation| debug!("Found managed installation `{installation}`"))
|
.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))));
|
return Box::new(iter::once(Err(Error::InvalidVersionRequest(err))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box::new({
|
Box::new({
|
||||||
debug!("Searching for {request} in {sources}");
|
debug!("Searching for {request} in {sources}");
|
||||||
python_interpreters(
|
python_interpreters(
|
||||||
|
|
@ -1229,7 +1253,9 @@ pub fn find_python_installations<'a>(
|
||||||
cache,
|
cache,
|
||||||
preview,
|
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)))
|
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -3186,6 +3212,7 @@ mod tests {
|
||||||
arch: None,
|
arch: None,
|
||||||
os: None,
|
os: None,
|
||||||
libc: None,
|
libc: None,
|
||||||
|
build: None,
|
||||||
prereleases: None
|
prereleases: None
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -3205,6 +3232,7 @@ mod tests {
|
||||||
))),
|
))),
|
||||||
os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
|
os: Some(Os::new(target_lexicon::OperatingSystem::Darwin(None))),
|
||||||
libc: Some(Libc::None),
|
libc: Some(Libc::None),
|
||||||
|
build: None,
|
||||||
prereleases: None
|
prereleases: None
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -3221,6 +3249,7 @@ mod tests {
|
||||||
arch: None,
|
arch: None,
|
||||||
os: None,
|
os: None,
|
||||||
libc: None,
|
libc: None,
|
||||||
|
build: None,
|
||||||
prereleases: None
|
prereleases: None
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -3240,6 +3269,7 @@ mod tests {
|
||||||
))),
|
))),
|
||||||
os: None,
|
os: None,
|
||||||
libc: None,
|
libc: None,
|
||||||
|
build: None,
|
||||||
prereleases: None
|
prereleases: None
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ use crate::implementation::{
|
||||||
};
|
};
|
||||||
use crate::installation::PythonInstallationKey;
|
use crate::installation::PythonInstallationKey;
|
||||||
use crate::managed::ManagedPythonInstallation;
|
use crate::managed::ManagedPythonInstallation;
|
||||||
|
use crate::python_version::{BuildVersionError, python_build_version_from_env};
|
||||||
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
use crate::{Interpreter, PythonRequest, PythonVersion, VersionRequest};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
|
@ -110,6 +111,8 @@ pub enum Error {
|
||||||
url: Box<Url>,
|
url: Box<Url>,
|
||||||
python_builds_dir: PathBuf,
|
python_builds_dir: PathBuf,
|
||||||
},
|
},
|
||||||
|
#[error(transparent)]
|
||||||
|
BuildVersion(#[from] BuildVersionError),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
|
|
@ -144,6 +147,7 @@ pub struct ManagedPythonDownload {
|
||||||
key: PythonInstallationKey,
|
key: PythonInstallationKey,
|
||||||
url: Cow<'static, str>,
|
url: Cow<'static, str>,
|
||||||
sha256: Option<Cow<'static, str>>,
|
sha256: Option<Cow<'static, str>>,
|
||||||
|
build: Option<&'static str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Default, Eq, PartialEq, Hash)]
|
||||||
|
|
@ -153,6 +157,7 @@ pub struct PythonDownloadRequest {
|
||||||
pub(crate) arch: Option<ArchRequest>,
|
pub(crate) arch: Option<ArchRequest>,
|
||||||
pub(crate) os: Option<Os>,
|
pub(crate) os: Option<Os>,
|
||||||
pub(crate) libc: Option<Libc>,
|
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
|
/// Whether to allow pre-releases or not. If not set, defaults to true if [`Self::version`] is
|
||||||
/// not None, and false otherwise.
|
/// not None, and false otherwise.
|
||||||
|
|
@ -255,6 +260,7 @@ impl PythonDownloadRequest {
|
||||||
arch,
|
arch,
|
||||||
os,
|
os,
|
||||||
libc,
|
libc,
|
||||||
|
build: None,
|
||||||
prereleases,
|
prereleases,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -311,6 +317,12 @@ impl PythonDownloadRequest {
|
||||||
self
|
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.
|
/// 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
|
/// Returns [`None`] if the request kind is not compatible with a download, e.g., it is
|
||||||
|
|
@ -356,11 +368,25 @@ impl PythonDownloadRequest {
|
||||||
Ok(self)
|
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> {
|
pub fn fill(mut self) -> Result<Self, Error> {
|
||||||
if self.implementation.is_none() {
|
if self.implementation.is_none() {
|
||||||
self.implementation = Some(ImplementationName::CPython);
|
self.implementation = Some(ImplementationName::CPython);
|
||||||
}
|
}
|
||||||
self = self.fill_platform()?;
|
self = self.fill_platform()?;
|
||||||
|
self = self.fill_build_from_env()?;
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -434,7 +460,31 @@ impl PythonDownloadRequest {
|
||||||
|
|
||||||
/// Whether this request is satisfied by a Python download.
|
/// Whether this request is satisfied by a Python download.
|
||||||
pub fn satisfied_by_download(&self, download: &ManagedPythonDownload) -> bool {
|
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.
|
/// Whether this download request opts-in to pre-release Python versions.
|
||||||
|
|
@ -753,6 +803,7 @@ struct JsonPythonDownload {
|
||||||
url: String,
|
url: String,
|
||||||
sha256: Option<String>,
|
sha256: Option<String>,
|
||||||
variant: Option<String>,
|
variant: Option<String>,
|
||||||
|
build: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[derive(Debug, Deserialize, Clone)]
|
||||||
|
|
@ -767,7 +818,25 @@ pub enum DownloadResult {
|
||||||
Fetched(PathBuf),
|
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 {
|
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.
|
/// Return the first [`ManagedPythonDownload`] matching a request, if any.
|
||||||
///
|
///
|
||||||
/// If there is no stable version matching the request, a compatible pre-release version will
|
/// 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()
|
self.sha256.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build(&self) -> Option<&'static str> {
|
||||||
|
self.build
|
||||||
|
}
|
||||||
|
|
||||||
/// Download and extract a Python distribution, retrying on failure.
|
/// Download and extract a Python distribution, retrying on failure.
|
||||||
#[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
|
#[instrument(skip(client, installation_dir, scratch_dir, reporter), fields(download = % self.key()))]
|
||||||
pub async fn fetch_with_retry(
|
pub async fn fetch_with_retry(
|
||||||
|
|
@ -1345,6 +1418,9 @@ fn parse_json_downloads(
|
||||||
|
|
||||||
let url = Cow::Owned(entry.url);
|
let url = Cow::Owned(entry.url);
|
||||||
let sha256 = entry.sha256.map(Cow::Owned);
|
let sha256 = entry.sha256.map(Cow::Owned);
|
||||||
|
let build = entry
|
||||||
|
.build
|
||||||
|
.map(|s| Box::leak(s.into_boxed_str()) as &'static str);
|
||||||
|
|
||||||
Some(ManagedPythonDownload {
|
Some(ManagedPythonDownload {
|
||||||
key: PythonInstallationKey::new_from_version(
|
key: PythonInstallationKey::new_from_version(
|
||||||
|
|
@ -1355,6 +1431,7 @@ fn parse_json_downloads(
|
||||||
),
|
),
|
||||||
url,
|
url,
|
||||||
sha256,
|
sha256,
|
||||||
|
build,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
|
.sorted_by(|a, b| Ord::cmp(&b.key, &a.key))
|
||||||
|
|
@ -1513,6 +1590,10 @@ async fn read_url(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::implementation::LenientImplementationName;
|
||||||
|
use crate::installation::PythonInstallationKey;
|
||||||
|
use uv_platform::{Arch, Libc, Os, Platform};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Parse a request with all of its fields.
|
/// Parse a request with all of its fields.
|
||||||
|
|
@ -1726,4 +1807,90 @@ mod tests {
|
||||||
|
|
||||||
assert!(matches!(result, Err(Error::TooManyParts(_))));
|
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_externally_managed()?;
|
||||||
installed.ensure_sysconfig_patched()?;
|
installed.ensure_sysconfig_patched()?;
|
||||||
installed.ensure_canonical_executables()?;
|
installed.ensure_canonical_executables()?;
|
||||||
|
installed.ensure_build_file()?;
|
||||||
|
|
||||||
let minor_version = installed.minor_version_key();
|
let minor_version = installed.minor_version_key();
|
||||||
let highest_patch = installations
|
let highest_patch = installations
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub use crate::interpreter::{
|
||||||
};
|
};
|
||||||
pub use crate::pointer_size::PointerSize;
|
pub use crate::pointer_size::PointerSize;
|
||||||
pub use crate::prefix::Prefix;
|
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::target::Target;
|
||||||
pub use crate::version_files::{
|
pub use crate::version_files::{
|
||||||
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,10 @@ pub struct ManagedPythonInstallation {
|
||||||
///
|
///
|
||||||
/// Empty when self was constructed from a path.
|
/// Empty when self was constructed from a path.
|
||||||
sha256: Option<Cow<'static, str>>,
|
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 {
|
impl ManagedPythonInstallation {
|
||||||
|
|
@ -329,6 +333,7 @@ impl ManagedPythonInstallation {
|
||||||
key: download.key().clone(),
|
key: download.key().clone(),
|
||||||
url: Some(download.url().clone()),
|
url: Some(download.url().clone()),
|
||||||
sha256: download.sha256().cloned(),
|
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))?;
|
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 {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
key,
|
key,
|
||||||
url: None,
|
url: None,
|
||||||
sha256: None,
|
sha256: None,
|
||||||
|
build,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -455,6 +468,11 @@ impl ManagedPythonInstallation {
|
||||||
self.key.platform()
|
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 {
|
pub fn minor_version_key(&self) -> &PythonInstallationMinorVersionKey {
|
||||||
PythonInstallationMinorVersionKey::ref_cast(&self.key)
|
PythonInstallationMinorVersionKey::ref_cast(&self.key)
|
||||||
}
|
}
|
||||||
|
|
@ -618,6 +636,15 @@ impl ManagedPythonInstallation {
|
||||||
Ok(())
|
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
|
/// Returns `true` if the path is a link to this installation's binary, e.g., as created by
|
||||||
/// [`create_bin_link`].
|
/// [`create_bin_link`].
|
||||||
pub fn is_bin_link(&self, path: &Path) -> bool {
|
pub fn is_bin_link(&self, path: &Path) -> bool {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,24 @@
|
||||||
#[cfg(feature = "schemars")]
|
#[cfg(feature = "schemars")]
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::env;
|
||||||
|
use std::ffi::OsString;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
use uv_pep440::Version;
|
use uv_pep440::Version;
|
||||||
use uv_pep508::{MarkerEnvironment, StringVersion};
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct PythonVersion(StringVersion);
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,26 @@ impl EnvVars {
|
||||||
/// Distributions can be read from a local directory by using the `file://` URL scheme.
|
/// 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";
|
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
|
/// Equivalent to the `--clear` command-line argument. If set, uv will remove any
|
||||||
/// existing files or directories at the target path.
|
/// existing files or directories at the target path.
|
||||||
pub const UV_VENV_CLEAR: &'static str = "UV_VENV_CLEAR";
|
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_externally_managed()?;
|
||||||
installation.ensure_sysconfig_patched()?;
|
installation.ensure_sysconfig_patched()?;
|
||||||
installation.ensure_canonical_executables()?;
|
installation.ensure_canonical_executables()?;
|
||||||
|
installation.ensure_build_file()?;
|
||||||
if let Err(e) = installation.ensure_dylib_patched() {
|
if let Err(e) = installation.ensure_dylib_patched() {
|
||||||
e.warn_user(installation);
|
e.warn_user(installation);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,28 +220,32 @@ impl TestContext {
|
||||||
/// and `.exe` suffixes.
|
/// and `.exe` suffixes.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_filtered_python_names(mut self) -> Self {
|
pub fn with_filtered_python_names(mut self) -> Self {
|
||||||
use env::consts::EXE_SUFFIX;
|
for name in ["python", "pypy"] {
|
||||||
let exe_suffix = regex::escape(EXE_SUFFIX);
|
// 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((
|
self.filters.push((
|
||||||
format!(r"python\d.\d\d{exe_suffix}"),
|
// We use a leading path separator to help disambiguate cases where the name is not
|
||||||
"[PYTHON]".to_string(),
|
// 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
|
self
|
||||||
|
|
@ -270,15 +274,34 @@ impl TestContext {
|
||||||
/// the virtual environment equivalent.
|
/// the virtual environment equivalent.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_filtered_python_install_bin(mut self) -> Self {
|
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) {
|
if cfg!(unix) {
|
||||||
self.filters.push((
|
self.filters.push((
|
||||||
r"[\\/]bin/python".to_string(),
|
format!(r"[\\/]bin/python({suffix})"),
|
||||||
"/[INSTALL-BIN]/python".to_string(),
|
"/[INSTALL-BIN]/python$1".to_string(),
|
||||||
|
));
|
||||||
|
self.filters.push((
|
||||||
|
format!(r"[\\/]bin/pypy({suffix})"),
|
||||||
|
"/[INSTALL-BIN]/pypy$1".to_string(),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
self.filters.push((
|
self.filters.push((
|
||||||
r"[\\/]python".to_string(),
|
format!(r"[\\/]python({suffix})"),
|
||||||
"/[INSTALL-BIN]/python".to_string(),
|
"/[INSTALL-BIN]/python$1".to_string(),
|
||||||
|
));
|
||||||
|
self.filters.push((
|
||||||
|
format!(r"[\\/]pypy({suffix})"),
|
||||||
|
"/[INSTALL-BIN]/pypy$1".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self
|
self
|
||||||
|
|
|
||||||
|
|
@ -56,14 +56,14 @@ fn missing_venv() -> Result<()> {
|
||||||
assert!(predicates::path::missing().eval(&context.venv));
|
assert!(predicates::path::missing().eval(&context.venv));
|
||||||
|
|
||||||
// If not "active", we hint to create one
|
// 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
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment
|
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));
|
assert!(predicates::path::missing().eval(&context.venv));
|
||||||
|
|
||||||
|
|
@ -5342,7 +5342,7 @@ fn target_no_build_isolation() -> Result<()> {
|
||||||
requirements_in.write_str("flit_core")?;
|
requirements_in.write_str("flit_core")?;
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.pip_sync()
|
uv_snapshot!(context.filters(), context.pip_sync()
|
||||||
.arg("requirements.in"), @r###"
|
.arg("requirements.in"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -5352,7 +5352,7 @@ fn target_no_build_isolation() -> Result<()> {
|
||||||
Prepared 1 package in [TIME]
|
Prepared 1 package in [TIME]
|
||||||
Installed 1 package in [TIME]
|
Installed 1 package in [TIME]
|
||||||
+ flit-core==3.9.0
|
+ flit-core==3.9.0
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Install `iniconfig` to the target directory.
|
// Install `iniconfig` to the target directory.
|
||||||
let requirements_in = context.temp_dir.child("requirements.in");
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
|
|
||||||
|
|
@ -412,13 +412,13 @@ fn python_find_venv() {
|
||||||
.with_filtered_virtualenv_bin();
|
.with_filtered_virtualenv_bin();
|
||||||
|
|
||||||
// Create a virtual environment
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// We should find it first
|
// We should find it first
|
||||||
// TODO(zanieb): On Windows, this has in a different display path for virtual environments which
|
// 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();
|
child_dir.create_dir_all().unwrap();
|
||||||
|
|
||||||
// Unless the system flag is passed
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Or, `UV_SYSTEM_PYTHON` is set
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Unless, `--no-system` is included
|
// Unless, `--no-system` is included
|
||||||
// TODO(zanieb): Report this as a bug upstream — this should be allowed.
|
// 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
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -481,7 +481,7 @@ fn python_find_venv() {
|
||||||
Usage: uv python find --cache-dir [CACHE_DIR] [REQUEST]
|
Usage: uv python find --cache-dir [CACHE_DIR] [REQUEST]
|
||||||
|
|
||||||
For more information, try '--help'.
|
For more information, try '--help'.
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// We should find virtual environments from a child directory
|
// We should find virtual environments from a child directory
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
|
|
@ -495,13 +495,13 @@ fn python_find_venv() {
|
||||||
");
|
");
|
||||||
|
|
||||||
// A virtual environment in the child directory takes precedence over the parent
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir).env_remove(EnvVars::VIRTUAL_ENV), @r"
|
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
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.12]
|
[PYTHON-3.12]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// If there's not a `pyvenv.cfg` file, it's also non-fatal, we ignore the environment
|
// 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();
|
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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.12]
|
[PYTHON-3.12]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -857,14 +857,14 @@ fn python_find_script() {
|
||||||
.with_filtered_python_names()
|
.with_filtered_python_names()
|
||||||
.with_filtered_exe_suffix();
|
.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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Initialized script at `foo.py`
|
Initialized script at `foo.py`
|
||||||
"###);
|
");
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.sync().arg("--script").arg("foo.py"), @r"
|
uv_snapshot!(context.filters(), context.sync().arg("--script").arg("foo.py"), @r"
|
||||||
success: true
|
success: true
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||||
use std::{env, path::Path, process::Command};
|
use std::{env, path::Path, process::Command};
|
||||||
|
|
||||||
use crate::common::{TestContext, uv_snapshot};
|
use crate::common::{TestContext, uv_snapshot};
|
||||||
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
use assert_fs::{
|
use assert_fs::{
|
||||||
assert::PathAssert,
|
assert::PathAssert,
|
||||||
prelude::{FileTouch, FileWriteStr, PathChild, PathCreateDir},
|
prelude::{FileTouch, FileWriteStr, PathChild, PathCreateDir},
|
||||||
|
|
@ -2062,8 +2063,9 @@ fn python_install_314() {
|
||||||
let context: TestContext = TestContext::new_with_versions(&[])
|
let context: TestContext = TestContext::new_with_versions(&[])
|
||||||
.with_filtered_python_keys()
|
.with_filtered_python_keys()
|
||||||
.with_managed_python_dirs()
|
.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
|
// Install 3.14
|
||||||
// For now, this provides test coverage of pre-release handling
|
// 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]
|
Installed Python 3.14.0a4 in [TIME]
|
||||||
+ cpython-3.14.0a4-[PLATFORM]
|
+ cpython-3.14.0a4-[PLATFORM]
|
||||||
");
|
");
|
||||||
|
}
|
||||||
|
|
||||||
// Add name filtering for the `find` tests, we avoid it in `install` tests because it clobbers
|
#[test]
|
||||||
// the version suffixes which matter in the install logs
|
fn python_find_314() {
|
||||||
let filters = context
|
let context: TestContext = TestContext::new_with_versions(&[])
|
||||||
.filters()
|
.with_filtered_python_keys()
|
||||||
.iter()
|
.with_managed_python_dirs()
|
||||||
.map(|(a, b)| ((*a).to_string(), (*b).to_string()))
|
.with_python_download_cache()
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let context = context
|
|
||||||
.with_filtered_python_install_bin()
|
.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
|
// We should be able to find this version without opt-in, because there is no stable release
|
||||||
// installed
|
// installed
|
||||||
|
|
@ -2130,7 +2136,7 @@ fn python_install_314() {
|
||||||
");
|
");
|
||||||
|
|
||||||
// If we install a stable version, that should be preferred though
|
// 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
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -3158,3 +3164,157 @@ fn python_install_pyodide() {
|
||||||
----- stderr -----
|
----- 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()?;
|
init.touch()?;
|
||||||
|
|
||||||
// `run` should fail
|
// `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
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: No `project` table found in: `[TEMP_DIR]/pyproject.toml`
|
error: No `project` table found in: `[TEMP_DIR]/pyproject.toml`
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// `run --no-project` should not
|
// `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"
|
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``
|
// Running `uv sync` should use the environment at `target``
|
||||||
uv_snapshot!(context.filters(), context.sync()
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r###"
|
.env(EnvVars::UV_PROJECT_ENVIRONMENT, "target"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -4101,7 +4101,7 @@ fn run_linked_environment_path() -> Result<()> {
|
||||||
+ packaging==24.0
|
+ packaging==24.0
|
||||||
+ pathspec==0.12.1
|
+ pathspec==0.12.1
|
||||||
+ platformdirs==4.2.0
|
+ platformdirs==4.2.0
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// `sys.prefix` and `sys.executable` should be from the `target` directory
|
// `sys.prefix` and `sys.executable` should be from the `target` directory
|
||||||
uv_snapshot!(context.filters(), context.run()
|
uv_snapshot!(context.filters(), context.run()
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ fn sync_json() -> Result<()> {
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.sync()
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
.arg("--locked")
|
.arg("--locked")
|
||||||
.arg("--output-format").arg("json"), @r###"
|
.arg("--output-format").arg("json"), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 1
|
exit_code: 1
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -430,7 +430,7 @@ fn sync_json() -> Result<()> {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Resolved 2 packages in [TIME]
|
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`.
|
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
|
// Test that JSON output is shown even with --quiet flag
|
||||||
uv_snapshot!(context.filters(), context.sync()
|
uv_snapshot!(context.filters(), context.sync()
|
||||||
|
|
@ -6367,7 +6367,7 @@ fn sync_active_script_environment() -> Result<()> {
|
||||||
fn sync_active_script_environment_json() -> Result<()> {
|
fn sync_active_script_environment_json() -> Result<()> {
|
||||||
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
let context = TestContext::new_with_versions(&["3.11", "3.12"])
|
||||||
.with_filtered_virtualenv_bin()
|
.with_filtered_virtualenv_bin()
|
||||||
.with_filtered_python_names();
|
.with_filtered_exe_suffix();
|
||||||
|
|
||||||
let script = context.temp_dir.child("script.py");
|
let script = context.temp_dir.child("script.py");
|
||||||
script.write_str(indoc! { r#"
|
script.write_str(indoc! { r#"
|
||||||
|
|
@ -6402,7 +6402,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
||||||
"environment": {
|
"environment": {
|
||||||
"path": "[CACHE_DIR]/environments-v2/script-[HASH]",
|
"path": "[CACHE_DIR]/environments-v2/script-[HASH]",
|
||||||
"python": {
|
"python": {
|
||||||
"path": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/[PYTHON]",
|
"path": "[CACHE_DIR]/environments-v2/script-[HASH]/[BIN]/python",
|
||||||
"version": "3.11.[X]",
|
"version": "3.11.[X]",
|
||||||
"implementation": "cpython"
|
"implementation": "cpython"
|
||||||
}
|
}
|
||||||
|
|
@ -6448,7 +6448,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
||||||
"environment": {
|
"environment": {
|
||||||
"path": "[TEMP_DIR]/foo",
|
"path": "[TEMP_DIR]/foo",
|
||||||
"python": {
|
"python": {
|
||||||
"path": "[TEMP_DIR]/foo/[BIN]/[PYTHON]",
|
"path": "[TEMP_DIR]/foo/[BIN]/python",
|
||||||
"version": "3.11.[X]",
|
"version": "3.11.[X]",
|
||||||
"implementation": "cpython"
|
"implementation": "cpython"
|
||||||
}
|
}
|
||||||
|
|
@ -6507,7 +6507,7 @@ fn sync_active_script_environment_json() -> Result<()> {
|
||||||
"environment": {
|
"environment": {
|
||||||
"path": "[TEMP_DIR]/foo",
|
"path": "[TEMP_DIR]/foo",
|
||||||
"python": {
|
"python": {
|
||||||
"path": "[TEMP_DIR]/foo/[BIN]/[PYTHON]",
|
"path": "[TEMP_DIR]/foo/[BIN]/python",
|
||||||
"version": "3.12.[X]",
|
"version": "3.12.[X]",
|
||||||
"implementation": "cpython"
|
"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
|
// 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::create_dir(context.temp_dir.join(".venv"))?;
|
||||||
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
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
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: Project virtual environment directory `[VENV]/` cannot be used because it is not a valid Python environment (no Python executable was found)
|
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...
|
// But if it's just an incompatible virtual environment...
|
||||||
fs_err::remove_dir_all(context.temp_dir.join(".venv"))?;
|
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"")?;
|
fs_err::write(context.temp_dir.join(".venv").join("file"), b"")?;
|
||||||
|
|
||||||
// We should never delete it
|
// We should never delete it
|
||||||
uv_snapshot!(context.filters(), context.sync(), @r###"
|
uv_snapshot!(context.filters(), context.sync(), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -8186,7 +8186,7 @@ fn sync_invalid_environment() -> Result<()> {
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
|
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
|
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
|
// Even if there's no Python executable
|
||||||
fs_err::remove_dir_all(&bin)?;
|
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
|
Specifies the directory for caching the archives of managed Python installations before
|
||||||
installation.
|
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`
|
### `UV_PYTHON_DOWNLOADS`
|
||||||
|
|
||||||
Equivalent to the
|
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.
|
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`
|
### `UV_PYTHON_INSTALL_BIN`
|
||||||
|
|
||||||
Whether to install the Python executable into the `UV_PYTHON_BIN_DIR` directory.
|
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.
|
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`
|
### `UV_REQUEST_TIMEOUT`
|
||||||
|
|
||||||
Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`.
|
Timeout (in seconds) for HTTP requests. Equivalent to `UV_HTTP_TIMEOUT`.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue