Skip to content

Commit 23827f7

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. The archival file is only written in default_build_sdist when no .git directory is present, mirroring the guard used elsewhere. Closes: #961 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 0a941fd commit 23827f7

2 files changed

Lines changed: 126 additions & 0 deletions

File tree

src/fromager/sources.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,11 @@ def default_build_sdist(
690690
sdist_root_dir=sdist_root_dir,
691691
build_dir=build_dir,
692692
)
693+
if not build_dir.joinpath(".git").exists():
694+
ensure_git_archival(
695+
version=version,
696+
target_dir=build_dir,
697+
)
693698
# The format argument is specified based on
694699
# https://peps.python.org/pep-0517/#build-sdist.
695700
with tarfile.open(sdist_filename, "x:gz", format=tarfile.PAX_FORMAT) as sdist:
@@ -771,6 +776,62 @@ def ensure_pkg_info(
771776
return had_pkg_info
772777

773778

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

tests/test_sources.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,68 @@ def test_scan_compiled_extensions(
312312
assert matches == [pathlib.Path(filename)]
313313
else:
314314
assert matches == []
315+
316+
317+
class TestEnsureGitArchival:
318+
"""Tests for ensure_git_archival()."""
319+
320+
def test_creates_file_when_missing(self, tmp_path: pathlib.Path) -> None:
321+
"""Verify file is created with correct content when absent."""
322+
version = Version("1.2.3")
323+
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)
324+
archival = tmp_path / ".git_archival.txt"
325+
326+
assert result is False
327+
assert archival.is_file()
328+
content = archival.read_text()
329+
assert "describe-name: 1.2.3\n" in content
330+
assert "node: " in content
331+
assert "$Format:" not in content
332+
333+
def test_replaces_unprocessed_file(self, tmp_path: pathlib.Path) -> None:
334+
"""Verify unprocessed template file is replaced."""
335+
archival = tmp_path / ".git_archival.txt"
336+
archival.write_text(
337+
"node: $Format:%H$\n"
338+
"node-date: $Format:%cI$\n"
339+
"describe-name: $Format:%(describe:tags=true)$\n"
340+
)
341+
version = Version("4.5.6")
342+
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)
343+
344+
assert result is False
345+
content = archival.read_text()
346+
assert "describe-name: 4.5.6\n" in content
347+
assert "$Format:" not in content
348+
349+
def test_preserves_valid_file(self, tmp_path: pathlib.Path) -> None:
350+
"""Verify a valid archival file is left untouched."""
351+
archival = tmp_path / ".git_archival.txt"
352+
original = (
353+
"node: abc123\n"
354+
"node-date: 2025-01-01T00:00:00+00:00\n"
355+
"describe-name: v1.0.0-0-gabc123\n"
356+
)
357+
archival.write_text(original)
358+
version = Version("9.9.9")
359+
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)
360+
361+
assert result is True
362+
assert archival.read_text() == original
363+
364+
def test_preserves_valid_file_with_nonzero_distance(
365+
self, tmp_path: pathlib.Path
366+
) -> None:
367+
"""Verify a valid archival file from a post-tag commit is preserved."""
368+
archival = tmp_path / ".git_archival.txt"
369+
original = (
370+
"node: 5d8d986d96be4575ae4b9fe9675f7eb3623057cb\n"
371+
"node-date: 2026-03-19T06:57:11+00:00\n"
372+
"describe-name: 0.77.0-13-g5d8d986\n"
373+
)
374+
archival.write_text(original)
375+
version = Version("0.77.0")
376+
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)
377+
378+
assert result is True
379+
assert archival.read_text() == original

0 commit comments

Comments
 (0)