scripts/sbom: add additional dependency sources for cmd graph

Add hardcoded dependencies and .incbin directive parsing to
discover dependencies not tracked by .cmd files.

Assisted-by: Cursor:claude-sonnet-4-5
Assisted-by: OpenCode:GLM-4-7
Co-developed-by: Maximilian Huber <maximilian.huber@tngtech.com>
Signed-off-by: Maximilian Huber <maximilian.huber@tngtech.com>
Signed-off-by: Luis Augenstein <luis.augenstein@tngtech.com>
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
This commit is contained in:
Luis Augenstein 2026-05-18 08:20:53 +02:00 committed by Greg Kroah-Hartman
parent 9c16c1ea46
commit d764b54e28
3 changed files with 161 additions and 1 deletions

View File

@ -2,15 +2,24 @@
# Copyright (C) 2025 TNG Technology Consulting GmbH
from dataclasses import dataclass, field
from itertools import chain
import logging
import os
from typing import Iterator, Protocol
from sbom import sbom_logging
from sbom.cmd_graph.cmd_file import CmdFile
from sbom.cmd_graph.hardcoded_dependencies import get_hardcoded_dependencies
from sbom.cmd_graph.incbin_parser import parse_incbin_statements
from sbom.path_utils import PathStr, has_link, is_relative_to
@dataclass
class IncbinDependency:
node: "CmdGraphNode"
full_statement: str
class CmdGraphNodeConfig(Protocol):
obj_tree: PathStr
src_tree: PathStr
@ -28,11 +37,17 @@ class CmdGraphNode:
"""Parsed .cmd file describing how the file at absolute_path was built, or None if not available."""
cmd_file_dependencies: list["CmdGraphNode"] = field(default_factory=list)
incbin_dependencies: list[IncbinDependency] = field(default_factory=list)
hardcoded_dependencies: list["CmdGraphNode"] = field(default_factory=list)
@property
def children(self) -> Iterator["CmdGraphNode"]:
seen: set[PathStr] = set()
for node in self.cmd_file_dependencies:
for node in chain(
self.cmd_file_dependencies,
(dep.node for dep in self.incbin_dependencies),
self.hardcoded_dependencies,
):
if node.absolute_path not in seen:
seen.add(node.absolute_path)
yield node
@ -95,6 +110,13 @@ class CmdGraphNode:
def _build_child_node(child_path: PathStr) -> "CmdGraphNode":
return CmdGraphNode.create(child_path, config, cache, depth + 1)
node.hardcoded_dependencies = [
_build_child_node(hardcoded_dependency_path)
for hardcoded_dependency_path in get_hardcoded_dependencies(
target_path_absolute, config.obj_tree, config.src_tree
)
]
if cmd_file is not None:
node.cmd_file_dependencies = [
_build_child_node(cmd_file_dependency_path)
@ -103,6 +125,15 @@ class CmdGraphNode:
)
]
if node.absolute_path.endswith(".S"):
node.incbin_dependencies = [
IncbinDependency(
node=_build_child_node(incbin_statement.path),
full_statement=incbin_statement.full_statement,
)
for incbin_statement in parse_incbin_statements(node.absolute_path)
]
return node

View File

@ -0,0 +1,87 @@
# SPDX-License-Identifier: GPL-2.0-only OR MIT
# Copyright (C) 2025 TNG Technology Consulting GmbH
import os
from typing import Callable
import sbom.sbom_logging as sbom_logging
from sbom.path_utils import PathStr, is_relative_to
from sbom.environment import Environment
HARDCODED_DEPENDENCIES: dict[str, list[str]] = {
# defined in linux/Kbuild
"include/generated/rq-offsets.h": ["kernel/sched/rq-offsets.s"],
"kernel/sched/rq-offsets.s": ["include/generated/asm-offsets.h"],
"include/generated/bounds.h": ["kernel/bounds.s"],
"include/generated/asm-offsets.h": ["arch/{arch}/kernel/asm-offsets.s"],
}
"""
Maps file paths to the list of dependencies required to build them
which are not tracked by the .cmd dependency mechanism.
Paths are relative to either the source tree or the object tree.
"""
def get_hardcoded_dependencies(path: PathStr, obj_tree: PathStr, src_tree: PathStr) -> list[PathStr]:
"""
Some files in the kernel build process are not tracked by the .cmd dependency mechanism.
Parsing these dependencies programmatically is too complex for the scope of this project.
Therefore, this function provides manually defined dependencies to be added to the build graph.
Args:
path: absolute path to a file within the src tree or object tree.
obj_tree: absolute Path to the base directory of the object tree.
src_tree: absolute Path to the `linux` source directory.
Returns:
list[PathStr]: A list of dependency file paths (relative to the object tree) required to build the file at the given path.
"""
if is_relative_to(path, obj_tree):
path = os.path.relpath(path, obj_tree)
elif is_relative_to(path, src_tree):
path = os.path.relpath(path, src_tree)
if path not in HARDCODED_DEPENDENCIES:
return []
template_variables: dict[str, Callable[[], str | None]] = {
"arch": lambda: _get_arch(path),
}
dependencies: list[PathStr] = []
for dependency_template in HARDCODED_DEPENDENCIES[path]:
dependency = _evaluate_template(dependency_template, template_variables)
if dependency is None:
continue
if os.path.exists(os.path.join(obj_tree, dependency)):
dependencies.append(dependency)
elif os.path.exists(dependency_absolute := os.path.join(src_tree, dependency)):
dependencies.append(os.path.relpath(dependency_absolute, obj_tree))
else:
sbom_logging.error(
"Skip hardcoded dependency '{dependency}' for '{path}' because the dependency lies neither in the src tree nor the object tree.",
dependency=dependency,
path=path,
)
return dependencies
def _evaluate_template(template: str, variables: dict[str, Callable[[], str | None]]) -> str | None:
for key, value_function in variables.items():
template_key = "{" + key + "}"
if template_key in template:
value = value_function()
if value is None:
return None
template = template.replace(template_key, value)
return template
def _get_arch(path: PathStr):
srcarch = Environment.SRCARCH()
if srcarch is None:
sbom_logging.error(
"Skipped architecture specific hardcoded dependency for '{path}' because the SRCARCH environment variable was not set.",
path=path,
)
return None
return srcarch

View File

@ -0,0 +1,42 @@
# SPDX-License-Identifier: GPL-2.0-only OR MIT
# Copyright (C) 2025 TNG Technology Consulting GmbH
from dataclasses import dataclass
import re
from sbom.path_utils import PathStr
INCBIN_PATTERN = re.compile(r'\s*\.incbin\s+"(?P<path>[^"]+)"')
"""Regex pattern for matching `.incbin "<path>"` statements."""
@dataclass
class IncbinStatement:
"""A parsed `.incbin "<path>"` directive."""
path: PathStr
"""path to the file referenced by the `.incbin` directive."""
full_statement: str
"""Full `.incbin "<path>"` statement as it originally appeared in the file."""
def parse_incbin_statements(absolute_path: PathStr) -> list[IncbinStatement]:
"""
Parses `.incbin` directives from an `.S` assembly file.
Args:
absolute_path: Absolute path to the `.S` assembly file.
Returns:
list[IncbinStatement]: Parsed `.incbin` statements.
"""
with open(absolute_path, "rt", encoding="utf-8") as f:
content = f.read()
return [
IncbinStatement(
path=match.group("path"),
full_statement=match.group(0).strip(),
)
for match in INCBIN_PATTERN.finditer(content)
]