diff --git a/src/scikit_build_core/_compat/importlib/resources.py b/src/scikit_build_core/_compat/importlib/resources.py index dba901f9..2e5c96d2 100644 --- a/src/scikit_build_core/_compat/importlib/resources.py +++ b/src/scikit_build_core/_compat/importlib/resources.py @@ -7,7 +7,17 @@ else: from importlib.resources import files -__all__ = ["files"] +if sys.version_info < (3, 10): + # Readers were introduced in 3.10 + from importlib_resources import readers +elif sys.version_info < (3, 11): + # In 3.10 readers were implemented in importlib.readers + from importlib import readers +else: + # From 3.11 it is accessed as importlib.resources.readers + from importlib.resources import readers + +__all__ = ["files", "readers"] def __dir__() -> list[str]: diff --git a/src/scikit_build_core/builder/builder.py b/src/scikit_build_core/builder/builder.py index 8492c469..d44cb835 100644 --- a/src/scikit_build_core/builder/builder.py +++ b/src/scikit_build_core/builder/builder.py @@ -23,6 +23,7 @@ if TYPE_CHECKING: from collections.abc import Generator, Iterable, Mapping, Sequence + from typing import Any from packaging.version import Version @@ -84,6 +85,68 @@ def _filter_env_cmake_args(env_cmake_args: list[str]) -> Generator[str, None, No yield arg +# Type-hinting for Traversable is rather hard because they were introduced in python 3.11. +# This avoids introducing importlib_resources dependency that may not be used in the actual package loader +def _sanitize_path(path: Any) -> list[Path] | None: + if isinstance(path, resources.readers.MultiplexedPath): + # pylint: disable-next=protected-access + return path._paths + if isinstance(path, Path): + return [path] + logger.warning("Unknown path type: [{}] {}", type(path), path) + return None + + +def _handle_search_paths( + entry_point: str, + settings_val: list[str] | dict[str, str] | None, + output: list[Path] | dict[str, list[Path]], +) -> None: + # Sanity checks + if isinstance(output, dict): + assert isinstance(settings_val, dict) + + # Get the search paths from the entry points + search_paths_dict = {} + eps = metadata.entry_points(group=entry_point) + if eps: + logger.debug( + "Loading search paths {} from entry-points: {}", entry_point, len(eps) + ) + for ep in eps: + ep_value = _sanitize_path(resources.files(ep.load())) + logger.debug("{}: {} -> {}", ep.name, ep.value, ep_value) + if ep_value: + search_paths_dict[ep.name] = ep_value + + # Update the search paths from the settings options + if isinstance(settings_val, dict) and settings_val: + logger.debug( + "Overriding search paths {} from config: {}", entry_point, settings_val + ) + for key, val in settings_val.items(): + if val: + # TODO: Allow settings_val to be dict[str, list[str]]? + search_paths_dict[key] = [Path(val)] + else: + search_paths_dict.pop(key) + + # Write to the output + if isinstance(output, list): + search_paths_list = [ + path for ep_values in search_paths_dict.values() for path in ep_values + ] + # If the settings options was a list the values are appended as-is + if isinstance(settings_val, list) and settings_val: + logger.debug( + "Appending search paths {} with config: {}", entry_point, settings_val + ) + search_paths_list += map(Path, settings_val) + output.extend(search_paths_list) + return + output.update(search_paths_dict) + + @dataclasses.dataclass class Builder: settings: ScikitBuildSettings @@ -123,47 +186,20 @@ def configure( } # Add any extra CMake modules - module_dirs_dict = { - ep.name: resources.files(ep.load()) - for ep in metadata.entry_points(group="cmake.module") - } - if isinstance(self.settings.search.modules, dict): - # Allow to override any entry-point definition - module_dirs_dict.update(self.settings.search.modules) - module_dirs = list(module_dirs_dict.values()) - if isinstance(self.settings.search.modules, list): - # If it was a list, append to the entry-point definitions - module_dirs += self.settings.search.modules - # Remove any empty paths - module_dirs = [path for path in module_dirs if path] - self.config.module_dirs.extend(module_dirs) + _handle_search_paths( + "cmake.module", self.settings.search.modules, self.config.module_dirs + ) # Add any extra CMake prefixes - prefix_dirs_dict = { - ep.name: resources.files(ep.load()) - for ep in metadata.entry_points(group="cmake.prefix") - } - if isinstance(self.settings.search.prefixes, dict): - # Allow to override any entry-point definition - prefix_dirs_dict.update(self.settings.search.prefixes) - prefix_dirs = list(prefix_dirs_dict.values()) - if isinstance(self.settings.search.prefixes, list): - # If it was a list, append to the entry-point definitions - prefix_dirs += self.settings.search.prefixes - # Remove any empty paths - prefix_dirs = [path for path in prefix_dirs if path] - self.config.prefix_dirs.extend(prefix_dirs) + _handle_search_paths( + "cmake.prefix", self.settings.search.prefixes, self.config.prefix_dirs + ) # Add all CMake roots - prefix_roots = { - ep.name: resources.files(ep.load()) - for ep in metadata.entry_points(group="cmake.root") - } # TODO: Check for unique uppercase names - prefix_roots.update(self.settings.search.roots) - # Remove any empty paths - prefix_roots = {pkg: path for pkg, path in prefix_roots.items() if path} - self.config.prefix_roots.update(prefix_roots) + _handle_search_paths( + "cmake.root", self.settings.search.roots, self.config.prefix_roots + ) # Add site-packages to the prefix path for CMake site_packages = Path(sysconfig.get_path("purelib")) diff --git a/src/scikit_build_core/cmake.py b/src/scikit_build_core/cmake.py index 403e5f12..5a14b52c 100644 --- a/src/scikit_build_core/cmake.py +++ b/src/scikit_build_core/cmake.py @@ -79,7 +79,7 @@ class CMaker: 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, Path] = dataclasses.field(default_factory=dict) + 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") @@ -185,14 +185,14 @@ def init_cache( f.write('set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE "BOTH" CACHE PATH "")\n') if self.prefix_roots: - for pkg, path in self.prefix_roots.items(): - path_str = str(path).replace("\\", "/") + for pkg, path_list in self.prefix_roots.items(): + paths_str = ";".join(map(str, path_list)).replace("\\", "/") f.write( - f'set({pkg}_ROOT [===[{path_str}]===] CACHE PATH "" FORCE)\n' + f'set({pkg}_ROOT [===[{paths_str}]===] CACHE PATH "" FORCE)\n' ) # Available since CMake 3.27 with CMP0144 f.write( - f'set({pkg.upper()}_ROOT [===[{path_str}]===] CACHE PATH "" FORCE)\n' + f'set({pkg.upper()}_ROOT [===[{paths_str}]===] CACHE PATH "" FORCE)\n' ) contents = self.init_cache_file.read_text(encoding="utf-8").strip() diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py index 6b3bbba8..0f8fc39e 100644 --- a/src/scikit_build_core/settings/skbuild_model.py +++ b/src/scikit_build_core/settings/skbuild_model.py @@ -93,19 +93,19 @@ class SearchSettings: Add the wheel build path to the CMake prefix paths. """ - modules: Optional[Union[List[Path], Dict[str, Path]]] = None + modules: Optional[Union[List[str], Dict[str, str]]] = None """ List or dict of CMake module search paths. Dict from is used to override another package's entry-point definition. Populates `CMAKE_MODULE_PATH`. """ - prefixes: Optional[Union[List[Path], Dict[str, Path]]] = None + prefixes: Optional[Union[List[str], Dict[str, str]]] = None """ List or dict of CMake prefix search paths. Dict from is used to override another package's entry-point definition. Populates `CMAKE_PREFIX_PATH`. """ - roots: Dict[str, Path] = dataclasses.field(default_factory=dict) + roots: Dict[str, str] = dataclasses.field(default_factory=dict) """ Dict of package names and prefix paths. Populates `_ROOT`. """