Repository: scikit-build/scikit-build-core
Pull request #826 (2024-07-25 18:32:26)

feat: adding if.system-cmake and if.cmake-wheel
See #112.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/826?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.79%. Comparing base [(`d977a08`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/d977a084ff2716bd66e84ac2c5248618b069d6b5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`38126b4`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/38126b4237e6a465358bddaac56435cbf758c305?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #826      +/-   ##
==========================================
+ Coverage   83.76%   83.79%   +0.02%     
==========================================
  Files          74       74              
  Lines        4139     4158      +19     
==========================================
+ Hits         3467     3484      +17     
- Misses        672      674       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/826?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f663418..db73127 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -42,7 +42,7 @@ repos:
     rev: 1.18.0
     hooks:
       - id: blacken-docs
-        additional_dependencies: [black==23.*]
+        additional_dependencies: [black==24.*]
 
   - repo: https://github.com/cheshirekow/cmake-format-precommit
     rev: v0.6.13
@@ -84,6 +84,7 @@ repos:
           - nox
           - packaging
           - pytest<8
+          - pytest-subprocess
           - rich
           - setuptools-scm
           - tomli
diff --git a/docs/overrides.md b/docs/overrides.md
index 835a97f..6afd649 100644
--- a/docs/overrides.md
+++ b/docs/overrides.md
@@ -132,6 +132,38 @@ SDist.
 This will be true if the `PKG-INFO` file exists, that is, if this is coming
 from an SDist. Takes a bool.
 
+### `system-cmake` (version)
+
+This will match if there's a system CMake matching this version specification.
+
+```toml
+[[tool.scikit-build.overrides]]
+if.system-cmake = ">=3.15"
+cmake.version = ""
+message.after-success = "Built using a system CMake, not a wheel"
+```
+
+### `cmake-wheel` (bool)
+
+This matches true if a wheel is known to be provided for this platform, and
+false otherwise.  This is useful for specifying a pure Python fallback on
+systems that don't have provided CMake wheels. Ninja wheels are available on all
+platforms CMake is, so a separate override for Ninja isn't needed. Often
+combined with `system-cmake`.
+
+For example, this would be an optional build only on systems with CMake or
+supported by wheels:
+
+```toml
+[tool.scikit-build]
+wheel.cmake = false
+
+[[tool.scikit-build.overrides]]
+if.any.system-cmake = ">=3.15"
+if.any.cmake-wheel = true
+wheel.cmake = true
+```
+
 ### `failed` (bool)
 
 This override is a bit special. If a build fails, scikit-build-core will check
diff --git a/src/scikit_build_core/resources/known_wheels.toml b/src/scikit_build_core/resources/known_wheels.toml
index ba4f5cf..1f0165a 100644
--- a/src/scikit_build_core/resources/known_wheels.toml
+++ b/src/scikit_build_core/resources/known_wheels.toml
@@ -14,8 +14,8 @@ known-wheels = [
     "manylinux_2_17_aarch64",
     "manylinux_2_17_x86_64",
     "manylinux_2_17_i686",
-    "manylinux_2_5_x86_64",
-    "manylinux_2_5_i686",
+    "manylinux_2_12_x86_64",
+    "manylinux_2_12_i686",
 ]
 
 [tool.scikit-build.ninja]
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index ead6a26..b8191cf 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -640,6 +640,14 @@
           "type": "boolean",
           "description": "Matches if the build fails. A build will be retried if there is at least one matching override with this set to true."
         },
+        "system-cmake": {
+          "type": "string",
+          "description": "The version of CMake found on the system. Takes a specifier set."
+        },
+        "cmake-wheel": {
+          "type": "boolean",
+          "description": "Whether a cmake wheel is known to be provided for this system."
+        },
         "env": {
           "type": "object",
           "patternProperties": {
diff --git a/src/scikit_build_core/settings/skbuild_overrides.py b/src/scikit_build_core/settings/skbuild_overrides.py
index 47e1f10..88d11dd 100644
--- a/src/scikit_build_core/settings/skbuild_overrides.py
+++ b/src/scikit_build_core/settings/skbuild_overrides.py
@@ -7,9 +7,14 @@ import sys
 from pathlib import Path
 from typing import TYPE_CHECKING, Any
 
+import packaging.tags
 from packaging.specifiers import SpecifierSet
 
+from .._compat import tomllib
 from .._logging import logger
+from ..cmake import CMake
+from ..errors import CMakeNotFoundError
+from ..resources import resources
 
 __all__ = ["process_overides", "regex_match"]
 
@@ -69,6 +74,8 @@ def override_match(
     state: str | None = None,
     from_sdist: bool | None = None,
     failed: bool | None = None,
+    system_cmake: str | None = None,
+    cmake_wheel: bool | None = None,
 ) -> tuple[dict[str, str], set[str]]:
     """
     Check if the current environment matches the overrides. Returns a dict
@@ -161,6 +168,29 @@ def override_match(
         else:
             failed_set.add("from-sdist")
 
+    if system_cmake is not None:
+        try:
+            cmake = CMake.default_search(
+                version=SpecifierSet(system_cmake), module=False, env=current_env
+            )
+            passed_dict["system-cmake"] = (
+                f"system cmake {cmake.version} found at {cmake.cmake_path} passing {system_cmake}"
+            )
+        except CMakeNotFoundError:
+            failed_set.add("system-cmake")
+
+    if cmake_wheel is not None:
+        with resources.joinpath("known_wheels.toml").open("rb") as f:
+            known_wheels_toml = tomllib.load(f)
+        known_cmake_wheels = set(
+            known_wheels_toml["tool"]["scikit-build"]["cmake"]["known-wheels"]
+        )
+        cmake_plat = known_cmake_wheels.intersection(packaging.tags.sys_tags())
+        if cmake_plat:
+            passed_dict["cmake-wheel"] = f"cmake wheel available on {cmake_plat}"
+        else:
+            failed_set.add("cmake-wheel")
+
     if env:
         for key, value in env.items():
             if key not in current_env:
diff --git a/src/scikit_build_core/settings/skbuild_schema.py b/src/scikit_build_core/settings/skbuild_schema.py
index 797b336..1aaa7eb 100644
--- a/src/scikit_build_core/settings/skbuild_schema.py
+++ b/src/scikit_build_core/settings/skbuild_schema.py
@@ -124,6 +124,14 @@ def generate_skbuild_schema(tool_name: str = "scikit-build") -> dict[str, Any]:
                 "type": "boolean",
                 "description": "Matches if the build fails. A build will be retried if there is at least one matching override with this set to true.",
             },
+            "system-cmake": {
+                "type": "string",
+                "description": "The version of CMake found on the system. Takes a specifier set.",
+            },
+            "cmake-wheel": {
+                "type": "boolean",
+                "description": "Whether a cmake wheel is known to be provided for this system.",
+            },
             "env": {
                 "type": "object",
                 "patternProperties": {


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 6a44567..0755e51 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,7 @@ import subprocess
 import sys
 import sysconfig
 from pathlib import Path
+from typing import Any
 
 import virtualenv as _virtualenv
 
@@ -333,6 +334,34 @@ def package_simple_purelib_package(
     return package
 
 
+def which_mock(name: str) -> str | None:
+    if name in {"ninja", "ninja-build", "cmake3", "samu", "gmake", "make"}:
+        return None
+    if name == "cmake":
+        return "cmake/path"
+    return None
+
+
+@pytest.fixture()
+def protect_get_requires(fp, monkeypatch):
+    """
+    Protect get_requires from actually calling anything variable during tests.
+    """
+    # This needs to be passed due to packaging.tags 22 extra checks if macos 10.16 is reported
+    fp.pass_command([sys.executable, fp.any()])
+    monkeypatch.setattr(shutil, "which", which_mock)
+    monkeypatch.delenv("CMAKE_GENERATOR", raising=False)
+
+    orig_find_spec = importlib.util.find_spec
+
+    def find_spec(name: str, package: str | None = None) -> Any:
+        if name in {"cmake", "ninja"}:
+            return None
+        return orig_find_spec(name, package)
+
+    monkeypatch.setattr(importlib.util, "find_spec", find_spec)
+
+
 def pytest_collection_modifyitems(items: list[pytest.Item]) -> None:
     for item in items:
         # Ensure all tests using virtualenv are marked as such
diff --git a/tests/test_get_requires.py b/tests/test_get_requires.py
index cd431ed..df51581 100644
--- a/tests/test_get_requires.py
+++ b/tests/test_get_requires.py
@@ -1,11 +1,8 @@
 from __future__ import annotations
 
-import importlib.util
-import shutil
-import sys
 import sysconfig
 from pathlib import Path
-from typing import Any
+from typing import TYPE_CHECKING
 
 import pytest
 
@@ -16,38 +13,20 @@ from scikit_build_core.build import (
 )
 from scikit_build_core.builder.get_requires import GetRequires
 
-ninja = [] if sysconfig.get_platform().startswith("win") else ["ninja>=1.5"]
-
+if TYPE_CHECKING:
+    from pytest_subprocess import FakeProcess
 
-def which_mock(name: str) -> str | None:
-    if name in {"ninja", "ninja-build", "cmake3", "samu", "gmake", "make"}:
-        return None
-    if name == "cmake":
-        return "cmake/path"
-    return None
+ninja = [] if sysconfig.get_platform().startswith("win") else ["ninja>=1.5"]
 
 
 @pytest.fixture(autouse=True)
-def protect_get_requires(fp, monkeypatch):
+def protect_get_requires_autouse(protect_get_requires: None):  # noqa: ARG001
     """
-    Protect get_requires from actually calling anything variable during tests.
+    Autouse this fixture in this test.
     """
-    # This needs to be passed due to packaging.tags 22 extra checks if macos 10.16 is reported
-    fp.pass_command([sys.executable, fp.any()])
-    monkeypatch.setattr(shutil, "which", which_mock)
-    monkeypatch.delenv("CMAKE_GENERATOR", raising=False)
-
-    orig_find_spec = importlib.util.find_spec
-
-    def find_spec(name: str, package: str | None = None) -> Any:
-        if name in {"cmake", "ninja"}:
-            return None
-        return orig_find_spec(name, package)
-
-    monkeypatch.setattr(importlib.util, "find_spec", find_spec)
 
 
-def test_get_requires_parts(fp):
+def test_get_requires_parts(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.14.0"}}',
@@ -56,7 +35,7 @@ def test_get_requires_parts(fp):
     assert set(GetRequires().ninja()) == {*ninja}
 
 
-def test_get_requires_parts_uneeded(fp):
+def test_get_requires_parts_uneeded(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.18.0"}}',
@@ -65,7 +44,7 @@ def test_get_requires_parts_uneeded(fp):
     assert set(GetRequires().ninja()) == {*ninja}
 
 
-def test_get_requires_parts_settings(fp):
+def test_get_requires_parts_settings(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.18.0"}}',
@@ -75,7 +54,9 @@ def test_get_requires_parts_settings(fp):
     assert set(GetRequires.from_config_settings(config).ninja()) == {*ninja}
 
 
-def test_get_requires_parts_pyproject(fp, monkeypatch, tmp_path):
+def test_get_requires_parts_pyproject(
+    fp: FakeProcess, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+):
     monkeypatch.chdir(tmp_path)
     tmp_path.joinpath("pyproject.toml").write_text(
         """
@@ -92,7 +73,9 @@ def test_get_requires_parts_pyproject(fp, monkeypatch, tmp_path):
     assert set(GetRequires().ninja()) == {*ninja}
 
 
-def test_get_requires_parts_pyproject_old(fp, monkeypatch, tmp_path):
+def test_get_requires_parts_pyproject_old(
+    fp: FakeProcess, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
+):
     monkeypatch.chdir(tmp_path)
     tmp_path.joinpath("pyproject.toml").write_text(
         """
@@ -111,7 +94,7 @@ def test_get_requires_parts_pyproject_old(fp, monkeypatch, tmp_path):
     assert set(GetRequires().ninja()) == {*ninja}
 
 
-def test_get_requires_for_build_sdist(fp):
+def test_get_requires_for_build_sdist(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.14.0"}}',
@@ -119,7 +102,7 @@ def test_get_requires_for_build_sdist(fp):
     assert set(get_requires_for_build_sdist({})) == set()
 
 
-def test_get_requires_for_build_sdist_cmake(fp):
+def test_get_requires_for_build_sdist_cmake(fp: FakeProcess):
     expected = {"cmake>=3.15", *ninja}
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
@@ -128,7 +111,7 @@ def test_get_requires_for_build_sdist_cmake(fp):
     assert set(get_requires_for_build_sdist({"sdist.cmake": "True"})) == expected
 
 
-def test_get_requires_for_build_wheel(fp):
+def test_get_requires_for_build_wheel(fp: FakeProcess):
     expected = {"cmake>=3.15", *ninja}
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
@@ -137,7 +120,7 @@ def test_get_requires_for_build_wheel(fp):
     assert set(get_requires_for_build_wheel({})) == expected
 
 
-def test_get_requires_for_build_wheel_pure(fp):
+def test_get_requires_for_build_wheel_pure(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.14.0"}}',
@@ -145,7 +128,7 @@ def test_get_requires_for_build_wheel_pure(fp):
     assert set(get_requires_for_build_wheel({"wheel.cmake": "False"})) == set()
 
 
-def test_get_requires_for_build_editable(fp):
+def test_get_requires_for_build_editable(fp: FakeProcess):
     expected = {"cmake>=3.15", *ninja}
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
@@ -154,7 +137,7 @@ def test_get_requires_for_build_editable(fp):
     assert set(get_requires_for_build_editable({})) == expected
 
 
-def test_get_requires_for_build_editable_pure(fp):
+def test_get_requires_for_build_editable_pure(fp: FakeProcess):
     fp.register(
         [Path("cmake/path"), "-E", "capabilities"],
         stdout='{"version":{"string":"3.14.0"}}',
diff --git a/tests/test_settings_overrides.py b/tests/test_settings_overrides.py
index 406ab8a..bd49222 100644
--- a/tests/test_settings_overrides.py
+++ b/tests/test_settings_overrides.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import typing
+from pathlib import Path
 from textwrap import dedent
 
 import pytest
@@ -9,7 +10,7 @@ from scikit_build_core.settings.skbuild_overrides import regex_match
 from scikit_build_core.settings.skbuild_read_settings import SettingsReader
 
 if typing.TYPE_CHECKING:
-    from pathlib import Path
+    from pytest_subprocess import FakeProcess
 
 
 class VersionInfo(typing.NamedTuple):
@@ -603,3 +604,65 @@ def test_failed_retry(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
     settings = settings_reader.settings
     assert settings.wheel.cmake
     assert not settings.sdist.cmake
+
+
+@pytest.mark.parametrize("sys_tag", ["win32", "linux"])
+def test_wheel_platform(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, sys_tag: str):
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [tool.scikit-build]
+            wheel.cmake = false
+
+            [[tool.scikit-build.overrides]]
+            if.cmake-wheel = true
+            wheel.cmake = true
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    monkeypatch.chdir(tmp_path)
+
+    monkeypatch.setattr("packaging.tags.sys_tags", lambda: [sys_tag])
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, retry=False)
+    settings = settings_reader.settings
+    assert settings.wheel.cmake == (sys_tag == "win32")
+
+
+@pytest.mark.parametrize("cmake_version", ["3.21", "3.27"])
+@pytest.mark.usefixtures("protect_get_requires")
+def test_system_cmake(
+    tmp_path: Path,
+    monkeypatch: pytest.MonkeyPatch,
+    cmake_version: str | None,
+    fp: FakeProcess,
+) -> None:
+    if cmake_version:
+        fp.register(
+            [Path("cmake/path"), "-E", "capabilities"],
+            stdout=f'{{"version":{{"string": "{cmake_version}"}}}}',
+        )
+
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text(
+        dedent(
+            """\
+            [tool.scikit-build]
+            wheel.cmake = false
+
+            [[tool.scikit-build.overrides]]
+            if.system-cmake = ">=3.24"
+            wheel.cmake = true
+            """
+        ),
+        encoding="utf-8",
+    )
+
+    monkeypatch.chdir(tmp_path)
+
+    settings_reader = SettingsReader.from_file(pyproject_toml, retry=False)
+    settings = settings_reader.settings
+    assert settings.wheel.cmake == (cmake_version == "3.27")