2424 from pex .hashing import HintedDigest
2525
2626
27+ def _project_name_re (project_name ):
28+ # type: (ProjectName) -> str
29+ return project_name .normalized .replace ("-" , "[-_.]+" )
30+
31+
2732def _built_source_dist_pattern (project_name ):
2833 # type: (ProjectName) -> Pattern
2934 return re .compile (
30- r"(?P<project_name>{project_name })-(?P<version>.+)\.zip" .format (
31- project_name = project_name . normalized . replace ( "-" , "[-_.]+" )
35+ r"(?P<project_name>{project_name_re })-(?P<version>.+)\.zip" .format (
36+ project_name_re = _project_name_re ( project_name )
3237 ),
3338 re .IGNORECASE ,
3439 )
@@ -78,8 +83,11 @@ def fingerprint_downloaded_vcs_archive(
7883 return Fingerprint .from_digest (digest ), archive_path
7984
8085
81- def _vcs_dir_filter (vcs ):
82- # type: (VCS.Value) -> Callable[[Text], bool]
86+ def _vcs_dir_filter (
87+ vcs , # type: VCS.Value
88+ project_name , # type: ProjectName
89+ ):
90+ # type: (...) -> Callable[[Text], bool]
8391
8492 # Ignore VCS control directories for the purposes of fingerprinting the version controlled
8593 # source tree. VCS control directories can contain non-reproducible content (Git at least
@@ -92,10 +100,27 @@ def _vcs_dir_filter(vcs):
92100 # hashes the same.
93101 vcs_control_dir = ".{vcs}" .format (vcs = vcs )
94102
95- return lambda dir_path : (
96- not is_pyc_dir (dir_path ) and os .path .basename (dir_path ) != vcs_control_dir
103+ # N.B.: If the VCS project uses setuptools as its build backend, depending on the version of
104+ # Pip used, the VCS checkout can have a `<project name>.egg-info/` directory littering its root
105+ # left over from Pip generating project metadata to determine version and dependencies. No other
106+ # well known build-backend has this problem at this time (checked hatchling, poetry-core,
107+ # pdm-backend and uv_build).
108+ # C.F.: https://github.com/pypa/pip/pull/13602
109+ egg_info_dir_re = re .compile (
110+ r"^{project_name_re}\.egg-info$" .format (project_name_re = _project_name_re (project_name )),
111+ re .IGNORECASE ,
97112 )
98113
114+ def vcs_dir_filter (dir_path ):
115+ # type: (Text) -> bool
116+ if is_pyc_dir (dir_path ):
117+ return False
118+
119+ base_dir_name = dir_path .split (os .sep )[0 ]
120+ return base_dir_name != vcs_control_dir and not egg_info_dir_re .match (base_dir_name )
121+
122+ return vcs_dir_filter
123+
99124
100125def _vcs_file_filter (vcs ):
101126 # type: (VCS.Value) -> Callable[[Text], bool]
@@ -132,12 +157,13 @@ def digest_vcs_archive(
132157 zip_path = archive_path ,
133158 digest = digest ,
134159 relpath = top_dir ,
135- dir_filter = _vcs_dir_filter (vcs ),
160+ dir_filter = _vcs_dir_filter (vcs , project_name ),
136161 file_filter = _vcs_file_filter (vcs ),
137162 )
138163
139164
140165def digest_vcs_repo (
166+ project_name , # type: ProjectName
141167 repo_path , # type: str
142168 vcs , # type: VCS.Value
143169 digest , # type: HintedDigest
@@ -148,6 +174,6 @@ def digest_vcs_repo(
148174 hashing .dir_hash (
149175 directory = os .path .join (repo_path , subdirectory ) if subdirectory else repo_path ,
150176 digest = digest ,
151- dir_filter = _vcs_dir_filter (vcs ),
177+ dir_filter = _vcs_dir_filter (vcs , project_name ),
152178 file_filter = _vcs_file_filter (vcs ),
153179 )
0 commit comments