| __lazy_modules__ = [ | |
| "functools", | |
| "json", | |
| "os", | |
| "pathlib", | |
| "subprocess", | |
| "sysconfig", | |
| "tempfile", | |
| "tomllib", | |
| ] | |
| import functools | |
| import json | |
| import os | |
| import pathlib | |
| import subprocess | |
| import sysconfig | |
| import tempfile | |
| import tomllib | |
| class Context: | |
| clean = False | |
| def __init__(self): | |
| self.here = pathlib.Path(__file__).parent | |
| self.orig_cwd = pathlib.Path.cwd() | |
| @functools.cached_property | |
| def checkout(self): | |
| checkout = self.here | |
| while checkout != checkout.parent: | |
| if (checkout / "configure").is_file(): | |
| return checkout | |
| checkout = checkout.parent | |
| raise FileNotFoundError( | |
| "Unable to find the root of the CPython checkout by looking for 'configure'" | |
| ) | |
| def _pybuilddir(self, build_path): | |
| relative_dir = (build_path / "pybuilddir.txt").read_text().strip() | |
| return build_path / relative_dir | |
| @functools.cached_property | |
| def host_triple(self): | |
| if self._host_triple: | |
| return self._host_triple | |
| with (self.here / "config.toml").open("rb") as file: | |
| config = tomllib.load(file) | |
| return config["targets"]["host-triple"] | |
| @functools.cached_property | |
| def cross_build_path(self): | |
| return self.checkout / "cross-build" | |
| @functools.cached_property | |
| def build_python_path(self): | |
| # Build platform can also be found via `config.guess`. | |
| return self.cross_build_path / sysconfig.get_config_var( | |
| "BUILD_GNU_TYPE" | |
| ) | |
| @functools.cached_property | |
| def build_python_interpreter(self): | |
| binary = self.build_python_path / "python" | |
| if not binary.is_file(): | |
| binary = binary.with_suffix(".exe") | |
| if not binary.is_file(): | |
| raise FileNotFoundError( | |
| f"Unable to find `python(.exe)` in {self.build_python_path}" | |
| ) | |
| return binary | |
| @functools.cached_property | |
| def is_debug(self): | |
| pybuilddir = self._pybuilddir(self.build_python_path) | |
| if pybuilddir.exists(): | |
| build_details_path = pybuilddir / "build-details.json" | |
| build_details = json.loads(build_details_path.read_text()) | |
| return "d" in build_details["abi"]["flags"] | |
| else: | |
| # Python 3.13 and older. | |
| test = "import sys, test.support; sys.exit(test.support.Py_DEBUG)" | |
| result = subprocess.run( | |
| [self.build_python_interpreter, "-c", test], | |
| capture_output=True, | |
| ) | |
| return bool(result.returncode) | |
| @functools.cached_property | |
| def wasi_build_path(self): | |
| return self.cross_build_path / self.host_triple | |
| @functools.cached_property | |
| def wasi_pybuilddir(self): | |
| return self._pybuilddir(self.wasi_build_path) | |
| @functools.cached_property | |
| def wasi_build_details(self): | |
| with (self.wasi_pybuilddir / "build-details.json").open() as f: | |
| return json.load(f) | |
| @functools.cached_property | |
| def wasi_build_version(self): | |
| return self.wasi_build_details["language"]["version_info"] | |
| @functools.cached_property | |
| def wasi_sdk_path(self): | |
| if wasi_sdk_path := self._wasi_sdk_path: | |
| if not wasi_sdk_path.exists(): | |
| raise ValueError( | |
| "WASI SDK not found; " | |
| "download from " | |
| "https://github.com/WebAssembly/wasi-sdk and/or " | |
| "specify via $WASI_SDK_PATH or --wasi-sdk" | |
| ) | |
| return wasi_sdk_path | |
| with (self.here / "config.toml").open("rb") as file: | |
| config = tomllib.load(file) | |
| wasi_sdk_version = config["targets"]["wasi-sdk"] | |
| if wasi_sdk_path_env_var := os.environ.get("WASI_SDK_PATH"): | |
| wasi_sdk_path = pathlib.Path(wasi_sdk_path_env_var) | |
| if not wasi_sdk_path.exists(): | |
| raise ValueError( | |
| f"WASI SDK not found at $WASI_SDK_PATH ({wasi_sdk_path})" | |
| ) | |
| else: | |
| opt_path = pathlib.Path("/opt") | |
| # WASI SDK versions have a ``.0`` suffix, but it's a constant; the WASI SDK team | |
| # has said they don't plan to ever do a point release and all of their Git tags | |
| # lack the ``.0`` suffix. | |
| # Starting with WASI SDK 23, the tarballs went from containing a directory named | |
| # ``wasi-sdk-{WASI_SDK_VERSION}.0`` to e.g. | |
| # ``wasi-sdk-{WASI_SDK_VERSION}.0-x86_64-linux``. | |
| potential_sdks = [ | |
| path | |
| for path in opt_path.glob(f"wasi-sdk-{wasi_sdk_version}.0*") | |
| if path.is_dir() | |
| ] | |
| if not potential_sdks: | |
| raise ValueError( | |
| f"WASI SDK {wasi_sdk_version} not found in {opt_path}" | |
| ) | |
| elif len(potential_sdks) == 1: | |
| wasi_sdk_path = potential_sdks[0] | |
| elif (default_path := opt_path / "wasi-sdk").is_dir(): | |
| wasi_sdk_path = default_path | |
| elif potential_sdks: | |
| raise ValueError( | |
| f"Multiple WASI SDKs found in {opt_path} w/o knowing which to use" | |
| ) | |
| else: | |
| raise ValueError(f"WASI SDK not found in {opt_path}") | |
| # Starting with WASI SDK 25, a VERSION file is included in the root | |
| # of the SDK directory that we can read to warn folks when they are using | |
| # an unsupported version. | |
| if ( | |
| wasi_sdk_path | |
| and (version_file := wasi_sdk_path / "VERSION").is_file() | |
| ): | |
| version_details = version_file.read_text(encoding="utf-8") | |
| found_version = version_details.splitlines()[0] | |
| # Make sure there's a trailing dot to avoid false positives if somehow the | |
| # supported version is a prefix of the found version (e.g. `25` and `2567`). | |
| if not found_version.startswith(f"{wasi_sdk_version}."): | |
| major_version = found_version.partition(".")[0] | |
| log( | |
| "⚠️", | |
| f" Found WASI SDK {major_version}, " | |
| f"but WASI SDK {wasi_sdk_version} is the supported version", | |
| ) | |
| return wasi_sdk_path | |
| @functools.cached_property | |
| def log_path(self): | |
| if self._log_path is not None: | |
| if not (path := self._log_path).is_absolute(): | |
| path = (self.orig_cwd / self._log_path).resolve() | |
| return path | |
| return pathlib.Path(tempfile.gettempdir()) | |
| @functools.cached_property | |
| def dist_path(self): | |
| return self.checkout / "dist" | |
| @functools.cached_property | |
| def archive_stem(self): | |
| version_info = self.wasi_build_details["language"]["version_info"] | |
| version = f"python-{version_info['major']}.{version_info['minor']}.{version_info['micro']}" | |
| if version_info["releaselevel"] != "final": | |
| version += version_info["releaselevel"][0] + str( | |
| version_info["serial"] | |
| ) | |
| return f"{version}-{self.host_triple}" | |
| @functools.cached_property | |
| def archive_dir(self): | |
| return self.dist_path / self.archive_stem | |
| def log(emoji, message, *, spacing=None): | |
| """Print a notification with an emoji. | |
| If 'spacing' is None, calculate the spacing based on the number of code points | |
| in the emoji as terminals "eat" a space when the emoji has multiple code points. | |
| """ | |
| if spacing is None: | |
| spacing = " " if len(emoji) == 1 else " " | |
| print("".join([emoji, spacing, message])) |