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

feat: Rework CMake search path settings #880

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,24 @@ messages.after-failure = ""
# A message to print after a successful build.
messages.after-success = ""

# Add the install (or build isolation) site_packages folder to the CMake prefix
# paths.
search.use-site-packages = true

# Entry points to ignore. Any entry-point in `cmake.module`, `cmake.prefix`,
# `cmake.root` with a key value matching a value in this list will be ignored
# when building the search paths.
search.ignore-entry-point = []

# List of additional CMake module search paths. Populates `CMAKE_MODULE_PATH`.
search.modules = []

# List of additional CMake prefix search paths. Populates `CMAKE_PREFIX_PATH`.
search.prefixes = []

# Dict of package names and prefix paths. Populates `<Pkg>_ROOT`.
search.roots = {}

# List dynamic metadata fields and hook locations in this table.
metadata = {}

Expand Down
16 changes: 7 additions & 9 deletions docs/cmakelists.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,13 @@ succeed.

## Finding other packages

Scikit-build-core includes the site-packages directory in CMake's search path,
so packages can provide a find package config with a name matching the package
name - such as the `pybind11` package.

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.
Scikit-build-core includes various pythonic paths to the CMake search paths by
default so that usually you only need to include the dependent project inside
the `build-system.requires` section. Note that `cmake` and `ninja` should not
be included in that section.

See [search paths section](search_paths.md) for more details on how the search
paths are constructed and how to override them.

## Install directories

Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ getting_started
configuration
overrides
cmakelists
search_paths
crosscompile
migration_guide
build
Expand Down
197 changes: 197 additions & 0 deletions docs/search_paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# Search paths

Scikit-build-core populates CMake search paths to take into account any other
CMake project installed in the same environment.

## `<PackageName>_ROOT`

This is the most recommended interface to be used for importing dependent
packages using `find_package`. This variable is populated by the dependent
project's entry-point `cmake.root` or by the current project's `search.roots`
option, the latter having a higher priority.

To configure the `cmake.root` entry-point to export to other projects, you
can use the CMake standard install paths in you `CMakeLists.txt` if you use
`wheel.install-dir` option, e.g.

```{code-block} cmake
:caption: CMakeLists.txt
:emphasize-lines: 14-16

include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
write_basic_package_version_file(
MyProjectConfigVersion.cmake
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
configure_package_config_file(
cmake/MyProjectConfig.cmake.in
MyProjectConfig.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
install(FILES
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfigVersion.cmake
${CMAKE_CURRENT_BINARY_DIR}/MyProjectConfig.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/MyProject
)
```
```{code-block} toml
:caption: pyproject.toml
:emphasize-lines: 2,5

[tool.scikit-build]
wheel.install-dir = "myproject"

[project.entry-points."cmake.root"]
MyProject = "myproject"
```

:::{note}

Scikit-build-core does not currently support dynamic entry-points population.

:::

With this any consuming project that depends on this would automatically work
with `find_package(MyProject)` as long as it is in the `build-system.requires`
list. When consuming a project, you can ignore these entry-points by setting
`search.ignore_entry_point` or expand them with `search.roots`, e.g.

````{tab} pyproject.toml

```toml
[tool.scikit-build.search]
ignore_entry_point = ["MyProject"]
[tool.scikit-build.search.roots]
OtherProject = "/path/to/other_project"
```

````

`````{tab} config-settings


````{tab} pip

```console
$ pip install . -v --config-settings=search.ignore_entry_point="MyProject" --config-settings=search.roots.OtherProject="/path/to/other_project"
```

````

````{tab} build

```console
$ pipx run build --wheel -Csearch.ignore_entry_point="MyProject" -Csearch.roots.OtherProject="/path/to/other_project"
```

````

````{tab} cibuildwheel

```toml
[tool.cibuildwheel.config-settings]
"search.ignore_entry_point" = ["MyProject"]
"search.roots.OtherProject" = "/path/to/other_project"
```

````

`````

````{tab} Environment


```yaml
SKBUILD_SEARCH_IGNORE_ENTRY_POINT: "MyProject"
SKBUILD_SEARCH_ROOTS_OtherProject: "/path/to/other_project"
```

````

## `CMAKE_PREFIX_PATH`

Another common search path that scikit-build-core populates is the
`CMAKE_PREFIX_PATH` which is a common catch-all for all CMake search paths,
e.g. `find_package`, `find_program`, `find_path`. This is populated by default
with the `site-packages` folder where the project will be installed or the build
isolation's `site-packages` folder. This default can be disabled by setting

```toml
[tool.scikit-build.search]
search.use-site-packages = false
```

Additionally, scikit-build-core reads the entry-point `cmake.prefix`, which you
can similarly export this as

``` toml
[project.entry-points."cmake.prefix"]
MyProject = "myproject"
```

and you can similarly alter them with `search.ignore_entry_point` and
`search.prefixes`

````{tab} pyproject.toml

```toml
[tool.scikit-build.search]
ignore_entry_point = ["MyProject"]
prefixes = ["/path/to/prefixA", "/path/to/prefixB"]
```

````

`````{tab} config-settings


````{tab} pip

```console
$ pip install . -v --config-settings=search.ignore_entry_point="MyProject" --config-settings=search.prefixes="/path/to/prefixA;/path/to/prefixB"
```

````

````{tab} build

```console
$ pipx run build --wheel -Csearch.ignore_entry_point="MyProject" -Csearch.prefixes="/path/to/prefixA;/path/to/prefixB"
```

````

````{tab} cibuildwheel

```toml
[tool.cibuildwheel.config-settings]
"search.ignore_entry_point" = ["MyProject"]
"search.prefixes" = ["/path/to/prefixA", "/path/to/prefixB"]
```

````

`````

````{tab} Environment


```yaml
SKBUILD_SEARCH_IGNORE_ENTRY_POINT: "MyProject"
SKBUILD_SEARCH_PREFIXES: "/path/to/prefixA;/path/to/prefixB"
```

````

## `CMAKE_MODULE_PATH`

Scikit-build-core also populates `CMAKE_MODULE_PATH` variable used to search
for CMake modules using the `include()` command (if the `.cmake` suffix is
omitted).

This variable is populated from the entry-point `cmake.module` and the option
`search.modules` similar to [`CMAKE_PREFIX_PATH`] section.

[`CMAKE_PREFIX_PATH`]: #cmake-prefix-path
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link does indeed work, but the CI is reporting as not working 🤷

81 changes: 69 additions & 12 deletions src/scikit_build_core/builder/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,32 @@
return [Path(os.fspath(path))]


def _merge_search_paths(
entry_point_search_path: dict[str, list[Path]],
settings_val: list[str] | dict[str, str],
output: list[Path] | dict[str, list[Path]],
) -> None:
if isinstance(settings_val, dict):
# if the settings and output search paths are dicts, just override them
# and update the output
assert isinstance(output, dict)
# No need to clone this because the search paths are not used anywhere else.
# Just renaming for readability
search_paths_dict = entry_point_search_path
for key, val in settings_val.items():
search_paths_dict[key] = [Path(val)]

Check warning on line 113 in src/scikit_build_core/builder/builder.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/builder/builder.py#L113

Added line #L113 was not covered by tests
output.update(search_paths_dict)
return
# Otherwise the settings and outputs are lists.
# We flatten out the dict into a list and append the settings values.
assert isinstance(output, list)
search_paths_list = [
path for ep_values in entry_point_search_path.values() for path in ep_values
]
search_paths_list += map(Path, settings_val)
output.extend(search_paths_list)


@dataclasses.dataclass
class Builder:
settings: ScikitBuildSettings
Expand All @@ -120,6 +146,23 @@
def get_generator(self, *args: str) -> str | None:
return self.config.get_generator(*self.get_cmake_args(), *args)

def _get_entry_point_search_path(self, entry_point: str) -> dict[str, list[Path]]:
"""Get the search path dict from the entry points"""
search_paths = {}
eps = metadata.entry_points(group=entry_point)
if eps:
logger.debug(

Check warning on line 154 in src/scikit_build_core/builder/builder.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/builder/builder.py#L154

Added line #L154 was not covered by tests
"Loading search paths {} from entry-points: {}", entry_point, len(eps)
)
for ep in eps:
if ep.name in self.settings.search.ignore_entry_point:
logger.debug("Ignoring entry-point: {}", ep.name)
ep_value = _sanitize_path(resources.files(ep.load()))
logger.debug("{}: {} -> {}", ep.name, ep.value, ep_value)
if ep_value:
search_paths[ep.name] = ep_value

Check warning on line 163 in src/scikit_build_core/builder/builder.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/builder/builder.py#L157-L163

Added lines #L157 - L163 were not covered by tests
return search_paths

def configure(
self,
*,
Expand All @@ -136,25 +179,39 @@
}

# Add any extra CMake modules
eps = metadata.entry_points(group="cmake.module")
self.config.module_dirs.extend(
p for ep in eps for p in _sanitize_path(resources.files(ep.load()))
_merge_search_paths(
self._get_entry_point_search_path("cmake.module"),
self.settings.search.modules,
self.config.module_dirs,
)
logger.debug("cmake.modules: {}", self.config.module_dirs)

# Add any extra CMake prefixes
eps = metadata.entry_points(group="cmake.prefix")
self.config.prefix_dirs.extend(
p for ep in eps for p in _sanitize_path(resources.files(ep.load()))
_merge_search_paths(
self._get_entry_point_search_path("cmake.prefix"),
self.settings.search.prefixes,
self.config.prefix_dirs,
)
logger.debug("cmake.prefix: {}", self.config.prefix_dirs)

# Add all CMake roots
# TODO: Check for unique uppercase names
_merge_search_paths(
self._get_entry_point_search_path("cmake.root"),
self.settings.search.roots,
self.config.prefix_roots,
)
logger.debug("cmake.root: {}", self.config.prefix_roots)

# Add site-packages to the prefix path for CMake
site_packages = Path(sysconfig.get_path("purelib"))
self.config.prefix_dirs.append(site_packages)
logger.debug("SITE_PACKAGES: {}", site_packages)
if site_packages != DIR.parent.parent:
self.config.prefix_dirs.append(DIR.parent.parent)
logger.debug("Extra SITE_PACKAGES: {}", DIR.parent.parent)
logger.debug("PATH: {}", sys.path)
if self.settings.search.use_site_packages:
self.config.prefix_dirs.append(site_packages)
logger.debug("SITE_PACKAGES: {}", site_packages)
if site_packages != DIR.parent.parent:
self.config.prefix_dirs.append(DIR.parent.parent)
logger.debug("Extra SITE_PACKAGES: {}", DIR.parent.parent)
logger.debug("PATH: {}", sys.path)

# Add the FindPython backport if needed
if self.config.cmake.version < self.settings.backport.find_python:
Expand Down
12 changes: 12 additions & 0 deletions src/scikit_build_core/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
build_type: str
module_dirs: list[Path] = dataclasses.field(default_factory=list)
prefix_dirs: list[Path] = dataclasses.field(default_factory=list)
prefix_roots: dict[str, list[Path]] = dataclasses.field(default_factory=dict)
init_cache_file: Path = dataclasses.field(init=False, default=Path())
env: dict[str, str] = dataclasses.field(init=False, default_factory=os.environ.copy)
single_config: bool = not sysconfig.get_platform().startswith("win")
Expand Down Expand Up @@ -183,6 +184,17 @@
)
f.write('set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE "BOTH" CACHE PATH "")\n')

if self.prefix_roots:
for pkg, path_list in self.prefix_roots.items():
paths_str = ";".join(map(str, path_list)).replace("\\", "/")
f.write(

Check warning on line 190 in src/scikit_build_core/cmake.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/cmake.py#L188-L190

Added lines #L188 - L190 were not covered by tests
f'set({pkg}_ROOT [===[{paths_str}]===] CACHE PATH "" FORCE)\n'
)
# Available since CMake 3.27 with CMP0144
f.write(

Check warning on line 194 in src/scikit_build_core/cmake.py

View check run for this annotation

Codecov / codecov/patch

src/scikit_build_core/cmake.py#L194

Added line #L194 was not covered by tests
f'set({pkg.upper()}_ROOT [===[{paths_str}]===] CACHE PATH "" FORCE)\n'
)

contents = self.init_cache_file.read_text(encoding="utf-8").strip()
logger.debug(
"{}:\n{}",
Expand Down
Loading
Loading