Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
61 changes: 61 additions & 0 deletions src/fromager/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,11 @@ def default_build_sdist(
sdist_root_dir=sdist_root_dir,
build_dir=build_dir,
)
if not build_dir.joinpath(".git").exists():
ensure_git_archival(
version=version,
target_dir=build_dir,
)
# The format argument is specified based on
# https://peps.python.org/pep-0517/#build-sdist.
with tarfile.open(sdist_filename, "x:gz", format=tarfile.PAX_FORMAT) as sdist:
Expand Down Expand Up @@ -762,6 +767,62 @@ def ensure_pkg_info(
return had_pkg_info


# Template .git_archival.txt files contain "$Format:…$" placeholders that
# `git archive` expands into real values. If they survive unexpanded,
# setuptools-scm detects "$FORMAT" in the node field and returns no version
# (see setuptools_scm.git.archival_to_version).
_UNPROCESSED_ARCHIVAL_MARKER = "$Format:"

# Dummy commit hash used when synthesizing .git_archival.txt without a
# real git repository. The value is never interpreted by setuptools-scm
# beyond checking that it is not an unprocessed $Format:…$ placeholder.
_DUMMY_NODE = "0" * 40

_GIT_ARCHIVAL_CONTENT = """\
node: {node}
node-date: 1970-01-01T00:00:00+00:00
describe-name: {version}-0-g{node}
"""


def ensure_git_archival(
*,
version: Version,
target_dir: pathlib.Path,
) -> bool:
"""Ensure that sdist has a usable ``.git_archival.txt`` for setuptools-scm.

When building from source archives without a ``.git`` directory,
setuptools-scm cannot determine the package version. A synthesized
``.git_archival.txt`` provides the version through the ``describe-name``
field so that setuptools-scm resolves it without requiring an environment
variable override.

See https://setuptools-scm.readthedocs.io/en/latest/usage/#git-archives

Returns True if a valid archival file was already present (no changes
made), False if a new file was written (file was missing or contained
unprocessed placeholders).
"""
archival_file = target_dir.joinpath(".git_archival.txt")

if archival_file.is_file():
content = archival_file.read_text()
if _UNPROCESSED_ARCHIVAL_MARKER not in content:
logger.debug("valid .git_archival.txt already present in %s", target_dir)
return True
logger.warning("replacing unprocessed .git_archival.txt in %s", target_dir)

archival_file.write_text(
_GIT_ARCHIVAL_CONTENT.format(
node=_DUMMY_NODE,
version=str(version),
)
)
logger.info("created .git_archival.txt for version %s in %s", version, target_dir)
return False


def validate_sdist_filename(
req: Requirement,
version: Version,
Expand Down
48 changes: 48 additions & 0 deletions tests/test_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,51 @@ def test_validate_sdist_file(
else:
with pytest.raises(ValueError):
sources.validate_sdist_filename(req, version, sdist_file)


class TestEnsureGitArchival:
"""Tests for ensure_git_archival()."""

def test_creates_file_when_missing(self, tmp_path: pathlib.Path) -> None:
"""Verify file is created with correct content when absent."""
version = Version("1.2.3")
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)
archival = tmp_path / ".git_archival.txt"

assert result is False
assert archival.is_file()
content = archival.read_text()
assert "describe-name: 1.2.3-0-g" in content
assert "node: " in content
assert "$Format:" not in content

def test_replaces_unprocessed_file(self, tmp_path: pathlib.Path) -> None:
"""Verify unprocessed template file is replaced."""
archival = tmp_path / ".git_archival.txt"
archival.write_text(
"node: $Format:%H$\n"
"node-date: $Format:%cI$\n"
"describe-name: $Format:%(describe:tags=true)$\n"
)
version = Version("4.5.6")
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)

assert result is False
content = archival.read_text()
assert "describe-name: 4.5.6-0-g" in content
assert "$Format:" not in content

def test_preserves_valid_file(self, tmp_path: pathlib.Path) -> None:
"""Verify a valid archival file is left untouched."""
archival = tmp_path / ".git_archival.txt"
original = (
"node: abc123\n"
"node-date: 2025-01-01T00:00:00+00:00\n"
"describe-name: v1.0.0-0-gabc123\n"
)
archival.write_text(original)
version = Version("9.9.9")
result = sources.ensure_git_archival(version=version, target_dir=tmp_path)

assert result is True
assert archival.read_text() == original
Loading