[RFC PATCH 06/12] build: add script to generate dependencies list

Bruce Richardson bruce.richardson at intel.com
Fri Oct 16 12:59:44 CEST 2026


At start of the build configuration, generate a list of all components
and their dependencies on other internal components. Future changes can
use this list to have smarter behaviour for enabling and disabling
drivers and libs.

Signed-off-by: Bruce Richardson <bruce.richardson at intel.com>
---
 buildtools/get-deps.py | 203 +++++++++++++++++++++++++++++++++++++++++
 buildtools/meson.build |   1 +
 meson.build            |   7 ++
 3 files changed, 211 insertions(+)
 create mode 100644 buildtools/get-deps.py

diff --git a/buildtools/get-deps.py b/buildtools/get-deps.py
new file mode 100644
index 0000000000..ca3ee35ff7
--- /dev/null
+++ b/buildtools/get-deps.py
@@ -0,0 +1,203 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2026 Intel Corporation
+
+"""Report the internal DPDK dependencies of every library, driver and app.
+
+Scans the meson.build file of each component for "deps" and "optional_deps"
+assignments, without needing to execute them.  This lets the build work out what
+each component requires before any component has been configured, which in turn
+allows enabling a component to also enable whatever it depends on.
+
+Output is one line per component:
+
+    kind|name|path|deps|optional_deps
+
+"kind" is one of lib, drv or app, and the two dependency fields are
+comma-separated and may be empty.  Dependency names are the names components are
+known by once built, i.e. a library is named after its directory, while a driver
+is "<class>_<name>".
+"""
+
+import re
+import sys
+from pathlib import Path
+from typing import NamedTuple
+
+# an assignment to one of the dependency variables, capturing the operator so
+# that "deps = " (replacing the class defaults) can be told from "deps += "
+ASSIGN = re.compile(r"^(deps|std_deps|optional_deps)\s*(\+?=)\s*(.*)$")
+# meson has no double-quoted strings, so anything quoted is quoted with '
+QUOTED = re.compile(r"'([^']+)'")
+
+
+class Component(NamedTuple):
+    kind: str  # lib, drv or app
+    name: str  # the name the component is known by once built
+    path: str  # source directory, relative to the DPDK root
+    deps: list
+    optional_deps: list
+
+
+def strip_comment(line):
+    """Remove a trailing comment. Dependency names never contain '#'."""
+    return line.split("#", 1)[0]
+
+
+def logical_lines(path):
+    """Yield (indent, text) with multi-line list literals joined into one line."""
+    if not path.is_file():
+        return
+    pending = ""
+    depth = 0
+    indent = 0
+    for raw in path.read_text().splitlines():
+        text = strip_comment(raw)
+        if not text.strip():
+            continue
+        # depth == 0 means we are not inside any brackets or parentheses
+        if depth == 0:
+            indent = len(text) - len(text.lstrip())
+            pending = text.strip()
+        else:
+            # inside a multi-line list or parentheses, so just append the stripped text
+            pending += " " + text.strip()
+        # brackets and the parentheses of calls such as files(...) may both span lines
+        depth = pending.count("[") - pending.count("]") + pending.count("(") - pending.count(")")
+        if depth <= 0:
+            depth = 0
+            yield indent, pending
+            pending = ""
+
+
+def dedup(items):
+    return list(dict.fromkeys(items))
+
+
+def list_variable(path, varname):
+    """Collect every value assigned or appended to a list variable."""
+    found = []
+    pattern = re.compile(r"^%s\s*\+?=\s*(.*)$" % re.escape(varname))
+    for _, line in logical_lines(path):
+        m = pattern.match(line)
+        if m:
+            found += QUOTED.findall(m.group(1))
+    return dedup(found)
+
+
+def scan(path):
+    """Return (deps, optional_deps, replaces_std_deps) for a component.
+
+    Conditions around an assignment are deliberately ignored: a dependency that
+    is only needed on some platforms is still recorded, which over-approximates
+    in the safe direction.  Use "optional_deps" for a dependency that should be
+    linked against only when it is already being built.
+    """
+    deps = []
+    optional = []
+    replaces = False
+
+    for indent, line in logical_lines(path):
+        m = ASSIGN.match(line)
+        if not m:
+            continue
+        var, op, rhs = m.groups()
+
+        items = QUOTED.findall(rhs)
+        if not items:
+            # a computed value such as "deps += some_variable" cannot be scanned
+            continue
+
+        if var == "optional_deps":
+            optional += items
+        elif op == "=" and var == "deps" and indent == 0:
+            # only a top-level "deps =" discards the class defaults; one inside a
+            # conditional may not run at all, so treat it as an addition
+            replaces = True
+            deps = list(items)
+        else:
+            deps += items
+
+    return dedup(deps), dedup(optional), replaces
+
+
+def scan_libs(root):
+    libdir = root / "lib"
+    for lib in list_variable(libdir / "meson.build", "libraries"):
+        deps, optional, _ = scan(libdir / lib / "meson.build")
+        yield Component("lib", lib, "lib/" + lib, deps, optional)
+
+
+def scan_drivers(root):
+    drvdir = root / "drivers"
+    for subpath in list_variable(drvdir / "meson.build", "subdirs"):
+        if "/" in subpath:
+            # a driver listed on its own gets no class-wide default dependencies
+            cls, rest = subpath.split("/", 1)
+            drivers, std_deps = [rest], []
+        else:
+            cls = subpath
+            classfile = drvdir / cls / "meson.build"
+            drivers = list_variable(classfile, "drivers")
+            std_deps, _, _ = scan(classfile)
+
+        for drv in drivers:
+            path = cls + "/" + drv
+            deps, optional, replaces = scan(drvdir / path / "meson.build")
+            if not replaces:
+                deps = dedup(std_deps + deps)
+            yield Component(
+                "drv", cls + "_" + drv.split("/")[-1], "drivers/" + path, deps, optional
+            )
+
+
+def scan_apps(root):
+    appdir = root / "app"
+    for app in list_variable(appdir / "meson.build", "apps"):
+        deps, optional, _ = scan(appdir / app / "meson.build")
+        yield Component("app", app, "app/" + app, deps, optional)
+
+
+def validate(components):
+    """Reject anything that would make the dependency graph unusable."""
+    errors = []
+    provider = {}
+    for c in components:
+        # apps are never depended upon, so they are outside the component namespace
+        if c.kind == "app":
+            continue
+        if c.name in provider:
+            errors.append(
+                '"%s" is provided by both %s and %s' % (c.name, provider[c.name], c.path)
+            )
+        provider[c.name] = c.path
+
+    for c in components:
+        for d in c.deps + c.optional_deps:
+            if d not in provider:
+                errors.append('%s depends on "%s", which is not a known component' % (c.path, d))
+    return errors
+
+
+def main():
+    if len(sys.argv) != 2:
+        sys.exit("usage: %s <dpdk-source-root>" % sys.argv[0])
+    root = Path(sys.argv[1])
+
+    components = list(scan_libs(root)) + list(scan_drivers(root)) + list(scan_apps(root))
+
+    errors = validate(components)
+    if errors:
+        for e in errors:
+            print("error: " + e, file=sys.stderr)
+        sys.exit(1)
+
+    for c in components:
+        print(
+            "%s|%s|%s|%s|%s"
+            % (c.kind, c.name, c.path, ",".join(c.deps), ",".join(c.optional_deps))
+        )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/buildtools/meson.build b/buildtools/meson.build
index 32cea7cff7..02c66a5580 100644
--- a/buildtools/meson.build
+++ b/buildtools/meson.build
@@ -22,6 +22,7 @@ gen_version_map = py3 + files('gen-version-map.py')
 list_dir_globs = py3 + files('list-dir-globs.py')
 sphinx_wrapper = py3 + files('call-sphinx-build.py')
 get_cpu_count_cmd = py3 + files('get-cpu-count.py')
+get_deps_cmd = py3 + files('get-deps.py')
 get_numa_count_cmd = py3 + files('get-numa-count.py')
 get_min_meson_version_cmd = py3 + files('get-min-meson-version.py')
 get_test_suites_cmd = py3 + files('get-test-suites.py')
diff --git a/meson.build b/meson.build
index 6b8cfa8b2e..e153894ac7 100644
--- a/meson.build
+++ b/meson.build
@@ -88,6 +88,13 @@ if is_linux
     global_inc += include_directories('kernel/linux')
 endif
 
+# record what each component depends on, so that enabling a component can also
+# enable the components it needs
+dpdk_deps_graph = configure_file(
+        command: [get_deps_cmd, dpdk_source_root],
+        capture: true,
+        output: 'dependencies.txt')
+
 # build libs and drivers
 subdir('lib')
 subdir('drivers')
-- 
2.53.0



More information about the dev mailing list