mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-25 09:28:17 +00:00
Allow selection of pyodide interpreters with "pyodide" (#15256)
This commit is contained in:
parent
88a7b2d864
commit
2c54d3929c
8 changed files with 243 additions and 93 deletions
|
|
@ -1190,11 +1190,7 @@ pub fn find_python_installations<'a>(
|
||||||
cache,
|
cache,
|
||||||
preview,
|
preview,
|
||||||
)
|
)
|
||||||
.filter_ok(|(_source, interpreter)| {
|
.filter_ok(|(_source, interpreter)| implementation.matches_interpreter(interpreter))
|
||||||
interpreter
|
|
||||||
.implementation_name()
|
|
||||||
.eq_ignore_ascii_case(implementation.into())
|
|
||||||
})
|
|
||||||
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
||||||
}),
|
}),
|
||||||
PythonRequest::ImplementationVersion(implementation, version) => {
|
PythonRequest::ImplementationVersion(implementation, version) => {
|
||||||
|
|
@ -1212,11 +1208,7 @@ pub fn find_python_installations<'a>(
|
||||||
cache,
|
cache,
|
||||||
preview,
|
preview,
|
||||||
)
|
)
|
||||||
.filter_ok(|(_source, interpreter)| {
|
.filter_ok(|(_source, interpreter)| implementation.matches_interpreter(interpreter))
|
||||||
interpreter
|
|
||||||
.implementation_name()
|
|
||||||
.eq_ignore_ascii_case(implementation.into())
|
|
||||||
})
|
|
||||||
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
.map_ok(|tuple| Ok(PythonInstallation::from_tuple(tuple)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1260,7 +1252,6 @@ pub(crate) fn find_python_installation(
|
||||||
let mut first_prerelease = None;
|
let mut first_prerelease = None;
|
||||||
let mut first_managed = None;
|
let mut first_managed = None;
|
||||||
let mut first_error = None;
|
let mut first_error = None;
|
||||||
let mut emscripten_installation = None;
|
|
||||||
for result in installations {
|
for result in installations {
|
||||||
// Iterate until the first critical error or happy result
|
// Iterate until the first critical error or happy result
|
||||||
if !result.as_ref().err().is_none_or(Error::is_critical) {
|
if !result.as_ref().err().is_none_or(Error::is_critical) {
|
||||||
|
|
@ -1278,15 +1269,6 @@ pub(crate) fn find_python_installation(
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
if installation.os().is_emscripten() {
|
|
||||||
// We want to pick a native Python over an Emscripten Python if we
|
|
||||||
// can find any native Python.
|
|
||||||
if emscripten_installation.is_none() {
|
|
||||||
emscripten_installation = Some(installation.clone());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
|
// Check if we need to skip the interpreter because it is "not allowed", e.g., if it is a
|
||||||
// pre-release version or an alternative implementation, using it requires opt-in.
|
// pre-release version or an alternative implementation, using it requires opt-in.
|
||||||
|
|
||||||
|
|
@ -1363,10 +1345,6 @@ pub(crate) fn find_python_installation(
|
||||||
return Ok(Ok(installation));
|
return Ok(Ok(installation));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(emscripten_python) = emscripten_installation {
|
|
||||||
return Ok(Ok(emscripten_python));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found a Python, but it was unusable for some reason, report that instead of saying we
|
// If we found a Python, but it was unusable for some reason, report that instead of saying we
|
||||||
// couldn't find any Python interpreters.
|
// couldn't find any Python interpreters.
|
||||||
if let Some(err) = first_error {
|
if let Some(err) = first_error {
|
||||||
|
|
@ -1964,8 +1942,10 @@ impl PythonRequest {
|
||||||
Self::Any => true,
|
Self::Any => true,
|
||||||
Self::Version(_) => false,
|
Self::Version(_) => false,
|
||||||
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
|
Self::Directory(_) | Self::File(_) | Self::ExecutableName(_) => true,
|
||||||
Self::Implementation(_) => true,
|
Self::Implementation(implementation)
|
||||||
Self::ImplementationVersion(_, _) => true,
|
| Self::ImplementationVersion(implementation, _) => {
|
||||||
|
!matches!(implementation, ImplementationName::CPython)
|
||||||
|
}
|
||||||
Self::Key(request) => request.allows_alternative_implementations(),
|
Self::Key(request) => request.allows_alternative_implementations(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3680,6 +3660,7 @@ mod tests {
|
||||||
"any",
|
"any",
|
||||||
&[
|
&[
|
||||||
"python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
|
"python", "python3", "cpython", "cpython3", "pypy", "pypy3", "graalpy", "graalpy3",
|
||||||
|
"pyodide", "pyodide3",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -261,7 +261,17 @@ impl PythonDownloadRequest {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_implementation(mut self, implementation: ImplementationName) -> Self {
|
pub fn with_implementation(mut self, implementation: ImplementationName) -> Self {
|
||||||
self.implementation = Some(implementation);
|
match implementation {
|
||||||
|
// Pyodide is actually CPython with an Emscripten OS, we paper over that for usability
|
||||||
|
ImplementationName::Pyodide => {
|
||||||
|
self = self.with_os(Os::new(target_lexicon::OperatingSystem::Emscripten));
|
||||||
|
self = self.with_arch(Arch::new(target_lexicon::Architecture::Wasm32, None));
|
||||||
|
self = self.with_libc(Libc::Some(target_lexicon::Environment::Musl));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.implementation = Some(implementation);
|
||||||
|
}
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -438,7 +448,9 @@ impl PythonDownloadRequest {
|
||||||
|
|
||||||
/// Whether this download request opts-in to alternative Python implementations.
|
/// Whether this download request opts-in to alternative Python implementations.
|
||||||
pub fn allows_alternative_implementations(&self) -> bool {
|
pub fn allows_alternative_implementations(&self) -> bool {
|
||||||
self.implementation.is_some()
|
self.implementation
|
||||||
|
.is_some_and(|implementation| !matches!(implementation, ImplementationName::CPython))
|
||||||
|
|| self.os.is_some_and(|os| os.is_emscripten())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
|
pub fn satisfied_by_interpreter(&self, interpreter: &Interpreter) -> bool {
|
||||||
|
|
@ -461,12 +473,10 @@ impl PythonDownloadRequest {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if let Some(implementation) = self.implementation() {
|
if let Some(implementation) = self.implementation() {
|
||||||
let interpreter_implementation = interpreter.implementation_name();
|
if !implementation.matches_interpreter(interpreter) {
|
||||||
if LenientImplementationName::from(interpreter_implementation)
|
|
||||||
!= LenientImplementationName::from(*implementation)
|
|
||||||
{
|
|
||||||
debug!(
|
debug!(
|
||||||
"Skipping interpreter at `{executable}`: implementation `{interpreter_implementation}` does not match request `{implementation}`"
|
"Skipping interpreter at `{executable}`: implementation `{}` does not match request `{implementation}`",
|
||||||
|
interpreter.implementation_name(),
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -1512,13 +1522,16 @@ mod tests {
|
||||||
request.version,
|
request.version,
|
||||||
Some(VersionRequest::from_str("3.12.0").unwrap())
|
Some(VersionRequest::from_str("3.12.0").unwrap())
|
||||||
);
|
);
|
||||||
assert_eq!(request.os, Some(Os(target_lexicon::OperatingSystem::Linux)));
|
assert_eq!(
|
||||||
|
request.os,
|
||||||
|
Some(Os::new(target_lexicon::OperatingSystem::Linux))
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.arch,
|
request.arch,
|
||||||
Some(ArchRequest::Explicit(Arch {
|
Some(ArchRequest::Explicit(Arch::new(
|
||||||
family: target_lexicon::Architecture::X86_64,
|
target_lexicon::Architecture::X86_64,
|
||||||
variant: None,
|
None
|
||||||
}))
|
)))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.libc,
|
request.libc,
|
||||||
|
|
@ -1540,10 +1553,10 @@ mod tests {
|
||||||
assert_eq!(request.os, None);
|
assert_eq!(request.os, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.arch,
|
request.arch,
|
||||||
Some(ArchRequest::Explicit(Arch {
|
Some(ArchRequest::Explicit(Arch::new(
|
||||||
family: target_lexicon::Architecture::X86_64,
|
target_lexicon::Architecture::X86_64,
|
||||||
variant: None,
|
None
|
||||||
}))
|
)))
|
||||||
);
|
);
|
||||||
assert_eq!(request.libc, None);
|
assert_eq!(request.libc, None);
|
||||||
}
|
}
|
||||||
|
|
@ -1556,7 +1569,10 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(request.implementation, Some(ImplementationName::PyPy));
|
assert_eq!(request.implementation, Some(ImplementationName::PyPy));
|
||||||
assert_eq!(request.version, None);
|
assert_eq!(request.version, None);
|
||||||
assert_eq!(request.os, Some(Os(target_lexicon::OperatingSystem::Linux)));
|
assert_eq!(
|
||||||
|
request.os,
|
||||||
|
Some(Os::new(target_lexicon::OperatingSystem::Linux))
|
||||||
|
);
|
||||||
assert_eq!(request.arch, None);
|
assert_eq!(request.arch, None);
|
||||||
assert_eq!(request.libc, None);
|
assert_eq!(request.libc, None);
|
||||||
}
|
}
|
||||||
|
|
@ -1598,14 +1614,14 @@ mod tests {
|
||||||
assert_eq!(request.version, None);
|
assert_eq!(request.version, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.os,
|
request.os,
|
||||||
Some(Os(target_lexicon::OperatingSystem::Windows))
|
Some(Os::new(target_lexicon::OperatingSystem::Windows))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.arch,
|
request.arch,
|
||||||
Some(ArchRequest::Explicit(Arch {
|
Some(ArchRequest::Explicit(Arch::new(
|
||||||
family: target_lexicon::Architecture::X86_64,
|
target_lexicon::Architecture::X86_64,
|
||||||
variant: None,
|
None
|
||||||
}))
|
)))
|
||||||
);
|
);
|
||||||
assert_eq!(request.libc, None);
|
assert_eq!(request.libc, None);
|
||||||
}
|
}
|
||||||
|
|
@ -1669,10 +1685,10 @@ mod tests {
|
||||||
assert_eq!(request.os, None);
|
assert_eq!(request.os, None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
request.arch,
|
request.arch,
|
||||||
Some(ArchRequest::Explicit(Arch {
|
Some(ArchRequest::Explicit(Arch::new(
|
||||||
family: target_lexicon::Architecture::X86_64,
|
target_lexicon::Architecture::X86_64,
|
||||||
variant: None,
|
None
|
||||||
}))
|
)))
|
||||||
);
|
);
|
||||||
assert_eq!(request.libc, None);
|
assert_eq!(request.libc, None);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ use std::{
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::Interpreter;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("Unknown Python implementation `{0}`")]
|
#[error("Unknown Python implementation `{0}`")]
|
||||||
|
|
@ -12,6 +14,7 @@ pub enum Error {
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, PartialOrd, Ord, Hash)]
|
||||||
pub enum ImplementationName {
|
pub enum ImplementationName {
|
||||||
|
Pyodide,
|
||||||
GraalPy,
|
GraalPy,
|
||||||
PyPy,
|
PyPy,
|
||||||
#[default]
|
#[default]
|
||||||
|
|
@ -30,11 +33,11 @@ impl ImplementationName {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn long_names() -> impl Iterator<Item = &'static str> {
|
pub(crate) fn long_names() -> impl Iterator<Item = &'static str> {
|
||||||
["cpython", "pypy", "graalpy"].into_iter()
|
["cpython", "pypy", "graalpy", "pyodide"].into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn iter_all() -> impl Iterator<Item = Self> {
|
pub(crate) fn iter_all() -> impl Iterator<Item = Self> {
|
||||||
[Self::CPython, Self::PyPy, Self::GraalPy].into_iter()
|
[Self::CPython, Self::PyPy, Self::GraalPy, Self::Pyodide].into_iter()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pretty(self) -> &'static str {
|
pub fn pretty(self) -> &'static str {
|
||||||
|
|
@ -42,15 +45,25 @@ impl ImplementationName {
|
||||||
Self::CPython => "CPython",
|
Self::CPython => "CPython",
|
||||||
Self::PyPy => "PyPy",
|
Self::PyPy => "PyPy",
|
||||||
Self::GraalPy => "GraalPy",
|
Self::GraalPy => "GraalPy",
|
||||||
|
Self::Pyodide => "Pyodide",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn executable_name(self) -> &'static str {
|
pub fn executable_name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::CPython => "python",
|
Self::CPython | Self::Pyodide => "python",
|
||||||
Self::PyPy | Self::GraalPy => self.into(),
|
Self::PyPy | Self::GraalPy => self.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches_interpreter(self, interpreter: &Interpreter) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Pyodide => interpreter.os().is_emscripten(),
|
||||||
|
_ => interpreter
|
||||||
|
.implementation_name()
|
||||||
|
.eq_ignore_ascii_case(self.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LenientImplementationName {
|
impl LenientImplementationName {
|
||||||
|
|
@ -75,6 +88,7 @@ impl From<&ImplementationName> for &'static str {
|
||||||
ImplementationName::CPython => "cpython",
|
ImplementationName::CPython => "cpython",
|
||||||
ImplementationName::PyPy => "pypy",
|
ImplementationName::PyPy => "pypy",
|
||||||
ImplementationName::GraalPy => "graalpy",
|
ImplementationName::GraalPy => "graalpy",
|
||||||
|
ImplementationName::Pyodide => "pyodide",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +119,7 @@ impl FromStr for ImplementationName {
|
||||||
"cpython" | "cp" => Ok(Self::CPython),
|
"cpython" | "cp" => Ok(Self::CPython),
|
||||||
"pypy" | "pp" => Ok(Self::PyPy),
|
"pypy" | "pp" => Ok(Self::PyPy),
|
||||||
"graalpy" | "gp" => Ok(Self::GraalPy),
|
"graalpy" | "gp" => Ok(Self::GraalPy),
|
||||||
|
"pyodide" => Ok(Self::Pyodide),
|
||||||
_ => Err(Error::UnknownImplementation(s.to_string())),
|
_ => Err(Error::UnknownImplementation(s.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
@ -310,7 +311,7 @@ impl PythonInstallation {
|
||||||
!matches!(
|
!matches!(
|
||||||
self.implementation(),
|
self.implementation(),
|
||||||
LenientImplementationName::Known(ImplementationName::CPython)
|
LenientImplementationName::Known(ImplementationName::CPython)
|
||||||
)
|
) || self.os().is_emscripten()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
|
/// Return the [`Arch`] of the Python installation as reported by its interpreter.
|
||||||
|
|
@ -394,8 +395,12 @@ impl PythonInstallationKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn implementation(&self) -> &LenientImplementationName {
|
pub fn implementation(&self) -> Cow<'_, LenientImplementationName> {
|
||||||
&self.implementation
|
if self.os().is_emscripten() {
|
||||||
|
Cow::Owned(LenientImplementationName::from(ImplementationName::Pyodide))
|
||||||
|
} else {
|
||||||
|
Cow::Borrowed(&self.implementation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn version(&self) -> PythonVersion {
|
pub fn version(&self) -> PythonVersion {
|
||||||
|
|
@ -484,7 +489,7 @@ impl fmt::Display for PythonInstallationKey {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"{}-{}.{}.{}{}{}-{}",
|
"{}-{}.{}.{}{}{}-{}",
|
||||||
self.implementation,
|
self.implementation(),
|
||||||
self.major,
|
self.major,
|
||||||
self.minor,
|
self.minor,
|
||||||
self.patch,
|
self.patch,
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,7 @@ mod tests {
|
||||||
|
|
||||||
fn add_pyodide_version(&mut self, version: &'static str) -> Result<()> {
|
fn add_pyodide_version(&mut self, version: &'static str) -> Result<()> {
|
||||||
let path = self.new_search_path_directory(format!("pyodide-{version}"))?;
|
let path = self.new_search_path_directory(format!("pyodide-{version}"))?;
|
||||||
let python = format!("python{}", env::consts::EXE_SUFFIX);
|
let python = format!("pyodide{}", env::consts::EXE_SUFFIX);
|
||||||
Self::create_mock_pyodide_interpreter(
|
Self::create_mock_pyodide_interpreter(
|
||||||
&path.join(python),
|
&path.join(python),
|
||||||
&PythonVersion::from_str(version).unwrap(),
|
&PythonVersion::from_str(version).unwrap(),
|
||||||
|
|
@ -2705,7 +2705,9 @@ mod tests {
|
||||||
let mut context = TestContext::new()?;
|
let mut context = TestContext::new()?;
|
||||||
|
|
||||||
context.add_pyodide_version("3.13.2")?;
|
context.add_pyodide_version("3.13.2")?;
|
||||||
let python = context.run(|| {
|
|
||||||
|
// We should not find the Pyodide interpreter by default
|
||||||
|
let result = context.run(|| {
|
||||||
find_python_installation(
|
find_python_installation(
|
||||||
&PythonRequest::Default,
|
&PythonRequest::Default,
|
||||||
EnvironmentPreference::Any,
|
EnvironmentPreference::Any,
|
||||||
|
|
@ -2713,14 +2715,28 @@ mod tests {
|
||||||
&context.cache,
|
&context.cache,
|
||||||
Preview::default(),
|
Preview::default(),
|
||||||
)
|
)
|
||||||
|
})?;
|
||||||
|
assert!(
|
||||||
|
result.is_err(),
|
||||||
|
"We should not find an python; got {result:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// With `Any`, it should be discoverable
|
||||||
|
let python = context.run(|| {
|
||||||
|
find_python_installation(
|
||||||
|
&PythonRequest::Any,
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
PythonPreference::OnlySystem,
|
||||||
|
&context.cache,
|
||||||
|
Preview::default(),
|
||||||
|
)
|
||||||
})??;
|
})??;
|
||||||
// We should find the Pyodide interpreter
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
python.interpreter().python_full_version().to_string(),
|
python.interpreter().python_full_version().to_string(),
|
||||||
"3.13.2"
|
"3.13.2"
|
||||||
);
|
);
|
||||||
|
|
||||||
// We should prefer any native Python to the Pyodide Python
|
// We should prefer the native Python to the Pyodide Python
|
||||||
context.add_python_versions(&["3.15.7"])?;
|
context.add_python_versions(&["3.15.7"])?;
|
||||||
|
|
||||||
let python = context.run(|| {
|
let python = context.run(|| {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ use uv_configuration::{Preview, PreviewFeatures};
|
||||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
||||||
|
|
||||||
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
|
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
|
||||||
use uv_platform::Error as PlatformError;
|
use uv_platform::{Error as PlatformError, Os};
|
||||||
use uv_platform::{LibcDetectionError, Platform};
|
use uv_platform::{LibcDetectionError, Platform};
|
||||||
use uv_state::{StateBucket, StateStore};
|
use uv_state::{StateBucket, StateStore};
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
|
|
@ -358,8 +358,6 @@ impl ManagedPythonInstallation {
|
||||||
/// If windowed is true, `pythonw.exe` is selected over `python.exe` on windows, with no changes
|
/// If windowed is true, `pythonw.exe` is selected over `python.exe` on windows, with no changes
|
||||||
/// on non-windows.
|
/// on non-windows.
|
||||||
pub fn executable(&self, windowed: bool) -> PathBuf {
|
pub fn executable(&self, windowed: bool) -> PathBuf {
|
||||||
let implementation = self.implementation().executable_name();
|
|
||||||
|
|
||||||
let version = match self.implementation() {
|
let version = match self.implementation() {
|
||||||
ImplementationName::CPython => {
|
ImplementationName::CPython => {
|
||||||
if cfg!(unix) {
|
if cfg!(unix) {
|
||||||
|
|
@ -370,12 +368,14 @@ impl ManagedPythonInstallation {
|
||||||
}
|
}
|
||||||
// PyPy uses a full version number, even on Windows.
|
// PyPy uses a full version number, even on Windows.
|
||||||
ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
|
ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
|
||||||
|
// Pyodide and GraalPy do not have a version suffix.
|
||||||
|
ImplementationName::Pyodide => String::new(),
|
||||||
ImplementationName::GraalPy => String::new(),
|
ImplementationName::GraalPy => String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// On Windows, the executable is just `python.exe` even for alternative variants
|
// On Windows, the executable is just `python.exe` even for alternative variants
|
||||||
// GraalPy always uses `graalpy.exe` as the main executable
|
// GraalPy always uses `graalpy.exe` as the main executable
|
||||||
let variant = if *self.implementation() == ImplementationName::GraalPy {
|
let variant = if self.implementation() == ImplementationName::GraalPy {
|
||||||
""
|
""
|
||||||
} else if cfg!(unix) {
|
} else if cfg!(unix) {
|
||||||
self.key.variant.suffix()
|
self.key.variant.suffix()
|
||||||
|
|
@ -388,13 +388,15 @@ impl ManagedPythonInstallation {
|
||||||
|
|
||||||
let name = format!(
|
let name = format!(
|
||||||
"{implementation}{version}{variant}{exe}",
|
"{implementation}{version}{variant}{exe}",
|
||||||
|
implementation = self.implementation().executable_name(),
|
||||||
exe = std::env::consts::EXE_SUFFIX
|
exe = std::env::consts::EXE_SUFFIX
|
||||||
);
|
);
|
||||||
|
|
||||||
let executable = executable_path_from_base(
|
let executable = executable_path_from_base(
|
||||||
self.python_dir().as_path(),
|
self.python_dir().as_path(),
|
||||||
&name,
|
&name,
|
||||||
&LenientImplementationName::from(*self.implementation()),
|
&LenientImplementationName::from(self.implementation()),
|
||||||
|
*self.key.os(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Workaround for python-build-standalone v20241016 which is missing the standard
|
// Workaround for python-build-standalone v20241016 which is missing the standard
|
||||||
|
|
@ -431,8 +433,8 @@ impl ManagedPythonInstallation {
|
||||||
self.key.version()
|
self.key.version()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn implementation(&self) -> &ImplementationName {
|
pub fn implementation(&self) -> ImplementationName {
|
||||||
match self.key.implementation() {
|
match self.key.implementation().into_owned() {
|
||||||
LenientImplementationName::Known(implementation) => implementation,
|
LenientImplementationName::Known(implementation) => implementation,
|
||||||
LenientImplementationName::Unknown(_) => {
|
LenientImplementationName::Unknown(_) => {
|
||||||
panic!("Managed Python installations should have a known implementation")
|
panic!("Managed Python installations should have a known implementation")
|
||||||
|
|
@ -466,10 +468,10 @@ impl ManagedPythonInstallation {
|
||||||
.file_name()
|
.file_name()
|
||||||
.is_some_and(|filename| filename.to_string_lossy() == *name),
|
.is_some_and(|filename| filename.to_string_lossy() == *name),
|
||||||
PythonRequest::Implementation(implementation) => {
|
PythonRequest::Implementation(implementation) => {
|
||||||
implementation == self.implementation()
|
*implementation == self.implementation()
|
||||||
}
|
}
|
||||||
PythonRequest::ImplementationVersion(implementation, version) => {
|
PythonRequest::ImplementationVersion(implementation, version) => {
|
||||||
implementation == self.implementation() && version.matches_version(&self.version())
|
*implementation == self.implementation() && version.matches_version(&self.version())
|
||||||
}
|
}
|
||||||
PythonRequest::Version(version) => version.matches_version(&self.version()),
|
PythonRequest::Version(version) => version.matches_version(&self.version()),
|
||||||
PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
|
PythonRequest::Key(request) => request.satisfied_by_key(self.key()),
|
||||||
|
|
@ -579,7 +581,7 @@ impl ManagedPythonInstallation {
|
||||||
// sysconfig directly
|
// sysconfig directly
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if *self.implementation() == ImplementationName::CPython {
|
if self.implementation() == ImplementationName::CPython {
|
||||||
sysconfig::update_sysconfig(
|
sysconfig::update_sysconfig(
|
||||||
self.path(),
|
self.path(),
|
||||||
self.key.major,
|
self.key.major,
|
||||||
|
|
@ -600,7 +602,7 @@ impl ManagedPythonInstallation {
|
||||||
pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
|
pub fn ensure_dylib_patched(&self) -> Result<(), macos_dylib::Error> {
|
||||||
if cfg!(target_os = "macos") {
|
if cfg!(target_os = "macos") {
|
||||||
if self.key().os().is_like_darwin() {
|
if self.key().os().is_like_darwin() {
|
||||||
if *self.implementation() == ImplementationName::CPython {
|
if self.implementation() == ImplementationName::CPython {
|
||||||
let dylib_path = self.python_dir().join("lib").join(format!(
|
let dylib_path = self.python_dir().join("lib").join(format!(
|
||||||
"{}python{}{}{}",
|
"{}python{}{}{}",
|
||||||
std::env::consts::DLL_PREFIX,
|
std::env::consts::DLL_PREFIX,
|
||||||
|
|
@ -716,7 +718,7 @@ impl PythonMinorVersionLink {
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
let implementation = key.implementation();
|
let implementation = key.implementation();
|
||||||
if !matches!(
|
if !matches!(
|
||||||
implementation,
|
implementation.as_ref(),
|
||||||
LenientImplementationName::Known(ImplementationName::CPython)
|
LenientImplementationName::Known(ImplementationName::CPython)
|
||||||
) {
|
) {
|
||||||
// We don't currently support transparent upgrades for PyPy or GraalPy.
|
// We don't currently support transparent upgrades for PyPy or GraalPy.
|
||||||
|
|
@ -755,7 +757,8 @@ impl PythonMinorVersionLink {
|
||||||
let symlink_executable = executable_path_from_base(
|
let symlink_executable = executable_path_from_base(
|
||||||
symlink_directory.as_path(),
|
symlink_directory.as_path(),
|
||||||
&executable_name.to_string_lossy(),
|
&executable_name.to_string_lossy(),
|
||||||
implementation,
|
&implementation,
|
||||||
|
*key.os(),
|
||||||
);
|
);
|
||||||
let minor_version_link = Self {
|
let minor_version_link = Self {
|
||||||
symlink_directory,
|
symlink_directory,
|
||||||
|
|
@ -839,18 +842,28 @@ fn executable_path_from_base(
|
||||||
base: &Path,
|
base: &Path,
|
||||||
executable_name: &str,
|
executable_name: &str,
|
||||||
implementation: &LenientImplementationName,
|
implementation: &LenientImplementationName,
|
||||||
|
os: Os,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
if cfg!(unix)
|
if matches!(
|
||||||
|
implementation,
|
||||||
|
&LenientImplementationName::Known(ImplementationName::GraalPy)
|
||||||
|
) {
|
||||||
|
// GraalPy is always in `bin/` regardless of the os
|
||||||
|
base.join("bin").join(executable_name)
|
||||||
|
} else if os.is_emscripten()
|
||||||
|| matches!(
|
|| matches!(
|
||||||
implementation,
|
implementation,
|
||||||
&LenientImplementationName::Known(ImplementationName::GraalPy)
|
&LenientImplementationName::Known(ImplementationName::Pyodide)
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
base.join("bin").join(executable_name)
|
// Emscripten's canonical executable is in the base directory
|
||||||
} else if cfg!(windows) {
|
base.join(executable_name)
|
||||||
|
} else if os.is_windows() {
|
||||||
|
// On Windows, the executable is in the base directory
|
||||||
base.join(executable_name)
|
base.join(executable_name)
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Only Windows and Unix systems are supported.")
|
// On Unix, the executable is in `bin/`
|
||||||
|
base.join("bin").join(executable_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,13 @@ impl InstallRequest {
|
||||||
|
|
||||||
impl std::fmt::Display for InstallRequest {
|
impl std::fmt::Display for InstallRequest {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{}", self.request)
|
let request = self.request.to_canonical_string();
|
||||||
|
let download = self.download_request.to_string();
|
||||||
|
if request != download {
|
||||||
|
write!(f, "{request} ({download})")
|
||||||
|
} else {
|
||||||
|
write!(f, "{request}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,6 +229,12 @@ pub(crate) async fn install(
|
||||||
.with_preference(VersionFilePreference::Versions),
|
.with_preference(VersionFilePreference::Versions),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
.inspect(|file| {
|
||||||
|
debug!(
|
||||||
|
"Found Python version file at: {}",
|
||||||
|
file.path().user_display()
|
||||||
|
);
|
||||||
|
})
|
||||||
.map(PythonVersionFile::into_versions)
|
.map(PythonVersionFile::into_versions)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
// If no version file is found and no requests were made
|
// If no version file is found and no requests were made
|
||||||
|
|
@ -237,14 +249,14 @@ pub(crate) async fn install(
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref()))
|
.map(|request| InstallRequest::new(request, python_downloads_json_url.as_deref()))
|
||||||
.collect::<Result<Vec<_>>>()?
|
.collect::<Result<Vec<_>>>()?
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
targets
|
targets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|target| PythonRequest::parse(target.as_str()))
|
.map(|target| PythonRequest::parse(target.as_str()))
|
||||||
.map(|a| InstallRequest::new(a, python_downloads_json_url.as_deref()))
|
.map(|request| InstallRequest::new(request, python_downloads_json_url.as_deref()))
|
||||||
.collect::<Result<Vec<_>>>()?
|
.collect::<Result<Vec<_>>>()?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -565,10 +577,14 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
if !changelog.installed.is_empty() {
|
if !changelog.installed.is_empty() {
|
||||||
for install_key in &changelog.installed {
|
for install_key in &changelog.installed {
|
||||||
// Make a note if the selected python is non-native for the architecture,
|
// Make a note if the selected python is non-native for the architecture, if none of the
|
||||||
// if none of the matching user requests were explicit
|
// matching user requests were explicit.
|
||||||
|
//
|
||||||
|
// Emscripten is exempted as it is always "emulated".
|
||||||
let native_arch = Arch::from_env();
|
let native_arch = Arch::from_env();
|
||||||
if install_key.arch().family() != native_arch.family() {
|
if install_key.arch().family() != native_arch.family()
|
||||||
|
&& !install_key.os().is_emscripten()
|
||||||
|
{
|
||||||
let not_explicit =
|
let not_explicit =
|
||||||
requests_by_new_installation
|
requests_by_new_installation
|
||||||
.get(install_key)
|
.get(install_key)
|
||||||
|
|
|
||||||
|
|
@ -2991,8 +2991,9 @@ fn uninstall_last_patch() {
|
||||||
#[cfg(unix)] // Pyodide cannot be used on Windows
|
#[cfg(unix)] // Pyodide cannot be used on Windows
|
||||||
#[test]
|
#[test]
|
||||||
fn python_install_pyodide() {
|
fn python_install_pyodide() {
|
||||||
|
use assert_cmd::assert::OutputAssertExt;
|
||||||
|
|
||||||
let context: TestContext = TestContext::new_with_versions(&[])
|
let context: TestContext = TestContext::new_with_versions(&[])
|
||||||
.with_filtered_python_keys()
|
|
||||||
.with_filtered_exe_suffix()
|
.with_filtered_exe_suffix()
|
||||||
.with_managed_python_dirs()
|
.with_managed_python_dirs()
|
||||||
.with_python_download_cache();
|
.with_python_download_cache();
|
||||||
|
|
@ -3004,7 +3005,7 @@ fn python_install_pyodide() {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
Installed Python 3.13.2 in [TIME]
|
Installed Python 3.13.2 in [TIME]
|
||||||
+ cpython-3.13.2-[PLATFORM] (python3.13)
|
+ pyodide-3.13.2-emscripten-wasm32-musl (python3.13)
|
||||||
");
|
");
|
||||||
|
|
||||||
let bin_python = context
|
let bin_python = context
|
||||||
|
|
@ -3014,8 +3015,7 @@ fn python_install_pyodide() {
|
||||||
// The executable should be installed in the bin directory
|
// The executable should be installed in the bin directory
|
||||||
bin_python.assert(predicate::path::exists());
|
bin_python.assert(predicate::path::exists());
|
||||||
|
|
||||||
// On Unix, it should be a link
|
// It should be a link
|
||||||
#[cfg(unix)]
|
|
||||||
bin_python.assert(predicate::path::is_symlink());
|
bin_python.assert(predicate::path::is_symlink());
|
||||||
|
|
||||||
// The link should be a path to the binary
|
// The link should be a path to the binary
|
||||||
|
|
@ -3023,7 +3023,7 @@ fn python_install_pyodide() {
|
||||||
filters => context.filters(),
|
filters => context.filters(),
|
||||||
}, {
|
}, {
|
||||||
insta::assert_snapshot!(
|
insta::assert_snapshot!(
|
||||||
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.2-[PLATFORM]/bin/python3.13"
|
read_link(&bin_python), @"[TEMP_DIR]/managed/pyodide-3.13.2-emscripten-wasm32-musl/python"
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3043,7 +3043,7 @@ fn python_install_pyodide() {
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[TEMP_DIR]/managed/cpython-3.13.2-[PLATFORM]/bin/python3.13
|
[TEMP_DIR]/managed/pyodide-3.13.2-emscripten-wasm32-musl/python
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
");
|
");
|
||||||
|
|
@ -3069,4 +3069,92 @@ fn python_install_pyodide() {
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
");
|
");
|
||||||
|
|
||||||
|
context.python_uninstall().arg("--all").assert().success();
|
||||||
|
fs_err::remove_dir_all(&context.venv).unwrap();
|
||||||
|
|
||||||
|
// Install via `pyodide`
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("pyodide"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Installed Python 3.13.2 in [TIME]
|
||||||
|
+ pyodide-3.13.2-emscripten-wasm32-musl (python3.13)
|
||||||
|
");
|
||||||
|
|
||||||
|
context.python_uninstall().arg("--all").assert().success();
|
||||||
|
|
||||||
|
// Install via `pyodide@<version>`
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("pyodide@3.13"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Installed Python 3.13.2 in [TIME]
|
||||||
|
+ pyodide-3.13.2-emscripten-wasm32-musl (python3.13)
|
||||||
|
");
|
||||||
|
|
||||||
|
// Find via `pyodide``
|
||||||
|
uv_snapshot!(context.filters(), context.python_find().arg("pyodide"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
[TEMP_DIR]/managed/pyodide-3.13.2-emscripten-wasm32-musl/python
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// Find without a request should fail
|
||||||
|
uv_snapshot!(context.filters(), context.python_find(), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found in virtual environments, managed installations, or search path
|
||||||
|
");
|
||||||
|
// Find with "cpython" should also fail
|
||||||
|
uv_snapshot!(context.filters(), context.python_find().arg("cpython"), @r"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: No interpreter found for CPython in virtual environments, managed installations, or search path
|
||||||
|
");
|
||||||
|
|
||||||
|
// Install a CPython interpreter
|
||||||
|
let context = context.with_filtered_python_keys();
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("cpython"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Installed Python 3.13.6 in [TIME]
|
||||||
|
+ cpython-3.13.6-[PLATFORM]
|
||||||
|
");
|
||||||
|
|
||||||
|
// Now, we should prefer that
|
||||||
|
uv_snapshot!(context.filters(), context.python_find().arg("any"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
[TEMP_DIR]/managed/cpython-3.13.6-[PLATFORM]/bin/python3.13
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// Unless we request pyodide
|
||||||
|
uv_snapshot!(context.filters(), context.python_find().arg("pyodide"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
[TEMP_DIR]/managed/pyodide-3.13.2-emscripten-wasm32-musl/python
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue