From: Luis Augenstein <luis.augenstein@tngtech.com>
To: nathan@kernel.org, nsc@kernel.org
Cc: linux-kbuild@vger.kernel.org, linux-kernel@vger.kernel.org,
akpm@linux-foundation.org, gregkh@linuxfoundation.org,
kstewart@linuxfoundation.org, maximilian.huber@tngtech.com,
Luis Augenstein <luis.augenstein@tngtech.com>
Subject: [PATCH v4 6/7] scripts/sbom: use make_prereqs to determine build inputs
Date: Mon, 2 Nov 2026 21:22:59 +0100 [thread overview]
Message-ID: <20261102202308.3209536-7-luis.augenstein@tngtech.com> (raw)
In-Reply-To: <20261102202308.3209536-1-luis.augenstein@tngtech.com>
Build the list of file dependencies of a target from the
make_prereqs_<target> field and the deps_<target> entries of the .cmd
file instead of parsing the shell command stored in savedcmd_.
The prerequisites include more than KernelSbom wants to track: files
should be tracked if they end up in the kernel image or loadable
modules, but not each and every host tool or helper script used to
build them. Add a manually maintained exclude list for such
dependencies. It is still manual, but much easier to maintain than
the command-specific parsers that were needed to extract the relevant
inputs from each individual build command.
Assisted-by: LLM
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>
---
scripts/sbom/sbom/cmd_graph/cmd_file.py | 47 ++-----------
scripts/sbom/sbom/cmd_graph/cmd_graph.py | 6 +-
scripts/sbom/sbom/cmd_graph/cmd_graph_node.py | 56 ++++++++--------
.../sbom/sbom/cmd_graph/dependency_filter.py | 66 +++++++++++++++++++
scripts/sbom/sbom/spdx_graph/kernel_file.py | 64 +++++++++++-------
5 files changed, 145 insertions(+), 94 deletions(-)
create mode 100644 scripts/sbom/sbom/cmd_graph/dependency_filter.py
diff --git a/scripts/sbom/sbom/cmd_graph/cmd_file.py b/scripts/sbom/sbom/cmd_graph/cmd_file.py
index cc576c7c6be..58ca6aa5946 100644
--- a/scripts/sbom/sbom/cmd_graph/cmd_file.py
+++ b/scripts/sbom/sbom/cmd_graph/cmd_file.py
@@ -5,7 +5,6 @@ import os
import re
from dataclasses import dataclass, field
from sbom.cmd_graph.deps_parser import parse_cmd_file_deps
-from sbom.cmd_graph.savedcmd_parser import parse_inputs_from_commands
import sbom.sbom_logging as sbom_logging
from sbom.path_utils import PathStr
@@ -71,65 +70,27 @@ class CmdFile:
return CmdFile(cmd_file_path, savedcmd, make_prereqs, deps)
- def get_dependencies(
- self: "CmdFile", target_path: PathStr, obj_tree: PathStr, fail_on_unknown_build_command: bool
- ) -> list[PathStr]:
+ def get_dependencies(self: "CmdFile", obj_tree: PathStr) -> list[PathStr]:
"""
Parses all dependencies required to build a target file from its cmd file.
Args:
- target_path: path to the target file relative to `obj_tree`.
obj_tree: absolute path to the object tree.
fail_on_unknown_build_command: Whether to fail if an unknown build command is encountered.
Returns:
list[PathStr]: dependency file paths relative to `obj_tree`.
"""
- input_files: list[PathStr] = [
- str(p) for p in parse_inputs_from_commands(self.savedcmd, fail_on_unknown_build_command)
- ]
+ input_files: list[PathStr] = list(self.make_prereqs)
+
if self.deps:
input_files += [str(p) for p in parse_cmd_file_deps(self.deps)]
- input_files = _expand_resolve_files(input_files, obj_tree)
cmd_file_dependencies: list[PathStr] = []
for input_file in input_files:
- # input files are either absolute or relative to the object tree
+ # input files are either absolute or already relative to the object tree
if os.path.isabs(input_file):
input_file = os.path.relpath(input_file, obj_tree)
- if input_file == target_path:
- # Skip target file to prevent cycles. This is necessary because some multi stage commands first create an output and then pass it as input to the next command, e.g., objcopy.
- continue
cmd_file_dependencies.append(input_file)
unique_cmd_file_dependencies = list(dict.fromkeys(cmd_file_dependencies))
return unique_cmd_file_dependencies
-
-
-def _expand_resolve_files(input_files: list[PathStr], obj_tree: PathStr) -> list[PathStr]:
- """
- Expands resolve files which may reference additional files via '@' notation.
-
- Args:
- input_files (list[PathStr]): List of file paths relative to the object tree, where paths starting with '@' refer to files
- containing further file paths, each on a separate line.
- obj_tree: Absolute path to the root of the object tree.
-
- Returns:
- list[PathStr]: Flattened list of all input file paths, with any nested '@' file references resolved recursively.
- """
- expanded_input_files: list[PathStr] = []
- for input_file in input_files:
- if not input_file.startswith("@"):
- expanded_input_files.append(input_file)
- continue
- resolve_file_path = os.path.join(obj_tree, input_file.removeprefix("@"))
- if not os.path.exists(resolve_file_path):
- sbom_logging.error(
- "Skip resolving '{resolve_file_path}' because the response file does not exist.",
- resolve_file_path=resolve_file_path,
- )
- continue
- with open(resolve_file_path, "rt", encoding="utf-8") as f:
- resolve_file_content = [line_stripped for line in f.readlines() if (line_stripped := line.strip())]
- expanded_input_files += _expand_resolve_files(resolve_file_content, obj_tree)
- return expanded_input_files
diff --git a/scripts/sbom/sbom/cmd_graph/cmd_graph.py b/scripts/sbom/sbom/cmd_graph/cmd_graph.py
index 2f57965237f..f511167de00 100644
--- a/scripts/sbom/sbom/cmd_graph/cmd_graph.py
+++ b/scripts/sbom/sbom/cmd_graph/cmd_graph.py
@@ -3,6 +3,7 @@
from collections import deque
from dataclasses import dataclass, field
+import os
from typing import Iterator
from sbom.cmd_graph.cmd_graph_node import CmdGraphNode, CmdGraphNodeConfig
@@ -29,7 +30,10 @@ class CmdGraph:
CmdGraph: A graph of all build dependencies for the given root files.
"""
node_cache: dict[PathStr, CmdGraphNode] = {}
- root_nodes = [CmdGraphNode.create(root_path, config, node_cache) for root_path in root_paths]
+ root_nodes = [
+ CmdGraphNode.create(os.path.realpath(os.path.join(config.obj_tree, root_path)), config, node_cache)
+ for root_path in root_paths
+ ]
return CmdGraph(root_nodes)
def __iter__(self) -> Iterator[CmdGraphNode]:
diff --git a/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py
index 61f3a8140ce..c608cb1ca82 100644
--- a/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py
+++ b/scripts/sbom/sbom/cmd_graph/cmd_graph_node.py
@@ -9,6 +9,7 @@ from typing import Iterator, Protocol
from sbom import sbom_logging
from sbom.cmd_graph.cmd_file import CmdFile
+from sbom.cmd_graph.dependency_filter import should_include_dependency
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
@@ -65,7 +66,7 @@ class CmdGraphNode:
Dependencies are mainly discovered by parsing the `.<target_path.name>.cmd` file.
Args:
- target_path: Path to the target file relative to obj_tree.
+ target_path: Absolute normalized path to the target file without symbolic links.
config: Config options
cache: Tracks processed nodes to prevent cycles.
depth: Internal parameter to track the current recursion depth.
@@ -76,62 +77,65 @@ class CmdGraphNode:
if cache is None:
cache = {}
- target_path_absolute = (
- os.path.realpath(p)
- if has_link(p:=os.path.join(config.obj_tree, target_path))
- else os.path.normpath(p)
- )
-
- if target_path_absolute in cache:
- return cache[target_path_absolute]
+ if target_path in cache:
+ return cache[target_path]
if depth == 0:
logging.debug(f"Build node: {target_path}")
- cmd_file_path = _to_cmd_path(target_path_absolute)
+ cmd_file_path = _to_cmd_path(target_path)
cmd_file = CmdFile.create(cmd_file_path) if os.path.exists(cmd_file_path) else None
- node = CmdGraphNode(target_path_absolute, cmd_file)
- cache[target_path_absolute] = node
+ node = CmdGraphNode(target_path, cmd_file)
+ cache[target_path] = node
- if not os.path.exists(target_path_absolute):
+ if not os.path.exists(target_path):
error_or_warning = (
sbom_logging.error
- if is_relative_to(target_path_absolute, config.obj_tree)
- or is_relative_to(target_path_absolute, config.src_tree)
+ if is_relative_to(target_path, config.obj_tree)
+ or is_relative_to(target_path, config.src_tree)
else sbom_logging.warning
)
error_or_warning(
- "Skip parsing '{target_path_absolute}' because file does not exist",
- target_path_absolute=target_path_absolute,
+ "Skip parsing '{target_path}' because file does not exist",
+ target_path=target_path,
)
return node
- # Search for dependencies to add to the graph as child nodes. Child paths are always relative to the output tree.
- def _build_child_node(child_path: PathStr) -> "CmdGraphNode":
- return CmdGraphNode.create(child_path, config, cache, depth + 1)
+ # Search for dependencies to add to the graph as child nodes.
+ def _build_child_node(child_path_absolute: PathStr) -> "CmdGraphNode":
+ return CmdGraphNode.create(child_path_absolute, config, cache, depth + 1)
+
+ # Resolve a dependency path relative to the object tree to an absolute path
+ def _to_absolute_path(child_path_relative: PathStr) -> PathStr:
+ path_absolute = os.path.join(config.obj_tree, child_path_relative)
+ return os.path.realpath(path_absolute) if has_link(path_absolute) else os.path.normpath(path_absolute)
node.hardcoded_dependencies = [
- _build_child_node(hardcoded_dependency_path)
+ _build_child_node(_to_absolute_path(hardcoded_dependency_path))
for hardcoded_dependency_path in get_hardcoded_dependencies(
- target_path_absolute, config.obj_tree, config.src_tree
+ target_path, config.obj_tree, config.src_tree
)
]
if cmd_file is not None:
node.cmd_file_dependencies = [
- _build_child_node(cmd_file_dependency_path)
- for cmd_file_dependency_path in cmd_file.get_dependencies(
- target_path, config.obj_tree, config.fail_on_unknown_build_command
+ _build_child_node(child_path_absolute)
+ for cmd_file_dependency_path in cmd_file.get_dependencies(config.obj_tree)
+ if should_include_dependency(
+ child_path_absolute := _to_absolute_path(cmd_file_dependency_path), config.obj_tree, config.src_tree
)
]
if node.absolute_path.endswith(".S"):
node.incbin_dependencies = [
IncbinDependency(
- node=_build_child_node(incbin_statement.path),
+ node=_build_child_node(child_path_absolute),
full_statement=incbin_statement.full_statement,
)
for incbin_statement in parse_incbin_statements(node.absolute_path)
+ if should_include_dependency(
+ child_path_absolute := _to_absolute_path(incbin_statement.path), config.obj_tree, config.src_tree
+ )
]
return node
diff --git a/scripts/sbom/sbom/cmd_graph/dependency_filter.py b/scripts/sbom/sbom/cmd_graph/dependency_filter.py
new file mode 100644
index 00000000000..5176cc748a9
--- /dev/null
+++ b/scripts/sbom/sbom/cmd_graph/dependency_filter.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: GPL-2.0-only OR MIT
+# Copyright (C) 2025 TNG Technology Consulting GmbH
+
+import re
+
+from sbom.path_utils import PathStr
+
+# Regex patterns for dependencies that should not be tracked in the dependency graph.
+# Paths relative to the object tree or source tree.
+_COMPILED_EXCLUDE_PATTERNS: tuple[re.Pattern[str], ...] = (
+ # scripts in source tree (except module-common.c, which is compiled into every module)
+ re.compile(r"^scripts/(?!module-common\.c$)"),
+ # scripts and tools in obj tree
+ re.compile(r"^tools/"),
+ re.compile(r"^arch/[^/]+/tools/"),
+ # Host helper scripts
+ re.compile(r".*\.sh$"),
+ re.compile(r".*\.py$"),
+ re.compile(r".*\.pl$"),
+ # Host-tool executables
+ re.compile(r"^certs/extract-cert$"),
+ re.compile(r"^security/selinux/genheaders$"),
+ re.compile(r"^usr/gen_init_cpio$"),
+ re.compile(r"^drivers/gpu/drm/radeon/mkregtable$"),
+ re.compile(r"^drivers/video/logo/pnmtologo$"),
+ re.compile(r"^lib/raid/raid6/mktables$"),
+ re.compile(r"^fs/smb/client/gen_smb1_mapping$"),
+ re.compile(r"^fs/smb/client/gen_smb2_mapping$"),
+ # Other
+ re.compile(r".*\.xsd$"),
+ re.compile(r".*purgatory\.chk$"),
+ re.compile(r".*\.genkey$"),
+ re.compile(r".*\.md$"),
+ re.compile(r"^include/config/"),
+ # x86
+ re.compile(r"^arch/x86/boot/compressed/mkpiggy$"),
+ re.compile(r"^arch/x86/boot/mkcpustr$"),
+ # arm
+ re.compile(r"^arch/arm/vdso/vdsomunge$"),
+ # arm64
+ re.compile(r"^arch/arm64/kvm/hyp/nvhe/gen-hyprel$"),
+ re.compile(r"^arch/arm64/kernel/pi/relacheck$"),
+)
+
+
+def should_include_dependency(path: PathStr, obj_tree: PathStr, src_tree: PathStr) -> bool:
+ """
+ Return whether a `.cmd` dependency should appear in the tracked dependency graph.
+
+ Track files that are inputs to compiling or linking artifacts that end up in the
+ kernel image or loadable modules (e.g. source → object → archive → image/module).
+ Exclude host tools and helper scripts whose code is not linked into the kernel.
+
+ Args:
+ path: absolute path to the dependency.
+ obj_tree: absolute path to the object tree.
+ src_tree: absolute path to the source tree.
+
+ Returns:
+ True if the dependency should be included in the dependency graph.
+ """
+ if path.startswith(obj_tree + "/") or not path.startswith(src_tree + "/"):
+ filter_path = path.removeprefix(obj_tree + "/")
+ else:
+ filter_path = path.removeprefix(src_tree + "/")
+ return not any(pattern.match(filter_path) for pattern in _COMPILED_EXCLUDE_PATTERNS)
diff --git a/scripts/sbom/sbom/spdx_graph/kernel_file.py b/scripts/sbom/sbom/spdx_graph/kernel_file.py
index 505f25f66eb..cea14cb3538 100644
--- a/scripts/sbom/sbom/spdx_graph/kernel_file.py
+++ b/scripts/sbom/sbom/spdx_graph/kernel_file.py
@@ -245,7 +245,7 @@ def _get_primary_purpose(absolute_path: PathStr) -> SoftwarePurpose | None:
return any(segment in absolute_path for segment in path_segments)
# Source code
- if ends_with([".c", ".h", ".S", ".s", ".rs", ".pl", "gen_smb1_mapping", "gen_smb2_mapping"]):
+ if ends_with([".c", ".h", ".S", ".s", ".rs"]):
return "source"
# Libraries
@@ -271,45 +271,61 @@ def _get_primary_purpose(absolute_path: PathStr) -> SoftwarePurpose | None:
# Data files
if ends_with(
[
- ".tbl",
- ".relocs",
- ".rmeta",
- ".in",
+ ".awk",
".dbg",
- ".x509",
- ".pbm",
- ".ppm",
".dtb",
- ".uc",
- ".inc",
+ ".dtbo",
".dts",
".dtsi",
- ".dtbo",
- ".xml",
+ ".dtso",
+ ".in",
+ ".inc",
+ ".json",
+ ".ld",
+ ".lds",
+ ".mod",
+ ".order",
+ ".pbm",
+ ".ppm",
+ ".relocs",
+ ".rmeta",
".ro",
- "initramfs_inc_data",
- "default_cpio_list",
- "x509_certificate_list",
- "utf8data.c_shipped",
+ ".tbl",
+ ".uc",
+ ".x509",
+ ".xml",
"blacklist_hash_list",
- "x509_revocation_list",
"cpucaps",
- "sysreg",
+ "default_cpio_list",
+ "initramfs_inc_data",
+ "kheaders.md5",
+ "kheaders-objlist",
+ "kheaders-srclist",
"mach-types",
+ "sysreg",
+ "utf8data.c_shipped",
+ "x509_certificate_list",
+ "x509_revocation_list",
]
) or includes_path_segments(["drivers/gpu/drm/radeon/reg_srcs/"]):
return "data"
# Configuration files
- if ends_with([".pem", ".key", ".conf", ".config", ".cfg", ".bconf"]):
+ if ends_with([
+ ".pem",
+ ".key",
+ ".conf",
+ ".config",
+ ".cfg",
+ ".bconf",
+ "config_data",
+ ".genkey",
+ "bindgen_parameters",
+ ]):
return "configuration"
- # Documentation
- if ends_with([".md"]):
- return "documentation"
-
# Other / miscellaneous
- if ends_with([".o", ".tmp"]):
+ if ends_with([".o"]):
return "other"
sbom_logging.warning("Could not infer primary purpose for {absolute_path}", absolute_path=absolute_path)
--
2.43.0
next prev parent reply other threads:[~2026-11-02 20:23 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-02 20:22 [PATCH v4 0/7] kbuild: record cmd-prereqs in .cmd files Luis Augenstein
2026-11-02 20:22 ` [PATCH v4 1/7] scripts/make_fit: limit .cmd file parsing to the savedcmd_ line Luis Augenstein
2026-11-02 20:22 ` [PATCH v4 2/7] kbuild: refactor fixdep to collect dependencies before printing Luis Augenstein
2026-11-02 20:22 ` [PATCH v4 3/7] scripts/sbom: skip make_prereqs lines when parsing .cmd files Luis Augenstein
2026-11-02 20:22 ` [PATCH v4 4/7] kbuild: record cmd-prereqs in " Luis Augenstein
2026-11-02 20:22 ` [PATCH v4 5/7] scripts/sbom: parse make_prereqs from " Luis Augenstein
2026-11-02 20:22 ` Luis Augenstein [this message]
2026-11-02 20:23 ` [PATCH v4 7/7] scripts/sbom: remove the command-specific parsers Luis Augenstein
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261102202308.3209536-7-luis.augenstein@tngtech.com \
--to=luis.augenstein@tngtech.com \
--cc=akpm@linux-foundation.org \
--cc=gregkh@linuxfoundation.org \
--cc=kstewart@linuxfoundation.org \
--cc=linux-kbuild@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=maximilian.huber@tngtech.com \
--cc=nathan@kernel.org \
--cc=nsc@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®