Repository: scikit-build/scikit-build-core
Pull request #430 (2023-07-16 04:19:15)

feat: add component support
This adds support for components & stripping. Stripping only applied by default if scikit-build-core version > 0.5.



Patch:
diff --git a/README.md b/README.md
index a8ca49b..9175854 100644
--- a/README.md
+++ b/README.md
@@ -109,11 +109,11 @@ More examples are in the
 
 ## Configuration
 
-All configuration options can be placed in `pyproject.toml`, passed via `-C` or
-`--config-setting` in build or `--config-settings` in `pip` (warning: pip
-doesn't support list options), or set as environment variables.
-`tool.scikit-build` is used in toml, `skbuild.` for `-C` options, or `SKBUILD_*`
-for environment variables. The defaults are listed below:
+All configuration options can be placed in `pyproject.toml`, passed via
+`-C`/`--config-setting` in build or `-C`/`--config-settings` in `pip` , or set
+as environment variables. `tool.scikit-build` is used in toml, `skbuild.` for
+`-C` options, or `SKBUILD_*` for environment variables. The defaults are listed
+below:
 
 ```toml
 [tool.scikit-build]
@@ -171,6 +171,12 @@ wheel.install-dir = "."
 # The licence file(s) to include in the wheel metadata directory.
 wheel.license-files = ["LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*"]
 
+# Strip the binaries. Defaults to True for scikit-build-core 0.5+.
+install.strip = true
+
+# Components to install. Leave empty to install all components.
+install.components = []
+
 # This will backport an internal copy of FindPython if CMake is less than this
 # value. Set to 0 or the empty string to disable. The default will be kept in
 # sync with the version of FindPython stored in scikit-build-core.
diff --git a/docs/conf.py b/docs/conf.py
index d15a85d..cebf2f9 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -6,6 +6,9 @@
 
 from __future__ import annotations
 
+import importlib
+import inspect
+import os
 import sys
 import warnings
 from pathlib import Path
@@ -15,6 +18,11 @@ if sys.version_info < (3, 8):
 else:
     import importlib.metadata as importlib_metadata
 
+try:
+    import scikit_build_core
+except ModuleNotFoundError:
+    scikit_build_core = None
+
 ROOT = Path(__file__).parent.parent.resolve()
 
 
@@ -52,11 +60,12 @@ author = "Henry Schreiner"
 extensions = [
     "myst_parser",
     "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.linkcode",
     "sphinx.ext.mathjax",
     "sphinx.ext.napoleon",
     "sphinx_copybutton",
     "sphinx_inline_tabs",
-    "sphinx.ext.intersphinx",
 ]
 
 # Add any paths that contain templates here, relative to this directory.
@@ -128,3 +137,43 @@ man_pages = [
         7,
     )
 ]
+
+
+commit = os.environ.get("READTHEDOCS_GIT_COMMIT_HASH", "main")
+code_url = "https://github.com/scikit-build/scikit-build-core/blob"
+
+
+def linkcode_resolve(domain: str, info: dict[str, str]) -> str | None:
+    if scikit_build_core is None:
+        return None
+
+    if domain != "py":
+        return None
+
+    mod = importlib.import_module(info["module"])
+    if "." in info["fullname"]:
+        objname, attrname = info["fullname"].split(".")
+        obj = getattr(mod, objname)
+        try:
+            # object is a method of a class
+            obj = getattr(obj, attrname)
+        except AttributeError:
+            # object is an attribute of a class
+            return None
+    else:
+        obj = getattr(mod, info["fullname"])
+
+    try:
+        file = Path(inspect.getsourcefile(obj))
+        lines = inspect.getsourcelines(obj)
+    except TypeError:
+        # e.g. object is a typing.Union
+        return None
+    try:
+        mod = Path(inspect.getsourcefile(scikit_build_core)).parent
+        file = file.relative_to(mod)
+    except ValueError:
+        return None
+    start, end = lines[1], lines[1] + len(lines[0]) - 1
+
+    return f"{code_url}/{commit}/src/scikit_build_core/{file}#L{start}-L{end}"
diff --git a/docs/configuration.md b/docs/configuration.md
index 882cb29..43e0766 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -227,6 +227,20 @@ historic compatibility if you'd like:
 wheel.expand-macos-universal-tags = false
 ```
 
+You can select only specific components to install:
+
+```toml
+[tool.scikit-build]
+install.components = ["python"]
+```
+
+And you can turn off binary stripping:
+
+```toml
+[tool.scikit-build]
+install.strip = false
+```
+
 ## Configuring CMake arguments and defines
 
 You can select a different build type, such as `Debug`:
diff --git a/src/scikit_build_core/builder/builder.py b/src/scikit_build_core/builder/builder.py
index 73f4fc6..e4aff95 100644
--- a/src/scikit_build_core/builder/builder.py
+++ b/src/scikit_build_core/builder/builder.py
@@ -191,4 +191,7 @@ class Builder:
         self.config.build(build_args=build_args, verbose=self.settings.cmake.verbose)
 
     def install(self, install_dir: Path) -> None:
-        self.config.install(install_dir)
+        components = self.settings.install.components
+        strip = self.settings.install.strip
+        assert strip is not None
+        self.config.install(install_dir, strip=strip, components=components)
diff --git a/src/scikit_build_core/cmake.py b/src/scikit_build_core/cmake.py
index fed2ba9..95309c3 100644
--- a/src/scikit_build_core/cmake.py
+++ b/src/scikit_build_core/cmake.py
@@ -220,17 +220,30 @@ class CMaker:
             msg = "CMake build failed"
             raise FailedLiveProcessError(msg) from None
 
-    def install(self, prefix: Path) -> None:
-        opts: list[str] = []
+    def install(
+        self, prefix: Path, *, strip: bool = False, components: Sequence[str] = ()
+    ) -> None:
+        opts: list[str] = ["--prefix", str(prefix)]
         if not self.single_config and self.build_type:
             opts += ["--config", self.build_type]
+        if strip:
+            opts.append("--strip")
+
+        if not components:
+            self._install(opts)
+            return
+
+        for comp in components:
+            opts_with_comp = [*opts, "--component", comp]
+            logger.info("Installing component {}", comp)
+            self._install(opts_with_comp)
+
+    def _install(self, opts: Sequence[str]) -> None:
         try:
             Run(env=self.env).live(
                 self.cmake,
                 "--install",
                 self.build_dir,
-                "--prefix",
-                prefix,
                 *opts,
             )
         except subprocess.CalledProcessError:
diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py
index ea87eb2..074231f 100644
--- a/src/scikit_build_core/settings/skbuild_model.py
+++ b/src/scikit_build_core/settings/skbuild_model.py
@@ -5,6 +5,7 @@ __all__ = [
     "BackportSettings",
     "CMakeSettings",
     "EditableSettings",
+    "InstallSettings",
     "LoggingSettings",
     "NinjaSettings",
     "SDistSettings",
@@ -125,6 +126,15 @@ class EditableSettings:
     rebuild: bool = False
 
 
+@dataclasses.dataclass
+class InstallSettings:
+    #: The components to install. If empty, the default is used.
+    components: List[str] = dataclasses.field(default_factory=list)
+
+    #: Whether to strip the binaries. True for scikit-build-core 0.5+.
+    strip: Optional[bool] = None
+
+
 @dataclasses.dataclass
 class ScikitBuildSettings:
     cmake: CMakeSettings = dataclasses.field(default_factory=CMakeSettings)
@@ -135,6 +145,7 @@ class ScikitBuildSettings:
     backport: BackportSettings = dataclasses.field(default_factory=BackportSettings)
     metadata: Dict[str, Dict[str, Any]] = dataclasses.field(default_factory=dict)
     editable: EditableSettings = dataclasses.field(default_factory=EditableSettings)
+    install: InstallSettings = dataclasses.field(default_factory=InstallSettings)
 
     #: Strictly check all config options. If False, warnings will be
     #: printed for unknown options. If True, an error will be raised.
diff --git a/src/scikit_build_core/settings/skbuild_read_settings.py b/src/scikit_build_core/settings/skbuild_read_settings.py
index d17fa93..d23cd29 100644
--- a/src/scikit_build_core/settings/skbuild_read_settings.py
+++ b/src/scikit_build_core/settings/skbuild_read_settings.py
@@ -61,6 +61,12 @@ class SettingsReader:
             )
             raise SystemExit(7)
 
+        install_policy = self.settings.minimum_version is None or Version(
+            self.settings.minimum_version
+        ) >= Version("0.5")
+        if self.settings.install.strip is None:
+            self.settings.install.strip = install_policy
+
     def unrecognized_options(self) -> Generator[str, None, None]:
         return self.sources.unrecognized_options(ScikitBuildSettings)
 


Test patch:
diff --git a/tests/packages/simplest_c/CMakeLists.txt b/tests/packages/simplest_c/CMakeLists.txt
index 2be0db9..9905b7b 100644
--- a/tests/packages/simplest_c/CMakeLists.txt
+++ b/tests/packages/simplest_c/CMakeLists.txt
@@ -9,12 +9,17 @@ find_package(Python COMPONENTS Interpreter Development.Module)
 
 python_add_library(_module MODULE src/module.c WITH_SOABI)
 
-install(TARGETS _module DESTINATION ${SKBUILD_PROJECT_NAME})
+install(
+  TARGETS _module
+  DESTINATION ${SKBUILD_PROJECT_NAME}
+  COMPONENT PythonModule)
 
 # Testing artifacts
 file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated.txt "Testing")
-install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated.txt
-        DESTINATION ${SKBUILD_PROJECT_NAME})
+install(
+  FILES ${CMAKE_CURRENT_BINARY_DIR}/generated.txt
+  DESTINATION ${SKBUILD_PROJECT_NAME}
+  COMPONENT Generated)
 
 file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated_ignored.txt "Testing")
 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated_ignored.txt
diff --git a/tests/test_simplest_c.py b/tests/test_simplest_c.py
index c276692..887fd60 100644
--- a/tests/test_simplest_c.py
+++ b/tests/test_simplest_c.py
@@ -46,13 +46,16 @@ def test_pep517_sdist(tmp_path, monkeypatch):
 
 @pytest.mark.compile()
 @pytest.mark.configure()
-def test_pep517_wheel(tmp_path, monkeypatch, virtualenv):
+@pytest.mark.parametrize(
+    "component", [[], ["PythonModule"], ["PythonModule", "Generated"]]
+)
+def test_pep517_wheel(tmp_path, monkeypatch, virtualenv, component):
     dist = tmp_path / "dist"
     dist.mkdir()
     monkeypatch.chdir(SIMPLEST)
     if Path("dist").is_dir():
         shutil.rmtree("dist")
-    out = build_wheel(str(dist))
+    out = build_wheel(str(dist), config_settings={"install.components": component})
     (wheel,) = dist.glob("simplest-0.0.1-*.whl")
     assert wheel == dist / out
 
@@ -65,17 +68,27 @@ def test_pep517_wheel(tmp_path, monkeypatch, virtualenv):
             simplest_pkg = {x.name for x in p.joinpath("simplest").iterdir()}
 
         filtered_pkg = {x for x in simplest_pkg if not x.startswith("_module")}
+        if not component or "PythonModule" in component:
+            assert filtered_pkg != simplest_pkg
+        else:
+            assert filtered_pkg == simplest_pkg
 
-        assert len(filtered_pkg) == len(simplest_pkg) - 2
-        assert {"simplest-0.0.1.dist-info", "simplest"} == file_names
-        assert {
+        expected_wheel_files = {
             "__init__.py",
             "data.txt",
             "excluded.txt",
-            "generated.txt",
             "sdist_only.txt",
-            "generated_ignored.txt",
-        } == filtered_pkg
+        }
+
+        if not component:
+            expected_wheel_files.add("generated_ignored.txt")
+
+        if not component or "Generated" in component:
+            expected_wheel_files.add("generated.txt")
+
+        assert len(filtered_pkg) == len(simplest_pkg) - 2
+        assert {"simplest-0.0.1.dist-info", "simplest"} == file_names
+        assert expected_wheel_files == filtered_pkg
         # Note that generated_ignored.txt is here because all CMake installed files are
         # present, CMake has the final say.
 
diff --git a/tests/test_skbuild_settings.py b/tests/test_skbuild_settings.py
index aefc4cd..21eeefd 100644
--- a/tests/test_skbuild_settings.py
+++ b/tests/test_skbuild_settings.py
@@ -52,6 +52,8 @@ def test_skbuild_settings_default(tmp_path):
     assert settings.editable.mode == "redirect"
     assert not settings.editable.rebuild
     assert settings.editable.verbose
+    assert settings.install.components == []
+    assert settings.install.strip
 
 
 def test_skbuild_settings_envvar(tmp_path, monkeypatch):
@@ -82,6 +84,8 @@ def test_skbuild_settings_envvar(tmp_path, monkeypatch):
     monkeypatch.setenv("SKBUILD_BUILD_DIR", "a/b/c")
     monkeypatch.setenv("SKBUILD_EDITABLE_REBUILD", "True")
     monkeypatch.setenv("SKBUILD_EDITABLE_VERBOSE", "False")
+    monkeypatch.setenv("SKBUILD_INSTALL_COMPONENTS", "a;b;c")
+    monkeypatch.setenv("SKBUILD_INSTALL_STRIP", "False")
 
     pyproject_toml = tmp_path / "pyproject.toml"
     pyproject_toml.write_text("", encoding="utf-8")
@@ -117,6 +121,8 @@ def test_skbuild_settings_envvar(tmp_path, monkeypatch):
     assert settings.editable.mode == "redirect"
     assert settings.editable.rebuild
     assert not settings.editable.verbose
+    assert settings.install.components == ["a", "b", "c"]
+    assert not settings.install.strip
 
 
 def test_skbuild_settings_config_settings(tmp_path, monkeypatch):
@@ -153,6 +159,8 @@ def test_skbuild_settings_config_settings(tmp_path, monkeypatch):
         "editable.mode": "redirect",
         "editable.rebuild": "True",
         "editable.verbose": "False",
+        "install.components": ["a", "b", "c"],
+        "install.strip": "True",
     }
 
     settings_reader = SettingsReader.from_file(pyproject_toml, config_settings)
@@ -184,6 +192,8 @@ def test_skbuild_settings_config_settings(tmp_path, monkeypatch):
     assert settings.editable.mode == "redirect"
     assert settings.editable.rebuild
     assert not settings.editable.verbose
+    assert settings.install.components == ["a", "b", "c"]
+    assert settings.install.strip
 
 
 def test_skbuild_settings_pyproject_toml(tmp_path, monkeypatch):
@@ -220,6 +230,8 @@ def test_skbuild_settings_pyproject_toml(tmp_path, monkeypatch):
             editable.mode = "redirect"
             editable.rebuild = true
             editable.verbose = false
+            install.components = ["a", "b", "c"]
+            install.strip = true
             """
         ),
         encoding="utf-8",
@@ -256,6 +268,8 @@ def test_skbuild_settings_pyproject_toml(tmp_path, monkeypatch):
     assert settings.editable.mode == "redirect"
     assert settings.editable.rebuild
     assert not settings.editable.verbose
+    assert settings.install.components == ["a", "b", "c"]
+    assert settings.install.strip
 
 
 def test_skbuild_settings_pyproject_toml_broken(tmp_path, capsys):
@@ -327,3 +341,24 @@ def test_skbuild_settings_pyproject_conf_broken(tmp_path, capsys):
         logger -> Did you mean: logging?
       """.split()
     )
+
+
+def test_skbuild_settings_min_version_defaults(tmp_path, monkeypatch):
+    monkeypatch.setattr(
+        scikit_build_core.settings.skbuild_read_settings, "__version__", "0.5.0"
+    )
+
+    pyproject_toml = tmp_path / "pyproject.toml"
+    pyproject_toml.write_text("", encoding="utf-8")
+
+    settings_reader = SettingsReader.from_file(
+        pyproject_toml, {"minimum-version": "0.4"}
+    )
+    settings = settings_reader.settings
+    assert not settings.install.strip
+
+    settings_reader = SettingsReader.from_file(
+        pyproject_toml, {"minimum-version": "0.5"}
+    )
+    settings = settings_reader.settings
+    assert settings.install.strip