* Fix a typo: pyoodide ➡️ pyodide * Add `pyodide_build_version` attribute * Add version to xbuildenv log step * Add version to Emscripten log step * Use `pyodide-build`'s version for updating constraints * Bump Pyodide constraints by updating `pyodide-build` * Add a schema for `pyodide-version` * Update Pyodide constraints * Bump `pyodide-build` to new 0.29.0 * Test out another Pyodide identifier * Update outdated Pyodide constraints * Add Pyodide version to temp directory name * Remove Pyodide 0.26.1 from build configurations * Retrieve + validate + install specific xbuildenvs * Test wheel builds with Pyodide 0.26.2 * Add correct Pyodide version to identifier temp dir * Don't pre-call Pyodide xbuildenv search * Fetch just the stable Pyodide versions * Refactor search + validation + install into one step * Move all of it under a lock * Reorder xbuildenv installation * Add env and cwd to xbuildenv search call * Temporarily lower to 0.26.2 target * Separate out search, validate, install; again * Run xbuildenv search in `CIBW_CACHE_PATH` * Remove prior `PYODIDE_ROOT` env vars, copy envs * Validate doesn't need to depend on searching * Add file lock when searching xbuildenvs * Test the original version: 0.26.1 * Update Pyodide constraints * Update constraints for `pyodide-build` 0.29.0 again * Bump Pyodide from version 0.26.1 ➡️ version 0.26.4 * Add note on compatibility for macOS + other archs * Note Pyodide version for Pyodide identifier * Docs about `CIBW_PYODIDE_VERSION` * Don't fetch just the stable versions * Discard a variable that's not used later * Rename `search_xbuildenv` ➡️ `get_xbuildenv_versions` * `validate_xbuildenv` ➡️ `validate_xbuildenv_version` * Replace ordered comment, add newline * Replace sentence on macOS support Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com> * Capitalise: "pyodide" ➡️ "Pyodide" Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com> * "work" ➡️ "may succeed" Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com> * Add another job to test a custom Pyodide version * Handle "v"-prefixed + non-prefixed versions * Convert to a proper toml-able option, and remove some hardcoded versions This removes the enscripten and pyodide-build version specs from pyproject.toml - pyodide-build is spec'd in the constraints file, and the emscripten version can be read from the pyodide-build output. * Add a schema entry * Add docs for CIBW_PYODIDE_VERSION * Rephrase * Add tests for pyodide-version * Apply suggestions from code review * Add python_build_standalone util * Hook up to python-build-standalone, removing dependency on host python * Remove python hard-code in action.yml * Add log step * Add workaround for https://github.com/pyodide/pyodide-build/issues/143 * Add emscripten pytest test * Remove unneeded checks * Fix a pytest invoke for emscripten * Generate pyodide-build constraints from the pinned pyodide version * Remove pyodide python-build-standalone workaround * Fixup paths from newer version of pyodide-build * Fix/skip some failing tests * Use `python -m pytest` on pyodide, even on Linux * Docs fixes * Don't call the github API at runtime, cache the release assets instead * Ignore pylint false positive * Add version auto-updating for pyodide * Add support for pyodide 3.13. * Fix tests for multiple pyodide wheels * Remove workaround for unreleased pyodide-build * Rename to "test_pyodide" * Fix pathname confusion * Remove extra github actions job * Fix expectation for test_abi_none * Fix the custom_repair_wheel test to actually have clashing names * Fix pinned version test * Document test-command limitation * Remove pyodide 0.28.0a1 for now * Update constraints files * Docs/test fixes post removing pyodide cp313 * Fix ABI test expectation * Docs improvements * Improve some comments * Remove logic duplication * remove pyodide special casing * Refactor constraints code to use a utility script, circumventing import issues * chore: nicer nox env Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * fix: typo in variable name found by copilot Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> * Apply suggestions from code review * Some more Pyodide version updates in the docs section * We haven't released Pyodide v0.27.6 yet * Back to the Github URL for cross-build-environments * `pyodide-build`, not `emsdk` for Windows skips Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com> * Move to a separate `_json_request` function Co-Authored-By: Hood Chatham <roberthoodchatham@gmail.com> * Rename "retries" ➡️ "retry_count" * Add some type hints * Copy env vars before `UV_CUSTOM_COMPILE_COMMAND` * Use `HTTPError.headers.get` instead * Remove extra end quote * Change download tests URL to `https://badssl.com/` Co-Authored-By: Joe Rickerby <1244307+joerick@users.noreply.github.com> * Download size changes, too * Use jsdelivr for Github asset mirroring * Bump to Pyodide v0.27.6 * Fix unit tests * Move to pyodide v0.27.6 again * Bump to pyodide-build 0.30.4 * Use new URL for cross-build environments metadata Co-authored-by: Joe Rickerby <joerick@mac.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Hood Chatham <roberthoodchatham@gmail.com> Co-authored-by: Joe Rickerby <joerick@mac.com> Co-authored-by: Henry Schreiner <henryschreineriii@gmail.com> Co-authored-by: Joe Rickerby <1244307+joerick@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
182 lines
6.1 KiB
Python
182 lines
6.1 KiB
Python
import fnmatch
|
|
import functools
|
|
import json
|
|
import platform
|
|
import typing
|
|
from pathlib import Path
|
|
|
|
from filelock import FileLock
|
|
|
|
from cibuildwheel.util.file import download, extract_tar
|
|
from cibuildwheel.util.resources import PYTHON_BUILD_STANDALONE_RELEASES
|
|
|
|
|
|
class PythonBuildStandaloneAsset(typing.TypedDict):
|
|
name: str
|
|
url: str
|
|
|
|
|
|
class PythonBuildStandaloneRelease(typing.TypedDict):
|
|
tag: str
|
|
assets: list[PythonBuildStandaloneAsset]
|
|
|
|
|
|
class PythonBuildStandaloneReleaseData(typing.TypedDict):
|
|
releases: list[PythonBuildStandaloneRelease]
|
|
|
|
|
|
@functools.cache
|
|
def get_python_build_standalone_release_data() -> PythonBuildStandaloneReleaseData:
|
|
with open(PYTHON_BUILD_STANDALONE_RELEASES, "rb") as f:
|
|
return typing.cast(PythonBuildStandaloneReleaseData, json.load(f))
|
|
|
|
|
|
class PythonBuildStandaloneError(Exception):
|
|
"""Errors related to python-build-standalone."""
|
|
|
|
|
|
def _get_platform_identifiers() -> tuple[str, str, str | None]:
|
|
"""
|
|
Detects the current platform and returns architecture, platform, and libc
|
|
identifiers.
|
|
"""
|
|
system = platform.system()
|
|
machine = platform.machine()
|
|
machine_lower = machine.lower()
|
|
|
|
arch_identifier: str
|
|
platform_identifier: str
|
|
libc_identifier: str | None = None
|
|
|
|
# Map Architecture
|
|
if machine_lower in ["x86_64", "amd64"]:
|
|
arch_identifier = "x86_64"
|
|
elif machine_lower in ["aarch64", "arm64"]:
|
|
arch_identifier = "aarch64"
|
|
else:
|
|
msg = f"Unsupported architecture: {system} {machine}. Cannot download appropriate Python build."
|
|
raise PythonBuildStandaloneError(msg)
|
|
|
|
# Map OS + Libc
|
|
if system == "Linux":
|
|
platform_identifier = "unknown-linux"
|
|
libc_identifier = "musl" if "musl" in (platform.libc_ver() or ("", "")) else "gnu"
|
|
elif system == "Darwin":
|
|
platform_identifier = "apple-darwin"
|
|
elif system == "Windows":
|
|
platform_identifier = "pc-windows-msvc"
|
|
else:
|
|
msg = f"Unsupported operating system: {system}. Cannot download appropriate Python build."
|
|
raise PythonBuildStandaloneError(msg)
|
|
|
|
print(
|
|
f"Detected platform: arch='{arch_identifier}', platform='{platform_identifier}', libc='{libc_identifier}'"
|
|
)
|
|
return arch_identifier, platform_identifier, libc_identifier
|
|
|
|
|
|
def _get_pbs_asset(
|
|
*,
|
|
python_version: str,
|
|
arch_identifier: str,
|
|
platform_identifier: str,
|
|
libc_identifier: str | None,
|
|
) -> tuple[str, str, str]:
|
|
"""Finds the asset, returning (tag, filename, url)."""
|
|
release_data = get_python_build_standalone_release_data()
|
|
|
|
expected_suffix = f"{arch_identifier}-{platform_identifier}"
|
|
if libc_identifier:
|
|
expected_suffix += f"-{libc_identifier}"
|
|
expected_suffix += "-install_only.tar.gz"
|
|
|
|
asset_pattern = f"cpython-{python_version}.*-{expected_suffix}"
|
|
print(f"Looking for file with pattern {asset_pattern}")
|
|
|
|
for release in release_data["releases"]:
|
|
for asset in release["assets"]:
|
|
asset_name = asset["name"]
|
|
if not fnmatch.fnmatch(asset_name, asset_pattern):
|
|
continue
|
|
|
|
asset_url = asset["url"]
|
|
return release["tag"], asset_url, asset_name
|
|
|
|
# If loop completes without finding a match
|
|
msg = f"Could not find python-build-standalone release asset matching {asset_pattern!r}."
|
|
raise PythonBuildStandaloneError(msg)
|
|
|
|
|
|
def _download_or_get_from_cache(asset_url: str, asset_filename: str, cache_dir: Path) -> Path:
|
|
with FileLock(cache_dir / (asset_filename + ".lock")):
|
|
asset_cache_path = cache_dir / asset_filename
|
|
if asset_cache_path.is_file():
|
|
print(f"Using cached python_build_standalone: {asset_cache_path}")
|
|
return asset_cache_path
|
|
|
|
print(f"Downloading python_build_standalone: {asset_url} to {asset_cache_path}")
|
|
download(asset_url, asset_cache_path)
|
|
return asset_cache_path
|
|
|
|
|
|
def _find_python_executable(extracted_dir: Path) -> Path:
|
|
"""Finds the python executable within the extracted directory structure."""
|
|
# Structure is typically 'python/bin/python' or 'python/python.exe'
|
|
base_install_dir = extracted_dir / "python"
|
|
|
|
if platform.system() == "Windows":
|
|
executable_path = base_install_dir / "python.exe"
|
|
else:
|
|
executable_path = base_install_dir / "bin" / "python"
|
|
|
|
if not executable_path.is_file():
|
|
msg = f"Could not locate python executable at expected path {executable_path} within {extracted_dir}."
|
|
raise PythonBuildStandaloneError(msg)
|
|
|
|
print(f"Found python executable: {executable_path}")
|
|
return executable_path.resolve() # Return absolute path
|
|
|
|
|
|
def create_python_build_standalone_environment(
|
|
python_version: str, temp_dir: Path, cache_dir: Path
|
|
) -> Path:
|
|
"""
|
|
Returns a Python environment from python-build-standalone, downloading it
|
|
if necessary using a cache, and expanding it into a fresh base path.
|
|
|
|
Args:
|
|
python_version: The Python version string (e.g., "3.12").
|
|
temp_dir: A directory where the Python environment will be created.
|
|
cache_dir: A directory to store/retrieve downloaded archives.
|
|
|
|
Returns:
|
|
The absolute path to the python executable within the created environment (in temp_dir).
|
|
|
|
Raises:
|
|
PythonBuildStandaloneError: If the platform is unsupported, the build cannot be found,
|
|
download/extraction fails, or configuration is invalid.
|
|
"""
|
|
|
|
print(f"Creating python-build-standalone environment: version={python_version}")
|
|
|
|
arch_id, platform_id, libc_id = _get_platform_identifiers()
|
|
|
|
pbs_tag, asset_url, asset_filename = _get_pbs_asset(
|
|
python_version=python_version,
|
|
arch_identifier=arch_id,
|
|
platform_identifier=platform_id,
|
|
libc_identifier=libc_id,
|
|
)
|
|
|
|
print(f"Using python-build-standalone release: {pbs_tag}")
|
|
|
|
archive_path = _download_or_get_from_cache(
|
|
asset_url=asset_url, asset_filename=asset_filename, cache_dir=cache_dir
|
|
)
|
|
|
|
python_base_dir = temp_dir / "pbs"
|
|
assert not python_base_dir.exists()
|
|
extract_tar(archive_path, python_base_dir)
|
|
|
|
return _find_python_executable(python_base_dir)
|