Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1493,7 +1493,7 @@ jobs:
# newer versions.
./uv pip install -p venv-native/bin/python pyodide-build==0.30.3 pip

- name: "Install pyodide interpreter"
- name: "Install Pyodide interpreter"
run: |
source ./venv-native/bin/activate
pyodide xbuildenv install 0.27.5
Expand All @@ -1502,13 +1502,25 @@ jobs:
echo "PYODIDE_PYTHON=$PYODIDE_PYTHON" >> $GITHUB_ENV
echo "PYODIDE_INDEX=$PYODIDE_INDEX" >> $GITHUB_ENV

- name: "Create pyodide virtual environment"
- name: "Create Pyodide virtual environment"
run: |
./uv venv -p $PYODIDE_PYTHON venv-pyodide
source ./venv-pyodide/bin/activate
./uv pip install --extra-index-url=$PYODIDE_INDEX --no-build 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:
timeout-minutes: 10
needs: build-binary-linux-libc
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-platform/src/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ impl Arch {
pub fn is_arm(&self) -> bool {
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 {
Expand Down
31 changes: 26 additions & 5 deletions crates/uv-platform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::cmp;
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
use tracing::trace;

pub use crate::arch::{Arch, ArchVariant};
pub use crate::libc::{Libc, LibcDetectionError, LibcVersion};
Expand Down Expand Up @@ -68,13 +69,20 @@ impl Platform {
return true;
}

// OS must match exactly
if self.os != other.os {
if !self.os.supports(other.os) {
trace!(
"Operating system `{}` is not compatible with `{}`",
self.os, other.os
);
return false;
}

// Libc must match exactly
if self.libc != other.libc {
// Libc must match exactly, unless we're on emscripten — in which case it doesn't matter
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;
}

Expand All @@ -94,10 +102,23 @@ impl Platform {
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.
// See https://github.com/astral-sh/uv/pull/9788
// 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
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/uv-platform/src/libc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ impl From<&uv_platform_tags::Os> for Libc {
match value {
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::Pyodide { .. } => Self::Some(target_lexicon::Environment::Musl),
_ => Self::None,
}
}
Expand Down
18 changes: 18 additions & 0 deletions crates/uv-platform/src/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ impl Os {
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 {
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 {
Expand Down
64 changes: 64 additions & 0 deletions crates/uv-python/download-metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -10511,6 +10511,22 @@
"sha256": "e5a904ecfb4061389773dd655d3b5665447c80cbf2948fcb1c07e92716eed955",
"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": {
"name": "cpython",
"arch": {
Expand Down Expand Up @@ -15055,6 +15071,22 @@
"sha256": "848405b92bda20fad1f9bba99234c7d3f11e0b31e46f89835d1cb3d735e932aa",
"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": {
"name": "cpython",
"arch": {
Expand Down Expand Up @@ -17103,6 +17135,22 @@
"sha256": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8",
"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": {
"name": "cpython",
"arch": {
Expand Down Expand Up @@ -21439,6 +21487,22 @@
"sha256": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310",
"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": {
"name": "cpython",
"arch": {
Expand Down
87 changes: 87 additions & 0 deletions crates/uv-python/fetch-download-metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,92 @@ async def _fetch_checksums(self, downloads: list[PythonDownload]) -> None:
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):
implementation = ImplementationName.GRAALPY

Expand Down Expand Up @@ -751,6 +837,7 @@ async def find() -> None:
CPythonFinder(client),
PyPyFinder(client),
GraalPyFinder(client),
PyodideFinder(client),
]
downloads = []

Expand Down
19 changes: 17 additions & 2 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,8 @@ fn python_executables_from_installed<'a>(
installed_installations.root().user_display()
);
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
.into_iter()
.filter(move |installation| {
Expand All @@ -351,7 +352,7 @@ fn python_executables_from_installed<'a>(
return false;
}
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;
}
true
Expand Down Expand Up @@ -1259,6 +1260,7 @@ pub(crate) fn find_python_installation(
let mut first_prerelease = None;
let mut first_managed = None;
let mut first_error = None;
let mut emscripten_installation = None;
for result in installations {
// Iterate until the first critical error or happy result
if !result.as_ref().err().is_none_or(Error::is_critical) {
Expand All @@ -1276,6 +1278,15 @@ pub(crate) fn find_python_installation(
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
// pre-release version or an alternative implementation, using it requires opt-in.

Expand Down Expand Up @@ -1352,6 +1363,10 @@ pub(crate) fn find_python_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
// couldn't find any Python interpreters.
if let Some(err) = first_error {
Expand Down
Loading
Loading