Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`pyodide xbuildenv search --all` and `pyodide xbuildenv search --all --json`.
[#349](https://github.com/pyodide/pyodide-build/pull/349)

- `pyodide xbuildenv search` now supports `--nightly` and `--debug` flags to search
nightly and nightly-debug cross-build environments respectively, and `pyodide xbuildenv
install` supports `--nightly` and `--debug` flags to install them.
Comment thread
agriyakhetarpal marked this conversation as resolved.
Outdated
[#350](https://github.com/pyodide/pyodide-build/pull/350)

## [0.34.4] - 2026/05/15

### Added
Expand Down
119 changes: 89 additions & 30 deletions pyodide_build/cli/xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from pyodide_build.views import MetadataView
from pyodide_build.xbuildenv import CrossBuildEnvManager
from pyodide_build.xbuildenv_releases import (
NIGHTLY_CROSS_BUILD_ENV_METADATA_URL,
NIGHTLY_DEBUG_CROSS_BUILD_ENV_METADATA_URL,
cross_build_env_metadata_url,
load_cross_build_env_metadata,
)
Expand Down Expand Up @@ -51,6 +53,18 @@ def check_xbuildenv_root(path: Path) -> None:
default=False,
help="force installation even if the version is not compatible.",
)
@click.option(
"--nightly",
is_flag=True,
default=False,
help="install a nightly cross-build environment instead of a stable release.",
)
@click.option(
"--debug",
is_flag=True,
default=False,
help="install the debug variant of the cross-build environment (nightly only).",
)
@click.option(
"--skip-cross-build-packages",
is_flag=True,
Expand All @@ -65,6 +79,8 @@ def _install(
path: Path,
url: str | None,
force_install: bool,
nightly: bool,
debug: bool,
skip_cross_build_packages: bool,
) -> None:
"""Install cross-build environment.
Expand All @@ -78,18 +94,21 @@ def _install(
Arguments:
VERSION: version of cross-build environment to install (optional)
"""
manager = CrossBuildEnvManager(path)
if nightly or debug:
metadata_url = (
NIGHTLY_DEBUG_CROSS_BUILD_ENV_METADATA_URL
if debug
else NIGHTLY_CROSS_BUILD_ENV_METADATA_URL
)
else:
metadata_url = None

manager = CrossBuildEnvManager(path, metadata_url=metadata_url)

if url:
manager.install(
url=url,
force_install=force_install,
)
manager.install(url=url, force_install=force_install)
else:
manager.install(
version=version,
force_install=force_install,
)
manager.install(version=version, force_install=force_install)

Comment thread
agriyakhetarpal marked this conversation as resolved.
click.echo(f"Pyodide cross-build environment installed at {path.resolve()}")

Expand Down Expand Up @@ -194,6 +213,18 @@ def _use(version: str, path: Path) -> None:
default=False,
help="search all versions, without filtering out incompatible ones.",
)
@click.option(
"--nightly",
is_flag=True,
default=False,
help="include nightly releases alongside stable ones.",
)
@click.option(
"--debug",
is_flag=True,
default=False,
help="include nightly debug releases alongside stable ones.",
Comment thread
agriyakhetarpal marked this conversation as resolved.
Outdated
)
@click.option(
"--json",
"json_output",
Expand All @@ -204,32 +235,26 @@ def _use(version: str, path: Path) -> None:
def _search(
metadata_path: str | None,
show_all: bool,
nightly: bool,
debug: bool,
json_output: bool,
) -> None:
"""Search for available versions of cross-build environment."""

# TODO: cache the metadata file somewhere to avoid downloading it every time

metadata_path = metadata_path or cross_build_env_metadata_url()
metadata = load_cross_build_env_metadata(metadata_path)
local = local_versions()

if show_all:
releases = metadata.list_compatible_releases()
else:
releases = metadata.list_compatible_releases(
python_version=local["python"],
pyodide_build_version=local["pyodide-build"],
)

if not releases:
click.echo(
"No compatible cross-build environment found for your system. Try using --all to see all versions."
)
raise SystemExit(1)
def _compat_kwargs() -> dict:
if show_all:
return {}
return {
"python_version": local["python"],
"pyodide_build_version": local["pyodide-build"],
}

views = [
MetadataView(
def _make_view(release, source: str = "stable") -> MetadataView:
return MetadataView(
version=release.version,
python=release.python_version,
emscripten=release.emscripten_version,
Expand All @@ -238,18 +263,52 @@ def _search(
"max": release.max_pyodide_build_version,
},
published_at=release.published_at,
source=source,
compatible=release.is_compatible(
python_version=local["python"],
pyodide_build_version=local["pyodide-build"],
),
)
for release in releases
]

views = []

# Stable releases (only when neither --nightly nor --debug is passed)
if not (nightly or debug):
stable_metadata = load_cross_build_env_metadata(
metadata_path or cross_build_env_metadata_url()
)
Comment thread
agriyakhetarpal marked this conversation as resolved.
views = [
_make_view(r, "stable")
for r in stable_metadata.list_compatible_releases(**_compat_kwargs())
]

# Nightly and/or debug releases
extra_sources = []
Comment thread
agriyakhetarpal marked this conversation as resolved.
Outdated
if nightly:
extra_sources.append(("nightly", NIGHTLY_CROSS_BUILD_ENV_METADATA_URL))
if debug:
extra_sources.append(
("nightly-debug", NIGHTLY_DEBUG_CROSS_BUILD_ENV_METADATA_URL)
)
for source, url in extra_sources:
nightly_metadata = load_cross_build_env_metadata(url)
views += [
_make_view(r, source)
for r in nightly_metadata.releases.values()
if show_all or r.is_compatible(**_compat_kwargs())
]

if not views:
click.echo(
"No compatible cross-build environment found for your system. Try using --all to see all versions."
)
raise SystemExit(1)

show_source = nightly or debug
if json_output:
print(MetadataView.to_json(views))
print(MetadataView.to_json(views, show_source=show_source))
else:
print(MetadataView.to_table(views))
print(MetadataView.to_table(views, show_source=show_source))


@app.command("install-emscripten")
Expand Down
179 changes: 179 additions & 0 deletions pyodide_build/tests/test_cli_xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,182 @@ def test_xbuildenv_search_json(tmp_path, fake_xbuildenv_releases_compatible):
assert any(env["compatible"] for env in output["environments"]), (
"There should be at least one compatible environment"
)


@pytest.fixture
def fake_nightly_release_metadata(tmp_path):
"""Fake nightly release metadata (non-debug), two entries: one compatible, one not."""
from pyodide_build import build_env

local = build_env.local_versions()
data = {
"releases": {
"20260520": {
"version": "20260520",
"url": "https://example.com/20260520/xbuildenv.tar.bz2",
"sha256": "abc123",
"python_version": f"{local['python']}.0",
"emscripten_version": "5.0.3",
"published_at": "2026-05-20T04:40:12Z",
"min_pyodide_build_version": "0.26.0",
"max_pyodide_build_version": None,
},
"20250101": {
"version": "20250101",
"url": "https://example.com/20250101/xbuildenv.tar.bz2",
"sha256": "def456",
"python_version": "3.12.0",
"emscripten_version": "3.1.58",
"published_at": "2025-01-01T02:53:30Z",
"min_pyodide_build_version": "0.26.0",
"max_pyodide_build_version": None,
},
}
}
path = tmp_path / "nightly-release.json"
path.write_text(json.dumps(data))
return path


@pytest.fixture
def fake_nightly_debug_metadata(tmp_path):
"""Fake nightly debug metadata — only entries that have a debug build."""
from pyodide_build import build_env

local = build_env.local_versions()
data = {
"releases": {
"20260520": {
"version": "20260520",
"url": "https://example.com/20260520/xbuildenv-debug.tar.bz2",
"sha256": "debug_abc123",
"python_version": f"{local['python']}.0",
"emscripten_version": "5.0.3",
"published_at": "2026-05-20T04:40:12Z",
"min_pyodide_build_version": "0.26.0",
"max_pyodide_build_version": None,
},
}
}
path = tmp_path / "nightly-debug.json"
path.write_text(json.dumps(data))
return path


def test_xbuildenv_search_nightly(
tmp_path,
fake_xbuildenv_releases_compatible,
fake_nightly_release_metadata,
monkeypatch,
):
monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.NIGHTLY_CROSS_BUILD_ENV_METADATA_URL",
str(fake_nightly_release_metadata),
)

result = runner.invoke(
xbuildenv.app,
[
"search",
"--metadata",
str(fake_xbuildenv_releases_compatible),
"--nightly",
"--all",
],
)

assert result.exit_code == 0, result.output

lines = result.output.splitlines()
header = lines[1].strip().split("│")[1:-1]
assert [col.strip() for col in header] == [
"Version",
"Python",
"Emscripten",
"pyodide-build",
"Published",
"Compatible",
"Source",
]

# Only nightly entries should be present. Stable versions are not mixed in.
assert "0.1.0" not in result.output
assert "0.2.0" not in result.output
assert "20260520" in result.output
assert "20250101" in result.output
assert "stable" not in result.output
assert "nightly" in result.output


def test_xbuildenv_search_debug(
tmp_path,
fake_xbuildenv_releases_compatible,
fake_nightly_debug_metadata,
monkeypatch,
):
monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.NIGHTLY_DEBUG_CROSS_BUILD_ENV_METADATA_URL",
str(fake_nightly_debug_metadata),
)

result = runner.invoke(
xbuildenv.app,
[
"search",
"--metadata",
str(fake_xbuildenv_releases_compatible),
"--debug",
"--all",
],
)

assert result.exit_code == 0, result.output

# Only nightly-debug entries should be present. Stable and nightly-release versions
# are not mixed in. 20250101 is absent because it has no debug build (not in the
# debug metadata file).
assert "stable" not in result.output
assert "nightly-debug" in result.output
assert "20260520" in result.output
assert "20250101" not in result.output


def test_xbuildenv_search_nightly_json(
tmp_path,
fake_xbuildenv_releases_compatible,
fake_nightly_release_metadata,
fake_nightly_debug_metadata,
monkeypatch,
):
monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.NIGHTLY_CROSS_BUILD_ENV_METADATA_URL",
str(fake_nightly_release_metadata),
)
monkeypatch.setattr(
"pyodide_build.cli.xbuildenv.NIGHTLY_DEBUG_CROSS_BUILD_ENV_METADATA_URL",
str(fake_nightly_debug_metadata),
)

result = runner.invoke(
xbuildenv.app,
[
"search",
"--metadata",
str(fake_xbuildenv_releases_compatible),
"--nightly",
"--debug",
"--all",
"--json",
],
)

assert result.exit_code == 0, result.output
assert is_valid_json(result.output)

output = json.loads(result.output)
sources = {env["source"] for env in output["environments"]}
assert sources == {"nightly", "nightly-debug"}

for env in output["environments"]:
assert "debug_url" not in env
assert "debug_sha256" not in env
Loading