|
10 | 10 | import glob |
11 | 11 | import os |
12 | 12 | import os.path |
| 13 | +import re |
13 | 14 | import shutil |
14 | 15 | import subprocess |
15 | 16 | import sys |
16 | 17 | import sysconfig |
17 | 18 | from enum import Enum |
| 19 | +from functools import lru_cache |
18 | 20 |
|
19 | 21 | from typing import Callable |
20 | 22 |
|
|
29 | 31 | PYTHON_LIB_DIR = os.path.join(SOURCE_DIR, "PythonLib") |
30 | 32 |
|
31 | 33 |
|
| 34 | +@lru_cache(maxsize=1) |
| 35 | +def get_compiler() -> tuple[str, str]: |
| 36 | + """ |
| 37 | + Prefers GCC if a new enough version is installed as this is what the |
| 38 | + cibuildwheel environment uses. |
| 39 | +
|
| 40 | + Returns: |
| 41 | + A tuple of (c_compiler, cxx_compiler) paths. |
| 42 | + """ |
| 43 | + gcc_path = shutil.which("gcc") |
| 44 | + gxx_path = shutil.which("g++") |
| 45 | + |
| 46 | + if gcc_path and gxx_path: |
| 47 | + try: |
| 48 | + result = subprocess.run( |
| 49 | + [gcc_path, "--version"], |
| 50 | + capture_output=True, |
| 51 | + text=True, |
| 52 | + check=True, |
| 53 | + timeout=5, |
| 54 | + ) |
| 55 | + version_output = result.stdout |
| 56 | + |
| 57 | + # Parse GCC version from output like "gcc (GCC) 14.1.0" |
| 58 | + # The version is typically in the first line |
| 59 | + match = re.search( |
| 60 | + r"gcc.*?(\d+)\.(\d+)(?:\.(\d+))?", version_output, re.IGNORECASE |
| 61 | + ) |
| 62 | + if match: |
| 63 | + major_version = int(match.group(1)) |
| 64 | + print(f"Found GCC version {major_version}.{match.group(2)}") |
| 65 | + |
| 66 | + if major_version >= 14: |
| 67 | + print(f"Using GCC: {gcc_path}, {gxx_path}") |
| 68 | + return (gcc_path, gxx_path) |
| 69 | + else: |
| 70 | + print(f"GCC version {major_version} < 14, checking for Clang") |
| 71 | + except (subprocess.SubprocessError, subprocess.TimeoutExpired) as e: |
| 72 | + print(f"Failed to determine GCC version: {e}, checking for Clang") |
| 73 | + |
| 74 | + # Fall back to Clang |
| 75 | + clang_path = shutil.which("clang") |
| 76 | + clangxx_path = shutil.which("clang++") |
| 77 | + |
| 78 | + if clang_path and clangxx_path: |
| 79 | + print(f"Using Clang: {clang_path}, {clangxx_path}") |
| 80 | + return (clang_path, clangxx_path) |
| 81 | + |
| 82 | + raise RuntimeError("Cannot find suitable C/C++ compiler (tried gcc and clang)") |
| 83 | + |
| 84 | + |
32 | 85 | class PgoStage(Enum): |
33 | 86 | DISABLED = 0 |
34 | 87 | GENERATE = 1 |
@@ -80,7 +133,7 @@ def print_section(title: str) -> None: |
80 | 133 | print(title) |
81 | 134 | print(separator) |
82 | 135 |
|
83 | | - cc = self._find_binary(["clang", "gcc"]) |
| 136 | + cc, _ = get_compiler() |
84 | 137 | is_clang = "clang" in cc |
85 | 138 |
|
86 | 139 | print_section("PGO STAGE 1/3: Building with profile generation instrumentation") |
@@ -143,7 +196,9 @@ def main(): |
143 | 196 | if is_clang: |
144 | 197 | print_section("PGO STAGE 2b: Merging profile data") |
145 | 198 |
|
146 | | - llvm_profdata = self._find_binary(["llvm-profdata"]) |
| 199 | + llvm_profdata = shutil.which("llvm-profdata") |
| 200 | + if not llvm_profdata: |
| 201 | + raise RuntimeError("Cannot find llvm-profdata") |
147 | 202 | profraw_files = glob.glob(os.path.join(clang_pgo_dir, "*.profraw")) |
148 | 203 |
|
149 | 204 | if not profraw_files: |
@@ -208,13 +263,6 @@ def main(): |
208 | 263 |
|
209 | 264 | print_section("PGO BUILD COMPLETE!") |
210 | 265 |
|
211 | | - def _find_binary(self, name_options: list[str]) -> str: |
212 | | - for name in name_options: |
213 | | - result = shutil.which(name) |
214 | | - if result is not None: |
215 | | - return result |
216 | | - raise RuntimeError(f"Cannot find any binaries out of {name_options}") |
217 | | - |
218 | 266 |
|
219 | 267 | class BuildPy(build_py): |
220 | 268 | def run(self) -> None: |
@@ -284,10 +332,7 @@ def _run_cmake(self, extension: CMakeExtension) -> None: |
284 | 332 | extension_dir = os.path.abspath(self.get_ext_fullpath(extension.name)) |
285 | 333 | os.makedirs(extension_dir, exist_ok=True) |
286 | 334 |
|
287 | | - # Prefer Clang because that's what we develop against but some systems |
288 | | - # including the manylinux build environment only have GCC. |
289 | | - cc = self._find_binary(["clang", "gcc"]) |
290 | | - cxx = self._find_binary(["clang++", "g++"]) |
| 335 | + cc, cxx = get_compiler() |
291 | 336 |
|
292 | 337 | build_type = os.environ.get("CMAKE_BUILD_TYPE", "RelWithDebInfo") |
293 | 338 | verbose_makefile = os.environ.get("CMAKE_VERBOSE_MAKEFILE", "OFF") |
@@ -370,13 +415,6 @@ def set_option(var: str, default: object) -> None: |
370 | 415 | self.spawn(["cmake"] + cmake_args + ["-B", build_dir, CHECKOUT_ROOT_DIR]) |
371 | 416 | self.spawn(["cmake", "--build", build_dir] + build_args) |
372 | 417 |
|
373 | | - def _find_binary(self, name_options: list[str]) -> str: |
374 | | - for name in name_options: |
375 | | - result = shutil.which(name) |
376 | | - if result is not None: |
377 | | - return result |
378 | | - raise RuntimeError(f"Cannot find any binaries out of {name_options}") |
379 | | - |
380 | 418 | def _find_python(self) -> str: |
381 | 419 | # Normally this would use "data", but that goes to a temporary build directory |
382 | 420 | # under uv. Work off of the include directory instead. |
|
0 commit comments