Repository: scikit-build/scikit-build-core
Pull request #255 (2023-04-03 22:39:10)

feat: support modules/prefix dirs
Adding support for both prefix & module entry-points.

codecov[bot]: ## [Codecov](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
> Merging [#255](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) (838dc6c) into [main](https://codecov.io/gh/scikit-build/scikit-build-core/commit/e2a1479d8392938fbcadb28f824eba0934e9b2da?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) (e2a1479) will **increase** coverage by `0.20%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##             main     #255      +/-   ##
==========================================
+ Coverage   88.10%   88.30%   +0.20%     
==========================================
  Files          51       53       +2     
  Lines        2236     2258      +22     
==========================================
+ Hits         1970     1994      +24     
+ Misses        266      264       -2     
```


| [Impacted Files](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) | Coverage Δ | |
|---|---|---|
| [...rc/scikit\_build\_core/\_compat/importlib/\_\_init\_\_.py](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL19jb21wYXQvaW1wb3J0bGliL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [...rc/scikit\_build\_core/\_compat/importlib/metadata.py](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL19jb21wYXQvaW1wb3J0bGliL21ldGFkYXRhLnB5) | `100.00% <100.00%> (ø)` | |
| [...c/scikit\_build\_core/\_compat/importlib/resources.py](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL19jb21wYXQvaW1wb3J0bGliL3Jlc291cmNlcy5weQ==) | `100.00% <100.00%> (ø)` | |
| [src/scikit\_build\_core/builder/builder.py](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL2J1aWxkZXIvYnVpbGRlci5weQ==) | `91.08% <100.00%> (+0.46%)` | :arrow_up: |

... and [1 file with indirect coverage changes](https://codecov.io/gh/scikit-build/scikit-build-core/pull/255/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build)

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/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 d798b19..07d8ff4 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,9 @@ Features over classic Scikit-build:
 - SDists are reproducible by default (UNIX, Python 3.9+)
 - Support for caching between builds (opt-in by setting `build-dir`)
 - Support for writing out to extra wheel folders (scripts, headers, data)
-- [WIP] Supports a couple of dynamic metadata plugins (proposing wider support)
+- Dedicated entrypoints for module and prefix directories
+- Several integrated dynamic metadata plugins (proposing standardized support
+  soon)
 
 The following limitations are present compared to classic scikit-build:
 
@@ -43,7 +45,6 @@ The following limitations are present compared to classic scikit-build:
 Some known missing features that will be developed soon:
 
 - No editable mode support
-- Dedicated entrypoints are planned for projects wanting to support discovery
 - No support for other targets besides install
 - Wheels are not fully reproducible yet
 
@@ -53,7 +54,7 @@ Other backends are also planned:
 - The extensionlib integration is missing
 - No hatchling plugin yet
 
-The recommended interface is the PEP 517 interface. There is also a WIP
+The recommended interface is the native pyproject builder. There is also a WIP
 setuptools-based interface that is being developed to provide a transition path
 for classic scikit-build.
 
diff --git a/docs/cmakelists.md b/docs/cmakelists.md
index 737ff3a..7b24d90 100644
--- a/docs/cmakelists.md
+++ b/docs/cmakelists.md
@@ -32,6 +32,12 @@ so packages can provide a find package config with a name matching the package
 name - such as the `pybind11` package. Later versions of scikit-build core will
 include a design for package to provide arbitrary CMake code.
 
+Third party packages can declare entry-points `cmake.module` and `cmake.prefix`,
+and the specified module will be added to `CMAKE_PREFIX_PATH` and
+`CMAKE_MODULE_PATH`, respectively. Currently, the key is not used, but
+eventually there might be a way to request or exclude certain entry-points by
+key.
+
 ## Install directories
 
 Scikit-build-core will install directly into platlib, which will end up in
@@ -51,9 +57,7 @@ configuration, with the variables:
 - `${SKBUILD_HEADERS_DIR}`: The header directory. Anything in here gets
   installed to Python's header directory.
 - `${SKBUILD_SCRIPTS_DIR}`: The scripts directory. Anything placed in here will
-  go to `bin` (Unix) or `Scripts` (Windows). Use a `#!python` shebang line at
-  the top of Python scripts (scikit-build-core does not rewrite traditional
-  `#!/usr/bin/env python` shabangs yet).
+  go to `bin` (Unix) or `Scripts` (Windows).
 - `${SKBUILD_NULL_DIR}`: Anything installed here will not be placed in the
   wheel.
 
diff --git a/pyproject.toml b/pyproject.toml
index 09519ad..aaa2844 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,6 +34,7 @@ dynamic = ["version"]
 dependencies = [
     "exceptiongroup; python_version<'3.11'",
     "importlib_resources >=1.3; python_version<'3.9'",
+    "importlib_metadata; python_version<'3.8'",
     "packaging >=20.9",
     "tomli >=1.1; python_version<'3.11'",
     "typing_extensions >=3.10.0; python_version<'3.8'",
diff --git a/src/scikit_build_core/_compat/importlib/__init__.py b/src/scikit_build_core/_compat/importlib/__init__.py
new file mode 100644
index 0000000..bdec2fc
--- /dev/null
+++ b/src/scikit_build_core/_compat/importlib/__init__.py
@@ -0,0 +1,3 @@
+from __future__ import annotations
+
+__all__: list[str] = []
diff --git a/src/scikit_build_core/_compat/importlib/metadata.py b/src/scikit_build_core/_compat/importlib/metadata.py
new file mode 100644
index 0000000..5568996
--- /dev/null
+++ b/src/scikit_build_core/_compat/importlib/metadata.py
@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+import sys
+
+if sys.version_info < (3, 8):
+    import importlib_metadata as _metadata
+else:
+    import importlib.metadata as _metadata
+
+__all__ = ["entry_points"]
+
+
+def entry_points(*, group: str) -> _metadata.EntryPoints:
+    if sys.version_info >= (3, 10):
+        return _metadata.entry_points(group=group)
+
+    epg = _metadata.entry_points()
+
+    if sys.version_info < (3, 8) and hasattr(epg, "select"):
+        return epg.select(group=group)  # type: ignore[no-any-return, no-untyped-call]
+
+    return epg.get(group, [])  # type: ignore[no-any-return, attr-defined]
+
+
+def __dir__() -> list[str]:
+    return __all__
diff --git a/src/scikit_build_core/_compat/importlib.py b/src/scikit_build_core/_compat/importlib/resources.py
similarity index 56%
rename from src/scikit_build_core/_compat/importlib.py
rename to src/scikit_build_core/_compat/importlib/resources.py
index b3487d8..dba901f 100644
--- a/src/scikit_build_core/_compat/importlib.py
+++ b/src/scikit_build_core/_compat/importlib/resources.py
@@ -3,11 +3,11 @@ from __future__ import annotations
 import sys
 
 if sys.version_info < (3, 9):
-    import importlib_resources as resources
+    from importlib_resources import files
 else:
-    from importlib import resources
+    from importlib.resources import files
 
-__all__ = ["resources"]
+__all__ = ["files"]
 
 
 def __dir__() -> list[str]:
diff --git a/src/scikit_build_core/builder/builder.py b/src/scikit_build_core/builder/builder.py
index d1b2c12..a3340d8 100644
--- a/src/scikit_build_core/builder/builder.py
+++ b/src/scikit_build_core/builder/builder.py
@@ -11,6 +11,7 @@ from typing import Mapping
 from packaging.version import Version
 
 from .. import __version__
+from .._compat.importlib import metadata, resources
 from .._logging import logger
 from ..cmake import CMaker
 from ..resources import find_python
@@ -86,6 +87,14 @@ class Builder:
     ) -> None:
         cmake_defines = dict(defines)
 
+        # Add any extra CMake modules
+        eps = metadata.entry_points(group="cmake.module")
+        self.config.module_dirs.extend(resources.files(ep.load()) for ep in eps)
+
+        # Add any extra CMake prefixes
+        eps = metadata.entry_points(group="cmake.prefix")
+        self.config.prefix_dirs.extend(resources.files(ep.load()) for ep in eps)
+
         # Add site-packages to the prefix path for CMake
         site_packages = Path(sysconfig.get_path("purelib"))
         self.config.prefix_dirs.append(site_packages)


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 79402e1..8d1b221 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -43,6 +43,7 @@ def pep518_wheelhouse(tmp_path_factory: pytest.TempPathFactory) -> Path:
     )
     packages = [
         "build",
+        "hatchling",
         "pybind11",
         "rich",
         "setuptools",
@@ -151,8 +152,9 @@ class VEnv(EnvBuilder):
     def module(self, *args: str) -> None:
         return self.run(str(self.executable), "-m", *args)
 
-    def install(self, *args: str) -> None:
-        self.module("pip", "install", *args)
+    def install(self, *args: str, isolated: bool = True) -> None:
+        isolated_flags = "" if isolated else ["--no-build-isolation"]
+        self.module("pip", "install", *isolated_flags, *args)
 
 
 @pytest.fixture()
diff --git a/tests/packages/custom_cmake/CMakeLists.txt b/tests/packages/custom_cmake/CMakeLists.txt
new file mode 100644
index 0000000..3cfea57
--- /dev/null
+++ b/tests/packages/custom_cmake/CMakeLists.txt
@@ -0,0 +1,17 @@
+cmake_minimum_required(VERSION 3.15...3.26)
+
+project(
+  ${SKBUILD_PROJECT_NAME}
+  LANGUAGES
+  VERSION ${SKBUILD_PROJECT_VERSION})
+
+find_package(ExamplePkg REQUIRED)
+
+include(ExampleInclude)
+
+if(NOT EXAMPLE_INCLUDE_FOUND)
+  message(FATAL_ERROR "ExampleInclude not found")
+endif()
+
+# Testing scripts
+install(FILES scripts/script1 DESTINATION "${SKBUILD_SCRIPTS_DIR}")
diff --git a/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/__init__.py b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_modules/ExampleInclude.cmake b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_modules/ExampleInclude.cmake
new file mode 100644
index 0000000..915f684
--- /dev/null
+++ b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_modules/ExampleInclude.cmake
@@ -0,0 +1,1 @@
+set(EXAMPLE_INCLUDE_FOUND "YES")
diff --git a/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_modules/__init__.py b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_modules/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_prefix/ExamplePkgConfig.cmake b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_prefix/ExamplePkgConfig.cmake
new file mode 100644
index 0000000..107972a
--- /dev/null
+++ b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_prefix/ExamplePkgConfig.cmake
@@ -0,0 +1,3 @@
+set(ExamplePkg_FOUND
+    TRUE
+    CACHE BOOL "ExamplePkg found" FORCE)
diff --git a/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_prefix/__init__.py b/tests/packages/custom_cmake/extern/custom_cmake_testing_stuff/cmake_prefix/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/packages/custom_cmake/extern/pyproject.toml b/tests/packages/custom_cmake/extern/pyproject.toml
new file mode 100644
index 0000000..1a81af7
--- /dev/null
+++ b/tests/packages/custom_cmake/extern/pyproject.toml
@@ -0,0 +1,13 @@
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "custom_cmake_testing_stuff"
+version = "0.1.0"
+
+[project.entry-points."cmake.module"]
+any = "custom_cmake_testing_stuff.cmake_modules"
+
+[project.entry-points."cmake.prefix"]
+any = "custom_cmake_testing_stuff.cmake_prefix"
diff --git a/tests/packages/custom_cmake/pyproject.toml b/tests/packages/custom_cmake/pyproject.toml
new file mode 100644
index 0000000..c72e803
--- /dev/null
+++ b/tests/packages/custom_cmake/pyproject.toml
@@ -0,0 +1,7 @@
+[build-system]
+requires = ["scikit-build-core"]
+build-backend = "scikit_build_core.build"
+
+[project]
+name = "custom_modules"
+version = "0.0.1"
diff --git a/tests/packages/custom_cmake/scripts/script1 b/tests/packages/custom_cmake/scripts/script1
new file mode 100755
index 0000000..0d83742
--- /dev/null
+++ b/tests/packages/custom_cmake/scripts/script1
@@ -0,0 +1,5 @@
+#/usr/bin/env python3
+
+import sys
+
+print(f"Ran script 1 from {sys.executable}")
diff --git a/tests/test_custom_modules.py b/tests/test_custom_modules.py
new file mode 100644
index 0000000..3d9267b
--- /dev/null
+++ b/tests/test_custom_modules.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+from pathlib import Path
+
+DIR = Path(__file__).parent
+PROJECT_DIR = DIR / "packages" / "custom_cmake"
+
+
+def test_ep(isolated):
+    isolated.install("hatchling", "scikit-build-core[pyproject]")
+    isolated.install(PROJECT_DIR / "extern", isolated=False)
+    isolated.install(PROJECT_DIR, isolated=False)
+    # Needs script fix: assert isolated.run("script1") == ""