mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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


  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®