Skip to content

Commit d7d9adf

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 29350ce commit d7d9adf

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

src/fromager/sources.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,11 @@ def default_build_sdist(
681681
sdist_root_dir=sdist_root_dir,
682682
build_dir=build_dir,
683683
)
684+
if not build_dir.joinpath(".git").exists():
685+
ensure_git_archival(
686+
version=version,
687+
target_dir=build_dir,
688+
)
684689
# The format argument is specified based on
685690
# https://peps.python.org/pep-0517/#build-sdist.
686691
with tarfile.open(sdist_filename, "x:gz", format=tarfile.PAX_FORMAT) as sdist:
@@ -762,6 +767,62 @@ def ensure_pkg_info(
762767
return had_pkg_info
763768

764769

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

0 commit comments

Comments
 (0)