Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
29ae702
Add install-emscripten CLI command
bulenty584 Oct 15, 2025
d71ae08
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 15, 2025
ea08570
Emsdk installation comments fixed in xbuildenv.py and unneeded tests …
bulenty584 Oct 16, 2025
d86a11d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 16, 2025
8f20c44
TestInstallEmscripten class removed from test_install_emscripten.py a…
bulenty584 Oct 16, 2025
8041a1a
Following @ryanking's comments,
bulenty584 Oct 17, 2025
709ef84
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 17, 2025
7b4736b
Fixed integration test command to use xbuildenv and activate emsdk
bulenty584 Oct 19, 2025
83fddb9
1. emsdk activation directory changed in main.yml
bulenty584 Oct 20, 2025
41ffece
Merge branch 'main' into feat/add-install-emscripten-cli
ryanking13 Oct 21, 2025
abfe731
1. Fixed unittests to follow new code changes
bulenty584 Oct 21, 2025
1fd6457
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 21, 2025
8b52aa5
Updated Python version to 3.13 for xbuildenv and changed back lines 9…
bulenty584 Oct 22, 2025
7b63dfc
Update .github/workflows/main.yml
ryanking13 Oct 26, 2025
9d9921b
Remove emsdk dependency check
ryanking13 Oct 26, 2025
1c1572a
Restore emsdk activation for integration tests
ryanking13 Oct 26, 2025
75ffd74
Update Python version to 3.13 in workflow
ryanking13 Oct 26, 2025
c960d9d
Change Python version from 3.13 to 3.12 for unittest
ryanking13 Oct 26, 2025
1b09fa3
Update NumPy version to 2.2.5 in integration test
ryanking13 Oct 26, 2025
15b5d0b
Change workflow OS to only use Ubuntu
ryanking13 Oct 26, 2025
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
24 changes: 9 additions & 15 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
python-version: "3.12" # TODO: update to 3.13

- name: Set up Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
Expand Down Expand Up @@ -107,7 +107,8 @@ jobs:
{ name: test-src, installer: uv },
{ name: test-integration-marker, installer: pip }, # installer doesn't matter
]
os: [ubuntu-latest, macos-latest]
# os: [ubuntu-latest, macos-latest] # FIXME: https://github.com/oracle/graal/issues/11855
os: [ubuntu-latest]
pyodide-version: [stable]
include:
# Run no-isolation tests and Pyodide minimum version testing only
Expand All @@ -131,7 +132,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.12"
python-version: "3.13"

- name: Set up Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
Expand All @@ -154,33 +155,26 @@ jobs:
fi
echo EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version) >> $GITHUB_ENV

- name: Cache emsdk
uses: actions/cache@v4
with:
path: ${{ env.EMSDK_CACHE_FOLDER }}
key: ${{ env.EMSDK_CACHE_NUMBER }}-${{ env.EMSCRIPTEN_VERSION }}-${{ runner.os }}

- name: Install Emscripten
uses: mymindstorm/setup-emsdk@6ab9eb1bda2574c4ddb79809fc9247783eaf9021 # v14
with:
version: ${{ env.EMSCRIPTEN_VERSION }}
actions-cache-folder: ${{env.EMSDK_CACHE_FOLDER}}
run: pyodide xbuildenv install-emscripten

- name: Get number of cores on the runner
id: get-cores
run: echo "CORES=$(nproc)" >> $GITHUB_OUTPUT

- name: Run tests marked with integration
if: matrix.task.name == 'test-integration-marker'
run: pytest --junitxml=test-results/junit.xml --cov=pyodide-build pyodide_build -m "integration"
run: |
source $(pyodide config get pyodide_root)/../../emsdk/emsdk_env.sh
pytest --junitxml=test-results/junit.xml --cov=pyodide-build pyodide_build -m "integration"

- name: Run the recipe integration tests (${{ matrix.task.name }})
if: matrix.task.name != 'test-integration-marker'
env:
PYODIDE_JOBS: ${{ steps.get-cores.outputs.CORES }}
working-directory: integration_tests
run: |

source $(pyodide config get pyodide_root)/../../emsdk/emsdk_env.sh
# https://github.com/pyodide/pyodide-build/issues/147
# disable package with scikit-build-core
if [[ ${{ matrix.os }} == "macos-latest" ]]; then
Expand Down
1 change: 0 additions & 1 deletion integration_tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ check:
@echo "... Checking dependencies"

@which pyodide > /dev/null || (echo "pyodide-build is not installed"; exit 1)
@which emsdk > /dev/null || (echo "emscripten is not installed"; exit 1)

@echo "... Passed"

Expand Down
2 changes: 1 addition & 1 deletion integration_tests/src/numpy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

set -e

VERSION="2.0.2"
VERSION="2.2.5"
URL="https://files.pythonhosted.org/packages/source/n/numpy/numpy-${VERSION}.tar.gz"

wget $URL
Expand Down
30 changes: 29 additions & 1 deletion pyodide_build/cli/xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import typer

from pyodide_build.build_env import local_versions
from pyodide_build.build_env import get_build_flag, local_versions
from pyodide_build.common import default_xbuildenv_path
from pyodide_build.views import MetadataView
from pyodide_build.xbuildenv import CrossBuildEnvManager
Expand Down Expand Up @@ -205,3 +205,31 @@ def _search(
print(MetadataView.to_json(views))
else:
print(MetadataView.to_table(views))


@app.command("install-emscripten")
def _install_emscripten(
version: str = typer.Option(
None,
help="Emscripten version corresponding to the target Pyodide version",
),
path: Path = typer.Option(DEFAULT_PATH, help="Pyodide cross-env path"),
) -> None:
"""
Install Emscripten SDK into the cross-build environment.

This command clones the emsdk repository, installs and activates the specified
Emscripten version, and applies Pyodide-specific patches.
"""
check_xbuildenv_root(path)
manager = CrossBuildEnvManager(path)

if version is None:
version = get_build_flag("PYODIDE_EMSCRIPTEN_VERSION")

print("Installing emsdk...")

emsdk_dir = manager.install_emscripten(version)

print("Installing emsdk complete.")
print(f"Use `source {emsdk_dir}/emsdk_env.sh` to set up the environment.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of requiring people to manually source the directory, it would be nice to detect the emscripten installation directory and use it during the build.

Let's do that in a separate PR to reduce the diff, for now, it looks good.

224 changes: 224 additions & 0 deletions pyodide_build/tests/test_cli_install_emscripten.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
"""Tests for install-emscripten CLI command"""

import subprocess
from pathlib import Path

from typer.testing import CliRunner

from pyodide_build.cli import xbuildenv

runner = CliRunner()


def test_install_emscripten_no_xbuildenv(tmp_path):
"""Test that install-emscripten fails when no xbuildenv exists"""
envpath = Path(tmp_path) / ".xbuildenv"

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

assert result.exit_code != 0, result.stdout
assert "Cross-build environment not found" in result.stdout, result.stdout


def test_install_emscripten_default_version(tmp_path, monkeypatch):
"""Test installing Emscripten with default version"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.get_build_flag",
lambda name: "3.1.46",
)

called = {}

def fake_install(self, version):
called["version"] = version
return self.env_dir / "emsdk"

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
fake_install,
)

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

assert result.exit_code == 0, result.stdout
assert "Installing emsdk..." in result.stdout, result.stdout
assert "Installing emsdk complete." in result.stdout, result.stdout
assert "Use `source" in result.stdout, result.stdout
assert "emsdk_env.sh` to set up the environment." in result.stdout, result.stdout
assert called["version"] == "3.1.46"


def test_install_emscripten_specific_version(tmp_path, monkeypatch):
"""Test installing Emscripten with a specific version"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

called = {}

def fake_install(self, version):
called["version"] = version
return self.env_dir / "emsdk"

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
fake_install,
)

emscripten_version = "3.1.46"
result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--version",
emscripten_version,
"--path",
str(envpath),
],
)

assert result.exit_code == 0, result.stdout
assert "Installing emsdk..." in result.stdout, result.stdout
assert "Installing emsdk complete." in result.stdout, result.stdout
assert called["version"] == emscripten_version


def test_install_emscripten_with_existing_emsdk(tmp_path, monkeypatch):
"""Test installing Emscripten when emsdk already exists (should pull updates)"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

existing_emsdk = envpath / "emsdk"
existing_emsdk.mkdir()

monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.get_build_flag",
lambda name: "latest",
)

def fake_install(self, version):
assert version == "latest"
assert existing_emsdk.exists()
return existing_emsdk

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
fake_install,
)

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

assert result.exit_code == 0, result.stdout
assert "Installing emsdk..." in result.stdout, result.stdout
assert "Installing emsdk complete." in result.stdout, result.stdout
assert str(existing_emsdk / "emsdk_env.sh") in result.stdout


def test_install_emscripten_git_failure(tmp_path, monkeypatch):
"""Test handling of git clone failure"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
lambda self, version: (_ for _ in ()).throw(
subprocess.CalledProcessError(1, "git clone")
),
)

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

# Should fail due to git clone error
assert result.exit_code != 0
assert isinstance(result.exception, subprocess.CalledProcessError)


def test_install_emscripten_emsdk_install_failure(tmp_path, monkeypatch):
"""Test handling of emsdk install command failure"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
lambda self, version: (_ for _ in ()).throw(
subprocess.CalledProcessError(1, "./emsdk install")
),
)

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

# Should fail due to emsdk install error
assert result.exit_code != 0
assert isinstance(result.exception, subprocess.CalledProcessError)


def test_install_emscripten_output_format(tmp_path, monkeypatch):
"""Test that the output message format is correct"""
envpath = Path(tmp_path) / ".xbuildenv"
envpath.mkdir()

monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.get_build_flag",
lambda name: "latest",
)

expected_path = envpath / "emsdk"

monkeypatch.setattr(
"pyodide_build.xbuildenv.CrossBuildEnvManager.install_emscripten",
lambda self, version: expected_path,
)

result = runner.invoke(
xbuildenv.app,
[
"install-emscripten",
"--path",
str(envpath),
],
)

assert result.exit_code == 0, result.stdout

# Verify output format - check for key messages (logger adds extra lines)
assert "Installing emsdk..." in result.stdout
assert "Installing emsdk complete." in result.stdout
assert "Use `source" in result.stdout
assert "emsdk_env.sh` to set up the environment." in result.stdout
13 changes: 9 additions & 4 deletions pyodide_build/tests/test_cli_xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def test_xbuildenv_install(tmp_path, mock_xbuildenv_url):
assert result.exit_code == 0, result.stdout
assert "Downloading Pyodide cross-build environment" in result.stdout, result.stdout
assert "Installing Pyodide cross-build environment" in result.stdout, result.stdout
assert str(envpath.resolve()) in result.stdout, result.stdout
assert (envpath / "xbuildenv").is_symlink()
assert (envpath / "xbuildenv").resolve().exists()

Expand Down Expand Up @@ -121,8 +122,10 @@ def test_xbuildenv_install_version(tmp_path, fake_xbuildenv_releases_compatible)
os.environ.pop(CROSS_BUILD_ENV_METADATA_URL_ENV_VAR, None)

assert result.exit_code == 0, result.stdout
assert "Downloading Pyodide cross-build environment" in result.stdout, result.stdout
assert "Installing Pyodide cross-build environment" in result.stdout, result.stdout
assert "Pyodide cross-build environment installed at" in result.stdout, (
result.stdout
)
assert str(envpath.resolve()) in result.stdout, result.stdout
assert (envpath / "xbuildenv").is_symlink()
assert (envpath / "xbuildenv").resolve().exists()
assert (envpath / "0.1.0").exists()
Expand Down Expand Up @@ -166,8 +169,10 @@ def test_xbuildenv_install_force_install(
)

assert result.exit_code == 0, result.stdout
assert "Downloading Pyodide cross-build environment" in result.stdout, result.stdout
assert "Installing Pyodide cross-build environment" in result.stdout, result.stdout
assert "Pyodide cross-build environment installed at" in result.stdout, (
result.stdout
)
assert str(envpath.resolve()) in result.stdout, result.stdout
assert (envpath / "xbuildenv").is_symlink()
assert (envpath / "xbuildenv").resolve().exists()
assert (envpath / "0.1.0").exists()
Expand Down
Loading