Skip to content

Commit

Permalink
Validate Python version when initializing xbuildenv (#62)
Browse files Browse the repository at this point in the history
This adds a check when initializing xbuildenv (== `init_environment`) to
make sure the local Python version used to install the xbuildenv was not
changed.

This prevents the following scenario:

1. One has a local Python version (3.11.3) and installs Pyodide
xbuildenv (0.25.1).
2. Then, one changes the local Python version to (3.12.1) using pyenv.
3. Pyodide xbuildenv 0.25.1 is no longer compatible to the local Python
version, so it should fail.

Related issue: #44
  • Loading branch information
ryanking13 authored Nov 25, 2024
1 parent eb09824 commit ea85b85
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
variable to skip emscripten version check.
[#53](https://github.com/pyodide/pyodide-build/pull/53)

- The `pyodide build` command will now raise an error if the local Python version has been changed,
after the cross-build environment has been set up.
[#62](https://github.com/pyodide/pyodide-build/pull/62)

## [0.29.0] - 2024/09/19

### Added
Expand Down
2 changes: 2 additions & 0 deletions pyodide_build/build_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ def _init_xbuild_env(*, quiet: bool = False) -> Path:
if manager.current_version is None:
manager.install()

manager.check_version_marker()

return manager.pyodide_root


Expand Down
4 changes: 2 additions & 2 deletions pyodide_build/cli/xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def _uninstall(
"""
check_xbuildenv_root(path)
manager = CrossBuildEnvManager(path)
manager.uninstall_version(version)
typer.echo(f"Pyodide cross-build environment {version} uninstalled")
v = manager.uninstall_version(version)
typer.echo(f"Pyodide cross-build environment {v} uninstalled")


@app.command("use")
Expand Down
43 changes: 43 additions & 0 deletions pyodide_build/tests/test_xbuildenv.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import sys

import pytest

from pyodide_build.xbuildenv import CrossBuildEnvManager, _url_to_version
Expand Down Expand Up @@ -156,6 +158,11 @@ def test_install_version(
).exists()
assert (manager.symlink_dir / "xbuildenv" / "site-packages-extras").exists()

assert (manager.symlink_dir / ".build-python-version").exists()
assert (
manager.symlink_dir / ".build-python-version"
).read_text() == f"{sys.version_info.major}.{sys.version_info.minor}"

# installing the same version again should be a no-op
manager.install(version)

Expand All @@ -180,6 +187,11 @@ def test_install_url(
).exists()
assert (manager.symlink_dir / "xbuildenv" / "site-packages-extras").exists()

assert (manager.symlink_dir / ".build-python-version").exists()
assert (
manager.symlink_dir / ".build-python-version"
).read_text() == f"{sys.version_info.major}.{sys.version_info.minor}"

def test_install_force(
self,
tmp_path,
Expand Down Expand Up @@ -278,6 +290,37 @@ def test_uninstall_version(self, tmp_path):

assert set(manager.list_versions()) == set(versions) - {"0.25.0", "0.25.1"}

def test_version_marker(
self,
tmp_path,
dummy_xbuildenv_url,
monkeypatch,
monkeypatch_subprocess_run_pip,
fake_xbuildenv_releases_compatible,
):
manager = CrossBuildEnvManager(
tmp_path, str(fake_xbuildenv_releases_compatible)
)
version = "0.1.0"

manager.install(version)

assert (manager.symlink_dir / ".build-python-version").exists()
assert (
manager.symlink_dir / ".build-python-version"
).read_text() == f"{sys.version_info.major}.{sys.version_info.minor}"

# No error
assert manager.check_version_marker() is None

(manager.symlink_dir / ".build-python-version").write_text("2.7.10")

with pytest.raises(
ValueError,
match="does not match the Python version",
):
manager.check_version_marker()


@pytest.mark.parametrize(
"url, version",
Expand Down
40 changes: 39 additions & 1 deletion pyodide_build/xbuildenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)

CDN_BASE = "https://cdn.jsdelivr.net/pyodide/v{version}/full/"
PYTHON_VERSION_MARKER_FILE = ".build-python-version"


class CrossBuildEnvManager:
Expand Down Expand Up @@ -215,6 +216,7 @@ def install(

install_marker.touch()
self.use_version(version)
self._add_version_marker()
except Exception as e:
# if the installation failed, remove the downloaded directory
shutil.rmtree(download_path)
Expand Down Expand Up @@ -366,7 +368,7 @@ def _create_package_index(self, xbuildenv_pyodide_root: Path, version: str) -> N
lockfile = PyodideLockSpec(**json.loads(lockfile_path.read_bytes()))
create_package_index(lockfile.packages, xbuildenv_pyodide_root, cdn_base)

def uninstall_version(self, version: str) -> None:
def uninstall_version(self, version: str | None) -> str:
"""
Uninstall the installed xbuildenv version.
Expand All @@ -375,6 +377,12 @@ def uninstall_version(self, version: str) -> None:
version
The version of xbuildenv to uninstall.
"""
if version is None:
version = self.current_version

if version is None:
raise ValueError("No xbuildenv version is currently in use")

version_path = self._path_for_version(version)

# if the target version is the current version, remove the symlink
Expand All @@ -389,6 +397,36 @@ def uninstall_version(self, version: str) -> None:
f"Cannot find cross-build environment version {version}, available versions: {self.list_versions()}"
)

return version

def _add_version_marker(self) -> None:
"""
Store the Python version in the xbuildenv directory, so we can check compatibility later.
"""
if not self.symlink_dir.is_dir():
raise ValueError("cross-build env directory does not exist")

version_file = self.symlink_dir / PYTHON_VERSION_MARKER_FILE
version_file.write_text(build_env.local_versions()["python"])

def check_version_marker(self):
if not self.symlink_dir.is_dir():
raise ValueError("cross-build env directory does not exist")

version_file = self.symlink_dir / PYTHON_VERSION_MARKER_FILE
if not version_file.exists():
raise ValueError("Python version marker file not found")

version_local = build_env.local_versions()["python"]
version_on_install = version_file.read_text().strip()
if version_on_install != version_local:
raise ValueError(
f"local Python version ({version_local}) does not match the Python version ({version_on_install}) "
"used to create the Pyodide cross-build environment. "
"Please switch back to the original Python version, "
"or reinstall the xbuildenv, by running `pyodide xbuildenv uninstall` and then `pyodide xbuildenv install`"
)


def _url_to_version(url: str) -> str:
return url.replace("://", "_").replace(".", "_").replace("/", "_")

0 comments on commit ea85b85

Please sign in to comment.