Skip to content

Commit 5ba6e67

Browse files
committed
Add support for installing pyodide Pythons
WIP
1 parent afcbcc7 commit 5ba6e67

File tree

5 files changed

+275
-50
lines changed

5 files changed

+275
-50
lines changed

crates/uv-python/download-metadata.json

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8847,6 +8847,22 @@
88478847
"sha256": "e5a904ecfb4061389773dd655d3b5665447c80cbf2948fcb1c07e92716eed955",
88488848
"variant": null
88498849
},
8850+
"cpython-3.13.2-emscripten-wasm32-musl": {
8851+
"name": "cpython",
8852+
"arch": {
8853+
"family": "wasm32",
8854+
"variant": null
8855+
},
8856+
"os": "emscripten",
8857+
"libc": "musl",
8858+
"major": 3,
8859+
"minor": 13,
8860+
"patch": 2,
8861+
"prerelease": "",
8862+
"url": "https://github.com/pyodide/pyodide/releases/download/0.28.0/xbuildenv-0.28.0.tar.bz2",
8863+
"sha256": null,
8864+
"variant": null
8865+
},
88508866
"cpython-3.13.2-linux-aarch64-gnu": {
88518867
"name": "cpython",
88528868
"arch": {
@@ -13391,6 +13407,22 @@
1339113407
"sha256": "848405b92bda20fad1f9bba99234c7d3f11e0b31e46f89835d1cb3d735e932aa",
1339213408
"variant": null
1339313409
},
13410+
"cpython-3.12.7-emscripten-wasm32-musl": {
13411+
"name": "cpython",
13412+
"arch": {
13413+
"family": "wasm32",
13414+
"variant": null
13415+
},
13416+
"os": "emscripten",
13417+
"libc": "musl",
13418+
"major": 3,
13419+
"minor": 12,
13420+
"patch": 7,
13421+
"prerelease": "",
13422+
"url": "https://github.com/pyodide/pyodide/releases/download/0.27.7/xbuildenv-0.27.7.tar.bz2",
13423+
"sha256": null,
13424+
"variant": null
13425+
},
1339413426
"cpython-3.12.7-linux-aarch64-gnu": {
1339513427
"name": "cpython",
1339613428
"arch": {
@@ -15439,6 +15471,22 @@
1543915471
"sha256": "eca96158c1568dedd9a0b3425375637a83764d1fa74446438293089a8bfac1f8",
1544015472
"variant": null
1544115473
},
15474+
"cpython-3.12.1-emscripten-wasm32-musl": {
15475+
"name": "cpython",
15476+
"arch": {
15477+
"family": "wasm32",
15478+
"variant": null
15479+
},
15480+
"os": "emscripten",
15481+
"libc": "musl",
15482+
"major": 3,
15483+
"minor": 12,
15484+
"patch": 1,
15485+
"prerelease": "",
15486+
"url": "https://github.com/pyodide/pyodide/releases/download/0.26.4/xbuildenv-0.26.4.tar.bz2",
15487+
"sha256": null,
15488+
"variant": null
15489+
},
1544215490
"cpython-3.12.1-linux-aarch64-gnu": {
1544315491
"name": "cpython",
1544415492
"arch": {
@@ -19775,6 +19823,22 @@
1977519823
"sha256": "f710b8d60621308149c100d5175fec39274ed0b9c99645484fd93d1716ef4310",
1977619824
"variant": null
1977719825
},
19826+
"cpython-3.11.3-emscripten-wasm32-musl": {
19827+
"name": "cpython",
19828+
"arch": {
19829+
"family": "wasm32",
19830+
"variant": null
19831+
},
19832+
"os": "emscripten",
19833+
"libc": "musl",
19834+
"major": 3,
19835+
"minor": 11,
19836+
"patch": 3,
19837+
"prerelease": "",
19838+
"url": "https://github.com/pyodide/pyodide/releases/download/0.25.1/xbuildenv-0.25.1.tar.bz2",
19839+
"sha256": null,
19840+
"variant": null
19841+
},
1977819842
"cpython-3.11.3-linux-aarch64-gnu": {
1977919843
"name": "cpython",
1978019844
"arch": {

crates/uv-python/fetch-download-metadata.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,98 @@ async def _fetch_checksums(self, downloads: list[PythonDownload]) -> None:
541541
download.sha256 = checksums.get(download.filename)
542542

543543

544+
class PyodideFinder(Finder):
545+
implementation = ImplementationName.CPYTHON
546+
547+
RELEASE_URL = "https://api.github.com/repos/pyodide/pyodide/releases"
548+
METADATA_URL = (
549+
"https://pyodide.github.io/pyodide/api/pyodide-cross-build-environments.json"
550+
)
551+
552+
TRIPLE = PlatformTriple(
553+
platform="emscripten",
554+
arch=Arch("wasm32"),
555+
libc="musl",
556+
)
557+
558+
def __init__(self, client: httpx.AsyncClient):
559+
self.client = client
560+
561+
async def find(self) -> list[PythonDownload]:
562+
downloads = await self._fetch_downloads()
563+
await self._fetch_checksums(downloads, n=10)
564+
return downloads
565+
566+
async def _fetch_downloads(self) -> list[PythonDownload]:
567+
# This will only download the first page, i.e., ~30 releases
568+
[release_resp, meta_resp] = await asyncio.gather(
569+
self.client.get(self.RELEASE_URL), self.client.get(self.METADATA_URL)
570+
)
571+
release_resp.raise_for_status()
572+
meta_resp.raise_for_status()
573+
releases = release_resp.json()
574+
metadata = meta_resp.json()["releases"]
575+
576+
maj_minor_seen = set()
577+
results = []
578+
for release in releases:
579+
pyodide_version = release["tag_name"]
580+
meta = metadata.get(pyodide_version, None)
581+
if meta is None:
582+
continue
583+
584+
maj_min = pyodide_version.rpartition(".")[0]
585+
# Only keep latest
586+
if maj_min in maj_minor_seen:
587+
continue
588+
maj_minor_seen.add(maj_min)
589+
590+
python_version = Version.from_str(meta["python_version"])
591+
# Find xbuildenv asset
592+
for asset in release["assets"]:
593+
if asset["name"].startswith("xbuildenv"):
594+
break
595+
596+
url = asset["browser_download_url"]
597+
results.append(
598+
PythonDownload(
599+
release=0,
600+
version=python_version,
601+
triple=self.TRIPLE,
602+
flavor=pyodide_version,
603+
implementation=self.implementation,
604+
filename=asset["name"],
605+
url=url,
606+
)
607+
)
608+
609+
return results
610+
611+
def _normalize_arch(self, arch: str) -> Arch:
612+
return Arch(self.ARCH_MAPPING.get(arch, arch), None)
613+
614+
def _normalize_os(self, os: str) -> str:
615+
return self.PLATFORM_MAPPING.get(os, os)
616+
617+
async def _fetch_checksums(self, downloads: list[PythonDownload], n: int) -> None:
618+
for idx, batch in enumerate(batched(downloads, n)):
619+
logging.info("Fetching Pyodide checksums: %d/%d", idx * n, len(downloads))
620+
checksum_requests = []
621+
for download in batch:
622+
url = download.url + ".sha256"
623+
checksum_requests.append(self.client.get(url))
624+
for download, resp in zip(
625+
batch, await asyncio.gather(*checksum_requests), strict=False
626+
):
627+
try:
628+
resp.raise_for_status()
629+
except httpx.HTTPStatusError as e:
630+
if e.response.status_code == 404:
631+
continue
632+
raise
633+
download.sha256 = resp.text.strip()
634+
635+
544636
class GraalPyFinder(Finder):
545637
implementation = ImplementationName.GRAALPY
546638

@@ -670,6 +762,7 @@ def sort_key(download: PythonDownload) -> tuple:
670762
ImplementationName.CPYTHON,
671763
ImplementationName.PYPY,
672764
ImplementationName.GRAALPY,
765+
ImplementationName.PYODIDE,
673766
]
674767
prerelease = prerelease_sort_key(download.version.prerelease)
675768
return (
@@ -737,6 +830,7 @@ async def find() -> None:
737830
CPythonFinder(client),
738831
PyPyFinder(client),
739832
GraalPyFinder(client),
833+
PyodideFinder(client),
740834
]
741835
downloads = []
742836

0 commit comments

Comments
 (0)