@@ -690,6 +690,14 @@ def default_build_sdist(
690690 sdist_root_dir = sdist_root_dir ,
691691 build_dir = build_dir ,
692692 )
693+ has_git_metadata = build_dir .joinpath (".git" ).exists () or (
694+ sdist_root_dir != build_dir and sdist_root_dir .joinpath (".git" ).exists ()
695+ )
696+ if not has_git_metadata :
697+ ensure_git_archival (
698+ version = version ,
699+ target_dir = build_dir ,
700+ )
693701 # The format argument is specified based on
694702 # https://peps.python.org/pep-0517/#build-sdist.
695703 with tarfile .open (sdist_filename , "x:gz" , format = tarfile .PAX_FORMAT ) as sdist :
@@ -771,6 +779,74 @@ def ensure_pkg_info(
771779 return had_pkg_info
772780
773781
782+ # Template .git_archival.txt files contain "$Format:…$" placeholders that
783+ # `git archive` expands into real values. If they survive unexpanded,
784+ # setuptools-scm detects "$FORMAT" in the node field and returns no version
785+ # (see setuptools_scm.git.archival_to_version).
786+ _UNPROCESSED_ARCHIVAL_MARKER = "$Format:"
787+
788+ # Dummy commit hash used when synthesizing .git_archival.txt without a
789+ # real git repository. The value is never interpreted by setuptools-scm
790+ # beyond checking that it is not an unprocessed $Format:…$ placeholder.
791+ _DUMMY_NODE = "0" * 40
792+
793+ # Fields that must be present for setuptools-scm to resolve a version.
794+ _REQUIRED_ARCHIVAL_FIELDS = {"node" , "describe-name" }
795+
796+ _GIT_ARCHIVAL_CONTENT = """\
797+ node: {node}
798+ describe-name: {version}
799+ """
800+
801+
802+ def ensure_git_archival (
803+ * ,
804+ version : Version ,
805+ target_dir : pathlib .Path ,
806+ ) -> bool :
807+ """Ensure that sdist has a usable ``.git_archival.txt`` for setuptools-scm.
808+
809+ When building from source archives without a ``.git`` directory,
810+ setuptools-scm cannot determine the package version. A synthesized
811+ ``.git_archival.txt`` provides the version through the ``describe-name``
812+ field so that setuptools-scm resolves it without requiring an environment
813+ variable override.
814+
815+ See https://setuptools-scm.readthedocs.io/en/latest/usage/#git-archives
816+
817+ Returns True if a valid archival file was already present (no changes
818+ made), False if a new file was written (file was missing or contained
819+ unprocessed placeholders).
820+ """
821+ archival_file = target_dir .joinpath (".git_archival.txt" )
822+
823+ if archival_file .is_file ():
824+ content = archival_file .read_text ()
825+ parsed_fields : dict [str , str ] = {}
826+ for line in content .splitlines ():
827+ if ":" not in line :
828+ continue
829+ key , _ , value = line .partition (":" )
830+ parsed_fields [key .strip ()] = value .strip ()
831+ if (
832+ _UNPROCESSED_ARCHIVAL_MARKER not in content
833+ and _REQUIRED_ARCHIVAL_FIELDS <= set (parsed_fields )
834+ and all (parsed_fields .get (f ) for f in _REQUIRED_ARCHIVAL_FIELDS )
835+ ):
836+ logger .debug ("valid .git_archival.txt already present in %s" , target_dir )
837+ return True
838+ logger .warning ("replacing invalid .git_archival.txt in %s" , target_dir )
839+
840+ archival_file .write_text (
841+ _GIT_ARCHIVAL_CONTENT .format (
842+ node = _DUMMY_NODE ,
843+ version = str (version ),
844+ )
845+ )
846+ logger .info ("created .git_archival.txt for version %s in %s" , version , target_dir )
847+ return False
848+
849+
774850def validate_sdist_filename (
775851 req : Requirement ,
776852 version : Version ,
0 commit comments