|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import os |
| 4 | + |
| 5 | +from scikit_build_core import build as _orig |
| 6 | + |
| 7 | +if hasattr(_orig, "prepare_metadata_for_build_editable"): |
| 8 | + prepare_metadata_for_build_editable = _orig.prepare_metadata_for_build_editable |
| 9 | +if hasattr(_orig, "prepare_metadata_for_build_wheel"): |
| 10 | + prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel |
| 11 | +build_editable = _orig.build_editable |
| 12 | +build_sdist = _orig.build_sdist |
| 13 | +get_requires_for_build_editable = _orig.get_requires_for_build_editable |
| 14 | +get_requires_for_build_sdist = _orig.get_requires_for_build_sdist |
| 15 | + |
| 16 | + |
| 17 | +def _strtobool(value: str) -> bool: |
| 18 | + """ |
| 19 | + Converts a environment variable string into a boolean value. |
| 20 | + """ |
| 21 | + if not value: |
| 22 | + return False |
| 23 | + value = value.lower() |
| 24 | + if value.isdigit(): |
| 25 | + return bool(int(value)) |
| 26 | + return value not in {"n", "no", "off", "false", "f"} |
| 27 | + |
| 28 | + |
| 29 | +def get_requires_for_build_wheel( |
| 30 | + config_settings: dict[str, str | list[str]] | None = None, |
| 31 | +) -> list[str]: |
| 32 | + packages_orig = _orig.get_requires_for_build_wheel(config_settings) |
| 33 | + allow_cmake = _strtobool(os.environ.get("CMAKE_PYTHON_DIST_ALLOW_CMAKE_DEP", "")) |
| 34 | + allow_ninja = any( |
| 35 | + _strtobool(os.environ.get(var, "")) |
| 36 | + for var in ("CMAKE_PYTHON_DIST_FORCE_NINJA_DEP", "CMAKE_PYTHON_DIST_ALLOW_NINJA_DEP") |
| 37 | + ) |
| 38 | + packages = [] |
| 39 | + for package in packages_orig: |
| 40 | + package_name = package.lower().split(">")[0].strip() |
| 41 | + if package_name == "cmake" and not allow_cmake: |
| 42 | + continue |
| 43 | + if package_name == "ninja" and not allow_ninja: |
| 44 | + continue |
| 45 | + packages.append(package) |
| 46 | + return packages |
| 47 | + |
| 48 | + |
| 49 | +def _bootstrap_build(temp_path: str, config_settings: dict[str, list[str] | str] | None = None) -> str: |
| 50 | + import hashlib |
| 51 | + import platform |
| 52 | + import re |
| 53 | + import shutil |
| 54 | + import subprocess |
| 55 | + import tarfile |
| 56 | + import urllib.request |
| 57 | + import zipfile |
| 58 | + from pathlib import Path |
| 59 | + |
| 60 | + env = os.environ.copy() |
| 61 | + temp_path_ = Path(temp_path) |
| 62 | + |
| 63 | + archive_dir = temp_path_ |
| 64 | + if config_settings: |
| 65 | + archive_dir = Path(config_settings.get("cmake.define.CMakePythonDistributions_ARCHIVE_DOWNLOAD_DIR", archive_dir)) |
| 66 | + archive_dir.mkdir(parents=True, exist_ok=True) |
| 67 | + |
| 68 | + if os.name == "posix": |
| 69 | + if "MAKE" not in env: |
| 70 | + make_path = None |
| 71 | + make_candidates = ("gmake", "make", "smake") |
| 72 | + for candidate in make_candidates: |
| 73 | + make_path = shutil.which(candidate) |
| 74 | + if make_path is not None: |
| 75 | + break |
| 76 | + if make_path is None: |
| 77 | + msg = f"Could not find a make program. Tried {make_candidates!r}" |
| 78 | + raise ValueError(msg) |
| 79 | + env["MAKE"] = make_path |
| 80 | + make_path = env["MAKE"] |
| 81 | + kind = "unix_source" |
| 82 | + else: |
| 83 | + assert os.name == "nt" |
| 84 | + machine = platform.machine() |
| 85 | + kinds = { |
| 86 | + "x86": "win32_binary", |
| 87 | + "AMD64": "win64_binary", |
| 88 | + "ARM64": "winarm64_binary", |
| 89 | + } |
| 90 | + if machine not in kinds: |
| 91 | + msg = f"Could not find CMake required to build on a {machine} system" |
| 92 | + raise ValueError(msg) |
| 93 | + kind = kinds[machine] |
| 94 | + |
| 95 | + |
| 96 | + cmake_urls = Path("CMakeUrls.cmake").read_text() |
| 97 | + archive_url = re.findall(rf'set\({kind}_url\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0] |
| 98 | + archive_sha256 = re.findall(rf'set\({kind}_sha256\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0] |
| 99 | + |
| 100 | + archive_name = archive_url.rsplit("/", maxsplit=1)[1] |
| 101 | + archive_path = archive_dir / archive_name |
| 102 | + if not archive_path.exists(): |
| 103 | + with urllib.request.urlopen(archive_url) as response: |
| 104 | + archive_path.write_bytes(response.read()) |
| 105 | + |
| 106 | + sha256 = hashlib.sha256(archive_path.read_bytes()).hexdigest() |
| 107 | + if archive_sha256.lower() != sha256.lower(): |
| 108 | + msg = f"Invalid sha256 for {archive_url!r}. Expected {archive_sha256!r}, got {sha256!r}" |
| 109 | + raise ValueError(msg) |
| 110 | + |
| 111 | + if os.name == "posix": |
| 112 | + assert archive_name.endswith(".tar.gz") |
| 113 | + tar_filter_kwargs = {"filter": "tar"} if hasattr(tarfile, "tar_filter") else {} |
| 114 | + with tarfile.open(archive_path) as tar: |
| 115 | + tar.extractall(path=temp_path_, **tar_filter_kwargs) |
| 116 | + |
| 117 | + parallel_str = env.get("CMAKE_BUILD_PARALLEL_LEVEL", "1") |
| 118 | + parallel = max(0, int(parallel_str) if parallel_str.isdigit() else 1) or os.cpu_count() or 1 |
| 119 | + |
| 120 | + bootstrap_path = next(temp_path_.glob("cmake-*/bootstrap")) |
| 121 | + prefix_path = temp_path_ / "cmake-install" |
| 122 | + cmake_path = prefix_path / "bin" / "cmake" |
| 123 | + bootstrap_args = [f"--prefix={prefix_path}", "--no-qt-gui", "--no-debugger", "--parallel={parallel}", "--", "-DBUILD_TESTING=OFF", "-DBUILD_CursesDialog:BOOL=OFF"] |
| 124 | + previous_cwd = Path().absolute() |
| 125 | + os.chdir(bootstrap_path.parent) |
| 126 | + try: |
| 127 | + subprocess.run([bootstrap_path, *bootstrap_args], env=env, check=True) |
| 128 | + subprocess.run([make_path, "-j", f"{parallel}"], env=env, check=True) |
| 129 | + subprocess.run([make_path, "install"], env=env, check=True) |
| 130 | + finally: |
| 131 | + os.chdir(previous_cwd) |
| 132 | + else: |
| 133 | + assert archive_name.endswith(".zip") |
| 134 | + with zipfile.ZipFile(archive_path) as zip_: |
| 135 | + zip_.extractall(path=temp_path_) |
| 136 | + cmake_path = next(temp_path_.glob("cmake-*/bin/cmake.exe")) |
| 137 | + |
| 138 | + return str(cmake_path) |
| 139 | + |
| 140 | + |
| 141 | +def build_wheel( |
| 142 | + wheel_directory: str, |
| 143 | + config_settings: dict[str, list[str] | str] | None = None, |
| 144 | + metadata_directory: str | None = None, |
| 145 | +) -> str: |
| 146 | + from scikit_build_core.errors import CMakeNotFoundError |
| 147 | + |
| 148 | + try: |
| 149 | + return _orig.build_wheel(wheel_directory, config_settings, metadata_directory) |
| 150 | + except CMakeNotFoundError: |
| 151 | + if os.name not in {"posix", "nt"}: |
| 152 | + raise |
| 153 | + # Let's try bootstrapping CMake |
| 154 | + import tempfile |
| 155 | + with tempfile.TemporaryDirectory() as temp_path: |
| 156 | + cmake_path = _bootstrap_build(temp_path, config_settings) |
| 157 | + assert cmake_path |
| 158 | + os.environ["CMAKE_EXECUTABLE"] = cmake_path |
| 159 | + return _orig.build_wheel(wheel_directory, config_settings, metadata_directory) |
0 commit comments