Allow selection of pyodide interpreters with "pyodide" (#15256)

This commit is contained in:
Zanie Blue 2025-08-13 14:08:55 -05:00 committed by GitHub
parent 88a7b2d864
commit 2c54d3929c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 243 additions and 93 deletions

View file

@ -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",
], ],
); );

View file

@ -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);
} }

View file

@ -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())),
} }
} }

View file

@ -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,

View file

@ -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(|| {

View file

@ -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)
} }
} }

View file

@ -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)

View file

@ -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 -----
");
} }