Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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 @@ -202,6 +202,9 @@ sdist.reproducible = true
# If set to True, CMake will be run before building the SDist.
sdist.cmake = false

# Resolve symlinks in the SDist, copying file contents instead of storing symlinks.
sdist.resolve-symlinks = true

# A list of packages to auto-copy into the wheel.
wheel.packages = ["src/<package>", "python/<package>", "<package>"]

Expand Down
8 changes: 8 additions & 0 deletions docs/reference/configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,14 @@ print(mk_skbuild_docs())
``SOURCE_DATE_EPOCH`` will be used for timestamps, or a fixed value if not set.
```

```{eval-rst}
.. confval:: sdist.resolve-symlinks
:type: ``bool``
:default: true

Resolve symlinks in the SDist, copying file contents instead of storing symlinks.
```

## search

```{eval-rst}
Expand Down
7 changes: 6 additions & 1 deletion src/scikit_build_core/build/sdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ def build_sdist(
)
)
tar = stack.enter_context(
tarfile.TarFile(fileobj=gzip_container, mode="w", format=tarfile.PAX_FORMAT)
tarfile.TarFile(
fileobj=gzip_container,
mode="w",
format=tarfile.PAX_FORMAT,
dereference=settings.sdist.resolve_symlinks,
)
Comment thread
henryiii marked this conversation as resolved.
)
assert settings.sdist.inclusion_mode is not None
paths = sorted(
Expand Down
5 changes: 5 additions & 0 deletions src/scikit_build_core/resources/scikit-build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@
"type": "boolean",
"default": false,
"description": "If set to True, CMake will be run before building the SDist."
},
"resolve-symlinks": {
"type": "boolean",
"default": true,
"description": "Resolve symlinks in the SDist, copying file contents instead of storing symlinks."
}
}
},
Expand Down
5 changes: 5 additions & 0 deletions src/scikit_build_core/settings/skbuild_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,11 @@ class SDistSettings:
If set to True, CMake will be run before building the SDist.
"""

resolve_symlinks: bool = True
"""
Resolve symlinks in the SDist, copying file contents instead of storing symlinks.
"""


@dataclasses.dataclass
class WheelSettings:
Expand Down
50 changes: 50 additions & 0 deletions tests/test_pep517_sdist_symlink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import annotations

import tarfile
from pathlib import Path

import pytest

from scikit_build_core.build import build_sdist


@pytest.fixture
def can_symlink() -> None:
"""Skip the test if symlinks are not supported on this OS."""
try:
Path("_symlink_check").symlink_to("_symlink_check_target")
except OSError:
pytest.skip(
"Creating symlinks is not supported/allowed on this OS without privileges"
)
else:
Path("_symlink_check").unlink(missing_ok=True)
Comment on lines +12 to +21
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can_symlink creates and deletes a fixed-name path in the current working directory (_symlink_check). Since CI runs tests with pytest -n auto (xdist), this can race with other workers if they share the same CWD at fixture setup time. Consider creating the check symlink in a unique temp directory (e.g., via tmp_path_factory.mktemp(...)) to avoid cross-test interference.

Suggested change
def can_symlink() -> None:
"""Skip the test if symlinks are not supported on this OS."""
try:
Path("_symlink_check").symlink_to("_symlink_check_target")
except OSError:
pytest.skip(
"Creating symlinks is not supported/allowed on this OS without privileges"
)
else:
Path("_symlink_check").unlink(missing_ok=True)
def can_symlink(tmp_path_factory) -> None:
"""Skip the test if symlinks are not supported on this OS."""
check_dir = tmp_path_factory.mktemp("symlink_check")
check_link = check_dir / "_symlink_check"
try:
check_link.symlink_to("_symlink_check_target")
except OSError:
pytest.skip(
"Creating symlinks is not supported/allowed on this OS without privileges"
)
else:
check_link.unlink(missing_ok=True)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this also could be cached, vs. being computed each time.



@pytest.mark.usefixtures("package_simple_pyproject_ext", "can_symlink")
@pytest.mark.parametrize(
("config_settings", "expected_type"),
[
pytest.param({}, "reg", id="dereference_default"),
pytest.param({"sdist.resolve-symlinks": "false"}, "sym", id="no_dereference"),
],
)
def test_pep517_sdist_symlink(
tmp_path: Path,
config_settings: dict[str, list[str] | str],
expected_type: str,
) -> None:
Path("CMakeLists_link.txt").symlink_to("CMakeLists.txt")

out = build_sdist(str(tmp_path), config_settings=config_settings or None)

with tarfile.open(tmp_path / out, "r:gz") as tar:
link_member = tar.getmember("cmake_example-0.0.1/CMakeLists_link.txt")
if expected_type == "reg":
assert link_member.isreg(), (
"The symlink should have been stored as a regular file"
)
else:
assert link_member.issym(), (
"The symlink should have been stored as a symlink"
)
Loading