Skip to content

Commit 47e072b

Browse files
authored
fix: bootstrap build (#568)
* fix: do not require Ninja This should prevent bootstrapping issues between CMake PyPI distribution and Ninja PyPI distribution when building either one from sources. * fix: bootstrap build on Unix * fix: bootstrap build on Windows * test: bootstrap build on macOS * chore: parse CMake version from CMakeLists.txt
1 parent cfb38e0 commit 47e072b

File tree

3 files changed

+236
-8
lines changed

3 files changed

+236
-8
lines changed

.github/workflows/build.yml

+57-1
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,65 @@ jobs:
219219
- name: Test installed SDist
220220
run: .venv/bin/pytest ./tests
221221

222+
bootstrap_build:
223+
name: Source only build on ${{ matrix.os }}
224+
needs: [lint]
225+
runs-on: ${{ matrix.os }}
226+
strategy:
227+
fail-fast: false
228+
matrix:
229+
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
230+
231+
steps:
232+
- uses: actions/checkout@v4
233+
- uses: actions/setup-python@v5
234+
id: python
235+
with:
236+
python-version: "3.x"
237+
238+
- name: Remove cmake and ninja
239+
shell: bash
240+
run: |
241+
# Remove cmake and ninja
242+
set -euxo pipefail
243+
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L51
244+
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L70
245+
for TOOL in cmake cmake3 ninja-build ninja samu; do
246+
while which ${TOOL}; do
247+
if [ "$RUNNER_OS" == "Windows" ]; then
248+
rm -f "$(which ${TOOL})"
249+
else
250+
sudo rm -f $(which -a ${TOOL})
251+
fi
252+
done
253+
done
254+
255+
- name: Build SDist
256+
run: pipx run --python '${{ steps.python.outputs.python-path }}' build --sdist
257+
258+
- name: Install dependencies
259+
if: runner.os == 'Linux'
260+
run: |
261+
sudo apt-get update
262+
sudo apt-get install -y --no-install-recommends libssl-dev
263+
264+
- name: Install SDist
265+
shell: bash
266+
env:
267+
CMAKE_ARGS: "-DBUILD_CMAKE_FROM_SOURCE:BOOL=OFF"
268+
CMAKE_BUILD_PARALLEL_LEVEL: "4"
269+
MACOSX_DEPLOYMENT_TARGET: "10.10"
270+
run: |
271+
python -m pip install -v --no-binary='cmake,ninja' dist/*.tar.gz
272+
rm -rf dist
273+
274+
- name: Test installed SDist
275+
shell: bash
276+
run: python -m pip install pytest pytest-cov && pytest ./tests
277+
222278
check_dist:
223279
name: Check dist
224-
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist]
280+
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist, bootstrap_build]
225281
runs-on: ubuntu-latest
226282
steps:
227283
- uses: actions/download-artifact@v4

_build_backend/backend.py

+159
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
from __future__ import annotations
2+
3+
import os
4+
5+
from scikit_build_core import build as _orig
6+
7+
if hasattr(_orig, "prepare_metadata_for_build_editable"):
8+
prepare_metadata_for_build_editable = _orig.prepare_metadata_for_build_editable
9+
if hasattr(_orig, "prepare_metadata_for_build_wheel"):
10+
prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel
11+
build_editable = _orig.build_editable
12+
build_sdist = _orig.build_sdist
13+
get_requires_for_build_editable = _orig.get_requires_for_build_editable
14+
get_requires_for_build_sdist = _orig.get_requires_for_build_sdist
15+
16+
17+
def _strtobool(value: str) -> bool:
18+
"""
19+
Converts a environment variable string into a boolean value.
20+
"""
21+
if not value:
22+
return False
23+
value = value.lower()
24+
if value.isdigit():
25+
return bool(int(value))
26+
return value not in {"n", "no", "off", "false", "f"}
27+
28+
29+
def get_requires_for_build_wheel(
30+
config_settings: dict[str, str | list[str]] | None = None,
31+
) -> list[str]:
32+
packages_orig = _orig.get_requires_for_build_wheel(config_settings)
33+
allow_cmake = _strtobool(os.environ.get("CMAKE_PYTHON_DIST_ALLOW_CMAKE_DEP", ""))
34+
allow_ninja = any(
35+
_strtobool(os.environ.get(var, ""))
36+
for var in ("CMAKE_PYTHON_DIST_FORCE_NINJA_DEP", "CMAKE_PYTHON_DIST_ALLOW_NINJA_DEP")
37+
)
38+
packages = []
39+
for package in packages_orig:
40+
package_name = package.lower().split(">")[0].strip()
41+
if package_name == "cmake" and not allow_cmake:
42+
continue
43+
if package_name == "ninja" and not allow_ninja:
44+
continue
45+
packages.append(package)
46+
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 platform
52+
import re
53+
import shutil
54+
import subprocess
55+
import tarfile
56+
import urllib.request
57+
import zipfile
58+
from pathlib import Path
59+
60+
env = os.environ.copy()
61+
temp_path_ = Path(temp_path)
62+
63+
archive_dir = temp_path_
64+
if config_settings:
65+
archive_dir = Path(config_settings.get("cmake.define.CMakePythonDistributions_ARCHIVE_DOWNLOAD_DIR", archive_dir))
66+
archive_dir.mkdir(parents=True, exist_ok=True)
67+
68+
if os.name == "posix":
69+
if "MAKE" not in env:
70+
make_path = None
71+
make_candidates = ("gmake", "make", "smake")
72+
for candidate in make_candidates:
73+
make_path = shutil.which(candidate)
74+
if make_path is not None:
75+
break
76+
if make_path is None:
77+
msg = f"Could not find a make program. Tried {make_candidates!r}"
78+
raise ValueError(msg)
79+
env["MAKE"] = make_path
80+
make_path = env["MAKE"]
81+
kind = "unix_source"
82+
else:
83+
assert os.name == "nt"
84+
machine = platform.machine()
85+
kinds = {
86+
"x86": "win32_binary",
87+
"AMD64": "win64_binary",
88+
"ARM64": "winarm64_binary",
89+
}
90+
if machine not in kinds:
91+
msg = f"Could not find CMake required to build on a {machine} system"
92+
raise ValueError(msg)
93+
kind = kinds[machine]
94+
95+
96+
cmake_urls = Path("CMakeUrls.cmake").read_text()
97+
archive_url = re.findall(rf'set\({kind}_url\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0]
98+
archive_sha256 = re.findall(rf'set\({kind}_sha256\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0]
99+
100+
archive_name = archive_url.rsplit("/", maxsplit=1)[1]
101+
archive_path = archive_dir / archive_name
102+
if not archive_path.exists():
103+
with urllib.request.urlopen(archive_url) as response:
104+
archive_path.write_bytes(response.read())
105+
106+
sha256 = hashlib.sha256(archive_path.read_bytes()).hexdigest()
107+
if archive_sha256.lower() != sha256.lower():
108+
msg = f"Invalid sha256 for {archive_url!r}. Expected {archive_sha256!r}, got {sha256!r}"
109+
raise ValueError(msg)
110+
111+
if os.name == "posix":
112+
assert archive_name.endswith(".tar.gz")
113+
tar_filter_kwargs = {"filter": "tar"} if hasattr(tarfile, "tar_filter") else {}
114+
with tarfile.open(archive_path) as tar:
115+
tar.extractall(path=temp_path_, **tar_filter_kwargs)
116+
117+
parallel_str = env.get("CMAKE_BUILD_PARALLEL_LEVEL", "1")
118+
parallel = max(0, int(parallel_str) if parallel_str.isdigit() else 1) or os.cpu_count() or 1
119+
120+
bootstrap_path = next(temp_path_.glob("cmake-*/bootstrap"))
121+
prefix_path = temp_path_ / "cmake-install"
122+
cmake_path = prefix_path / "bin" / "cmake"
123+
bootstrap_args = [f"--prefix={prefix_path}", "--no-qt-gui", "--no-debugger", "--parallel={parallel}", "--", "-DBUILD_TESTING=OFF", "-DBUILD_CursesDialog:BOOL=OFF"]
124+
previous_cwd = Path().absolute()
125+
os.chdir(bootstrap_path.parent)
126+
try:
127+
subprocess.run([bootstrap_path, *bootstrap_args], env=env, check=True)
128+
subprocess.run([make_path, "-j", f"{parallel}"], env=env, check=True)
129+
subprocess.run([make_path, "install"], env=env, check=True)
130+
finally:
131+
os.chdir(previous_cwd)
132+
else:
133+
assert archive_name.endswith(".zip")
134+
with zipfile.ZipFile(archive_path) as zip_:
135+
zip_.extractall(path=temp_path_)
136+
cmake_path = next(temp_path_.glob("cmake-*/bin/cmake.exe"))
137+
138+
return str(cmake_path)
139+
140+
141+
def build_wheel(
142+
wheel_directory: str,
143+
config_settings: dict[str, list[str] | str] | None = None,
144+
metadata_directory: str | None = None,
145+
) -> str:
146+
from scikit_build_core.errors import CMakeNotFoundError
147+
148+
try:
149+
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
150+
except CMakeNotFoundError:
151+
if os.name not in {"posix", "nt"}:
152+
raise
153+
# Let's try bootstrapping CMake
154+
import tempfile
155+
with tempfile.TemporaryDirectory() as temp_path:
156+
cmake_path = _bootstrap_build(temp_path, config_settings)
157+
assert cmake_path
158+
os.environ["CMAKE_EXECUTABLE"] = cmake_path
159+
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)

pyproject.toml

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[build-system]
2-
requires = ["scikit-build-core"]
3-
build-backend = "scikit_build_core.build"
2+
requires = ["scikit-build-core>=0.10"]
3+
build-backend = "backend"
4+
backend-path = ["_build_backend"]
45

56
[project]
67
name = "cmake"
@@ -51,10 +52,10 @@ cpack = "cmake:cpack"
5152
ctest = "cmake:ctest"
5253

5354
[tool.scikit-build]
54-
minimum-version = "0.8"
55+
minimum-version = "build-system.requires"
5556
build-dir = "build/{wheel_tag}"
56-
cmake.version = "" # We are cmake, so don't request cmake
57-
ninja.make-fallback = false
57+
cmake.version = "CMakeLists.txt"
58+
ninja.make-fallback = true
5859
wheel.py-api = "py3"
5960
wheel.expand-macos-universal-tags = true
6061
wheel.install-dir = "cmake/data"
@@ -65,22 +66,34 @@ template = '''
6566
version = "${version}"
6667
'''
6768

69+
[[tool.scikit-build.overrides]]
70+
if.env.CMAKE_PYTHON_DIST_FORCE_NINJA_DEP = true
71+
ninja.make-fallback = false
72+
73+
[[tool.scikit-build.overrides]]
74+
if.state = "metadata_wheel"
75+
wheel.cmake = false
76+
wheel.platlib = true
77+
6878

6979
[tool.cibuildwheel]
7080
build = "cp39-*"
7181
test-extras = "test"
7282
test-command = "pytest {project}/tests"
7383
build-verbosity = 1
7484
build-frontend = "build[uv]"
85+
environment = { CMAKE_PYTHON_DIST_FORCE_NINJA_DEP = "1" }
7586
musllinux-x86_64-image = "musllinux_1_1"
7687
musllinux-i686-image = "musllinux_1_1"
7788
musllinux-aarch64-image = "musllinux_1_1"
7889
musllinux-ppc64le-image = "musllinux_1_1"
7990
musllinux-s390x-image = "musllinux_1_1"
8091
musllinux-armv7l-image = "musllinux_1_2"
8192

82-
[tool.cibuildwheel.macos.environment]
83-
MACOSX_DEPLOYMENT_TARGET = "10.10"
93+
[[tool.cibuildwheel.overrides]]
94+
select = "*-macos*"
95+
inherit.environment = "append"
96+
environment = { MACOSX_DEPLOYMENT_TARGET = "10.10" }
8497

8598
[tool.cibuildwheel.linux]
8699
before-all = "./scripts/manylinux-build-and-install-openssl.sh"

0 commit comments

Comments
 (0)