Skip to content

Commit 5d9ec1d

Browse files
committed
fix: bootstrap build on Unix
1 parent 8009f0b commit 5d9ec1d

File tree

3 files changed

+145
-7
lines changed

3 files changed

+145
-7
lines changed

.github/workflows/build.yml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,51 @@ jobs:
219219
- name: Test installed SDist
220220
run: .venv/bin/pytest ./tests
221221

222+
bootstrap_build:
223+
name: Bootstrap build
224+
needs: [lint]
225+
runs-on: ubuntu-latest
226+
steps:
227+
- uses: actions/checkout@v4
228+
- uses: actions/setup-python@v5
229+
id: python
230+
with:
231+
python-version: "3.x"
232+
233+
- name: Remove cmake and ninja
234+
run: |
235+
# Remove cmake and ninja
236+
set -euxo pipefail
237+
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L51
238+
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L70
239+
for TOOL in cmake cmake3 ninja-build ninja samu; do
240+
while which ${TOOL}; do
241+
sudo rm -f $(which -a ${TOOL})
242+
done
243+
done
244+
245+
- name: Build SDist
246+
run: pipx run --python '${{ steps.python.outputs.python-path }}' build --sdist
247+
248+
- name: Install dependencies
249+
run: |
250+
sudo apt-get update
251+
sudo apt-get install -y --no-install-recommends libssl-dev
252+
253+
- name: Install SDist
254+
env:
255+
CMAKE_ARGS: "-DBUILD_CMAKE_FROM_SOURCE:BOOL=OFF"
256+
CMAKE_BUILD_PARALLEL_LEVEL: "4"
257+
run: |
258+
python -m pip install -v --no-binary='cmake,ninja' dist/*.tar.gz
259+
rm -rf dist
260+
261+
- name: Test installed SDist
262+
run: python -m pip install pytest pytest-cov && pytest ./tests
263+
222264
check_dist:
223265
name: Check dist
224-
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist]
266+
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist, bootstrap_build]
225267
runs-on: ubuntu-latest
226268
steps:
227269
- uses: actions/download-artifact@v4

_build_backend/backend.py

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import os
24

35
from scikit_build_core import build as _orig
@@ -7,13 +9,12 @@
79
if hasattr(_orig, "prepare_metadata_for_build_wheel"):
810
prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel
911
build_editable = _orig.build_editable
10-
build_wheel = _orig.build_wheel
1112
build_sdist = _orig.build_sdist
1213
get_requires_for_build_editable = _orig.get_requires_for_build_editable
1314
get_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)

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ version = "${version}"
7070
if.env.CMAKE_PYTHON_DIST_FORCE_NINJA_DEP = true
7171
ninja.make-fallback = false
7272

73+
[[tool.scikit-build.overrides]]
74+
if.state = "metadata_wheel"
75+
wheel.cmake = false
76+
wheel.platlib = true
77+
7378

7479
[tool.cibuildwheel]
7580
build = "cp39-*"

0 commit comments

Comments
 (0)