Skip to content

Commit db82968

Browse files
authored
BLD Use metadata file when installing cross build env (#4830)
1 parent aabd2bb commit db82968

8 files changed

+587
-19
lines changed

pyodide_build/build_env.py

+14
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import os
66
import re
77
import subprocess
8+
import sys
89
from collections.abc import Iterator
910
from contextlib import nullcontext, redirect_stdout
1011
from io import StringIO
1112
from pathlib import Path
1213

1314
from packaging.tags import Tag, compatible_tags, cpython_tags
1415

16+
from . import __version__
1517
from .common import search_pyproject_toml, xbuildenv_dirname
1618
from .config import ConfigManager
1719
from .recipe import load_all_recipes
@@ -257,3 +259,15 @@ def check_emscripten_version() -> None:
257259
raise RuntimeError(
258260
f"Incorrect Emscripten version {installed_version}. Need Emscripten version {needed_version}"
259261
)
262+
263+
264+
def local_versions() -> dict[str, str]:
265+
"""
266+
Returns the versions of the local Python interpreter and the pyodide-build.
267+
This information is used for checking compatibility with the cross-build environment.
268+
"""
269+
return {
270+
"python": f"{sys.version_info.major}.{sys.version_info.minor}",
271+
"pyodide-build": __version__,
272+
# "emscripten": "TODO"
273+
}

pyodide_build/cli/xbuildenv.py

+89-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
import typer
44

5+
from ..build_env import local_versions
56
from ..common import xbuildenv_dirname
67
from ..xbuildenv import CrossBuildEnvManager
8+
from ..xbuildenv_releases import (
9+
cross_build_env_metadata_url,
10+
load_cross_build_env_metadata,
11+
)
712

813
DIRNAME = xbuildenv_dirname()
914

@@ -32,6 +37,12 @@ def _install(
3237
DIRNAME, help="path to cross-build environment directory"
3338
),
3439
url: str = typer.Option(None, help="URL to download cross-build environment from"),
40+
force_install: bool = typer.Option(
41+
False,
42+
"--force",
43+
"-f",
44+
help="force installation even if the version is not compatible",
45+
),
3546
) -> None:
3647
"""
3748
Install cross-build environment.
@@ -44,9 +55,9 @@ def _install(
4455
manager = CrossBuildEnvManager(path)
4556

4657
if url:
47-
manager.install(url=url)
58+
manager.install(url=url, force_install=force_install)
4859
else:
49-
manager.install(version=version)
60+
manager.install(version=version, force_install=force_install)
5061

5162
typer.echo(f"Pyodide cross-build environment installed at {path.resolve()}")
5263

@@ -125,3 +136,79 @@ def _use(
125136
manager = CrossBuildEnvManager(path)
126137
manager.use_version(version)
127138
typer.echo(f"Pyodide cross-build environment {version} is now in use")
139+
140+
141+
@app.command("search")
142+
def _search(
143+
metadata_path: str = typer.Option(
144+
None,
145+
"--metadata",
146+
help="path to cross-build environment metadata file. It can be a URL or a local file. If not given, the default metadata file is used.",
147+
),
148+
show_all: bool = typer.Option(
149+
False,
150+
"--all",
151+
"-a",
152+
help="search all versions, without filtering out incompatible ones",
153+
),
154+
) -> None:
155+
"""
156+
Search for available versions of cross-build environment.
157+
"""
158+
159+
# TODO: cache the metadata file somewhere to avoid downloading it every time
160+
metadata_path = metadata_path or cross_build_env_metadata_url()
161+
metadata = load_cross_build_env_metadata(metadata_path)
162+
local = local_versions()
163+
164+
if show_all:
165+
releases = metadata.list_compatible_releases()
166+
else:
167+
releases = metadata.list_compatible_releases(
168+
python_version=local["python"],
169+
pyodide_build_version=local["pyodide-build"],
170+
)
171+
172+
if not releases:
173+
typer.echo(
174+
"No compatible cross-build environment found for your system. Try using --all to see all versions."
175+
)
176+
raise typer.Exit(1)
177+
178+
table = []
179+
columns = [
180+
# column name, width
181+
("Version", 10),
182+
("Python", 10),
183+
("Emscripten", 10),
184+
("pyodide-build", 25),
185+
("Compatible", 10),
186+
]
187+
header = [f"{name:{width}}" for name, width in columns]
188+
divider = ["-" * width for _, width in columns]
189+
190+
table.append("\t".join(header))
191+
table.append("\t".join(divider))
192+
193+
for release in releases:
194+
compatible = (
195+
"Yes"
196+
if release.is_compatible(
197+
python_version=local["python"],
198+
pyodide_build_version=local["pyodide-build"],
199+
)
200+
else "No"
201+
)
202+
pyodide_build_range = f"{release.min_pyodide_build_version or ''} - {release.max_pyodide_build_version or ''}"
203+
204+
row = [
205+
f"{release.version:{columns[0][1]}}",
206+
f"{release.python_version:{columns[1][1]}}",
207+
f"{release.emscripten_version:{columns[2][1]}}",
208+
f"{pyodide_build_range:{columns[3][1]}}",
209+
f"{compatible:{columns[4][1]}}",
210+
]
211+
212+
table.append("\t".join(row))
213+
214+
print("\n".join(table))

pyodide_build/tests/fixture.py

+55
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
from pathlib import Path
34

@@ -162,3 +163,57 @@ def mock_emscripten(tmp_path, dummy_xbuildenv, reset_env_vars, reset_cache):
162163
}
163164

164165
os.environ["PATH"] = original_path
166+
167+
168+
@pytest.fixture(scope="function")
169+
def fake_xbuildenv_releases_compatible(tmp_path, dummy_xbuildenv_url):
170+
"""
171+
Create a fake metadata file with a single release that is compatible with the local environment.
172+
"""
173+
local = build_env.local_versions()
174+
fake_releases = {
175+
"releases": {
176+
"0.1.0": {
177+
"version": "0.1.0",
178+
"url": dummy_xbuildenv_url,
179+
"sha256": "1234567890abcdef",
180+
"python_version": f"{local['python']}.0",
181+
"emscripten_version": "1.39.8",
182+
},
183+
"0.2.0": {
184+
"version": "0.2.0",
185+
"url": dummy_xbuildenv_url,
186+
"sha256": "1234567890abcdef",
187+
"python_version": f"{local['python']}.0",
188+
"emscripten_version": "2.39.8",
189+
},
190+
},
191+
}
192+
193+
metadata_path = Path(tmp_path) / "metadata-compat.json"
194+
metadata_path.write_text(json.dumps(fake_releases))
195+
196+
yield metadata_path
197+
198+
199+
@pytest.fixture(scope="function")
200+
def fake_xbuildenv_releases_incompatible(tmp_path, dummy_xbuildenv_url):
201+
"""
202+
Create a fake metadata file with a single release that is incompatible with the local environment.
203+
"""
204+
fake_releases = {
205+
"releases": {
206+
"0.1.0": {
207+
"version": "0.1.0",
208+
"url": dummy_xbuildenv_url,
209+
"sha256": "1234567890abcdef",
210+
"python_version": "4.5.6",
211+
"emscripten_version": "1.39.8",
212+
},
213+
},
214+
}
215+
216+
metadata_path = Path(tmp_path) / "metadata-incompat.json"
217+
metadata_path.write_text(json.dumps(fake_releases))
218+
219+
yield metadata_path

pyodide_build/tests/test_cli_xbuildenv.py

+143
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# flake8: noqa
2+
3+
import os
14
import shutil
25
from pathlib import Path
36

@@ -9,6 +12,13 @@
912
xbuildenv,
1013
)
1114
from pyodide_build.common import chdir
15+
from pyodide_build.xbuildenv_releases import CROSS_BUILD_ENV_METADATA_URL_ENV_VAR
16+
17+
from .fixture import (
18+
dummy_xbuildenv_url,
19+
fake_xbuildenv_releases_compatible,
20+
fake_xbuildenv_releases_incompatible,
21+
)
1222

1323

1424
def mock_pyodide_lock() -> PyodideLockSpec:
@@ -89,6 +99,84 @@ def test_xbuildenv_install(tmp_path, mock_xbuildenv_url):
8999
assert (concrete_path / ".installed").exists()
90100

91101

102+
def test_xbuildenv_install_version(tmp_path, fake_xbuildenv_releases_compatible):
103+
envpath = Path(tmp_path) / ".xbuildenv"
104+
105+
os.environ.pop(CROSS_BUILD_ENV_METADATA_URL_ENV_VAR, None)
106+
os.environ[CROSS_BUILD_ENV_METADATA_URL_ENV_VAR] = str(
107+
fake_xbuildenv_releases_compatible
108+
)
109+
110+
result = runner.invoke(
111+
xbuildenv.app,
112+
[
113+
"install",
114+
"0.1.0",
115+
"--path",
116+
str(envpath),
117+
],
118+
)
119+
120+
os.environ.pop(CROSS_BUILD_ENV_METADATA_URL_ENV_VAR, None)
121+
122+
assert result.exit_code == 0, result.stdout
123+
assert "Downloading Pyodide cross-build environment" in result.stdout, result.stdout
124+
assert "Installing Pyodide cross-build environment" in result.stdout, result.stdout
125+
assert (envpath / "xbuildenv").is_symlink()
126+
assert (envpath / "xbuildenv").resolve().exists()
127+
assert (envpath / "0.1.0").exists()
128+
129+
concrete_path = (envpath / "xbuildenv").resolve()
130+
assert (concrete_path / ".installed").exists()
131+
132+
133+
def test_xbuildenv_install_force_install(
134+
tmp_path, fake_xbuildenv_releases_incompatible
135+
):
136+
envpath = Path(tmp_path) / ".xbuildenv"
137+
138+
os.environ.pop(CROSS_BUILD_ENV_METADATA_URL_ENV_VAR, None)
139+
os.environ[CROSS_BUILD_ENV_METADATA_URL_ENV_VAR] = str(
140+
fake_xbuildenv_releases_incompatible
141+
)
142+
143+
result = runner.invoke(
144+
xbuildenv.app,
145+
[
146+
"install",
147+
"0.1.0",
148+
"--path",
149+
str(envpath),
150+
],
151+
)
152+
153+
# should fail if no force option is given
154+
assert result.exit_code != 0, result.stdout
155+
156+
result = runner.invoke(
157+
xbuildenv.app,
158+
[
159+
"install",
160+
"0.1.0",
161+
"--path",
162+
str(envpath),
163+
"--force",
164+
],
165+
)
166+
167+
assert result.exit_code == 0, result.stdout
168+
assert "Downloading Pyodide cross-build environment" in result.stdout, result.stdout
169+
assert "Installing Pyodide cross-build environment" in result.stdout, result.stdout
170+
assert (envpath / "xbuildenv").is_symlink()
171+
assert (envpath / "xbuildenv").resolve().exists()
172+
assert (envpath / "0.1.0").exists()
173+
174+
concrete_path = (envpath / "xbuildenv").resolve()
175+
assert (concrete_path / ".installed").exists()
176+
177+
os.environ.pop(CROSS_BUILD_ENV_METADATA_URL_ENV_VAR, None)
178+
179+
92180
def test_xbuildenv_version(tmp_path):
93181
envpath = Path(tmp_path) / ".xbuildenv"
94182

@@ -207,3 +295,58 @@ def test_xbuildenv_uninstall(tmp_path):
207295

208296
assert result.exit_code != 0, result.stdout
209297
assert isinstance(result.exception, ValueError), result.exception
298+
299+
300+
def test_xbuildenv_search(
301+
tmp_path, fake_xbuildenv_releases_compatible, fake_xbuildenv_releases_incompatible
302+
):
303+
result = runner.invoke(
304+
xbuildenv.app,
305+
[
306+
"search",
307+
"--metadata",
308+
str(fake_xbuildenv_releases_compatible),
309+
],
310+
)
311+
312+
assert result.exit_code == 0, result.stdout
313+
assert "0.1.0" in result.stdout, result.stdout
314+
315+
result = runner.invoke(
316+
xbuildenv.app,
317+
[
318+
"search",
319+
"--metadata",
320+
str(fake_xbuildenv_releases_incompatible),
321+
],
322+
)
323+
324+
assert result.exit_code != 0, result.stdout
325+
assert (
326+
"No compatible cross-build environment found for your system" in result.stdout
327+
)
328+
assert "0.1.0" not in result.stdout, result.stdout
329+
330+
result = runner.invoke(
331+
xbuildenv.app,
332+
[
333+
"search",
334+
"--metadata",
335+
str(fake_xbuildenv_releases_incompatible),
336+
"--all",
337+
],
338+
)
339+
340+
assert result.exit_code == 0, result.stdout
341+
342+
header = result.stdout.splitlines()[0]
343+
assert header.split() == [
344+
"Version",
345+
"Python",
346+
"Emscripten",
347+
"pyodide-build",
348+
"Compatible",
349+
]
350+
351+
row1 = result.stdout.splitlines()[2]
352+
assert row1.split() == ["0.1.0", "4.5.6", "1.39.8", "-", "No"]

0 commit comments

Comments
 (0)