1+ from __future__ import annotations
2+
13import os
24
35from scikit_build_core import build as _orig
79if hasattr (_orig , "prepare_metadata_for_build_wheel" ):
810 prepare_metadata_for_build_wheel = _orig .prepare_metadata_for_build_wheel
911build_editable = _orig .build_editable
10- build_wheel = _orig .build_wheel
1112build_sdist = _orig .build_sdist
1213get_requires_for_build_editable = _orig .get_requires_for_build_editable
1314get_requires_for_build_sdist = _orig .get_requires_for_build_sdist
1415
1516
16- def strtobool (value : str ) -> bool :
17+ def _strtobool (value : str ) -> bool :
1718 """
1819 Converts a environment variable string into a boolean value.
1920 """
@@ -25,19 +26,109 @@ def strtobool(value: str) -> bool:
2526 return value not in {"n" , "no" , "off" , "false" , "f" }
2627
2728
28- def get_requires_for_build_wheel (config_settings = None ):
29+ def get_requires_for_build_wheel (
30+ config_settings : dict [str , str | list [str ]] | None = None ,
31+ ) -> list [str ]:
2932 packages_orig = _orig .get_requires_for_build_wheel (config_settings )
3033 allow_ninja = any (
31- strtobool (os .environ .get (var , "" ))
34+ _strtobool (os .environ .get (var , "" ))
3235 for var in ("CMAKE_PYTHON_DIST_FORCE_NINJA_DEP" , "CMAKE_PYTHON_DIST_ALLOW_NINJA_DEP" )
3336 )
3437 packages = []
3538 for package in packages_orig :
3639 package_name = package .lower ().split (">" )[0 ].strip ()
3740 if package_name == "cmake" :
38- msg = f"CMake PyPI distibution requires { package } to be available on the build system"
39- raise ValueError (msg )
41+ continue
4042 if package_name == "ninja" and not allow_ninja :
4143 continue
4244 packages .append (package )
4345 return packages
46+
47+
48+ def _bootstrap_build (temp_path : str , config_settings : dict [str , list [str ] | str ] | None = None ) -> str :
49+ import hashlib
50+ import re
51+ import shutil
52+ import subprocess
53+ import tarfile
54+ import urllib .request
55+ from pathlib import Path
56+
57+ env = os .environ .copy ()
58+ temp_path_ = Path (temp_path )
59+
60+ if "MAKE" not in env :
61+ make_path = None
62+ make_candidates = ("gmake" , "make" , "smake" )
63+ for candidate in make_candidates :
64+ make_path = shutil .which (candidate )
65+ if make_path is not None :
66+ break
67+ if make_path is None :
68+ msg = f"Could not find a make program. Tried { make_candidates !r} "
69+ raise ValueError (msg )
70+ env ["MAKE" ] = make_path
71+ make_path = env ["MAKE" ]
72+
73+ archive_path = temp_path_
74+ if config_settings :
75+ archive_path = Path (config_settings .get ("cmake.define.CMakePythonDistributions_ARCHIVE_DOWNLOAD_DIR" , archive_path ))
76+ archive_path .mkdir (parents = True , exist_ok = True )
77+
78+ cmake_urls = Path ("CMakeUrls.cmake" ).read_text ()
79+ source_url = re .findall (r'set\(unix_source_url\s+"(?P<data>.*)"\)$' , cmake_urls , flags = re .MULTILINE )[0 ]
80+ source_sha256 = re .findall (r'set\(unix_source_sha256\s+"(?P<data>.*)"\)$' , cmake_urls , flags = re .MULTILINE )[0 ]
81+
82+ tarball_name = source_url .rsplit ("/" , maxsplit = 1 )[1 ]
83+ assert tarball_name .endswith (".tar.gz" )
84+ source_tarball = archive_path / tarball_name
85+ if not source_tarball .exists ():
86+ with urllib .request .urlopen (source_url ) as response :
87+ source_tarball .write_bytes (response .read ())
88+
89+ sha256 = hashlib .sha256 (source_tarball .read_bytes ()).hexdigest ()
90+ if source_sha256 .lower () != sha256 .lower ():
91+ msg = f"Invalid sha256 for { source_url !r} . Expected { source_sha256 !r} , got { sha256 !r} "
92+ raise ValueError (msg )
93+
94+ tar_filter_kwargs = {"filter" : "tar" } if hasattr (tarfile , "tar_filter" ) else {}
95+ with tarfile .open (source_tarball ) as tar :
96+ tar .extractall (path = temp_path_ , ** tar_filter_kwargs )
97+
98+ parallel_str = env .get ("CMAKE_BUILD_PARALLEL_LEVEL" , "1" )
99+ parallel = max (0 , int (parallel_str ) if parallel_str .isdigit () else 1 ) or os .cpu_count () or 1
100+
101+ bootstrap_path = next (temp_path_ .glob ("cmake-*/bootstrap" ))
102+ prefix_path = temp_path_ / "cmake-install"
103+ bootstrap_args = [f"--prefix={ prefix_path } " , "--no-qt-gui" , "--no-debugger" , "--parallel={parallel}" ]
104+ previous_cwd = Path ().absolute ()
105+ os .chdir (bootstrap_path .parent )
106+ try :
107+ subprocess .run ([bootstrap_path , * bootstrap_args ], env = env , check = True )
108+ subprocess .run ([make_path , "-j" , f"{ parallel } " ], env = env , check = True )
109+ subprocess .run ([make_path , "install" ], env = env , check = True )
110+ finally :
111+ os .chdir (previous_cwd )
112+
113+ return str (prefix_path / "bin" / "cmake" )
114+
115+
116+ def build_wheel (
117+ wheel_directory : str ,
118+ config_settings : dict [str , list [str ] | str ] | None = None ,
119+ metadata_directory : str | None = None ,
120+ ) -> str :
121+ from scikit_build_core .errors import CMakeNotFoundError
122+
123+ try :
124+ return _orig .build_wheel (wheel_directory , config_settings , metadata_directory )
125+ except CMakeNotFoundError :
126+ if os .name != "posix" :
127+ raise
128+ # Let's try bootstrapping CMake
129+ import tempfile
130+ with tempfile .TemporaryDirectory () as temp_path :
131+ cmake_path = _bootstrap_build (temp_path , config_settings )
132+ assert cmake_path
133+ os .environ ["CMAKE_EXECUTABLE" ] = cmake_path
134+ return _orig .build_wheel (wheel_directory , config_settings , metadata_directory )
0 commit comments