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 <[email protected]> --- 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

