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)
|