2121 tomllib = None
2222
2323try : # Prefer the installed packaging module when available.
24+ from packaging .markers import InvalidMarker , Marker
2425 from packaging .specifiers import InvalidSpecifier , SpecifierSet
2526 from packaging .version import InvalidVersion , Version
2627except ImportError : # pragma: no cover - fallback for environments without top-level packaging
28+ from pip ._vendor .packaging .markers import InvalidMarker , Marker
2729 from pip ._vendor .packaging .specifiers import InvalidSpecifier , SpecifierSet
2830 from pip ._vendor .packaging .version import InvalidVersion , Version
2931
3739_PACKAGE_LOCK_LINE = re .compile (r"([A-Za-z0-9][A-Za-z0-9_.-]*)==([^\s]+)\Z" )
3840_IMAGE_DIGEST = re .compile (r"sha256:[0-9a-f]{64}\Z" )
3941_BUILDER_IMAGE = "github-hosted:ubuntu-latest/python-3.11"
42+ # Extras installed by the release workflow (`.github/workflows/release.yml` runs
43+ # `pip install ... ".[all,test]"` before capturing the SBOM), so the captured
44+ # closure must include every marker-applicable requirement they declare.
45+ _RELEASE_EXTRAS = ("all" , "test" )
4046_BUILDER_TOOLCHAIN = {
4147 "build" : "1.5.0" ,
4248 "pip" : "26.2" ,
@@ -271,10 +277,13 @@ def _version_satisfies(version: str, specifier: str) -> bool:
271277
272278
273279def _declared_dependencies (root : Path ) -> dict [str , str | None ]:
274- """Return {canonical_name: specifier} from pyproject.toml [project].dependencies .
280+ """Return {canonical_name: specifier} required in the captured SBOM closure .
275281
276- Only core runtime dependencies are validated against the SBOM closure.
277- Optional extras are intentionally excluded.
282+ Covers [project].dependencies plus every requirement declared by the extras
283+ the release workflow installs (``_RELEASE_EXTRAS``). PEP 508 environment
284+ markers are evaluated against the running interpreter, which in the release
285+ workflow is the same environment that captures the SBOM; requirements whose
286+ markers do not apply are not required.
278287 """
279288 pyproject = root / "pyproject.toml"
280289 try :
@@ -288,9 +297,18 @@ def _declared_dependencies(root: Path) -> dict[str, str | None]:
288297 except (KeyError , ValueError ):
289298 parsed = {}
290299 project = parsed .get ("project" , {}) if isinstance (parsed , dict ) else {}
291- core = project .get ("dependencies" , []) if isinstance (project , dict ) else []
292- if isinstance (core , list ):
293- requirements .extend (item for item in core if isinstance (item , str ))
300+ if isinstance (project , dict ):
301+ core = project .get ("dependencies" , [])
302+ if isinstance (core , list ):
303+ requirements .extend (item for item in core if isinstance (item , str ))
304+ extras = project .get ("optional-dependencies" , {})
305+ if isinstance (extras , dict ):
306+ for extra in _RELEASE_EXTRAS :
307+ group = extras .get (extra )
308+ if isinstance (group , list ):
309+ requirements .extend (
310+ item for item in group if isinstance (item , str )
311+ )
294312 else :
295313 project = re .search (r"(?ms)^\[project\]\s*(.*?)(?=^\[|\Z)" , raw )
296314 if project is not None :
@@ -299,13 +317,38 @@ def _declared_dependencies(root: Path) -> dict[str, str | None]:
299317 )
300318 if deps_block is not None :
301319 requirements .extend (re .findall (r'"([^"]+)"' , deps_block .group (1 )))
320+ extras_table = re .search (
321+ r"(?ms)^\[project\.optional-dependencies\]\s*(.*?)(?=^\[|\Z)" , raw ,
322+ )
323+ if extras_table is not None :
324+ for extra in _RELEASE_EXTRAS :
325+ group = re .search (
326+ r"(?m)^" + re .escape (extra ) + r"\s*=\s*\[(.*?)\]" ,
327+ extras_table .group (1 ), re .DOTALL ,
328+ )
329+ if group is not None :
330+ requirements .extend (re .findall (r'"([^"]+)"' , group .group (1 )))
302331 deps : dict [str , str | None ] = {}
303332 for requirement in requirements :
304333 if not isinstance (requirement , str ) or not requirement .strip ():
305334 continue
306335 name , specifier = _parse_requirement (requirement )
307- if name :
308- deps [_canonical_package_name (name )] = specifier
336+ canonical = _canonical_package_name (name )
337+ if not canonical or canonical == PACKAGE :
338+ continue
339+ marker_text = requirement .split (";" , 1 )[1 ].strip () if ";" in requirement else ""
340+ if marker_text :
341+ try :
342+ applies = Marker (marker_text ).evaluate ()
343+ except InvalidMarker as exc :
344+ raise EvidenceError (
345+ "pyproject.toml declares an unparsable environment marker: "
346+ + marker_text
347+ ) from exc
348+ if not applies :
349+ continue
350+ if canonical not in deps or deps [canonical ] is None :
351+ deps [canonical ] = specifier
309352 return deps
310353
311354def _python_sbom_packages (document : dict [str , Any ]) -> set [tuple [str , str ]]:
0 commit comments