Skip to content
Open
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
18 changes: 7 additions & 11 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.13"

- name: Set up Node.js
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
Expand Down Expand Up @@ -154,17 +154,13 @@ 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: Activate emsdk
run: |
source $(pyodide config get pyodide_root)/../emsdk/emsdk_env.sh
emcc --version

- name: Get number of cores on the runner
id: get-cores
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