Skip to content

Commit 82768f0

Browse files
RonnyPfannschmidtCursor AICursor Grok 4.5
committed
fix: omit SCM egg-info JSON from wheels
Sdists still include scm_version.json and scm_file_list.json for fallback discovery. setuptools egg2dist was copying them into .dist-info; strip them after conversion so wheels do not ship them. Fixes #1473 Co-authored-by: Cursor AI <[email protected]> Co-authored-by: Cursor Grok 4.5 <[email protected]>
1 parent 5b91e2f commit 82768f0

6 files changed

Lines changed: 156 additions & 1 deletion

File tree

docs/extending.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ SCM results always take priority over fallback results.
8989
| `hg-git` | vcs-versioning | SCM (Git + Mercurial) |
9090
| `archival` | vcs-versioning | Fallback (`.git_archival.txt`) |
9191
| `pkginfo` | setuptools-scm | Fallback (`PKG-INFO`) |
92-
| `egg-info` | setuptools-scm | Fallback (`*.egg-info/scm_version.json`) |
92+
| `egg-info` | setuptools-scm | Fallback (`*.egg-info/scm_version.json`; written for sdists, omitted from wheels) |
9393

9494

9595
## Version number construction
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Omit ``scm_version.json`` and ``scm_file_list.json`` from wheel ``.dist-info``
2+
while still including them in sdists for fallback discovery.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""bdist_wheel mixin that keeps SCM egg-info JSON out of wheels.
2+
3+
``egg_info`` writes ``scm_version.json`` / ``scm_file_list.json`` for sdist
4+
fallback discovery. setuptools' ``egg2dist`` copies unknown egg-info files
5+
into ``.dist-info``, so wheels would otherwise ship them. Wheels already
6+
have ``METADATA`` and ``RECORD``; strip our files after conversion.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
from pathlib import Path
12+
13+
from setuptools.command.bdist_wheel import bdist_wheel as _bdist_wheel
14+
from vcs_versioning._scm_metadata import SCM_FILE_LIST_FILENAME
15+
from vcs_versioning._scm_metadata import SCM_VERSION_FILENAME
16+
17+
_SCM_DIST_INFO_FILES = (SCM_VERSION_FILENAME, SCM_FILE_LIST_FILENAME)
18+
19+
20+
def _unlink_scm_metadata(distinfo_path: Path) -> None:
21+
"""Remove SCM metadata files from a ``.dist-info`` directory if present.
22+
23+
Mirrors setuptools ``egg2dist``'s ``adios`` for plain files: ``unlink``
24+
when the path exists (including symlinks), never ``rmtree``.
25+
"""
26+
for name in _SCM_DIST_INFO_FILES:
27+
path = distinfo_path / name
28+
if path.exists() or path.is_symlink():
29+
path.unlink()
30+
31+
32+
class ScmBdistWheelMixin(_bdist_wheel):
33+
"""Mixin that strips SCM egg-info JSON from ``.dist-info`` after egg2dist."""
34+
35+
def egg2dist(self, egginfo_path: str, distinfo_path: str) -> None:
36+
super().egg2dist(egginfo_path, distinfo_path)
37+
_unlink_scm_metadata(Path(distinfo_path))
38+
39+
40+
class bdist_wheel(ScmBdistWheelMixin, _bdist_wheel):
41+
"""Default bdist_wheel that omits SCM metadata from wheels."""

setuptools-scm/src/setuptools_scm/_integration/egg_info.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Also writes ``scm_version.json`` and ``scm_file_list.json`` into the
99
egg-info directory after ``run()`` creates it, so that sdists carry
1010
the metadata needed for fallback discovery when no VCS is present.
11+
Wheels omit these files via the ``bdist_wheel`` egg2dist mixin.
1112
"""
1213

1314
from __future__ import annotations

setuptools-scm/src/setuptools_scm/_integration/setuptools.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
from vcs_versioning.overrides import GlobalOverrides
1414
from vcs_versioning.overrides import ensure_context
1515

16+
from .bdist_wheel import ScmBdistWheelMixin
17+
from .bdist_wheel import bdist_wheel as scm_bdist_wheel
1618
from .build_py import ScmVersionFileMixin
1719
from .build_py import build_py as scm_build_py
1820
from .egg_info import ScmEggInfoMixin
@@ -94,6 +96,37 @@ def _register_egg_info_command(dist: setuptools.Distribution) -> None:
9496
log.debug("Wrapped project egg_info with setuptools_scm egg-info mixin")
9597

9698

99+
def _register_bdist_wheel_command(dist: setuptools.Distribution) -> None:
100+
"""Register bdist_wheel that strips SCM JSON from wheel ``.dist-info``.
101+
102+
Sdists keep ``scm_version.json`` / ``scm_file_list.json`` for fallback
103+
discovery; wheels already have ``METADATA`` and ``RECORD``.
104+
"""
105+
if not dist.cmdclass:
106+
dist.cmdclass = {}
107+
108+
existing_bdist_wheel = dist.cmdclass.get("bdist_wheel")
109+
110+
if existing_bdist_wheel is None:
111+
dist.cmdclass["bdist_wheel"] = scm_bdist_wheel
112+
log.debug("Registered setuptools_scm bdist_wheel command")
113+
return
114+
115+
project_bdist_wheel = cast("type[setuptools.Command]", existing_bdist_wheel)
116+
117+
if issubclass(project_bdist_wheel, ScmBdistWheelMixin):
118+
return
119+
120+
wrapped = type(
121+
"_SetuptoolsScmWrappedBdistWheel",
122+
(ScmBdistWheelMixin, project_bdist_wheel),
123+
{},
124+
)
125+
126+
dist.cmdclass["bdist_wheel"] = wrapped
127+
log.debug("Wrapped project bdist_wheel with setuptools_scm egg2dist mixin")
128+
129+
97130
def _log_hookstart(hook: str, dist: setuptools.Distribution) -> None:
98131
log.debug(
99132
"%s %s %s %r",
@@ -177,6 +210,7 @@ def version_keyword(
177210

178211
_register_build_py_command(dist)
179212
_register_egg_info_command(dist)
213+
_register_bdist_wheel_command(dist)
180214

181215

182216
@ensure_context("SETUPTOOLS_SCM", additional_loggers=_setuptools_scm_logger)
@@ -237,3 +271,4 @@ def _infer_version_impl(
237271

238272
_register_build_py_command(dist)
239273
_register_egg_info_command(dist)
274+
_register_bdist_wheel_command(dist)

setuptools-scm/testing_scm/test_integration.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,6 +1672,82 @@ def test_manifest_in_excludes_scm_tracked_files(
16721672
)
16731673

16741674

1675+
@pytest.mark.issue(1473)
1676+
def test_scm_metadata_in_sdist_not_in_wheel(
1677+
wd: WorkDir, monkeypatch: pytest.MonkeyPatch
1678+
) -> None:
1679+
"""SCM egg-info JSON is for sdist fallback; wheels must not ship it."""
1680+
import shutil
1681+
import zipfile
1682+
1683+
from vcs_versioning._scm_metadata import SCM_FILE_LIST_FILENAME
1684+
from vcs_versioning._scm_metadata import SCM_VERSION_FILENAME
1685+
1686+
monkeypatch.chdir(wd.cwd)
1687+
1688+
wd.write(
1689+
"pyproject.toml",
1690+
textwrap.dedent("""\
1691+
[build-system]
1692+
requires = ["setuptools>=61", "setuptools-scm"]
1693+
build-backend = "setuptools.build_meta"
1694+
1695+
[project]
1696+
name = "test-pkg"
1697+
dynamic = ["version"]
1698+
1699+
[tool.setuptools_scm]
1700+
"""),
1701+
)
1702+
1703+
pkg_dir = wd.cwd / "test_pkg"
1704+
pkg_dir.mkdir()
1705+
(pkg_dir / "__init__.py").write_text("")
1706+
1707+
wd(wd.add_command)
1708+
wd.commit()
1709+
wd("git tag v1.0.0")
1710+
1711+
sdist_names = _sdist_names(wd)
1712+
assert any(SCM_VERSION_FILENAME in n for n in sdist_names), (
1713+
f"{SCM_VERSION_FILENAME} should be in sdist: {sdist_names}"
1714+
)
1715+
assert any(SCM_FILE_LIST_FILENAME in n for n in sdist_names), (
1716+
f"{SCM_FILE_LIST_FILENAME} should be in sdist: {sdist_names}"
1717+
)
1718+
1719+
dist_dir = wd.cwd / "dist"
1720+
if dist_dir.exists():
1721+
shutil.rmtree(dist_dir)
1722+
for egg_info_dir in wd.cwd.glob("*.egg-info"):
1723+
shutil.rmtree(egg_info_dir)
1724+
1725+
build_result = subprocess.run(
1726+
[sys.executable, "-m", "build", "--wheel", "--no-isolation"],
1727+
cwd=wd.cwd,
1728+
capture_output=True,
1729+
text=True,
1730+
check=False,
1731+
)
1732+
assert build_result.returncode == 0, (
1733+
f"wheel build failed:\nstdout: {build_result.stdout}\n"
1734+
f"stderr: {build_result.stderr}"
1735+
)
1736+
1737+
wheels = list(dist_dir.glob("*.whl"))
1738+
assert len(wheels) == 1, f"Expected 1 wheel, found {len(wheels)}"
1739+
1740+
with zipfile.ZipFile(wheels[0], "r") as whl:
1741+
names = whl.namelist()
1742+
1743+
assert not any(SCM_VERSION_FILENAME in n for n in names), (
1744+
f"{SCM_VERSION_FILENAME} must not be in wheel: {names}"
1745+
)
1746+
assert not any(SCM_FILE_LIST_FILENAME in n for n in names), (
1747+
f"{SCM_FILE_LIST_FILENAME} must not be in wheel: {names}"
1748+
)
1749+
1750+
16751751
class TestIsInsidePackage:
16761752
"""Unit tests for _is_inside_package."""
16771753

0 commit comments

Comments
 (0)