Skip to content

Allow the use of arbitrary Pyodide versions #2002

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 67 commits into
base: main
Choose a base branch
from

Conversation

agriyakhetarpal
Copy link
Contributor

@agriyakhetarpal agriyakhetarpal commented Sep 11, 2024

Description

This PR updates the Pyodide build procedure (see #1456) that is enabled with CIBW_PLATFORM: "pyodide" (or with the --platform pyodide CLI equivalent) post the changes in pyodide/pyodide#4882, where pyodide/pyodide-build was unvendored from the main Pyodide repository to accommodate faster updates and fixes.

This means that the Pyodide version and pyodide-build are not going to be in sync going forward, and that the Pyodide xbuildenv to install must be inferred by the versions available to install by pyodide-build through a recently added pyodide xbuildenv search command, which prints out this table:

Tap to expand table
┌────────────┬────────────┬────────────┬───────────────────────────┬────────────┐
│ Version    │ Python     │ Emscripten │ pyodide-build             │ Compatible │
├────────────┼────────────┼────────────┼───────────────────────────┼────────────┤
│ 0.27.0a2   │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
│ 0.26.4     │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
│ 0.26.3     │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
│ 0.26.2     │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
│ 0.26.1     │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
│ 0.26.0     │ 3.12.1     │ 3.1.58     │ 0.26.0 -                  │ Yes        │
└────────────┴────────────┴────────────┴───────────────────────────┴────────────┘

Alternatively, one may use pyodide xbuildenv search --all to return both compatible and non-compatible versions. This would, however, be better received as JSON (please see pyodide/pyodide-build#28).


Additionally, in this PR, support has been added for installing arbitrary Pyodide versions, or, more specifically, arbitrary versions for "Pyodide cross-build environments (xbuildenvs)" – though, only the ones that are supported for a given pyodide-build version. This has been implemented through an environment variable CIBW_PYODIDE_VERSION and an associated configuration variable in the schema (through a table implemented via pyodide/pyodide-build#26).

The rationale behind this is that WebAssembly/Pyodide builds are already experimental, and it would be useful to not tie the available Pyodide version to the cibuildwheel version – this would be helpful for downstream projects (statsmodels/statsmodels#9343, scikit-image/scikit-image#7525, scikit-learn/scikit-learn#29791, and so on) to allow testing against Pyodide's alpha releases and/or for the case of greater reproducibility against Pyodide's older releases.

cc: @hoodmane and @ryanking13 for visibility


Suggested CHANGELOG entry

Since I didn't find a way to add an entry without the pre-commit hook removing previous entries, I've added a few lines here based on the current state of this PR. Please feel free to suggest changes or modify these lines directly.

- 🛠 Provide [Pyodide version 0.26.4](https://github.com/pyodide/pyodide/releases/tag/0.26.4) with `cp312-pyodide_wasm32` (#2002)
- ✨ Allow the use of a custom Pyodide version to target by the use of the `CIBW_PYODIDE_VERSION` environment variable. This
is an experimental option and users are advised to look at the [compatible Pyodide versions](https://github.com/pyodide/pyodide/blob/main/pyodide-cross-build-environments.json) according to the [`pyodide-build`](https://github.com/pyodide/pyodide-build) version.

@agriyakhetarpal agriyakhetarpal marked this pull request as draft September 11, 2024 13:18
@agriyakhetarpal
Copy link
Contributor Author

The Windows test failures are unrelated. I'll try to fix them later in the day, but happy to step back if someone else does it before me, or wishes to.

@ryanking13
Copy link

I think @ryanking13's plan is that we will relax this, in the sense that we will continue supporting Python 3.12 in pyodide-build even after we upgrade to using Python 3.13. But what does always need to be guaranteed is that target Python version == build Python version.

Yes, exactly.

@hoodmane
Copy link
Contributor

I think keeping the build identifier tied to the Python minor version should suffice. Please correct me if I'm missing something though!

If a package uses numpy or scipy at build time, it may be sensitive to the specific Pyodide version and not just the Python minor version. But only insofar as it depends on a specific numpy/scipy version, and this dependency should be clear from its Requires-Dist information. So I agree that putting the Python minor version in the build identifier will suffice.

@hoodmane
Copy link
Contributor

Previously we've avoided using 3rd-party distributions of CPython, for fear of producing binaries with poor compatibility, but in this case we only need it to run the build, there's no implicit linking going on, right?

That's right, if pyodide-build is functioning correctly we shouldn't be using any headers or libs from the build Python.

@hoodmane
Copy link
Contributor

Wouldn't a pyodide_2025_0 wheel be forward compatible with a version of Pyodide that is released later?

Yes, assuming that we first determine the pyodide_2025_0 ABI and implement it in pyodide-build and then release pyodide-build. The pyodide_2025_0 isn't stable yet though so currently it's not a good idea to distribute wheels with that platform tag except for experiments.

@joerick
Copy link
Contributor

joerick commented Mar 26, 2025

Thanks for the responses @hoodmane and Pyodide folks!

So I think the next thing to do would be to remove the implicit reliance on the host Python version, perhaps with python-build-standalone. That can be a follow-up PR, no need to add that here.

The pyodide_2025_0 isn't stable yet though so currently it's not a good idea to distribute wheels with that platform tag except for experiments.

That's cool, I was speaking hypothetically, as in, "once the ABI is stable".

If a package uses numpy or scipy at build time, it may be sensitive to the specific Pyodide version and not just the Python minor version.

Just so I understand this- is that because pyodide bundles these libraries? And is this just a build-time concern or would that also limit the compatibility of the built wheels?

@hoodmane
Copy link
Contributor

is that because pyodide bundles these libraries?

Yes.

And is this just a build-time concern or would that also limit the compatibility of the built wheels?

I don't think it should limit compatibility of the built wheels beyond what they already say in their Requires Dist. If the wheel says it wants scipy >= 1.7 for instance then I think that is an assertion by the wheel that it works the same with scipy 1.7 and scipy 1.8 and can be build with either unless it has a more specific build_requires. If the wheel built against scipy 1.7 isn't compatible with scipy 1.8, then I think it's on the wheel to pin scipy==1.7, which would make it only compatible with Pyodide versions that bundle scipy 1.7. I don't think Pyodide specifically introduces any new limitations or special considerations here.

@agriyakhetarpal
Copy link
Contributor Author

agriyakhetarpal commented Mar 26, 2025

Based on these recent discussions, here's what I understand and propose:

  • We will continue to have the requirement/limitation of the xbuildenv/host Python version being the same as the Pyodide Python version for pyodide-build to operate.

    • so, would the idea be that we'll download a Python binary from python-build-standalone in cibuildwheel/platforms/pyodide.py, install it, install pyodide-build in a virtualenv with it as the creator (similar to how macOS downloads CPython binaries), and compile the requested package to WASM – and we can get what Python version we need to install for whatever is supplied to CIBW_PYODIDE_VERSION: using pyodide xbuildenv search --json --all?
    • the idea is that the PR wouldn't be usable without that, as the cibuildwheel GitHub Action won't be able to build against 0.28 when it lands, or even any nightly xbuildenv of Pyodide shall we implement grabbing it, as we've updated much later from Emscripten v3.1.58 and now bumped to Python 3.13 a few moments ago as well. This is a bit unfortunate, considering how convenient the GitHub Action is. However, it should be usable if someone were to do python3.13 -m pip install cibuildwheel && cibuildwheel --platform pyodide, so maybe we should document this case – i.e., don't use the action or any other appropriate note?
    • or, should we add the pyodide xbuildenv search --json --all logic to the GitHub Action instead (perhaps through a pipx step) so that it picks up the Python version needed for the requested Pyodide version (if a build for Pyodide is requested, that is, otherwise not) and then passes that along as an input to setup-python? It makes the action a bit more complex, but none of it is exposed that much to the user anyway and is probably minimal enough to incorporate.
  • Please feel free to push back on this thought, however, IMO, it's more elegant to do this:

     steps:
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_BUILD: "pyodide_2024_0 pyodide 2025_0"
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
           ...

    rather than to do this:

     steps:
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
     
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_PYODIDE_VERSION: "0.XY"
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
           ...

@joerick
Copy link
Contributor

joerick commented Mar 31, 2025

  • so, would the idea be that we'll download a Python binary from python-build-standalone in cibuildwheel/platforms/pyodide.py, install it, install pyodide-build in a virtualenv with it as the creator (similar to how macOS downloads CPython binaries)

Yeah, that's my proposal. That'll fix the issue with the implicit reliance on the version of the host python.

  • Please feel free to push back on this thought, however, IMO, it's more elegant to do this:

     steps:
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_BUILD: "pyodide_2024_0 pyodide 2025_0"
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
           ...

    rather than to do this:

     steps:
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
     
       - uses: pypa/[email protected]
         env:
           CIBW_PLATFORM: pyodide
           CIBW_PYODIDE_VERSION: "0.XY"
           CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
           ...

Not sure I'm on board with the build identifiers above, but I'd agree that we shouldn't have to run cibuildwheel twice in normal setups. But I don't see when such a setup would be required.

My understanding is that we'd create a new build identifier with each minor version of Python, because each ABI would be accompanied with a bump to the minor version of Python at the same time. See this conversion between myself and @hoodmane above:

PEP 776 draft: In order to balance the ABI stability needs of package maintainers with the ABI flexibility to allow the platform to move forward, Pyodide plans to adopt a new ABI for each feature release of Python.

@joerick: If that's the case, (i.e. a 1:1 mapping between Python minor version and wheel ABI) I think keeping the build identifier tied to the Python minor version should suffice. Please correct me if I'm missing something though!

@hoodmane: If a package uses numpy or scipy at build time, it may be sensitive to the specific Pyodide version and not just the Python minor version. But only insofar as it depends on a specific numpy/scipy version, and this dependency should be clear from its Requires-Dist information. So I agree that putting the Python minor version in the build identifier will suffice.

As such, your example above would look something like:

steps:
  - uses: pypa/[email protected]
    env:
      CIBW_PLATFORM: pyodide
      CIBW_BUILD: "cp312-pyodide_wasm32 cp313-pyodide_wasm32"
      CIBW_TEST_REQUIRES_PYODIDE: "<...>" # and so on
      ...

That would be functionally equivalent to your initial example because pyodide would not change ABI within a Python minor version.

@agriyakhetarpal
Copy link
Contributor Author

Unfortunately, the current failure is a known bug: pyodide/pyodide-build#143

@joerick
Copy link
Contributor

joerick commented Apr 9, 2025

Unfortunately, the python-build-standalone issue with symlinks appears to be cropping up again, this time inside the pyodide venv used for testing.

Testing wheel...

+ pyodide venv /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/venv-test
Creating Pyodide virtualenv at                                                  
/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312
-pyodide_wasm32/venv-test                                                       
... Configuring virtualenv                                                      
... Installing standard library                                                 
Successfully created Pyodide virtual environment!                               
+ which python
/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/venv-test/bin/python
+ pip install /private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/repaired_wheel/spam-0.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl
--------------------------------------------------------- Captured stderr call ---------------------------------------------------------
Could not find platform independent libraries <prefix>
Could not find platform dependent libraries <exec_prefix>
Python path configuration:
  PYTHONHOME = (not set)
  PYTHONPATH = (not set)
  program name = '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/venv-test/bin/python3.12-host'
  isolated = 0
  environment = 1
  user site = 0
  safe_path = 0
  import site = 1
  is in build tree = 0
  stdlib dir = '/install/lib/python3.12'
  sys._base_executable = '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/build/base/pbs-20250317-3.12/python/bin/python3.12'
  sys.base_prefix = '/install'
  sys.base_exec_prefix = '/install'
  sys.platlibdir = 'lib'
  sys.executable = '/private/var/folders/ld/k24nt7054698bctspqwrjq1r0000gn/T/cibw-run-7aphwbz2/cp312-pyodide_wasm32/venv-test/bin/python3.12-host'
  sys.prefix = '/install'
  sys.exec_prefix = '/install'
  sys.path = [
    '/install/lib/python312.zip',
    '/install/lib/python3.12',
    '/install/lib/python3.12/lib-dynload',
  ]
Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
Python runtime state: core initialized
ModuleNotFoundError: No module named 'encodings'

Current thread 0x00000001ff00c840 (most recent call first):
  <no Python frame>

The issue appears to be related to astral-sh/python-build-standalone#380 - previously (#2328) fixed by resolving the symlink before calling the binary. I'm not sure that would be possible in this case though, as pyodide venv is making the symlinks to create the cross-env, and I think that resolving the symlink would cause pip to install into the wrong virtualenv (?).

EDIT- As @agriyakhetarpal notes, pyodide/pyodide-build#143 is the best reference.

@agriyakhetarpal
Copy link
Contributor Author

If the approach that @hoodmane has to fix this in pyodide venv works, I see no reason for us not to immediately put out a new pyodide-build 0.31 release. :D

@hoodmane
Copy link
Contributor

hoodmane commented Apr 9, 2025

Okay, I'll actually make that PR if it's a blocker here.

@joerick
Copy link
Contributor

joerick commented Apr 11, 2025

I had a play with your proposed workaround here, @hoodmane - it kinda worked, but I had to do a couple extra things

  • the python3.12-host binary also needed updating (I just symlinked it to python-host)
  • the pip binary has python3.12-host as a shebang interpreter, which doesn't work (at least on macOS), because it's a script, not a binary file. The workaround is to call it using /usr/bin/env. I also removed the -s, as it's in the python-host script now.

However, I now see that, although the pip executable worked, now the pytest executable doesn't! It gets the same error as before. Now I'm confused! Because shouldn't this be running in node, not in Python-land? Perhaps the PYTHONHOME variable was set wrong in python-host?

@hoodmane
Copy link
Contributor

Ugh this workaround keeps getting more complicated. Thanks for testing it out! We apparently should add more test coverage in pyodide-build.

@hoodmane
Copy link
Contributor

the pytest executable doesn't! It gets the same error as before. Now I'm confused! Because shouldn't this be running in node, not in Python-land? Perhaps the PYTHONHOME variable was set wrong in python-host?

Yeah that is surprising. Steps to reproduce?

@joerick
Copy link
Contributor

joerick commented Apr 11, 2025

Actually, there might be something else going on. Still investigating...

@joerick
Copy link
Contributor

joerick commented Apr 11, 2025

Well, I'm pretty confused. But I'll share the minimal repros I'm working with, in case they're helpful @hoodmane.

# docker run -it --rm ghcr.io/astral-sh/uv:debian 
uv run --python=python3.13 --with pyodide-build python <<'EOF'

import subprocess
from pathlib import Path
import sys
import os
import textwrap
import shutil

venv_dir = Path("/tmp/pyodidevenv")

try:
    shutil.rmtree(venv_dir)
except Exception as e:
    pass

def run(args, env=None):
    result = subprocess.run(args, env=env)
    if result.returncode != 0:
        sys.exit(f'{' '.join(args)} returned {result.returncode}')

run(['pyodide', 'venv', venv_dir])

env = os.environ.copy()

env["PATH"] = str(venv_dir / "bin") + os.pathsep + env["PATH"]

print("we're running this pip:")
run(['which', 'pip'], env=env)

run(['pip', 'install', 'pytest'], env=env)
run(['pytest'], env=env)

EOF

This fails on debian - I get the encoding error

I've tried inserting the workaround, as shown here-

# docker run -it --rm ghcr.io/astral-sh/uv:debian 
uv run --python=python3.13 --with pyodide-build python <<'EOF'

import subprocess
from pathlib import Path
import sys
import os
import textwrap
import shutil

venv_dir = Path("/tmp/pyodidevenv")

try:
    shutil.rmtree(venv_dir)
except Exception as e:
    pass

def run(args, env=None):
    result = subprocess.run(args, env=env)
    if result.returncode != 0:
        sys.exit(f'{' '.join(args)} returned {result.returncode}')

run(['pyodide', 'venv', venv_dir])

# --WORKAROUND--
base_python = Path(sys.executable)
(venv_dir / "bin" / "python-host-link").symlink_to(base_python)
(venv_dir / "bin" / "python-host").unlink()
(venv_dir / "bin" / "python-host").write_text(
    textwrap.dedent(f"""
        #!/bin/bash
        export PYTHONHOME={base_python.parent.parent}
        exec {venv_dir / "bin" / "python-host-link"} -s "$@"
    """)
)

(venv_dir / "bin" / "python-host").chmod(0o755)

# make the python3.12-host bin point to the python-host script
(venv_dir / "bin" / "python3.13-host").unlink()
(venv_dir / "bin" / "python3.13-host").symlink_to(venv_dir / "bin" / "python-host")
# --END WORKAROUND--

env = os.environ.copy()

del env["VIRTUAL_ENV"]
env["PATH"] = str(venv_dir / "bin") + os.pathsep + env["PATH"]

print("we're running this pip:")
run(['which', 'pip'], env=env)

run(['pip', 'install', 'pytest'], env=env)
run(['pytest'], env=env)

EOF

But on linux, this fails with the following error:

we're running this pip:
/tmp/pyodidevenv/bin/pip

[notice] A new release of pip is available: 24.3.1 -> 25.0.1
[notice] To update, run: /root/.local/share/uv/python/cpython-3.13.3-linux-aarch64-gnu/bin/python3.13 -m pip install --upgrade pip
error: externally-managed-environment

× This environment is externally managed
╰─> This Python installation is managed by uv and should not be modified.

note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
pip install pytest returned 1

Which is weird, as we're in a virtualenv, but pip doesn't think so.

@hoodmane
Copy link
Contributor

Thanks! I'll try it out when I get home and see if I can figure out anything that helps.

@hoodmane
Copy link
Contributor

This works as expected for me:

uv run --python=python3.12 --with pyodide-build python <<'EOF'

import subprocess
from pathlib import Path
import sys
import os
import textwrap
import shutil

venv_dir = Path("pyodidevenv")

try:
    shutil.rmtree(venv_dir)
except Exception as e:
    pass

def run(args, env=None):
    result = subprocess.run(args, env=env)
    if result.returncode != 0:
        sys.exit(f'{' '.join(args)} returned {result.returncode}')

run(['pyodide', 'xbuildenv', 'use', '0.27.4'])
run(['pyodide', 'venv', venv_dir])



# --WORKAROUND--
base_python = Path(sys.executable)
(venv_dir / "bin" / "python-host-link").symlink_to(base_python)
(venv_dir / "bin" / "python-host").unlink()
(venv_dir / "bin" / "python-host").write_text(
    textwrap.dedent(f"""
        #!/bin/bash
        export PYTHONHOME={base_python.parent.parent}
        exec {venv_dir / "bin" / "python-host-link"} -s "$@"
    """)
)

(venv_dir / "bin" / "python-host").chmod(0o755)

# make the python3.12-host bin point to the python-host script
(venv_dir / "bin" / "python3.12-host").unlink()
(venv_dir / "bin" / "python3.12-host").symlink_to(venv_dir / "bin" / "python-host")
# --END WORKAROUND--

env = os.environ.copy()

del env["VIRTUAL_ENV"]
env["PATH"] = str(venv_dir / "bin") + os.pathsep + env["PATH"]

print("we're running this pip:")
run(['which', 'pip'], env=env)

run(['pip', 'install', 'pytest'], env=env)
run(['pytest'], env=env)

EOF

Maybe I should try with the docker image.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants