Repository: scikit-build/scikit-build-core
Pull request #824 (2024-07-24 20:00:30)

feat: add fail setting
Adding a `fail = true` setting. One possible use:

```toml
[[tool.scikit-build.overrides]]
if.python-version = ">=3.13"
fail = true
```

This will be nicer once we allow customizing the error message.

See #112.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/824?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.71%. Comparing base [(`e86f807`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/e86f8078747d97b82db44aa6aaac48a9a33c272c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`db9a85c`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/db9a85c3639271fd257120bb8fb8f32dcf1f275c?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     #824      +/-   ##
==========================================
- Coverage   83.71%   83.71%   -0.01%     
==========================================
  Files          74       74              
  Lines        4133     4139       +6     
==========================================
+ Hits         3460     3465       +5     
- Misses        673      674       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/824?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/README.md b/README.md
index da83ca5..a4d9d12 100644
--- a/README.md
+++ b/README.md
@@ -315,6 +315,9 @@ minimum-version = "0.9"  # current version
 # The build directory. Defaults to a temporary directory, but can be set.
 build-dir = ""
 
+# Immediately fail the build. This is only useful in overrides.
+fail = false
+
 ```
 
 <!-- [[[end]]] -->
diff --git a/docs/configuration.md b/docs/configuration.md
index ed758f3..f961e50 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -708,6 +708,10 @@ only be used if you enable them:
 experimental = true
 ```
 
+You can also fail the build with `fail = true`. This is useful with overrides if
+you want to make a specific configuration fail. If this is set, extra
+dependencies like `"cmake"` will not be requested.
+
 ## Overrides
 
 The overrides system allows you to customize for a wide variety of situations.
diff --git a/noxfile.py b/noxfile.py
index 3adc29d..1cbb4cc 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -1,3 +1,9 @@
+"""
+Scikit-build-core's nox configuration.
+
+Use `-t gen` to run all the generation jobs.
+"""
+
 from __future__ import annotations
 
 import argparse
@@ -80,7 +86,7 @@ def _run_tests(
     session.run("pytest", *run_args, *posargs, env=env)
 
 
-@nox.session(reuse_venv=True)
+@nox.session(reuse_venv=True, tags=["gen"])
 def generate_schema(session: nox.Session) -> None:
     """
     (Re)generate src/scikit_build_core/resources/scikit-build.schema.json from model.
@@ -102,7 +108,7 @@ def tests(session: nox.Session) -> None:
     _run_tests(session, extras=["test-meta,test-numpy,test-schema,test-hatchling"])
 
 
-@nox.session(reuse_venv=True)
+@nox.session(reuse_venv=True, tags=["gen"])
 def readme(session: nox.Session) -> None:
     """
     Update the readme with cog. Pass --check to check instead.
@@ -160,7 +166,7 @@ def docs(session: nox.Session) -> None:
         session.run("sphinx-build", "--keep-going", *shared_args)
 
 
-@nox.session
+@nox.session(tags=["gen"])
 def build_api_docs(session: nox.Session) -> None:
     """
     Build (regenerate) API docs.
diff --git a/src/scikit_build_core/_compat/importlib/metadata.py b/src/scikit_build_core/_compat/importlib/metadata.py
index e66c5cf..9e27dc0 100644
--- a/src/scikit_build_core/_compat/importlib/metadata.py
+++ b/src/scikit_build_core/_compat/importlib/metadata.py
@@ -33,6 +33,7 @@ def entry_points(*, group: str) -> EntryPoints:
     if sys.version_info < (3, 8) and hasattr(epg, "select"):
         return epg.select(group=group)  # type: ignore[no-any-return, no-untyped-call]
 
+    # pylint: disable-next=no-member
     return epg.get(group, [])
 
 
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index aed7966..510a64f 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -15,7 +15,7 @@ from packaging.utils import canonicalize_name
 from .. import __version__
 from .._compat import tomllib
 from .._compat.typing import Literal, assert_never
-from .._logging import LEVEL_VALUE, logger, rich_print
+from .._logging import LEVEL_VALUE, logger, rich_error, rich_print
 from .._shutil import fix_win_37_all_permissions
 from ..builder.builder import Builder, archs_to_tags, get_archs
 from ..builder.wheel_tag import WheelTag
@@ -145,6 +145,9 @@ def _build_wheel_impl(
 
     settings_reader.validate_may_exit()
 
+    if settings_reader.settings.fail:
+        rich_error("scikit-build-core's fail setting was enabled. Exiting immediately.")
+
     # Warn if cmake or ninja is in build-system.requires
     requirements = [
         canonicalize_name(Requirement(p).name)
diff --git a/src/scikit_build_core/builder/get_requires.py b/src/scikit_build_core/builder/get_requires.py
index 686048d..4df2766 100644
--- a/src/scikit_build_core/builder/get_requires.py
+++ b/src/scikit_build_core/builder/get_requires.py
@@ -64,7 +64,7 @@ class GetRequires:
         return cls(_load_scikit_build_settings(config_settings))
 
     def cmake(self) -> Generator[str, None, None]:
-        if os.environ.get("CMAKE_EXECUTABLE", ""):
+        if self.settings.fail or os.environ.get("CMAKE_EXECUTABLE", ""):
             return
 
         cmake_verset = self.settings.cmake.version
@@ -83,8 +83,10 @@ class GetRequires:
 
     def ninja(self) -> Generator[str, None, None]:
         # On Windows MSVC, Ninja is not default
-        if sysconfig.get_platform().startswith("win") and "Ninja" not in os.environ.get(
-            "CMAKE_GENERATOR", ""
+        if (
+            self.settings.fail
+            or sysconfig.get_platform().startswith("win")
+            and "Ninja" not in os.environ.get("CMAKE_GENERATOR", "")
         ):
             return
 
@@ -121,6 +123,9 @@ class GetRequires:
         yield f"ninja{ninja_verset}"
 
     def dynamic_metadata(self) -> Generator[str, None, None]:
+        if self.settings.fail:
+            return
+
         for dynamic_metadata in self.settings.metadata.values():
             if "provider" in dynamic_metadata:
                 config = dynamic_metadata.copy()
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index 8255f0c..ead6a26 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -431,6 +431,11 @@
       "default": "",
       "description": "The build directory. Defaults to a temporary directory, but can be set."
     },
+    "fail": {
+      "type": "boolean",
+      "default": false,
+      "description": "Immediately fail the build. This is only useful in overrides."
+    },
     "overrides": {
       "type": "array",
       "description": "A list of overrides to apply to the settings, based on the `if` selector.",
@@ -574,6 +579,9 @@
           },
           "build-dir": {
             "$ref": "#/properties/build-dir"
+          },
+          "fail": {
+            "$ref": "#/properties/fail"
           }
         }
       }
diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py
index 09bdf72..1e2af51 100644
--- a/src/scikit_build_core/settings/skbuild_model.py
+++ b/src/scikit_build_core/settings/skbuild_model.py
@@ -331,3 +331,8 @@ class ScikitBuildSettings:
     """
     The build directory. Defaults to a temporary directory, but can be set.
     """
+
+    fail: bool = False
+    """
+    Immediately fail the build. This is only useful in overrides.
+    """


Test patch:
diff --git a/tests/packages/broken_fallback/pyproject.toml b/tests/packages/broken_fallback/pyproject.toml
index 8ec6220..d8ce465 100644
--- a/tests/packages/broken_fallback/pyproject.toml
+++ b/tests/packages/broken_fallback/pyproject.toml
@@ -12,3 +12,7 @@ wheel.license-files = []
 [[tool.scikit-build.overrides]]
 if.failed = true
 wheel.cmake = false
+
+[[tool.scikit-build.overrides]]
+if.env.FAIL_NOW = true
+fail = true
diff --git a/tests/test_broken_fallback.py b/tests/test_broken_fallback.py
index 12cf11d..15512cd 100644
--- a/tests/test_broken_fallback.py
+++ b/tests/test_broken_fallback.py
@@ -5,6 +5,7 @@ import pytest
 
 from scikit_build_core.build import (
     build_wheel,
+    get_requires_for_build_wheel,
 )
 
 
@@ -34,3 +35,18 @@ def test_broken_code(broken_define: str, capfd: pytest.CaptureFixture[str]):
     else:
         assert "Broken code" in out
         assert "CMake build failed" in out
+
+
+@pytest.mark.usefixtures("broken_fallback")
+def test_fail_setting(
+    monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
+):
+    monkeypatch.setenv("FAIL_NOW", "1")
+
+    assert get_requires_for_build_wheel({}) == []
+    with pytest.raises(SystemExit) as exc:
+        build_wheel("dist")
+
+    assert exc.value.code == 7
+    out, _ = capsys.readouterr()
+    assert "fail setting was enabled" in out
diff --git a/tests/test_skbuild_settings.py b/tests/test_skbuild_settings.py
index da4a758..2da6e9b 100644
--- a/tests/test_skbuild_settings.py
+++ b/tests/test_skbuild_settings.py
@@ -67,6 +67,7 @@ def test_skbuild_settings_default(tmp_path: Path):
     assert settings.install.components == []
     assert settings.install.strip
     assert settings.generate == []
+    assert not settings.fail
 
 
 def test_skbuild_settings_envvar(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
@@ -104,6 +105,7 @@ def test_skbuild_settings_envvar(tmp_path: Path, monkeypatch: pytest.MonkeyPatch
     monkeypatch.setenv("SKBUILD_BUILD_TOOL_ARGS", "a;b")
     monkeypatch.setenv("SKBUILD_INSTALL_COMPONENTS", "a;b;c")
     monkeypatch.setenv("SKBUILD_INSTALL_STRIP", "False")
+    monkeypatch.setenv("SKBUILD_FAIL", "1")
 
     pyproject_toml = tmp_path / "pyproject.toml"
     pyproject_toml.write_text("", encoding="utf-8")
@@ -146,6 +148,7 @@ def test_skbuild_settings_envvar(tmp_path: Path, monkeypatch: pytest.MonkeyPatch
     assert settings.build.tool_args == ["a", "b"]
     assert settings.install.components == ["a", "b", "c"]
     assert not settings.install.strip
+    assert settings.fail
 
 
 @pytest.mark.parametrize("prefix", [True, False], ids=["skbuild", "noprefix"])
@@ -192,6 +195,7 @@ def test_skbuild_settings_config_settings(
         "build.tool-args": ["a", "b"],
         "install.components": ["a", "b", "c"],
         "install.strip": "True",
+        "fail": "1",
     }
 
     if prefix:
@@ -233,6 +237,7 @@ def test_skbuild_settings_config_settings(
     assert settings.build.tool_args == ["a", "b"]
     assert settings.install.components == ["a", "b", "c"]
     assert settings.install.strip
+    assert settings.fail
 
 
 def test_skbuild_settings_pyproject_toml(
@@ -278,6 +283,7 @@ def test_skbuild_settings_pyproject_toml(
             build.tool-args = ["a", "b"]
             install.components = ["a", "b", "c"]
             install.strip = true
+            fail = true
             [[tool.scikit-build.generate]]
             path = "a/b/c"
             template = "hello"
@@ -334,6 +340,7 @@ def test_skbuild_settings_pyproject_toml(
             path=Path("d/e/f"), template_path=Path("g/h/i"), location="build"
         ),
     ]
+    assert settings.fail
 
 
 def test_skbuild_settings_pyproject_toml_broken(
@@ -375,7 +382,7 @@ def test_skbuild_settings_pyproject_toml_broken(
         == """\
       ERROR: Unrecognized options in pyproject.toml:
         tool.scikit-build.cmake.verison -> Did you mean: tool.scikit-build.cmake.version, tool.scikit-build.cmake.verbose, tool.scikit-build.cmake.define?
-        tool.scikit-build.logger -> Did you mean: tool.scikit-build.logging, tool.scikit-build.generate, tool.scikit-build.wheel?
+        tool.scikit-build.logger -> Did you mean: tool.scikit-build.logging, tool.scikit-build.generate, tool.scikit-build.fail?
       """.split()
     )