Repository: scikit-build/scikit-build-core
Pull request #652 (2024-02-26 20:29:07)

fix!: exclude installed files if listed in exclude
As mentioned in our recent Community Meeting.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) Report
Attention: Patch coverage is `85.71429%` with `1 lines` in your changes are missing coverage. Please review.
> Project coverage is 86.29%. Comparing base [(`57e4ac9`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/commit/57e4ac91f766f426187748546a4956ef466914c6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) to head [(`25ff81d`)](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build).

| [Files](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build) | Patch % | Lines |
|---|---|---|
| [src/scikit\_build\_core/build/\_wheelfile.py](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=scikit-build#diff-c3JjL3NjaWtpdF9idWlsZF9jb3JlL2J1aWxkL193aGVlbGZpbGUucHk=) | 83.33% | [1 Missing :warning: ](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?src=pr&el=tree&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     #652      +/-   ##
==========================================
+ Coverage   86.28%   86.29%   +0.01%     
==========================================
  Files          63       63              
  Lines        3230     3233       +3     
==========================================
+ Hits         2787     2790       +3     
  Misses        443      443              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/scikit-build/scikit-build-core/pull/652?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/README.md b/README.md
index 478bb3a..532afd8 100644
--- a/README.md
+++ b/README.md
@@ -251,8 +251,9 @@ wheel.cmake = true
 wheel.platlib = ""
 
 # A set of patterns to exclude from the wheel. This is additive to the SDist
-# exclude patterns. This applies to the source files, not the final paths.
-# Editable installs may not respect this exclusion.
+# exclude patterns. This applies to the final paths in the wheel, and can
+# exclude files from CMake output as well.  Editable installs may not respect
+# this exclusion.
 wheel.exclude = []
 
 # The build tag to use for the wheel. If empty, no build tag is used.
diff --git a/docs/configuration.md b/docs/configuration.md
index e47c6de..3e27cc1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -269,6 +269,13 @@ as well (not guaranteed to be respected by editable installs):
 wheel.exclude = ["**.pyx"]
 ```
 
+:::{versionchanged} 0.9
+
+Before scikit-build-core 0.9, these were matched on the source path, rather than
+the wheel path, and didn't apply to CMake output.
+
+:::
+
 :::{note}
 
 There are two more settings that are primarily intended for `overrides` (see
diff --git a/pyproject.toml b/pyproject.toml
index 4e244da..63cdcdf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -53,6 +53,7 @@ test = [
     "build[virtualenv]",
     "cattrs >=22.2.0",
     "pathspec >=0.10.1",
+    "pip",
     "pybind11",
     "pyproject-metadata >=0.5",
     "pytest >=7.0",  # 7.2+ recommended for better tracebacks with ExceptionGroup
diff --git a/src/scikit_build_core/build/_pathutil.py b/src/scikit_build_core/build/_pathutil.py
index 107511f..f013f80 100644
--- a/src/scikit_build_core/build/_pathutil.py
+++ b/src/scikit_build_core/build/_pathutil.py
@@ -4,6 +4,8 @@ import os
 from pathlib import Path
 from typing import TYPE_CHECKING
 
+import pathspec
+
 from ._file_processor import each_unignored_file
 
 if TYPE_CHECKING:
@@ -37,20 +39,23 @@ def packages_to_file_mapping(
     packages: Sequence[str],
     platlib_dir: Path,
     include: Sequence[str],
-    exclude: Sequence[str],
+    src_exclude: Sequence[str],
+    target_exclude: Sequence[str],
 ) -> dict[str, str]:
     mapping = {}
+    exclude_spec = pathspec.GitIgnoreSpec.from_lines(target_exclude)
     for package in packages:
         source_package = Path(package)
         base_path = source_package.parent
         for filepath in each_unignored_file(
             source_package,
             include=include,
-            exclude=exclude,
+            exclude=src_exclude,
         ):
-            package_dir = platlib_dir / filepath.relative_to(base_path)
-            if not package_dir.is_file():
-                mapping[str(filepath)] = str(package_dir)
+            rel_path = filepath.relative_to(base_path)
+            target_path = platlib_dir / rel_path
+            if not exclude_spec.match_file(rel_path) and not target_path.is_file():
+                mapping[str(filepath)] = str(target_path)
 
     return mapping
 
diff --git a/src/scikit_build_core/build/_wheelfile.py b/src/scikit_build_core/build/_wheelfile.py
index 516e70d..b3ceb80 100644
--- a/src/scikit_build_core/build/_wheelfile.py
+++ b/src/scikit_build_core/build/_wheelfile.py
@@ -17,11 +17,12 @@ from typing import TYPE_CHECKING
 from zipfile import ZipInfo
 
 import packaging.utils
+import pathspec
 
 from .. import __version__
 
 if TYPE_CHECKING:
-    from collections.abc import Mapping, Set
+    from collections.abc import Mapping, Sequence, Set
 
     from packaging.tags import Tag
     from pyproject_metadata import StandardMetadata
@@ -141,7 +142,9 @@ class WheelWriter:
             **license_entries,
         }
 
-    def build(self, wheel_dirs: dict[str, Path]) -> None:
+    def build(
+        self, wheel_dirs: Mapping[str, Path], exclude: Sequence[str] = ()
+    ) -> None:
         (targetlib,) = {"platlib", "purelib"} & set(wheel_dirs)
         assert {targetlib, "data", "headers", "scripts", "null"} >= wheel_dirs.keys()
 
@@ -152,14 +155,21 @@ class WheelWriter:
         for key in sorted({"data", "headers", "scripts"} & wheel_dirs.keys()):
             plans[key] = wheel_dirs[key]
 
+        exclude_spec = pathspec.GitIgnoreSpec.from_lines(exclude)
+
         for key, path in plans.items():
             for filename in sorted(path.glob("**/*")):
-                is_in_dist_info = any(x.endswith(".dist-info") for x in filename.parts)
-                is_python_cache = filename.suffix in {".pyc", ".pyo"}
-                if filename.is_file() and not is_in_dist_info and not is_python_cache:
-                    relpath = filename.relative_to(path)
-                    target = Path(data_dir) / key / relpath if key else relpath
-                    self.write(str(filename), str(target))
+                if not filename.is_file():
+                    continue
+                if any(x.endswith(".dist-info") for x in filename.parts):
+                    continue
+                if filename.suffix in {".pyc", ".pyo"}:
+                    continue
+                relpath = filename.relative_to(path)
+                if exclude_spec.match_file(relpath):
+                    continue
+                target = Path(data_dir) / key / relpath if key else relpath
+                self.write(str(filename), str(target))
 
         dist_info_contents = self.dist_info_contents()
         for key, data in dist_info_contents.items():
diff --git a/src/scikit_build_core/build/wheel.py b/src/scikit_build_core/build/wheel.py
index 974d71c..5f3bcdf 100644
--- a/src/scikit_build_core/build/wheel.py
+++ b/src/scikit_build_core/build/wheel.py
@@ -344,7 +344,8 @@ def _build_wheel_impl(
             packages=packages,
             platlib_dir=wheel_dirs[targetlib],
             include=settings.sdist.include,
-            exclude=[*settings.sdist.exclude, *settings.wheel.exclude],
+            src_exclude=settings.sdist.exclude,
+            target_exclude=settings.wheel.exclude,
         )
 
         if not editable:
@@ -364,7 +365,7 @@ def _build_wheel_impl(
             ),
             license_files=license_files,
         ) as wheel:
-            wheel.build(wheel_dirs)
+            wheel.build(wheel_dirs, exclude=settings.wheel.exclude)
 
             str_pkgs = (str(Path.cwd().joinpath(p).parent.resolve()) for p in packages)
             if editable and settings.editable.mode == "redirect":
diff --git a/src/scikit_build_core/resources/scikit-build.schema.json b/src/scikit_build_core/resources/scikit-build.schema.json
index c02871e..cb8ca2c 100644
--- a/src/scikit_build_core/resources/scikit-build.schema.json
+++ b/src/scikit_build_core/resources/scikit-build.schema.json
@@ -209,7 +209,7 @@
           "items": {
             "type": "string"
           },
-          "description": "A set of patterns to exclude from the wheel. This is additive to the SDist exclude patterns. This applies to the source files, not the final paths. Editable installs may not respect this exclusion."
+          "description": "A set of patterns to exclude from the wheel. This is additive to the SDist exclude patterns. This applies to the final paths in the wheel, and can exclude files from CMake output as well.  Editable installs may not respect this exclusion."
         },
         "build-tag": {
           "type": "string",
diff --git a/src/scikit_build_core/settings/skbuild_model.py b/src/scikit_build_core/settings/skbuild_model.py
index 9cc413a..9258e14 100644
--- a/src/scikit_build_core/settings/skbuild_model.py
+++ b/src/scikit_build_core/settings/skbuild_model.py
@@ -193,8 +193,9 @@ class WheelSettings:
     exclude: List[str] = dataclasses.field(default_factory=list)
     """
     A set of patterns to exclude from the wheel. This is additive to the SDist
-    exclude patterns. This applies to the source files, not the final paths.
-    Editable installs may not respect this exclusion.
+    exclude patterns. This applies to the final paths in the wheel, and can
+    exclude files from CMake output as well.  Editable installs may not respect
+    this exclusion.
     """
 
     build_tag: str = ""


Test patch:
diff --git a/tests/packages/simplest_c/CMakeLists.txt b/tests/packages/simplest_c/CMakeLists.txt
index 61105f9..f610e44 100644
--- a/tests/packages/simplest_c/CMakeLists.txt
+++ b/tests/packages/simplest_c/CMakeLists.txt
@@ -34,3 +34,7 @@ install(
 file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated_ignored.txt "Testing")
 install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated_ignored.txt
         DESTINATION ${SKBUILD_PROJECT_NAME})
+
+file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/generated_no_wheel.txt "Testing")
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/generated_no_wheel.txt
+        DESTINATION ${SKBUILD_PROJECT_NAME})
diff --git a/tests/test_editable_unit.py b/tests/test_editable_unit.py
index 3dcc452..a0451f1 100644
--- a/tests/test_editable_unit.py
+++ b/tests/test_editable_unit.py
@@ -139,7 +139,8 @@ def test_navigate_editable_pkg(editable_package: EditablePackage, virtualenv: VE
         packages=packages,
         platlib_dir=site_packages,
         include=[],
-        exclude=[],
+        src_exclude=[],
+        target_exclude=[],
     )
     assert mapping == {
         str(Path("pkg/__init__.py")): str(pkg_dir / "__init__.py"),
diff --git a/tests/test_simplest_c.py b/tests/test_simplest_c.py
index c2961ca..cf6af48 100644
--- a/tests/test_simplest_c.py
+++ b/tests/test_simplest_c.py
@@ -82,6 +82,7 @@ def test_pep517_wheel(tmp_path, monkeypatch, virtualenv, component):
 
         if not component:
             expected_wheel_files.add("generated_ignored.txt")
+            expected_wheel_files.add("generated_no_wheel.txt")
 
         if not component or "Generated" in component:
             expected_wheel_files.add("generated.txt")
@@ -110,7 +111,10 @@ def test_pep517_wheel_incexl(tmp_path, monkeypatch, virtualenv):
         {
             "sdist.include": "src/simplest/*included*.txt",
             "sdist.exclude": "src/simplest/*excluded*.txt",
-            "wheel.exclude": "src/simplest/sdist_only.txt",
+            "wheel.exclude": [
+                "simplest/sdist_only.txt",
+                "simplest/generated_no_wheel.txt",
+            ],
             "wheel.packages": ["src/simplest", "src/not_a_package"],
         },
     )