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