Skip to content

Commit 57b9ad9

Browse files
committed
fix: bootstrap build on Unix
1 parent 21aab05 commit 57b9ad9

File tree

3 files changed

+146
-8
lines changed

3 files changed

+146
-8
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: 98 additions & 7 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,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)

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)