Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: bootstrap build #568

Merged
merged 5 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,65 @@ jobs:
- name: Test installed SDist
run: .venv/bin/pytest ./tests

bootstrap_build:
name: Source only build on ${{ matrix.os }}
needs: [lint]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "windows-latest", "macos-latest"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
id: python
with:
python-version: "3.x"

- name: Remove cmake and ninja
shell: bash
run: |
# Remove cmake and ninja
set -euxo pipefail
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L51
# https://github.com/scikit-build/scikit-build-core/blob/3943920fa267dc83f9295279bea1c774c0916f13/src/scikit_build_core/program_search.py#L70
for TOOL in cmake cmake3 ninja-build ninja samu; do
while which ${TOOL}; do
if [ "$RUNNER_OS" == "Windows" ]; then
rm -f "$(which ${TOOL})"
else
sudo rm -f $(which -a ${TOOL})
fi
done
done

- name: Build SDist
run: pipx run --python '${{ steps.python.outputs.python-path }}' build --sdist

- name: Install dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends libssl-dev

- name: Install SDist
shell: bash
env:
CMAKE_ARGS: "-DBUILD_CMAKE_FROM_SOURCE:BOOL=OFF"
CMAKE_BUILD_PARALLEL_LEVEL: "4"
MACOSX_DEPLOYMENT_TARGET: "10.10"
run: |
python -m pip install -v --no-binary='cmake,ninja' dist/*.tar.gz
rm -rf dist

- name: Test installed SDist
shell: bash
run: python -m pip install pytest pytest-cov && pytest ./tests

check_dist:
name: Check dist
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist]
needs: [build_wheels, build_manylinux2010_wheels, build_sdist, test_sdist, bootstrap_build]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
Expand Down
159 changes: 159 additions & 0 deletions _build_backend/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
from __future__ import annotations

import os

from scikit_build_core import build as _orig

if hasattr(_orig, "prepare_metadata_for_build_editable"):
prepare_metadata_for_build_editable = _orig.prepare_metadata_for_build_editable
if hasattr(_orig, "prepare_metadata_for_build_wheel"):
prepare_metadata_for_build_wheel = _orig.prepare_metadata_for_build_wheel
build_editable = _orig.build_editable
build_sdist = _orig.build_sdist
get_requires_for_build_editable = _orig.get_requires_for_build_editable
get_requires_for_build_sdist = _orig.get_requires_for_build_sdist


def _strtobool(value: str) -> bool:
"""
Converts a environment variable string into a boolean value.
"""
if not value:
return False
value = value.lower()
if value.isdigit():
return bool(int(value))
return value not in {"n", "no", "off", "false", "f"}


def get_requires_for_build_wheel(
config_settings: dict[str, str | list[str]] | None = None,
) -> list[str]:
packages_orig = _orig.get_requires_for_build_wheel(config_settings)
allow_cmake = _strtobool(os.environ.get("CMAKE_PYTHON_DIST_ALLOW_CMAKE_DEP", ""))
allow_ninja = any(
_strtobool(os.environ.get(var, ""))
for var in ("CMAKE_PYTHON_DIST_FORCE_NINJA_DEP", "CMAKE_PYTHON_DIST_ALLOW_NINJA_DEP")
)
packages = []
for package in packages_orig:
package_name = package.lower().split(">")[0].strip()
if package_name == "cmake" and not allow_cmake:
continue
if package_name == "ninja" and not allow_ninja:
continue
packages.append(package)
return packages


def _bootstrap_build(temp_path: str, config_settings: dict[str, list[str] | str] | None = None) -> str:
import hashlib
import platform
import re
import shutil
import subprocess
import tarfile
import urllib.request
import zipfile
from pathlib import Path

env = os.environ.copy()
temp_path_ = Path(temp_path)

archive_dir = temp_path_
if config_settings:
archive_dir = Path(config_settings.get("cmake.define.CMakePythonDistributions_ARCHIVE_DOWNLOAD_DIR", archive_dir))
archive_dir.mkdir(parents=True, exist_ok=True)

if os.name == "posix":
if "MAKE" not in env:
make_path = None
make_candidates = ("gmake", "make", "smake")
for candidate in make_candidates:
make_path = shutil.which(candidate)
if make_path is not None:
break
if make_path is None:
msg = f"Could not find a make program. Tried {make_candidates!r}"
raise ValueError(msg)
env["MAKE"] = make_path
make_path = env["MAKE"]
kind = "unix_source"
else:
assert os.name == "nt"
machine = platform.machine()
kinds = {
"x86": "win32_binary",
"AMD64": "win64_binary",
"ARM64": "winarm64_binary",
}
if machine not in kinds:
msg = f"Could not find CMake required to build on a {machine} system"
raise ValueError(msg)
kind = kinds[machine]


cmake_urls = Path("CMakeUrls.cmake").read_text()
archive_url = re.findall(rf'set\({kind}_url\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0]
archive_sha256 = re.findall(rf'set\({kind}_sha256\s+"(?P<data>.*)"\)$', cmake_urls, flags=re.MULTILINE)[0]

archive_name = archive_url.rsplit("/", maxsplit=1)[1]
archive_path = archive_dir / archive_name
if not archive_path.exists():
with urllib.request.urlopen(archive_url) as response:
archive_path.write_bytes(response.read())

sha256 = hashlib.sha256(archive_path.read_bytes()).hexdigest()
if archive_sha256.lower() != sha256.lower():
msg = f"Invalid sha256 for {archive_url!r}. Expected {archive_sha256!r}, got {sha256!r}"
raise ValueError(msg)

if os.name == "posix":
assert archive_name.endswith(".tar.gz")
tar_filter_kwargs = {"filter": "tar"} if hasattr(tarfile, "tar_filter") else {}
with tarfile.open(archive_path) as tar:
tar.extractall(path=temp_path_, **tar_filter_kwargs)

parallel_str = env.get("CMAKE_BUILD_PARALLEL_LEVEL", "1")
parallel = max(0, int(parallel_str) if parallel_str.isdigit() else 1) or os.cpu_count() or 1

bootstrap_path = next(temp_path_.glob("cmake-*/bootstrap"))
prefix_path = temp_path_ / "cmake-install"
cmake_path = prefix_path / "bin" / "cmake"
bootstrap_args = [f"--prefix={prefix_path}", "--no-qt-gui", "--no-debugger", "--parallel={parallel}", "--", "-DBUILD_TESTING=OFF", "-DBUILD_CursesDialog:BOOL=OFF"]
previous_cwd = Path().absolute()
os.chdir(bootstrap_path.parent)
try:
subprocess.run([bootstrap_path, *bootstrap_args], env=env, check=True)
subprocess.run([make_path, "-j", f"{parallel}"], env=env, check=True)
subprocess.run([make_path, "install"], env=env, check=True)
finally:
os.chdir(previous_cwd)
else:
assert archive_name.endswith(".zip")
with zipfile.ZipFile(archive_path) as zip_:
zip_.extractall(path=temp_path_)
cmake_path = next(temp_path_.glob("cmake-*/bin/cmake.exe"))

return str(cmake_path)


def build_wheel(
wheel_directory: str,
config_settings: dict[str, list[str] | str] | None = None,
metadata_directory: str | None = None,
) -> str:
from scikit_build_core.errors import CMakeNotFoundError

try:
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
except CMakeNotFoundError:
if os.name not in {"posix", "nt"}:
raise
# Let's try bootstrapping CMake
import tempfile
with tempfile.TemporaryDirectory() as temp_path:
cmake_path = _bootstrap_build(temp_path, config_settings)
assert cmake_path
os.environ["CMAKE_EXECUTABLE"] = cmake_path
return _orig.build_wheel(wheel_directory, config_settings, metadata_directory)
27 changes: 20 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[build-system]
requires = ["scikit-build-core"]
build-backend = "scikit_build_core.build"
requires = ["scikit-build-core>=0.10"]
build-backend = "backend"
backend-path = ["_build_backend"]

[project]
name = "cmake"
Expand Down Expand Up @@ -51,10 +52,10 @@ cpack = "cmake:cpack"
ctest = "cmake:ctest"

[tool.scikit-build]
minimum-version = "0.8"
minimum-version = "build-system.requires"
build-dir = "build/{wheel_tag}"
cmake.version = "" # We are cmake, so don't request cmake
ninja.make-fallback = false
cmake.version = "CMakeLists.txt"
ninja.make-fallback = true
wheel.py-api = "py3"
wheel.expand-macos-universal-tags = true
wheel.install-dir = "cmake/data"
Expand All @@ -65,22 +66,34 @@ template = '''
version = "${version}"
'''

[[tool.scikit-build.overrides]]
if.env.CMAKE_PYTHON_DIST_FORCE_NINJA_DEP = true
ninja.make-fallback = false

[[tool.scikit-build.overrides]]
if.state = "metadata_wheel"
wheel.cmake = false
wheel.platlib = true


[tool.cibuildwheel]
build = "cp39-*"
test-extras = "test"
test-command = "pytest {project}/tests"
build-verbosity = 1
build-frontend = "build[uv]"
environment = { CMAKE_PYTHON_DIST_FORCE_NINJA_DEP = "1" }
musllinux-x86_64-image = "musllinux_1_1"
musllinux-i686-image = "musllinux_1_1"
musllinux-aarch64-image = "musllinux_1_1"
musllinux-ppc64le-image = "musllinux_1_1"
musllinux-s390x-image = "musllinux_1_1"
musllinux-armv7l-image = "musllinux_1_2"

[tool.cibuildwheel.macos.environment]
MACOSX_DEPLOYMENT_TARGET = "10.10"
[[tool.cibuildwheel.overrides]]
select = "*-macos*"
inherit.environment = "append"
environment = { MACOSX_DEPLOYMENT_TARGET = "10.10" }

[tool.cibuildwheel.linux]
before-all = "./scripts/manylinux-build-and-install-openssl.sh"
Expand Down
Loading