Skip to content

Commit 87d777b

Browse files
feat(sources): generate .git_archival.txt for setuptools-scm builds
Packages using setuptools-scm fail when built from source archives without .git metadata. Add ensure_git_archival() to synthesize a .git_archival.txt with the resolved version, which setuptools-scm reads before PKG-INFO in its fallback chain. Closes: #961 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 8d850ff commit 87d777b

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

src/fromager/sources.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,12 +571,15 @@ def prepare_new_source(
571571
) -> None:
572572
"""Default steps for new sources
573573
574+
- ensure .git_archival.txt for setuptools-scm (sdist sources only)
574575
- patch sources
575576
- apply project overrides from settings
576577
- vendor Rust dependencies
577578
578579
:func:`~default_prepare_source` runs this function when the sources are new.
579580
"""
581+
if not (source_root_dir / ".git").exists():
582+
ensure_git_archival(version=version, sdist_root_dir=source_root_dir)
580583
patch_source(ctx, source_root_dir, req, version)
581584
pyproject.apply_project_override(
582585
ctx=ctx,
@@ -681,6 +684,10 @@ def default_build_sdist(
681684
sdist_root_dir=sdist_root_dir,
682685
build_dir=build_dir,
683686
)
687+
ensure_git_archival(
688+
version=version,
689+
sdist_root_dir=build_dir,
690+
)
684691
# The format argument is specified based on
685692
# https://peps.python.org/pep-0517/#build-sdist.
686693
with tarfile.open(sdist_filename, "x:gz", format=tarfile.PAX_FORMAT) as sdist:
@@ -762,6 +769,66 @@ def ensure_pkg_info(
762769
return had_pkg_info
763770

764771

772+
# Template .git_archival.txt files contain "$Format:…$" placeholders that
773+
# `git archive` expands into real values. If they survive unexpanded,
774+
# setuptools-scm detects "$FORMAT" in the node field and returns no version
775+
# (see setuptools_scm.git.archival_to_version).
776+
_UNPROCESSED_ARCHIVAL_MARKER = "$Format:"
777+
778+
# Dummy commit hash used when synthesizing .git_archival.txt without a
779+
# real git repository. The value is never interpreted by setuptools-scm
780+
# beyond checking that it is not an unprocessed $Format:…$ placeholder.
781+
_DUMMY_NODE = "0" * 40
782+
783+
_GIT_ARCHIVAL_CONTENT = """\
784+
node: {node}
785+
node-date: 1970-01-01T00:00:00+00:00
786+
describe-name: {version}-0-g{node}
787+
"""
788+
789+
790+
def ensure_git_archival(
791+
*,
792+
version: Version,
793+
sdist_root_dir: pathlib.Path,
794+
) -> bool:
795+
"""Ensure that sdist has a usable ``.git_archival.txt`` for setuptools-scm.
796+
797+
When building from source archives without a ``.git`` directory,
798+
setuptools-scm cannot determine the package version. A synthesized
799+
``.git_archival.txt`` provides the version through the ``describe-name``
800+
field so that setuptools-scm resolves it without requiring an environment
801+
variable override.
802+
803+
See https://setuptools-scm.readthedocs.io/en/latest/usage/#git-archives
804+
805+
Returns True if a valid archival file was already present (no changes
806+
made), False if a new file was written (file was missing or contained
807+
unprocessed placeholders).
808+
"""
809+
archival_file = sdist_root_dir / ".git_archival.txt"
810+
811+
if archival_file.is_file():
812+
content = archival_file.read_text()
813+
if _UNPROCESSED_ARCHIVAL_MARKER not in content:
814+
logger.debug(
815+
"valid .git_archival.txt already present in %s", sdist_root_dir
816+
)
817+
return True
818+
logger.warning("replacing unprocessed .git_archival.txt in %s", sdist_root_dir)
819+
820+
archival_file.write_text(
821+
_GIT_ARCHIVAL_CONTENT.format(
822+
node=_DUMMY_NODE,
823+
version=str(version),
824+
)
825+
)
826+
logger.info(
827+
"created .git_archival.txt for version %s in %s", version, sdist_root_dir
828+
)
829+
return False
830+
831+
765832
def validate_sdist_filename(
766833
req: Requirement,
767834
version: Version,

tests/test_sources.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,51 @@ def test_validate_sdist_file(
275275
else:
276276
with pytest.raises(ValueError):
277277
sources.validate_sdist_filename(req, version, sdist_file)
278+
279+
280+
class TestEnsureGitArchival:
281+
"""Tests for ensure_git_archival()."""
282+
283+
def test_creates_file_when_missing(self, tmp_path: pathlib.Path) -> None:
284+
"""Verify file is created with correct content when absent."""
285+
version = Version("1.2.3")
286+
result = sources.ensure_git_archival(version=version, sdist_root_dir=tmp_path)
287+
archival = tmp_path / ".git_archival.txt"
288+
289+
assert result is False
290+
assert archival.is_file()
291+
content = archival.read_text()
292+
assert "describe-name: 1.2.3-0-g" in content
293+
assert "node: " in content
294+
assert "$Format:" not in content
295+
296+
def test_replaces_unprocessed_file(self, tmp_path: pathlib.Path) -> None:
297+
"""Verify unprocessed template file is replaced."""
298+
archival = tmp_path / ".git_archival.txt"
299+
archival.write_text(
300+
"node: $Format:%H$\n"
301+
"node-date: $Format:%cI$\n"
302+
"describe-name: $Format:%(describe:tags=true)$\n"
303+
)
304+
version = Version("4.5.6")
305+
result = sources.ensure_git_archival(version=version, sdist_root_dir=tmp_path)
306+
307+
assert result is False
308+
content = archival.read_text()
309+
assert "describe-name: 4.5.6-0-g" in content
310+
assert "$Format:" not in content
311+
312+
def test_preserves_valid_file(self, tmp_path: pathlib.Path) -> None:
313+
"""Verify a valid archival file is left untouched."""
314+
archival = tmp_path / ".git_archival.txt"
315+
original = (
316+
"node: abc123\n"
317+
"node-date: 2025-01-01T00:00:00+00:00\n"
318+
"describe-name: v1.0.0-0-gabc123\n"
319+
)
320+
archival.write_text(original)
321+
version = Version("9.9.9")
322+
result = sources.ensure_git_archival(version=version, sdist_root_dir=tmp_path)
323+
324+
assert result is True
325+
assert archival.read_text() == original

0 commit comments

Comments
 (0)