Skip to content

Commit ed3b214

Browse files
committed
Merge branch 'fix/sbom_project_relationships' into 'master'
fix: include all used components in the project's SPDX dependencies Closes IDF-11410 See merge request espressif/esp-idf-sbom!58
2 parents f24a5d6 + 169d7f0 commit ed3b214

File tree

1 file changed

+62
-6
lines changed

1 file changed

+62
-6
lines changed

esp_idf_sbom/libsbom/spdx.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,27 @@ def get_tags(self, exclude_dirs: Optional[List[str]]=None) -> SPDXTags:
914914
tags |= package.tags
915915
return tags
916916

917+
def get_reachable_components(self, names: List[str]) -> Set[str]:
918+
# Return a set of all components that can be reached directly or
919+
# indirectly through requirements from the components in the names
920+
# list, including the names themselves.
921+
build_components = self.proj_desc['build_component_info']
922+
reachable = set()
923+
seen = set()
924+
todo = set(names)
925+
while todo:
926+
name = todo.pop()
927+
seen.add(name)
928+
if not self._component_used(build_components[name]):
929+
continue
930+
reachable.add(name)
931+
info = build_components[name]
932+
reqs = set(info['reqs'] + info['priv_reqs'] + info['managed_reqs'] + info['managed_priv_reqs'])
933+
todo |= reqs - seen
934+
return reachable
935+
917936
def add_relationships(self) -> None:
937+
918938
if not self.manifest['cpe']:
919939
# CPE for whole espressif:esp-idf.
920940
ver = self.proj_desc['git_revision']
@@ -926,10 +946,18 @@ def add_relationships(self) -> None:
926946
# Dependency on toolchain.
927947
self['Relationship'] += [f'{self["SPDXID"][0]} DEPENDS_ON {self.toolchain["SPDXID"][0]}']
928948

929-
# Dependencies on components. Only components, which are not required by other
930-
# components are added as direct dependency for the project binary.
949+
# 1. Gather all component requirements into a `requirements set`.
950+
# 2. Gather components not present in the `requirements set` into the
951+
# `project requirements set`.
952+
# 3. Create a `reachable set` by going through direct and indirect dependencies
953+
# reachable from the `project requirements set`.
954+
# 4. For each component, verify if it is in the `reachable set`. If it is
955+
# not, add it to the `project requirements set` and include its direct
956+
# and indirect dependencies in the `reachable set`.
957+
931958
build_components = self.proj_desc['build_component_info']
932-
reqs = set()
959+
960+
reqs: Set[str] = set()
933961
for name, info in build_components.items():
934962
if not self._component_used(build_components[name]):
935963
# Do not include requirements from a component that the project
@@ -942,15 +970,43 @@ def add_relationships(self) -> None:
942970
# If nvs_flash is not linked, newlib is not included in the SPDX
943971
# project package relationships.
944972
continue
945-
tmp = info['reqs'] + info['priv_reqs'] + info['managed_reqs'] + info['managed_priv_reqs']
946-
reqs |= set(tmp)
973+
reqs |= set(info['reqs'] + info['priv_reqs'] + info['managed_reqs'] + info['managed_priv_reqs'])
947974

975+
# Only components, which are not required by other components are added as direct
976+
# dependency for the project binary.
977+
proj_reqs: List[str] = []
948978
for name, info in build_components.items():
949979
if name in reqs:
950980
continue
951981
if not self._component_used(build_components[name]):
952982
continue
953-
self['Relationship'] += [f'{self["SPDXID"][0]} DEPENDS_ON {self.components[name]["SPDXID"][0]}']
983+
proj_reqs.append(name)
984+
985+
# Get all used components reachable from the immediate project dependencies.
986+
reachable = self.get_reachable_components(proj_reqs)
987+
988+
# Ensure that all components included in the project can be reached from the
989+
# project's SPDX package dependency tree. It's possible for a component to be
990+
# required by another component yet still not be reachable from the project
991+
# package. This situation can occur if the project doesn't explicitly define
992+
# its dependencies and instead depends on the build system's current behavior
993+
# which includes all discovered components in the build. For instance, esp_wifi
994+
# depends on wpa_supplicant and vice versa, but if the main component doesn't
995+
# specify a dependency on, say, esp_wifi, neither wpa_supplicant nor esp_wifi
996+
# will be included in the SPDX package dependencies.
997+
for name, info in build_components.items():
998+
if name in reachable:
999+
continue
1000+
if not self._component_used(build_components[name]):
1001+
continue
1002+
# The component is not reachable, so include it as a direct dependency in the
1003+
# SPDX project package. Additionally, add all its direct and indirect
1004+
# dependencies into the set of reachable components.
1005+
proj_reqs.append(name)
1006+
reachable |= self.get_reachable_components([name])
1007+
1008+
for req in proj_reqs:
1009+
self['Relationship'] += [f'{self["SPDXID"][0]} DEPENDS_ON {self.components[req]["SPDXID"][0]}']
9541010

9551011
def dump(self) -> str:
9561012
out = super().dump()

0 commit comments

Comments
 (0)