Skip to content
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ sdist.include = []
# Files to exclude from the SDist even if they are included by default. Supports gitignore syntax.
sdist.exclude = []

# Method to use to compute the files to include and exclude.
sdist.inclusion-mode = "default" # "classic"

# Try to build a reproducible distribution.
sdist.reproducible = true

Expand Down
19 changes: 19 additions & 0 deletions docs/reference/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,25 @@ print(mk_skbuild_docs())
:confval:`sdist.exclude`
```

```{eval-rst}
.. confval:: sdist.inclusion-mode
:type: ``"classic" | "default" | "manual"``
:default: "default" # "classic"

Method to use to compute the files to include and exclude.

The methods are:

* "default": Process the git ignore files. Shortcuts on ignored directories.
* "classic": The behavior before 0.12, like "default" but does not shortcut directories.
* "manual": No extra logic, based on include/exclude only.

Comment thread
henryiii marked this conversation as resolved.
If you don't set this, it will be "default" unless you set the minimum
version below 0.12, in which case it will be "classic".

.. versionadded: 0.12
```

```{eval-rst}
.. confval:: sdist.reproducible
:type: ``bool``
Expand Down
169 changes: 112 additions & 57 deletions src/scikit_build_core/build/_file_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import contextlib
import os
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

import pathspec

Expand Down Expand Up @@ -36,24 +36,31 @@ def each_unignored_file(
include: Sequence[str] = (),
exclude: Sequence[str] = (),
build_dir: str = "",
*,
mode: Literal["classic", "default", "manual"],
) -> Generator[Path, None, None]:
"""
Runs through all non-ignored files. Must be run from the root directory.
"""
global_exclude_lines = []
for gi in [Path(".git/info/exclude"), Path(".gitignore")]:
ignore_errs = [FileNotFoundError, NotADirectoryError]
with contextlib.suppress(*ignore_errs), gi.open(encoding="utf-8") as f:
global_exclude_lines += f.readlines()

nested_excludes = {
Path(dirpath): pathspec.GitIgnoreSpec.from_lines(
(Path(dirpath) / filename).read_text(encoding="utf-8").splitlines()
)
for dirpath, _, filenames in os.walk(".")
for filename in filenames
if filename == ".gitignore" and dirpath != "."
}
if mode != "manual":
for gi in [Path(".git/info/exclude"), Path(".gitignore")]:
ignore_errs = [FileNotFoundError, NotADirectoryError]
with contextlib.suppress(*ignore_errs), gi.open(encoding="utf-8") as f:
global_exclude_lines += f.readlines()

nested_excludes = (
{}
if mode == "manual"
else {
Path(dirpath): pathspec.GitIgnoreSpec.from_lines(
(Path(dirpath) / filename).read_text(encoding="utf-8").splitlines()
)
for dirpath, _, filenames in os.walk(".")
for filename in filenames
if filename == ".gitignore" and dirpath != "."
}
)

exclude_build_dir = build_dir.format(**pyproject_format(dummy=True))

Expand All @@ -67,49 +74,97 @@ def each_unignored_file(

include_spec = pathspec.GitIgnoreSpec.from_lines(include)

for dirstr, _, filenames in os.walk(str(starting_path), followlinks=True):
for dirstr, dirs, filenames in os.walk(str(starting_path), followlinks=True):
dirpath = Path(dirstr)
all_paths = (dirpath / fn for fn in filenames)
for p in all_paths:
# Always include something included
if include_spec.match_file(p):
logger.debug("Including {} because it is explicitly included.", p)
yield p
continue

# Always exclude something excluded
if user_exclude_spec.match_file(p):
logger.debug(
"Excluding {} because it is explicitly excluded by the user.", p
)
continue

# Ignore from global ignore
if global_exclude_spec.match_file(p):
logger.debug(
"Excluding {} because it is explicitly excluded by the global ignore.",
p,
)
continue

# Ignore built-in patterns
if builtin_exclude_spec.match_file(p):
logger.debug(
"Excluding {} because it is excluded by the built-in ignore patterns.",
p,
)
continue

# Check relative ignores (Python 3.9's is_relative_to workaround)
if any(
nex.match_file(p.relative_to(np))
for np, nex in nested_excludes.items()
if dirpath == np or np in dirpath.parents
if mode != "classic":
for dname in dirs:
if not match_path(
dirpath,
dirpath / dname,
Comment thread
henryiii marked this conversation as resolved.
include_spec,
global_exclude_spec,
builtin_exclude_spec,
user_exclude_spec,
nested_excludes,
is_path=True,
):
# Check to see if any include rules start with this
dstr = str(dirpath / dname).strip("/") + "/"
if not any(p.lstrip("/").startswith(dstr) for p in include):
dirs.remove(dname)
Comment thread
henryiii marked this conversation as resolved.

Comment thread
henryiii marked this conversation as resolved.
for fn in filenames:
path = dirpath / fn
if match_path(
dirpath,
path,
include_spec,
global_exclude_spec,
builtin_exclude_spec,
user_exclude_spec,
nested_excludes,
is_path=False,
):
logger.debug(
"Excluding {} because it is explicitly excluded by nested ignore.",
p,
)
continue
yield path


def match_path(
dirpath: Path,
p: Path,
include_spec: pathspec.GitIgnoreSpec,
global_exclude_spec: pathspec.GitIgnoreSpec,
builtin_exclude_spec: pathspec.GitIgnoreSpec,
user_exclude_spec: pathspec.GitIgnoreSpec,
nested_excludes: dict[Path, pathspec.GitIgnoreSpec],
*,
is_path: bool,
) -> bool:
ptype = "directory" if is_path else "file"

# Always include something included
if include_spec.match_file(p):
logger.debug("Including {} {} because it is explicitly included.", ptype, p)
return True

# Always exclude something excluded
if user_exclude_spec.match_file(p):
logger.debug(
"Excluding {} {} because it is explicitly excluded by the user.", ptype, p
)
return False

# Ignore from global ignore
if global_exclude_spec.match_file(p):
logger.debug(
"Excluding {} {} because it is explicitly excluded by the global ignore.",
ptype,
p,
)
return False

# Ignore built-in patterns
if builtin_exclude_spec.match_file(p):
logger.debug(
"Excluding {} {} because it is explicitly excluded by the built-in ignore.",
ptype,
p,
)
return False

# Check relative ignores (Python 3.9's is_relative_to workaround)
if any(
nex.match_file(p.relative_to(np))
for np, nex in nested_excludes.items()
if dirpath == np or np in dirpath.parents
):
logger.debug(
"Excluding {} {} because it is explicitly excluded by nested ignore.",
ptype,
p,
)
return False

yield p
logger.debug(
"Including {} {} because it exists (and isn't matched any other way).", ptype, p
)
return True
4 changes: 3 additions & 1 deletion src/scikit_build_core/build/_pathutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import os
from pathlib import Path
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal

import pathspec

Expand Down Expand Up @@ -44,6 +44,7 @@ def packages_to_file_mapping(
src_exclude: Sequence[str],
target_exclude: Sequence[str],
build_dir: str,
mode: Literal["classic", "default", "manual"],
) -> dict[str, str]:
"""
This will output a mapping of source files to target files.
Expand All @@ -59,6 +60,7 @@ def packages_to_file_mapping(
include=include,
exclude=src_exclude,
build_dir=build_dir,
mode=mode,
):
rel_path = filepath.relative_to(source_dir)
target_path = platlib_dir / package_dir / rel_path
Expand Down
2 changes: 2 additions & 0 deletions src/scikit_build_core/build/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,14 @@ def build_sdist(
tar = stack.enter_context(
tarfile.TarFile(fileobj=gzip_container, mode="w", format=tarfile.PAX_FORMAT)
)
assert settings.sdist.inclusion_mode is not None
paths = sorted(
each_unignored_file(
Path(),
include=settings.sdist.include,
exclude=settings.sdist.exclude,
build_dir=settings.build_dir,
mode=settings.sdist.inclusion_mode,
)
)
for filepath in paths:
Expand Down
2 changes: 2 additions & 0 deletions src/scikit_build_core/build/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,13 +474,15 @@ def _build_wheel_impl_impl(
packages=settings.wheel.packages,
name=normalized_name,
)
assert settings.sdist.inclusion_mode is not None
mapping = packages_to_file_mapping(
packages=packages,
platlib_dir=wheel_dirs[targetlib],
include=settings.sdist.include,
src_exclude=settings.sdist.exclude,
target_exclude=settings.wheel.exclude,
build_dir=settings.build_dir,
mode=settings.sdist.inclusion_mode,
)

if not editable:
Expand Down
8 changes: 8 additions & 0 deletions src/scikit_build_core/resources/scikit-build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,14 @@
},
"description": "Files to exclude from the SDist even if they are included by default. Supports gitignore syntax."
},
"inclusion-mode": {
"enum": [
"classic",
"default",
"manual"
],
"description": "Method to use to compute the files to include and exclude."
},
"reproducible": {
"type": "boolean",
"default": true,
Expand Down
21 changes: 21 additions & 0 deletions src/scikit_build_core/settings/skbuild_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,27 @@ class SDistSettings:
:confval:`sdist.include`
"""

inclusion_mode: Optional[Literal["classic", "default", "manual"]] = (
dataclasses.field(
default=None,
metadata=SettingsFieldMetadata(display_default='"default" # "classic"'),
)
)
"""
Method to use to compute the files to include and exclude.

The methods are:

* "default": Process the git ignore files. Shortcuts on ignored directories.
* "classic": The behavior before 0.12, like "default" but does not shortcut directories.
* "manual": No extra logic, based on include/exclude only.

If you don't set this, it will be "default" unless you set the minimum
version below 0.12, in which case it will be "classic".

.. versionadded: 0.12
"""

reproducible: bool = True
"""
Try to build a reproducible distribution.
Expand Down
16 changes: 16 additions & 0 deletions src/scikit_build_core/settings/skbuild_read_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,22 @@ def __init__(
and static_settings.build.targets == self.settings.build.targets,
)

if self.settings.sdist.inclusion_mode is not None:
if (
self.settings.minimum_version is not None
and self.settings.minimum_version < Version("0.12")
):
rich_error(
"minimum-version can't be less than 0.12 to use sdist.inclusion-mode"
)
elif (
self.settings.minimum_version is not None
and self.settings.minimum_version < Version("0.12")
):
self.settings.sdist.inclusion_mode = "classic"
else:
self.settings.sdist.inclusion_mode = "default"

def unrecognized_options(self) -> Generator[str, None, None]:
return self.sources.unrecognized_options(ScikitBuildSettings)

Expand Down
1 change: 1 addition & 0 deletions tests/test_editable_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ def test_navigate_editable_pkg(editable_package: EditablePackage, virtualenv: VE
src_exclude=[],
target_exclude=[],
build_dir="",
mode="classic",
)
assert mapping == {
str(Path("pkg/__init__.py")): str(pkg_dir / "__init__.py"),
Expand Down
Loading
Loading