Repository: scikit-build/scikit-build-core
Pull request #675 (2024-03-26 17:23:34)

fix: Use cmake -E capabilities instead of cmake --version
cmake -E capabilities produces a machine-readable version, and is the intended way for programs to get the CMake version.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/675?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 86.30%. Comparing base [(`3594f33`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/3594f33a852ec2ee4deb3ad65613782ee644d7b5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`d30360e`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/675?dropdown=coverage&src=pr&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     #675   +/-   ##
=======================================
  Coverage   86.30%   86.30%           
=======================================
  Files          64       64           
  Lines        3314     3315    +1     
=======================================
+ Hits         2860     2861    +1     
  Misses        454      454           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/675?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/docs/migration_guide.md b/docs/migration_guide.md
index d88b970..e5c8efc 100644
--- a/docs/migration_guide.md
+++ b/docs/migration_guide.md
@@ -25,7 +25,7 @@ may be out of date.
   shows the information that can be placed directly in the project table. For
   additional metadata, see [our configuration guide](./configuration.md). A
   useful trick for performing this migration is to change the `build-backend`
-  from `skbuild` to `setuptools`, install `hatch`, and run `hatch init --new`.
+  from `skbuild` to `setuptools`, install `hatch`, and run `hatch new --init`.
   This should automatically migrate the configuration to pyproject.toml, after
   which you can change the `build-backend` to `scikit-build-core`.
 - If you specify files to include in sdists via MANIFEST.in, with
diff --git a/src/scikit_build_core/program_search.py b/src/scikit_build_core/program_search.py
index 8208b24..99ac485 100644
--- a/src/scikit_build_core/program_search.py
+++ b/src/scikit_build_core/program_search.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import contextlib
+import json
 import shutil
 import subprocess
 from pathlib import Path
@@ -77,14 +78,14 @@ def get_cmake_program(cmake_path: Path) -> Program:
     None if it cannot be determined.
     """
     try:
-        result = Run().capture(cmake_path, "--version")
+        result = Run().capture(cmake_path, "-E", "capabilities")
     except (subprocess.CalledProcessError, PermissionError):
         return Program(cmake_path, None)
 
     try:
-        version = Version(result.stdout.splitlines()[0].split()[-1].split("-")[0])
-    except (IndexError, InvalidVersion):
-        logger.warning(f"Could not determine CMake version, got {result.stdout!r}")
+        version = Version(json.loads(result.stdout)["version"]["string"])
+    except (json.decoder.JSONDecodeError, KeyError, InvalidVersion):
+        logger.warning("Could not determine CMake version, got {!r}", result.stdout)
         return Program(cmake_path, None)
 
     logger.info("CMake version: {}", version)


Test patch:
diff --git a/tests/test_cmake_config.py b/tests/test_cmake_config.py
index 3f8e25f..ddc170a 100644
--- a/tests/test_cmake_config.py
+++ b/tests/test_cmake_config.py
@@ -32,8 +32,14 @@ def configure_args(config: CMaker, *, init: bool = False) -> Generator[str, None
 
 @pytest.mark.configure()
 def test_init_cache(fp, tmp_path):
-    fp.register([fp.program("cmake"), "--version"], stdout="3.14.0")
-    fp.register([fp.program("cmake3"), "--version"], stdout="3.14.0")
+    fp.register(
+        [fp.program("cmake"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
+    fp.register(
+        [fp.program("cmake3"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
 
     config = CMaker(
         CMake.default_search(),
@@ -66,8 +72,14 @@ set(SKBUILD_PATH [===[{source_dir_str}]===] CACHE PATH "" FORCE)
 @pytest.mark.configure()
 def test_too_old(fp, monkeypatch):
     monkeypatch.setattr(shutil, "which", lambda _: None)
-    fp.register([fp.program("cmake"), "--version"], stdout="3.14.0")
-    fp.register([fp.program("cmake3"), "--version"], stdout="3.14.0")
+    fp.register(
+        [fp.program("cmake"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
+    fp.register(
+        [fp.program("cmake3"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
 
     with pytest.raises(CMakeNotFoundError) as excinfo:
         CMake.default_search(version=SpecifierSet(">=3.15"))
@@ -76,8 +88,14 @@ def test_too_old(fp, monkeypatch):
 
 @pytest.mark.configure()
 def test_cmake_args(tmp_path, fp):
-    fp.register([fp.program("cmake"), "--version"], stdout="3.15.0")
-    fp.register([fp.program("cmake3"), "--version"], stdout="3.15.0")
+    fp.register(
+        [fp.program("cmake"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.15.0"}}',
+    )
+    fp.register(
+        [fp.program("cmake3"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.15.0"}}',
+    )
 
     config = CMaker(
         CMake.default_search(),
@@ -99,8 +117,14 @@ def test_cmake_args(tmp_path, fp):
 
 @pytest.mark.configure()
 def test_cmake_paths(tmp_path, fp):
-    fp.register([fp.program("cmake"), "--version"], stdout="3.15.0")
-    fp.register([fp.program("cmake3"), "--version"], stdout="3.15.0")
+    fp.register(
+        [fp.program("cmake"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.15.0"}}',
+    )
+    fp.register(
+        [fp.program("cmake3"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.15.0"}}',
+    )
 
     config = CMaker(
         CMake.default_search(),
@@ -124,7 +148,9 @@ def test_cmake_paths(tmp_path, fp):
 def test_get_cmake_via_envvar(monkeypatch: pytest.MonkeyPatch, fp):
     monkeypatch.setattr("shutil.which", lambda x: x)
     cmake_path = Path("some-prog")
-    fp.register([cmake_path, "--version"], stdout="cmake version 3.20.0\n")
+    fp.register(
+        [cmake_path, "-E", "capabilities"], stdout='{"version":{"string":"3.20.0"}}'
+    )
     monkeypatch.setenv("CMAKE_EXECUTABLE", str(cmake_path))
     result = CMake.default_search(env=os.environ)
     assert result.cmake_path == cmake_path
diff --git a/tests/test_get_requires.py b/tests/test_get_requires.py
index f939b8a..d673c33 100644
--- a/tests/test_get_requires.py
+++ b/tests/test_get_requires.py
@@ -48,19 +48,28 @@ def protect_get_requires(fp, monkeypatch):
 
 
 def test_get_requires_parts(fp):
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(GetRequires().cmake()) == {"cmake>=3.15"}
     assert set(GetRequires().ninja()) == {*ninja}
 
 
 def test_get_requires_parts_uneeded(fp):
-    fp.register([Path("cmake/path"), "--version"], stdout="3.18.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.18.0"}}',
+    )
     assert set(GetRequires().cmake()) == set()
     assert set(GetRequires().ninja()) == {*ninja}
 
 
 def test_get_requires_parts_settings(fp):
-    fp.register([Path("cmake/path"), "--version"], stdout="3.18.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.18.0"}}',
+    )
     config = {"cmake.version": ">=3.20"}
     assert set(GetRequires(config).cmake()) == {"cmake>=3.20"}
     assert set(GetRequires(config).ninja()) == {*ninja}
@@ -74,7 +83,10 @@ def test_get_requires_parts_pyproject(fp, monkeypatch, tmp_path):
         version = ">=3.21"
         """
     )
-    fp.register([Path("cmake/path"), "--version"], stdout="3.18.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.18.0"}}',
+    )
 
     assert set(GetRequires().cmake()) == {"cmake>=3.21"}
     assert set(GetRequires().ninja()) == {*ninja}
@@ -90,42 +102,63 @@ def test_get_requires_parts_pyproject_old(fp, monkeypatch, tmp_path):
         cmake.minimum-version = "3.21"
         """
     )
-    fp.register([Path("cmake/path"), "--version"], stdout="3.18.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.18.0"}}',
+    )
 
     assert set(GetRequires().cmake()) == {"cmake>=3.21"}
     assert set(GetRequires().ninja()) == {*ninja}
 
 
 def test_get_requires_for_build_sdist(fp):
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_sdist({})) == {"pathspec", "pyproject_metadata"}
 
 
 def test_get_requires_for_build_sdist_cmake(fp):
     expected = {"pathspec", "pyproject_metadata", "cmake>=3.15", *ninja}
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_sdist({"sdist.cmake": "True"})) == expected
 
 
 def test_get_requires_for_build_wheel(fp):
     expected = {"pathspec", "pyproject_metadata", "cmake>=3.15", *ninja}
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_wheel({})) == expected
 
 
 def test_get_requires_for_build_wheel_pure(fp):
     expected = {"pathspec", "pyproject_metadata"}
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_wheel({"wheel.cmake": "False"})) == expected
 
 
 def test_get_requires_for_build_editable(fp):
     expected = {"pathspec", "pyproject_metadata", "cmake>=3.15", *ninja}
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_editable({})) == expected
 
 
 def test_get_requires_for_build_editable_pure(fp):
     expected = {"pathspec", "pyproject_metadata"}
-    fp.register([Path("cmake/path"), "--version"], stdout="3.14.0")
+    fp.register(
+        [Path("cmake/path"), "-E", "capabilities"],
+        stdout='{"version":{"string":"3.14.0"}}',
+    )
     assert set(get_requires_for_build_editable({"wheel.cmake": "False"})) == expected
diff --git a/tests/test_prepare_metadata.py b/tests/test_prepare_metadata.py
index 3361c4f..7485703 100644
--- a/tests/test_prepare_metadata.py
+++ b/tests/test_prepare_metadata.py
@@ -17,8 +17,8 @@ from scikit_build_core.build import (
 def test_prepare_metadata_for_build(fp, editable):
     # Old versions of packaging call mac_ver via subprocess
     fp.pass_command([sys.executable, fp.any()])
-    fp.pass_command([fp.program("cmake"), "--version"])
-    fp.pass_command([fp.program("cmake3"), "--version"])
+    fp.pass_command([fp.program("cmake"), "-E", "capabilities"])
+    fp.pass_command([fp.program("cmake3"), "-E", "capabilities"])
 
     if editable:
         assert (
diff --git a/tests/test_program_search.py b/tests/test_program_search.py
index cf091de..319a8e3 100644
--- a/tests/test_program_search.py
+++ b/tests/test_program_search.py
@@ -42,12 +42,12 @@ def test_get_cmake_programs_all(monkeypatch, fp):
     cmake_path = Path("cmake")
     cmake3_path = Path("cmake3")
     fp.register(
-        [cmake_path, "--version"],
-        stdout="cmake version 3.20.0\n\nCMake suite maintained and supported by Kitware (kitware.com/cmake).",
+        [cmake_path, "-E", "capabilities"],
+        stdout='{"version":{"string":"3.20.0"}}',
     )
     fp.register(
-        [cmake3_path, "--version"],
-        stdout="cmake version 3.19.0\n\nCMake suite maintained and supported by Kitware (kitware.com/cmake).",
+        [cmake3_path, "-E", "capabilities"],
+        stdout='{"version":{"string":"3.19.0"}}',
     )
     programs = list(get_cmake_programs(module=False))
     assert len(programs) == 2
@@ -92,13 +92,20 @@ def test_get_cmake_programs_malformed(monkeypatch, fp, caplog):
     monkeypatch.setattr("shutil.which", lambda x: x)
     cmake_path = Path("cmake")
     cmake3_path = Path("cmake3")
-    fp.register([cmake_path, "--version"], stdout="scrambled output\n")
-    fp.register([cmake3_path, "--version"], stdout="cmake version 3.17.3\n")
+    fp.register([cmake_path, "-E", "capabilities"], stdout="scrambled output\n")
+    fp.register([cmake3_path, "-E", "capabilities"], stdout="{}")
     programs = list(get_cmake_programs(module=False))
     assert caplog.records
     assert "Could not determine CMake version" in str(caplog.records[0].msg)
+    assert "Could not determine CMake version" in str(caplog.records[1].msg)
     assert len(programs) == 2
 
+    fp.register([cmake_path, "-E", "capabilities"], stdout="scrambled output\n")
+    fp.register(
+        [cmake3_path, "-E", "capabilities"], stdout='{"version":{"string":"3.17.3"}}'
+    )
+    programs = list(get_cmake_programs(module=False))
+
     best_none = best_program(programs, version=None)
     assert best_none
     assert best_none.path.name == "cmake3"