Skip to content
Draft
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
7 changes: 5 additions & 2 deletions docs/reference/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,9 @@ print(mk_skbuild_docs())

Files to include in the SDist even if they are skipped by default. Supports gitignore syntax.

Always takes precedence over :confval:`sdist.exclude`
In ``"default"`` and ``"classic"``, this takes precedence over
:confval:`sdist.exclude`. In ``"manual"``, files must be included first,
then exclude rules are applied.

.. seealso::
:confval:`sdist.exclude`
Expand All @@ -473,7 +475,8 @@ print(mk_skbuild_docs())

* "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.
* "manual": Explicit allowlist mode; files must match ``include`` and then
are filtered by ``exclude``.

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".
Expand Down
18 changes: 15 additions & 3 deletions src/scikit_build_core/build/_file_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def each_unignored_file(

for dirstr, dirs, filenames in os.walk(str(starting_path), followlinks=True):
dirpath = Path(dirstr)
if mode != "classic":
if mode == "default":
for dname in dirs:
if not match_path(
dirpath,
Expand All @@ -86,6 +86,7 @@ def each_unignored_file(
builtin_exclude_spec,
user_exclude_spec,
nested_excludes,
manual_mode=False,
is_path=True,
):
# Check to see if any include rules start with this
Expand All @@ -103,6 +104,7 @@ def each_unignored_file(
builtin_exclude_spec,
user_exclude_spec,
nested_excludes,
manual_mode=mode == "manual",
is_path=False,
):
yield path
Expand All @@ -117,12 +119,22 @@ def match_path(
user_exclude_spec: pathspec.GitIgnoreSpec,
nested_excludes: dict[Path, pathspec.GitIgnoreSpec],
*,
manual_mode: bool,
is_path: bool,
) -> bool:
ptype = "directory" if is_path else "file"

# Always include something included
if include_spec.match_file(p):
included = include_spec.match_file(p)

# In manual mode, an item must be explicitly included first.
if manual_mode and not included:
logger.debug(
"Excluding {} {} because manual mode requires explicit include.", ptype, p
)
return False

# Always include something included in non-manual modes
if not manual_mode and included:
logger.debug("Including {} {} because it is explicitly included.", ptype, p)
return True

Expand Down
7 changes: 5 additions & 2 deletions src/scikit_build_core/settings/skbuild_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ class SDistSettings:
"""
Files to include in the SDist even if they are skipped by default. Supports gitignore syntax.

Always takes precedence over :confval:`sdist.exclude`
In ``"default"`` and ``"classic"``, this takes precedence over
:confval:`sdist.exclude`. In ``"manual"``, files must be included first,
then exclude rules are applied.

.. seealso::
:confval:`sdist.exclude`
Expand All @@ -240,7 +242,8 @@ class SDistSettings:

* "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.
* "manual": Explicit allowlist mode; files must match ``include`` and then
are filtered by ``exclude``.

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".
Expand Down
93 changes: 53 additions & 40 deletions tests/test_file_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@ def test_on_each_with_symlink(
nested_dir / ".gitignore",
}
if mode == "manual":
files |= {
hidden_file,
nested_dir.joinpath("ignored.txt"),
nested_dir.joinpath("more/ignored.txt"),
}
files = set()
assert set(each_unignored_file(Path(), mode=mode)) == files


Expand Down Expand Up @@ -188,26 +184,46 @@ def test_include_patterns(
]
}
if mode == "manual":
expected |= {Path("temp.tmp"), Path("local_ignored_file.txt")}
expected = {
Path(s)
for s in [
"setup.py",
"src/__init__.py",
"src/main.py",
"src/utils.py",
"tests/test_main.py",
"tests/test_utils.py",
"tests/tmp.py",
]
}
assert result == expected

# Test including specific files
result = set(each_unignored_file(Path(), include=["tests/tmp.py"], mode=mode))
assert result == expected | {Path("tests/tmp.py")}
if mode == "manual":
assert result == {Path("tests/tmp.py")}
else:
assert result == expected | {Path("tests/tmp.py")}

# Test including with wildcards
result = set(each_unignored_file(Path(), include=["tests/*"], mode=mode))
expected = expected | {Path("tests/tmp.py")}
assert result == expected
if mode == "manual":
expected = {
Path(s)
for s in ["tests/test_main.py", "tests/test_utils.py", "tests/tmp.py"]
}
assert result == expected
else:
assert result == expected | {Path("tests/tmp.py")}


def test_include_pattern_with_nested_path_and_broad_exclude(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""
Test that nested include patterns are not pruned by directory traversal,
even when exclude patterns match all paths.
Test that nested include patterns are not pruned by directory traversal.
In manual mode, excludes are applied after includes.
"""
monkeypatch.chdir(tmp_path)
nested_file = Path("a") / "b" / "c.txt"
Expand All @@ -222,7 +238,7 @@ def test_include_pattern_with_nested_path_and_broad_exclude(
mode="manual",
)
)
assert result == {nested_file}
assert result == set()


def test_exclude_patterns(
Expand Down Expand Up @@ -257,7 +273,7 @@ def test_exclude_patterns(
]
}
if mode == "manual":
expected |= {Path("tests/tmp.py"), Path("local_ignored_file.txt")}
expected = set()
assert result == expected

# Test excluding directories
Expand All @@ -280,7 +296,7 @@ def test_exclude_patterns(
]
}
if mode == "manual":
expected |= {Path("temp.tmp"), Path("local_ignored_file.txt")}
expected = set()
assert result == expected

# Test excluding with wildcards
Expand All @@ -299,7 +315,7 @@ def test_exclude_patterns(
]
}
if mode == "manual":
expected |= {Path("temp.tmp"), Path("local_ignored_file.txt")}
expected = set()
assert result == expected


Expand All @@ -324,6 +340,8 @@ def test_include_overrides_exclude(
)
)
expected = {Path(s) for s in ["src/main.py", "tests/test_main.py"]}
if mode == "manual":
expected = set()
assert result == expected

# Exclude everything but include a file from inside a directory
Expand All @@ -333,6 +351,8 @@ def test_include_overrides_exclude(
)
)
expected = {Path(s) for s in ["tests/test_main.py"]}
if mode == "manual":
expected = set()
assert result == expected


Expand Down Expand Up @@ -372,17 +392,15 @@ def test_gitignore_interaction(
]
}
if mode == "manual":
expected |= {
Path("cache.db"),
Path("debug.log"),
Path("local_ignored_file.txt"),
Path("temp.tmp"),
}
expected = set()
assert result == expected

# Test that include can override gitignore
result = set(each_unignored_file(Path(), include=["*.tmp"], mode=mode))
assert result == expected | {Path("temp.tmp")}
if mode == "manual":
assert result == {Path("temp.tmp")}
else:
assert result == expected | {Path("temp.tmp")}


def test_nested_gitignore(
Expand Down Expand Up @@ -422,17 +440,15 @@ def test_nested_gitignore(
]
}
if mode == "manual":
expected |= {
Path("local_ignored_file.txt"),
Path("src/utils.py"),
Path("temp.tmp"),
Path("tests/tmp.py"),
}
expected = set()
assert result == expected

# Test that include can override nested gitignore
result = set(each_unignored_file(Path(), include=["src/utils.py"], mode=mode))
assert result == expected | {Path("src/utils.py")}
if mode == "manual":
assert result == {Path("src/utils.py")}
else:
assert result == expected | {Path("src/utils.py")}


def test_build_dir_exclusion(
Expand Down Expand Up @@ -474,19 +490,18 @@ def test_build_dir_exclusion(
]
}
if mode == "manual":
expected |= {
Path("local_ignored_file.txt"),
Path("temp.tmp"),
Path("tests/tmp.py"),
}
expected = set()
assert result == expected
assert build_file.relative_to(tmp_path) not in result

# Test that include can override build_dir exclusion
result = set(
each_unignored_file(Path(), include=["build/*"], build_dir="build", mode=mode)
)
assert result == expected | {Path("build/output.so")}
if mode == "manual":
assert result == set()
else:
assert result == expected | {Path("build/output.so")}


def test_complex_combinations(
Expand Down Expand Up @@ -526,6 +541,8 @@ def test_complex_combinations(
expected = {
Path(s) for s in ["tests/test_main.py", "temp.tmp"]
} # Only these should match
if mode == "manual":
expected = set()
assert result == expected


Expand Down Expand Up @@ -590,9 +607,5 @@ def test_nonexistent_patterns(
]
}
if mode == "manual":
expected |= {
Path("local_ignored_file.txt"),
Path("temp.tmp"),
Path("tests/tmp.py"),
}
expected = set()
assert exclude_result == expected
Loading