mirror of https://github.com/astral-sh/uv
Add support for installing pyodide Pythons (#14518)
- [x] Add tests --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
b38edb9b7d
commit
c8d0bfba5c
|
|
@ -1493,7 +1493,7 @@ jobs:
|
||||||
# newer versions.
|
# newer versions.
|
||||||
./uv pip install -p venv-native/bin/python pyodide-build==0.30.3 pip
|
./uv pip install -p venv-native/bin/python pyodide-build==0.30.3 pip
|
||||||
|
|
||||||
- name: "Install pyodide interpreter"
|
- name: "Install Pyodide interpreter"
|
||||||
run: |
|
run: |
|
||||||
source ./venv-native/bin/activate
|
source ./venv-native/bin/activate
|
||||||
pyodide xbuildenv install 0.27.5
|
pyodide xbuildenv install 0.27.5
|
||||||
|
|
@ -1502,13 +1502,25 @@ jobs:
|
||||||
echo "PYODIDE_PYTHON=$PYODIDE_PYTHON" >> $GITHUB_ENV
|
echo "PYODIDE_PYTHON=$PYODIDE_PYTHON" >> $GITHUB_ENV
|
||||||
echo "PYODIDE_INDEX=$PYODIDE_INDEX" >> $GITHUB_ENV
|
echo "PYODIDE_INDEX=$PYODIDE_INDEX" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: "Create pyodide virtual environment"
|
- name: "Create Pyodide virtual environment"
|
||||||
run: |
|
run: |
|
||||||
./uv venv -p $PYODIDE_PYTHON venv-pyodide
|
./uv venv -p $PYODIDE_PYTHON venv-pyodide
|
||||||
source ./venv-pyodide/bin/activate
|
source ./venv-pyodide/bin/activate
|
||||||
./uv pip install --extra-index-url=$PYODIDE_INDEX --no-build numpy
|
./uv pip install --extra-index-url=$PYODIDE_INDEX --no-build numpy
|
||||||
python -c 'import numpy'
|
python -c 'import numpy'
|
||||||
|
|
||||||
|
- name: "Install Pyodide with uv python"
|
||||||
|
run: |
|
||||||
|
./uv python install cpython-3.13.2-emscripten-wasm32-musl
|
||||||
|
|
||||||
|
- name: "Create a Pyodide virtual environment using uv installed Python"
|
||||||
|
run: |
|
||||||
|
./uv venv -p cpython-3.13.2-emscripten-wasm32-musl venv-pyodide2
|
||||||
|
# TODO: be able to install Emscripten wheels here...
|
||||||
|
source ./venv-pyodide2/bin/activate
|
||||||
|
./uv pip install packaging
|
||||||
|
python -c 'import packaging'
|
||||||
|
|
||||||
integration-test-github-actions:
|
integration-test-github-actions:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: build-binary-linux-libc
|
needs: build-binary-linux-libc
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,10 @@ impl Arch {
|
||||||
pub fn is_arm(&self) -> bool {
|
pub fn is_arm(&self) -> bool {
|
||||||
matches!(self.family, target_lexicon::Architecture::Arm(_))
|
matches!(self.family, target_lexicon::Architecture::Arm(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_wasm(&self) -> bool {
|
||||||
|
matches!(self.family, target_lexicon::Architecture::Wasm32)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Arch {
|
impl std::fmt::Display for Arch {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use std::cmp;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
pub use crate::arch::{Arch, ArchVariant};
|
pub use crate::arch::{Arch, ArchVariant};
|
||||||
pub use crate::libc::{Libc, LibcDetectionError, LibcVersion};
|
pub use crate::libc::{Libc, LibcDetectionError, LibcVersion};
|
||||||
|
|
@ -68,13 +69,20 @@ impl Platform {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// OS must match exactly
|
if !self.os.supports(other.os) {
|
||||||
if self.os != other.os {
|
trace!(
|
||||||
|
"Operating system `{}` is not compatible with `{}`",
|
||||||
|
self.os, other.os
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Libc must match exactly
|
// Libc must match exactly, unless we're on emscripten — in which case it doesn't matter
|
||||||
if self.libc != other.libc {
|
if self.libc != other.libc && !(other.os.is_emscripten() || self.os.is_emscripten()) {
|
||||||
|
trace!(
|
||||||
|
"Libc `{}` is not compatible with `{}`",
|
||||||
|
self.libc, other.libc
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -94,10 +102,23 @@ impl Platform {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wasm32 can run on any architecture
|
||||||
|
if other.arch.is_wasm() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Allow inequal variants, as we don't implement variant support checks yet.
|
// TODO: Allow inequal variants, as we don't implement variant support checks yet.
|
||||||
// See https://github.com/astral-sh/uv/pull/9788
|
// See https://github.com/astral-sh/uv/pull/9788
|
||||||
// For now, allow same architecture family as a fallback
|
// For now, allow same architecture family as a fallback
|
||||||
self.arch.family() == other.arch.family()
|
if self.arch.family() != other.arch.family() {
|
||||||
|
trace!(
|
||||||
|
"Architecture `{}` is not compatible with `{}`",
|
||||||
|
self.arch, other.arch
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,7 @@ impl From<&uv_platform_tags::Os> for Libc {
|
||||||
match value {
|
match value {
|
||||||
uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
|
uv_platform_tags::Os::Manylinux { .. } => Self::Some(target_lexicon::Environment::Gnu),
|
||||||
uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
|
uv_platform_tags::Os::Musllinux { .. } => Self::Some(target_lexicon::Environment::Musl),
|
||||||
|
uv_platform_tags::Os::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
|
||||||
_ => Self::None,
|
_ => Self::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,27 @@ impl Os {
|
||||||
matches!(self.0, target_lexicon::OperatingSystem::Windows)
|
matches!(self.0, target_lexicon::OperatingSystem::Windows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_emscripten(&self) -> bool {
|
||||||
|
matches!(self.0, target_lexicon::OperatingSystem::Emscripten)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_macos(&self) -> bool {
|
pub fn is_macos(&self) -> bool {
|
||||||
matches!(self.0, target_lexicon::OperatingSystem::Darwin(_))
|
matches!(self.0, target_lexicon::OperatingSystem::Darwin(_))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether this OS can run the other OS.
|
||||||
|
pub fn supports(&self, other: Self) -> bool {
|
||||||
|
// Emscripten cannot run on Windows, but all other OSes can run Emscripten.
|
||||||
|
if other.is_emscripten() {
|
||||||
|
return !self.is_windows();
|
||||||
|
}
|
||||||
|
if self.is_windows() && other.is_emscripten() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we require an exact match
|
||||||
|
*self == other
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Os {
|
impl Display for Os {
|
||||||
|
|
|
||||||
|
|
@ -10511,6 +10511,22 @@
|
||||||
"sha256": "e5a904ecfb4061389773dd655d3b5665447c80cbf2948fcb1c07e92716eed955",
|
"sha256": "e5a904ecfb4061389773dd655d3b5665447c80cbf2948fcb1c07e92716eed955",
|
||||||
"variant": null
|
"variant": null
|
||||||
},
|
},
|
||||||
|
"cpython-3.13.2-emscripten-wasm32-musl": {
|
||||||
|
"name": "cpython",
|
||||||
|
"arch": {
|
||||||
|
"family": "wasm32",
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
|
"os": "emscripten",
|
||||||
|
"libc": "musl",
|
||||||
|
"major": 3,
|
||||||
|
"minor": 13,
|
||||||
|
"patch": 2,
|
||||||
|
"prerelease": "",
|
||||||
|
"url": "https://github.com/pyodide/pyodide/releases/download/0.28.0/xbuildenv-0.28.0.tar.bz2",
|
||||||
|
"sha256": null,
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
"cpython-3.13.2-linux-aarch64-gnu": {
|
"cpython-3.13.2-linux-aarch64-gnu": {
|
||||||
"name": "cpython",
|
"name": "cpython",
|
||||||
"arch": {
|
"arch": {
|
||||||
|
|
@ -15055,6 +15071,22 @@
|
||||||
"sha256": "848405b92bda20fad1f9bba99234c7d3f11e0b31e46f89835d1cb3d735e932aa",
|
"sha256": "848405b92bda20fad1f9bba99234c7d3f11e0b31e46f89835d1cb3d735e932aa",
|
||||||
"variant": null
|
"variant": null
|
||||||
},
|
},
|
||||||
|
"cpython-3.12.7-emscripten-wasm32-musl": {
|
||||||
|
"name": "cpython",
|
||||||
|
"arch": {
|
||||||
|
"family": "wasm32",
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
|
"os": "emscripten",
|
||||||
|
"libc": "musl",
|
||||||
|
"major": 3,
|
||||||
|
"minor": 12,
|
||||||
|
"patch": 7,
|
||||||
|
"prerelease": "",
|
||||||
|
"url": "https://github.com/pyodide/pyodide/releases/download/0.27.7/xbuildenv-0.27.7.tar.bz2",
|
||||||
|
"sha256": null,
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
"cpython-3.12.7-linux-aarch64-gnu": {
|
"cpython-3.12.7-linux-aarch64-gnu": {
|
||||||
"name": "cpython",
|
"name": "cpython",
|
||||||
"arch": {
|
"arch": {
|
||||||
|
|
@ -17103,6 +17135,22 @@
|
||||||
"sha256": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8",
|
"sha256": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8",
|
||||||
"variant": null
|
"variant": null
|
||||||
},
|
},
|
||||||
|
"cpython-3.12.1-emscripten-wasm32-musl": {
|
||||||
|
"name": "cpython",
|
||||||
|
"arch": {
|
||||||
|
"family": "wasm32",
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
|
"os": "emscripten",
|
||||||
|
"libc": "musl",
|
||||||
|
"major": 3,
|
||||||
|
"minor": 12,
|
||||||
|
"patch": 1,
|
||||||
|
"prerelease": "",
|
||||||
|
"url": "https://github.com/pyodide/pyodide/releases/download/0.26.4/xbuildenv-0.26.4.tar.bz2",
|
||||||
|
"sha256": null,
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
"cpython-3.12.1-linux-aarch64-gnu": {
|
"cpython-3.12.1-linux-aarch64-gnu": {
|
||||||
"name": "cpython",
|
"name": "cpython",
|
||||||
"arch": {
|
"arch": {
|
||||||
|
|
@ -21439,6 +21487,22 @@
|
||||||
"sha256": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310",
|
"sha256": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310",
|
||||||
"variant": null
|
"variant": null
|
||||||
},
|
},
|
||||||
|
"cpython-3.11.3-emscripten-wasm32-musl": {
|
||||||
|
"name": "cpython",
|
||||||
|
"arch": {
|
||||||
|
"family": "wasm32",
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
|
"os": "emscripten",
|
||||||
|
"libc": "musl",
|
||||||
|
"major": 3,
|
||||||
|
"minor": 11,
|
||||||
|
"patch": 3,
|
||||||
|
"prerelease": "",
|
||||||
|
"url": "https://github.com/pyodide/pyodide/releases/download/0.25.1/xbuildenv-0.25.1.tar.bz2",
|
||||||
|
"sha256": null,
|
||||||
|
"variant": null
|
||||||
|
},
|
||||||
"cpython-3.11.3-linux-aarch64-gnu": {
|
"cpython-3.11.3-linux-aarch64-gnu": {
|
||||||
"name": "cpython",
|
"name": "cpython",
|
||||||
"arch": {
|
"arch": {
|
||||||
|
|
|
||||||
|
|
@ -550,6 +550,92 @@ class PyPyFinder(Finder):
|
||||||
download.sha256 = checksums.get(download.filename)
|
download.sha256 = checksums.get(download.filename)
|
||||||
|
|
||||||
|
|
||||||
|
class PyodideFinder(Finder):
|
||||||
|
implementation = ImplementationName.CPYTHON
|
||||||
|
|
||||||
|
RELEASE_URL = "https://api.github.com/repos/pyodide/pyodide/releases"
|
||||||
|
METADATA_URL = (
|
||||||
|
"https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
TRIPLE = PlatformTriple(
|
||||||
|
platform="emscripten",
|
||||||
|
arch=Arch("wasm32"),
|
||||||
|
libc="musl",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, client: httpx.AsyncClient):
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
async def find(self) -> list[PythonDownload]:
|
||||||
|
downloads = await self._fetch_downloads()
|
||||||
|
await self._fetch_checksums(downloads, n=10)
|
||||||
|
return downloads
|
||||||
|
|
||||||
|
async def _fetch_downloads(self) -> list[PythonDownload]:
|
||||||
|
# This will only download the first page, i.e., ~30 releases
|
||||||
|
[release_resp, meta_resp] = await asyncio.gather(
|
||||||
|
self.client.get(self.RELEASE_URL), self.client.get(self.METADATA_URL)
|
||||||
|
)
|
||||||
|
release_resp.raise_for_status()
|
||||||
|
meta_resp.raise_for_status()
|
||||||
|
releases = release_resp.json()
|
||||||
|
metadata = meta_resp.json()["releases"]
|
||||||
|
|
||||||
|
maj_minor_seen = set()
|
||||||
|
results = []
|
||||||
|
for release in releases:
|
||||||
|
pyodide_version = release["tag_name"]
|
||||||
|
meta = metadata.get(pyodide_version, None)
|
||||||
|
if meta is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
maj_min = pyodide_version.rpartition(".")[0]
|
||||||
|
# Only keep latest
|
||||||
|
if maj_min in maj_minor_seen:
|
||||||
|
continue
|
||||||
|
maj_minor_seen.add(maj_min)
|
||||||
|
|
||||||
|
python_version = Version.from_str(meta["python_version"])
|
||||||
|
# Find xbuildenv asset
|
||||||
|
for asset in release["assets"]:
|
||||||
|
if asset["name"].startswith("xbuildenv"):
|
||||||
|
break
|
||||||
|
|
||||||
|
url = asset["browser_download_url"]
|
||||||
|
results.append(
|
||||||
|
PythonDownload(
|
||||||
|
release=0,
|
||||||
|
version=python_version,
|
||||||
|
triple=self.TRIPLE,
|
||||||
|
flavor=pyodide_version,
|
||||||
|
implementation=self.implementation,
|
||||||
|
filename=asset["name"],
|
||||||
|
url=url,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
async def _fetch_checksums(self, downloads: list[PythonDownload], n: int) -> None:
|
||||||
|
for idx, batch in enumerate(batched(downloads, n)):
|
||||||
|
logging.info("Fetching Pyodide checksums: %d/%d", idx * n, len(downloads))
|
||||||
|
checksum_requests = []
|
||||||
|
for download in batch:
|
||||||
|
url = download.url + ".sha256"
|
||||||
|
checksum_requests.append(self.client.get(url))
|
||||||
|
for download, resp in zip(
|
||||||
|
batch, await asyncio.gather(*checksum_requests), strict=False
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
resp.raise_for_status()
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
continue
|
||||||
|
raise
|
||||||
|
download.sha256 = resp.text.strip()
|
||||||
|
|
||||||
|
|
||||||
class GraalPyFinder(Finder):
|
class GraalPyFinder(Finder):
|
||||||
implementation = ImplementationName.GRAALPY
|
implementation = ImplementationName.GRAALPY
|
||||||
|
|
||||||
|
|
@ -751,6 +837,7 @@ async def find() -> None:
|
||||||
CPythonFinder(client),
|
CPythonFinder(client),
|
||||||
PyPyFinder(client),
|
PyPyFinder(client),
|
||||||
GraalPyFinder(client),
|
GraalPyFinder(client),
|
||||||
|
PyodideFinder(client),
|
||||||
]
|
]
|
||||||
downloads = []
|
downloads = []
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -342,7 +342,8 @@ 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()?;
|
||||||
// Check that the Python version and platform satisfy the request to avoid unnecessary interpreter queries later
|
// Check that the Python version and platform satisfy the request to avoid
|
||||||
|
// unnecessary interpreter queries later
|
||||||
Ok(installations
|
Ok(installations
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(move |installation| {
|
.filter(move |installation| {
|
||||||
|
|
@ -351,7 +352,7 @@ fn python_executables_from_installed<'a>(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if !platform.matches(installation.platform()) {
|
if !platform.matches(installation.platform()) {
|
||||||
debug!("Skipping managed installation `{installation}`: does not satisfy `{platform}`");
|
debug!("Skipping managed installation `{installation}`: does not satisfy requested platform `{platform}`");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
|
|
@ -1259,6 +1260,7 @@ 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) {
|
||||||
|
|
@ -1276,6 +1278,15 @@ 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.
|
||||||
|
|
||||||
|
|
@ -1352,6 +1363,10 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ impl PlatformRequest {
|
||||||
/// Check if this platform request is satisfied by a platform.
|
/// Check if this platform request is satisfied by a platform.
|
||||||
pub fn matches(&self, platform: &Platform) -> bool {
|
pub fn matches(&self, platform: &Platform) -> bool {
|
||||||
if let Some(os) = self.os {
|
if let Some(os) = self.os {
|
||||||
if platform.os != os {
|
if !platform.os.supports(os) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -452,25 +452,14 @@ impl PythonDownloadRequest {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(os) = self.os() {
|
let platform = self.platform();
|
||||||
if &interpreter.os() != os {
|
|
||||||
debug!(
|
|
||||||
"Skipping interpreter at `{executable}`: operating system `{}` does not match request `{os}`",
|
|
||||||
interpreter.os()
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(arch) = self.arch() {
|
|
||||||
let interpreter_platform = Platform::from(interpreter.platform());
|
let interpreter_platform = Platform::from(interpreter.platform());
|
||||||
if !arch.satisfied_by(&interpreter_platform) {
|
if !platform.matches(&interpreter_platform) {
|
||||||
debug!(
|
debug!(
|
||||||
"Skipping interpreter at `{executable}`: architecture `{}` does not match request `{arch}`",
|
"Skipping interpreter at `{executable}`: platform `{interpreter_platform}` does not match request `{platform}`",
|
||||||
interpreter.arch()
|
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(implementation) = self.implementation() {
|
if let Some(implementation) = self.implementation() {
|
||||||
let interpreter_implementation = interpreter.implementation_name();
|
let interpreter_implementation = interpreter.implementation_name();
|
||||||
if LenientImplementationName::from(interpreter_implementation)
|
if LenientImplementationName::from(interpreter_implementation)
|
||||||
|
|
@ -482,15 +471,6 @@ impl PythonDownloadRequest {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(libc) = self.libc() {
|
|
||||||
if &interpreter.libc() != libc {
|
|
||||||
debug!(
|
|
||||||
"Skipping interpreter at `{executable}`: libc `{}` does not match request `{libc}`",
|
|
||||||
interpreter.libc()
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1066,13 +1046,32 @@ impl ManagedPythonDownload {
|
||||||
// If the distribution is a `full` archive, the Python installation is in the `install` directory.
|
// If the distribution is a `full` archive, the Python installation is in the `install` directory.
|
||||||
if extracted.join("install").is_dir() {
|
if extracted.join("install").is_dir() {
|
||||||
extracted = extracted.join("install");
|
extracted = extracted.join("install");
|
||||||
|
// If the distribution is a Pyodide archive, the Python installation is in the `pyodide-root/dist` directory.
|
||||||
|
} else if self.os().is_emscripten() {
|
||||||
|
extracted = extracted.join("pyodide-root").join("dist");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the distribution is missing a `python`-to-`pythonX.Y` symlink, add it. PEP 394 permits
|
|
||||||
// it, and python-build-standalone releases after `20240726` include it, but releases prior
|
|
||||||
// to that date do not.
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
|
// Pyodide distributions require all of the supporting files to be alongside the Python
|
||||||
|
// executable, so they don't have a `bin` directory. We create it and link
|
||||||
|
// `bin/pythonX.Y` to `dist/python`.
|
||||||
|
if self.os().is_emscripten() {
|
||||||
|
fs_err::create_dir_all(extracted.join("bin"))?;
|
||||||
|
fs_err::os::unix::fs::symlink(
|
||||||
|
"../python",
|
||||||
|
extracted
|
||||||
|
.join("bin")
|
||||||
|
.join(format!("python{}.{}", self.key.major, self.key.minor)),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the distribution is missing a `python` -> `pythonX.Y` symlink, add it.
|
||||||
|
//
|
||||||
|
// Pyodide releases never contain this link by default.
|
||||||
|
//
|
||||||
|
// PEP 394 permits it, and python-build-standalone releases after `20240726` include it,
|
||||||
|
// but releases prior to that date do not.
|
||||||
match fs_err::os::unix::fs::symlink(
|
match fs_err::os::unix::fs::symlink(
|
||||||
format!("python{}.{}", self.key.major, self.key.minor),
|
format!("python{}.{}", self.key.major, self.key.minor),
|
||||||
extracted.join("bin").join("python"),
|
extracted.join("bin").join("python"),
|
||||||
|
|
|
||||||
|
|
@ -325,6 +325,89 @@ mod tests {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_mock_pyodide_interpreter(path: &Path, version: &PythonVersion) -> Result<()> {
|
||||||
|
let json = indoc! {r##"
|
||||||
|
{
|
||||||
|
"result": "success",
|
||||||
|
"platform": {
|
||||||
|
"os": {
|
||||||
|
"name": "pyodide",
|
||||||
|
"major": 2025,
|
||||||
|
"minor": 0
|
||||||
|
},
|
||||||
|
"arch": "wasm32"
|
||||||
|
},
|
||||||
|
"manylinux_compatible": false,
|
||||||
|
"standalone": false,
|
||||||
|
"markers": {
|
||||||
|
"implementation_name": "cpython",
|
||||||
|
"implementation_version": "{FULL_VERSION}",
|
||||||
|
"os_name": "posix",
|
||||||
|
"platform_machine": "wasm32",
|
||||||
|
"platform_python_implementation": "CPython",
|
||||||
|
"platform_release": "4.0.9",
|
||||||
|
"platform_system": "Emscripten",
|
||||||
|
"platform_version": "#1",
|
||||||
|
"python_full_version": "{FULL_VERSION}",
|
||||||
|
"python_version": "{VERSION}",
|
||||||
|
"sys_platform": "emscripten"
|
||||||
|
},
|
||||||
|
"sys_base_exec_prefix": "/",
|
||||||
|
"sys_base_prefix": "/",
|
||||||
|
"sys_prefix": "/",
|
||||||
|
"sys_executable": "{PATH}",
|
||||||
|
"sys_path": [
|
||||||
|
"",
|
||||||
|
"/lib/python313.zip",
|
||||||
|
"/lib/python{VERSION}",
|
||||||
|
"/lib/python{VERSION}/lib-dynload",
|
||||||
|
"/lib/python{VERSION}/site-packages"
|
||||||
|
],
|
||||||
|
"site_packages": [
|
||||||
|
"/lib/python{VERSION}/site-packages"
|
||||||
|
],
|
||||||
|
"stdlib": "//lib/python{VERSION}",
|
||||||
|
"scheme": {
|
||||||
|
"platlib": "//lib/python{VERSION}/site-packages",
|
||||||
|
"purelib": "//lib/python{VERSION}/site-packages",
|
||||||
|
"include": "//include/python{VERSION}",
|
||||||
|
"scripts": "//bin",
|
||||||
|
"data": "/"
|
||||||
|
},
|
||||||
|
"virtualenv": {
|
||||||
|
"purelib": "lib/python{VERSION}/site-packages",
|
||||||
|
"platlib": "lib/python{VERSION}/site-packages",
|
||||||
|
"include": "include/site/python{VERSION}",
|
||||||
|
"scripts": "bin",
|
||||||
|
"data": ""
|
||||||
|
},
|
||||||
|
"pointer_size": "32",
|
||||||
|
"gil_disabled": false
|
||||||
|
}
|
||||||
|
"##};
|
||||||
|
|
||||||
|
let json = json
|
||||||
|
.replace(
|
||||||
|
"{PATH}",
|
||||||
|
path.to_str().expect("Path can be represented as string"),
|
||||||
|
)
|
||||||
|
.replace("{FULL_VERSION}", &version.to_string())
|
||||||
|
.replace("{VERSION}", &version.without_patch().to_string());
|
||||||
|
|
||||||
|
fs_err::create_dir_all(path.parent().unwrap())?;
|
||||||
|
fs_err::write(
|
||||||
|
path,
|
||||||
|
formatdoc! {r"
|
||||||
|
#!/bin/sh
|
||||||
|
echo '{json}'
|
||||||
|
"},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
fs_err::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o770))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a mock Python 2 interpreter executable which returns a fixed error message mocking
|
/// Create a mock Python 2 interpreter executable which returns a fixed error message mocking
|
||||||
/// invocation of Python 2 with the `-I` flag as done by our query script.
|
/// invocation of Python 2 with the `-I` flag as done by our query script.
|
||||||
fn create_mock_python2_interpreter(path: &Path) -> Result<()> {
|
fn create_mock_python2_interpreter(path: &Path) -> Result<()> {
|
||||||
|
|
@ -372,6 +455,16 @@ mod tests {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_pyodide_version(&mut self, version: &'static str) -> Result<()> {
|
||||||
|
let path = self.new_search_path_directory(format!("pyodide-{version}"))?;
|
||||||
|
let python = format!("python{}", env::consts::EXE_SUFFIX);
|
||||||
|
Self::create_mock_pyodide_interpreter(
|
||||||
|
&path.join(python),
|
||||||
|
&PythonVersion::from_str(version).unwrap(),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Create fake Python interpreters the given Python versions.
|
/// Create fake Python interpreters the given Python versions.
|
||||||
///
|
///
|
||||||
/// Adds them to the test context search path.
|
/// Adds them to the test context search path.
|
||||||
|
|
@ -2606,4 +2699,44 @@ mod tests {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_python_pyodide() -> Result<()> {
|
||||||
|
let mut context = TestContext::new()?;
|
||||||
|
|
||||||
|
context.add_pyodide_version("3.13.2")?;
|
||||||
|
let python = context.run(|| {
|
||||||
|
find_python_installation(
|
||||||
|
&PythonRequest::Default,
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
PythonPreference::OnlySystem,
|
||||||
|
&context.cache,
|
||||||
|
Preview::default(),
|
||||||
|
)
|
||||||
|
})??;
|
||||||
|
// We should find the Pyodide interpreter
|
||||||
|
assert_eq!(
|
||||||
|
python.interpreter().python_full_version().to_string(),
|
||||||
|
"3.13.2"
|
||||||
|
);
|
||||||
|
|
||||||
|
// We should prefer any native Python to the Pyodide Python
|
||||||
|
context.add_python_versions(&["3.15.7"])?;
|
||||||
|
|
||||||
|
let python = context.run(|| {
|
||||||
|
find_python_installation(
|
||||||
|
&PythonRequest::Default,
|
||||||
|
EnvironmentPreference::Any,
|
||||||
|
PythonPreference::OnlySystem,
|
||||||
|
&context.cache,
|
||||||
|
Preview::default(),
|
||||||
|
)
|
||||||
|
})??;
|
||||||
|
assert_eq!(
|
||||||
|
python.interpreter().python_full_version().to_string(),
|
||||||
|
"3.15.7"
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,13 @@ impl ManagedPythonInstallations {
|
||||||
|
|
||||||
let iter = Self::from_settings(None)?
|
let iter = Self::from_settings(None)?
|
||||||
.find_all()?
|
.find_all()?
|
||||||
.filter(move |installation| platform.supports(installation.platform()));
|
.filter(move |installation| {
|
||||||
|
if !platform.supports(installation.platform()) {
|
||||||
|
debug!("Skipping managed installation `{installation}`: not supported by current platform `{platform}`");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
Ok(iter)
|
Ok(iter)
|
||||||
}
|
}
|
||||||
|
|
@ -538,6 +544,11 @@ impl ManagedPythonInstallation {
|
||||||
/// Ensure the environment is marked as externally managed with the
|
/// Ensure the environment is marked as externally managed with the
|
||||||
/// standard `EXTERNALLY-MANAGED` file.
|
/// standard `EXTERNALLY-MANAGED` file.
|
||||||
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
|
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
|
||||||
|
if self.key.os().is_emscripten() {
|
||||||
|
// Emscripten's stdlib is a zip file so we can't put an
|
||||||
|
// EXTERNALLY-MANAGED inside.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
// Construct the path to the `stdlib` directory.
|
// Construct the path to the `stdlib` directory.
|
||||||
let stdlib = if self.key.os().is_windows() {
|
let stdlib = if self.key.os().is_windows() {
|
||||||
self.python_dir().join("Lib")
|
self.python_dir().join("Lib")
|
||||||
|
|
@ -563,6 +574,11 @@ impl ManagedPythonInstallation {
|
||||||
/// Ensure that the `sysconfig` data is patched to match the installation path.
|
/// Ensure that the `sysconfig` data is patched to match the installation path.
|
||||||
pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
|
pub fn ensure_sysconfig_patched(&self) -> Result<(), Error> {
|
||||||
if cfg!(unix) {
|
if cfg!(unix) {
|
||||||
|
if self.key.os().is_emscripten() {
|
||||||
|
// Emscripten's stdlib is a zip file so we can't update the
|
||||||
|
// sysconfig directly
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if *self.implementation() == ImplementationName::CPython {
|
if *self.implementation() == ImplementationName::CPython {
|
||||||
sysconfig::update_sysconfig(
|
sysconfig::update_sysconfig(
|
||||||
self.path(),
|
self.path(),
|
||||||
|
|
|
||||||
|
|
@ -1790,14 +1790,14 @@ fn compile_python_build_version_different_than_target() -> Result<()> {
|
||||||
.arg("3.12")
|
.arg("3.12")
|
||||||
.arg("-p")
|
.arg("-p")
|
||||||
.arg("3.13")
|
.arg("3.13")
|
||||||
.env_remove(EnvVars::VIRTUAL_ENV), @r###"
|
.env_remove(EnvVars::VIRTUAL_ENV), @r"
|
||||||
success: false
|
success: false
|
||||||
exit_code: 2
|
exit_code: 2
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
error: No interpreter found for Python 3.13 in [PYTHON SOURCES]
|
error: No interpreter found for Python 3.13 in [PYTHON SOURCES]
|
||||||
"###
|
"
|
||||||
);
|
);
|
||||||
|
|
||||||
// `UV_PYTHON` is ignored if `--python-version` is set
|
// `UV_PYTHON` is ignored if `--python-version` is set
|
||||||
|
|
|
||||||
|
|
@ -5411,7 +5411,7 @@ fn target_system() -> Result<()> {
|
||||||
uv_snapshot!(context.filters(), context.pip_sync()
|
uv_snapshot!(context.filters(), context.pip_sync()
|
||||||
.arg("requirements.in")
|
.arg("requirements.in")
|
||||||
.arg("--target")
|
.arg("--target")
|
||||||
.arg("target"), @r###"
|
.arg("target"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
|
@ -5422,7 +5422,7 @@ fn target_system() -> Result<()> {
|
||||||
Prepared 1 package in [TIME]
|
Prepared 1 package in [TIME]
|
||||||
Installed 1 package in [TIME]
|
Installed 1 package in [TIME]
|
||||||
+ iniconfig==2.0.0
|
+ iniconfig==2.0.0
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Ensure that the package is present in the target directory.
|
// Ensure that the package is present in the target directory.
|
||||||
assert!(context.temp_dir.child("target").child("iniconfig").is_dir());
|
assert!(context.temp_dir.child("target").child("iniconfig").is_dir());
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,14 @@ fn python_find() {
|
||||||
");
|
");
|
||||||
|
|
||||||
// We find the first interpreter on the path
|
// We find the first interpreter on the path
|
||||||
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.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Request Python 3.12
|
// Request Python 3.12
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("3.12"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("3.12"), @r###"
|
||||||
|
|
@ -53,14 +53,14 @@ fn python_find() {
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Request CPython
|
// Request CPython
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("cpython"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("cpython"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Request CPython 3.12
|
// Request CPython 3.12
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("cpython@3.12"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("cpython@3.12"), @r###"
|
||||||
|
|
@ -119,14 +119,14 @@ fn python_find() {
|
||||||
// Swap the order of the Python versions
|
// Swap the order of the Python versions
|
||||||
context.python_versions.reverse();
|
context.python_versions.reverse();
|
||||||
|
|
||||||
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 -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Request Python 3.11
|
// Request Python 3.11
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("3.11"), @r###"
|
||||||
|
|
@ -174,14 +174,14 @@ fn python_find_pin() {
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Or `--no-config` is used
|
// Or `--no-config` is used
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-config"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("--no-config"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
let child_dir = context.temp_dir.child("child");
|
let child_dir = context.temp_dir.child("child");
|
||||||
child_dir.create_dir_all().unwrap();
|
child_dir.create_dir_all().unwrap();
|
||||||
|
|
@ -308,14 +308,14 @@ fn python_find_project() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// We should respect the project's required version, not the first on the path
|
// We should respect the project's required version, not the first on the path
|
||||||
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.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Unless explicitly requested
|
// Unless explicitly requested
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r"
|
uv_snapshot!(context.filters(), context.python_find().arg("3.10"), @r"
|
||||||
|
|
@ -329,14 +329,14 @@ fn python_find_project() {
|
||||||
");
|
");
|
||||||
|
|
||||||
// Or `--no-project` is used
|
// Or `--no-project` is used
|
||||||
uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r###"
|
uv_snapshot!(context.filters(), context.python_find().arg("--no-project"), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.10]
|
[PYTHON-3.10]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// But a pin should take precedence
|
// But a pin should take precedence
|
||||||
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
uv_snapshot!(context.filters(), context.python_pin().arg("3.12"), @r###"
|
||||||
|
|
@ -393,14 +393,14 @@ fn python_find_project() {
|
||||||
"#})
|
"#})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r###"
|
uv_snapshot!(context.filters(), context.python_find().current_dir(&child_dir), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -517,14 +517,14 @@ fn python_find_venv() {
|
||||||
fs_err::remove_dir_all(context.temp_dir.child(".venv")).unwrap();
|
fs_err::remove_dir_all(context.temp_dir.child(".venv")).unwrap();
|
||||||
|
|
||||||
// And query from there... we should not find the child virtual environment
|
// And query from there... we should not find the child virtual environment
|
||||||
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.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
|
|
||||||
// Unless, it is requested by path
|
// Unless, it is requested by path
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
|
|
@ -588,14 +588,14 @@ fn python_find_venv() {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @r###"
|
uv_snapshot!(context.filters(), context.python_find().env(EnvVars::UV_TEST_PYTHON_PATH, path.as_os_str()), @r"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
[PYTHON-3.11]
|
[PYTHON-3.11]
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
"###);
|
");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2987,3 +2987,86 @@ fn uninstall_last_patch() {
|
||||||
"#
|
"#
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)] // Pyodide cannot be used on Windows
|
||||||
|
#[test]
|
||||||
|
fn python_install_pyodide() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&[])
|
||||||
|
.with_filtered_python_keys()
|
||||||
|
.with_filtered_exe_suffix()
|
||||||
|
.with_managed_python_dirs()
|
||||||
|
.with_python_download_cache();
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("cpython-3.13.2-emscripten-wasm32-musl"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Installed Python 3.13.2 in [TIME]
|
||||||
|
+ cpython-3.13.2-[PLATFORM] (python3.13)
|
||||||
|
");
|
||||||
|
|
||||||
|
let bin_python = context
|
||||||
|
.bin_dir
|
||||||
|
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
|
// The executable should be installed in the bin directory
|
||||||
|
bin_python.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
// On Unix, it should be a link
|
||||||
|
#[cfg(unix)]
|
||||||
|
bin_python.assert(predicate::path::is_symlink());
|
||||||
|
|
||||||
|
// The link should be a path to the binary
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
insta::assert_snapshot!(
|
||||||
|
read_link(&bin_python), @"[TEMP_DIR]/managed/cpython-3.13.2-[PLATFORM]/bin/python3.13"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// The executable should "work"
|
||||||
|
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
|
||||||
|
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
hello world
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// We should be able to find the Pyodide interpreter
|
||||||
|
uv_snapshot!(context.filters(), context.python_find().arg("cpython-3.13.2-emscripten-wasm32-musl"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
[TEMP_DIR]/managed/cpython-3.13.2-[PLATFORM]/bin/python3.13
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
|
||||||
|
// We should be able to create a virtual environment with it
|
||||||
|
uv_snapshot!(context.filters(), context.venv().arg("--python").arg("cpython-3.13.2-emscripten-wasm32-musl"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Using CPython 3.13.2
|
||||||
|
Creating virtual environment at: .venv
|
||||||
|
Activate with: source .venv/[BIN]/activate
|
||||||
|
");
|
||||||
|
|
||||||
|
// We should be able to run the Python in the virtual environment
|
||||||
|
uv_snapshot!(context.filters(), context.python_command().arg("-c").arg("import subprocess; print('hello world')"), @r"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
hello world
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue