From c83f4f573e9a113a3315e47e120041a0b8618a07 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:02:20 -0600 Subject: [PATCH 01/16] Initial skeleton --- scripts/bbclasses | 250 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100755 scripts/bbclasses diff --git a/scripts/bbclasses b/scripts/bbclasses new file mode 100755 index 0000000..5c561bc --- /dev/null +++ b/scripts/bbclasses @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Generate a Mermaid dependency graph of all files involved in building a BitBake recipe. + +Uses bb.tinfoil to parse the recipe and discover nodes (files), then manually parses +those files to discover edges (inherit/require/include relationships). +""" + +import argparse +import logging +import os +import re +import sys + + +def parse_args(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + "recipe", + help="Recipe name (e.g. 'gzip') or path to a .bb file", + ) + parser.add_argument( + "--no-filter-base", + action="store_true", + help="Disable filtering of base BitBake infrastructure noise", + ) + parser.add_argument( + "--group-by-layer", + action="store_true", + help="Group files into Mermaid subgraphs by meta-layer", + ) + parser.add_argument( + "--log-level", + "-l", + type=str, + default="DEBUG", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + help="Set the logging output level. Defaults to INFO.", + ) + parser.add_argument( + "--output", + "-o", + type=str, + default=None, + help="Output file. Default: stdout", + ) + return parser.parse_args() + + +def parse_recipe(tinfoil, recipe): + """Parse a recipe by name or path, returning its datastore.""" + if os.path.exists(recipe): + logging.info("Parsing recipe file: %s", recipe) + d = tinfoil.parse_recipe_file(recipe) + else: + logging.info("Parsing recipe: %s", recipe) + d = tinfoil.parse_recipe(recipe) + if d is None: + sys.exit(f"Failed to parse recipe: {recipe}") + return d + + +def discover_nodes(tinfoil, d): + """Collect the set of all files involved in a recipe from the datastore.""" + recipe_file = d.getVar("FILE") + logging.info("Recipe file: %s", recipe_file) + + inherit_cache = d.getVar("__inherit_cache") or [] + logging.info("Found %d inherited bbclasses", len(inherit_cache)) + for path in inherit_cache: + logging.debug(" bbclass: %s", path) + + depends = d.getVar("__depends") or [] + dep_files = [ + filepath + for filepath, _mtime in depends + if any(filepath.endswith(ext) for ext in (".bb", ".inc", ".bbappend")) + ] + logging.info("Found %d file dependencies", len(dep_files)) + for path in dep_files: + logging.debug(" dep: %s", path) + + appends = tinfoil.get_file_appends(recipe_file) + logging.info("Found %d appends", len(appends)) + for path in appends: + logging.debug(" append: %s", path) + + nodes = {recipe_file} + nodes.update(inherit_cache) + nodes.update(dep_files) + nodes.update(appends) + logging.info("Total node count: %d", len(nodes)) + return nodes, recipe_file, inherit_cache, appends + + +BASE_FILTER_BASENAMES = { + "base.bbclass", + "bblayers.conf", + "bitbake.conf", + "documentation.bbclass", + "insane.bbclass", + "license.bbclass", + "local.conf", + "logging.bbclass", + "metadata_scm.bbclass", + "mirrors.bbclass", + "patch.bbclass", + "sanity.conf", + "sstate.bbclass", + "staging.bbclass", + "utility-tasks.bbclass", +} + + +def filter_base_infrastructure(nodes): + """Remove base BitBake infrastructure files that add noise to the graph.""" + filtered = set() + for path in nodes: + basename = os.path.basename(path) + if basename in BASE_FILTER_BASENAMES: + logging.debug("Filtering base infrastructure: %s", path) + continue + if path.endswith(".conf"): + logging.debug("Filtering conf file: %s", path) + continue + filtered.add(path) + return filtered + + +# Mermaid output helpers + +_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9_.\-]") + + +def sanitize_mermaid_id(path): + """Convert a file path into a valid Mermaid node ID.""" + node_id = _SANITIZE_RE.sub("_", path) + if node_id and node_id[0].isdigit(): + node_id = "_" + node_id + return node_id + + +def escape_label(text): + """Escape a label for use inside Mermaid quoted strings.""" + return text.replace("&", "&").replace('"', """) + + +def minimal_unique_labels(paths): + """Compute the shortest unique path suffix for each file path. + + Returns a dict mapping each full path to its minimal label. + """ + # Split each path into components (reversed for suffix matching) + split = {p: p.split("/") for p in paths} + + labels = {} + for path, parts in split.items(): + # Start with just the filename, extend until unique + for n in range(1, len(parts) + 1): + candidate = "/".join(parts[-n:]) + conflicts = [ + other + for other, oparts in split.items() + if other != path and "/".join(oparts[-n:]) == candidate + ] + if not conflicts: + labels[path] = candidate + break + else: + labels[path] = path + return labels + + +def node_shape(path, label): + """Return the Mermaid node definition with the appropriate shape.""" + escaped = escape_label(label) + if path.endswith(".bb"): + return f'["{escaped}"]' + elif path.endswith(".bbclass"): + return f'{{{{"{escaped}"}}}}' + elif path.endswith(".bbappend"): + return f'(["{escaped}"])' + elif path.endswith(".inc"): + return f'[["{escaped}"]]' + else: + return f'["{escaped}"]' + + +def emit_mermaid(nodes, edges, out): + """Write a Mermaid flowchart to the given file object.""" + labels = minimal_unique_labels(nodes) + + out.write("flowchart TB\n") + for path in sorted(nodes): + node_id = sanitize_mermaid_id(path) + shape = node_shape(path, labels[path]) + out.write(f" {node_id}{shape}\n") + + for source, target, edge_type in sorted(edges): + src_id = sanitize_mermaid_id(source) + tgt_id = sanitize_mermaid_id(target) + escaped_type = escape_label(edge_type) + out.write(f' {src_id} -->|"{escaped_type}"| {tgt_id}\n') + + +def main(args): + try: + import bb.tinfoil + except ImportError: + sys.exit( + "Could not import bb.tinfoil. " + "Make sure you have sourced the OE build environment " + "(e.g. 'source oe-init-build-env')." + ) + + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare(config_only=False) + d = parse_recipe(tinfoil, args.recipe) + nodes, recipe_file, inherit_cache, appends = discover_nodes(tinfoil, d) + + if not args.no_filter_base: + nodes = filter_base_infrastructure(nodes) + + edges = [] # TODO: discover_edges() + + if args.output: + with open(args.output, "w") as f: + emit_mermaid(nodes, edges, f) + logging.info("Wrote output to %s", args.output) + else: + emit_mermaid(nodes, edges, sys.stdout) + + +if __name__ == "__main__": + args = parse_args() + fmt = "%(asctime)s %(module)s %(levelname)s: %(message)s" + logging.basicConfig( + format=fmt, + datefmt="%Y-%m-%dT%H:%M:%S%z", + level=args.log_level, + stream=sys.stderr, + ) + try: + import coloredlogs + + coloredlogs.install(fmt=fmt, level=args.log_level, datefmt="%Y-%m-%dT%H:%M:%S%z") + except ImportError: + pass + main(args) From f7add2b58c9e803a1bde91c75b327d80c60b4979 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:17:26 -0600 Subject: [PATCH 02/16] Strip off common prefix from BBLAYERS --- scripts/bbclasses | 56 ++++++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 5c561bc..bb547f3 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -86,12 +86,16 @@ def discover_nodes(tinfoil, d): for path in appends: logging.debug(" append: %s", path) + bblayers = (d.getVar("BBLAYERS") or "").split() + common_prefix = os.path.commonpath(bblayers) + "/" if bblayers else "/" + logging.info("Common layer prefix: %s", common_prefix) + nodes = {recipe_file} nodes.update(inherit_cache) nodes.update(dep_files) nodes.update(appends) logging.info("Total node count: %d", len(nodes)) - return nodes, recipe_file, inherit_cache, appends + return nodes, recipe_file, inherit_cache, appends, common_prefix BASE_FILTER_BASENAMES = { @@ -130,7 +134,7 @@ def filter_base_infrastructure(nodes): # Mermaid output helpers -_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9_.\-]") +_SANITIZE_RE = re.compile(r"[^a-zA-Z0-9_./\-]") def sanitize_mermaid_id(path): @@ -146,30 +150,11 @@ def escape_label(text): return text.replace("&", "&").replace('"', """) -def minimal_unique_labels(paths): - """Compute the shortest unique path suffix for each file path. - - Returns a dict mapping each full path to its minimal label. - """ - # Split each path into components (reversed for suffix matching) - split = {p: p.split("/") for p in paths} - - labels = {} - for path, parts in split.items(): - # Start with just the filename, extend until unique - for n in range(1, len(parts) + 1): - candidate = "/".join(parts[-n:]) - conflicts = [ - other - for other, oparts in split.items() - if other != path and "/".join(oparts[-n:]) == candidate - ] - if not conflicts: - labels[path] = candidate - break - else: - labels[path] = path - return labels +def strip_prefix(path, prefix): + """Strip a common prefix from a path, returning the relative remainder.""" + if path.startswith(prefix): + return path[len(prefix):] + return path def node_shape(path, label): @@ -187,19 +172,18 @@ def node_shape(path, label): return f'["{escaped}"]' -def emit_mermaid(nodes, edges, out): +def emit_mermaid(nodes, edges, common_prefix, out): """Write a Mermaid flowchart to the given file object.""" - labels = minimal_unique_labels(nodes) - out.write("flowchart TB\n") for path in sorted(nodes): + path = strip_prefix(path, common_prefix) node_id = sanitize_mermaid_id(path) - shape = node_shape(path, labels[path]) + shape = node_shape(path, label=path) out.write(f" {node_id}{shape}\n") for source, target, edge_type in sorted(edges): - src_id = sanitize_mermaid_id(source) - tgt_id = sanitize_mermaid_id(target) + src_id = sanitize_mermaid_id(strip_prefix(source, common_prefix)) + tgt_id = sanitize_mermaid_id(strip_prefix(target, common_prefix)) escaped_type = escape_label(edge_type) out.write(f' {src_id} -->|"{escaped_type}"| {tgt_id}\n') @@ -217,7 +201,9 @@ def main(args): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=False) d = parse_recipe(tinfoil, args.recipe) - nodes, recipe_file, inherit_cache, appends = discover_nodes(tinfoil, d) + nodes, recipe_file, inherit_cache, appends, common_prefix = discover_nodes( + tinfoil, d + ) if not args.no_filter_base: nodes = filter_base_infrastructure(nodes) @@ -226,10 +212,10 @@ def main(args): if args.output: with open(args.output, "w") as f: - emit_mermaid(nodes, edges, f) + emit_mermaid(nodes, edges, common_prefix, f) logging.info("Wrote output to %s", args.output) else: - emit_mermaid(nodes, edges, sys.stdout) + emit_mermaid(nodes, edges, common_prefix, sys.stdout) if __name__ == "__main__": From 7088b719b4a3c801356df0d443f32355e81b5826 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:27:23 -0600 Subject: [PATCH 03/16] Add edge discovery scaffolding --- scripts/bbclasses | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index bb547f3..794cd1b 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -95,7 +95,32 @@ def discover_nodes(tinfoil, d): nodes.update(dep_files) nodes.update(appends) logging.info("Total node count: %d", len(nodes)) - return nodes, recipe_file, inherit_cache, appends, common_prefix + return nodes, recipe_file, inherit_cache, dep_files, appends, common_prefix + + +# Edge discovery helpers + + +def preprocess(text): + """Prepare file contents for directive scanning. + + Strips comment lines and joins backslash-continuation lines. + """ + lines = [] + for line in text.splitlines(): + stripped = line.lstrip() + if stripped.startswith("#"): + continue + lines.append(line) + joined = "\n".join(lines) + return joined.replace("\\\n", " ") + + +def discover_edges(nodes, inherit_cache, depends): + """Given a set of nodes, discover edges by parsing the files for directives.""" + edges = [] + + return edges BASE_FILTER_BASENAMES = { @@ -153,7 +178,7 @@ def escape_label(text): def strip_prefix(path, prefix): """Strip a common prefix from a path, returning the relative remainder.""" if path.startswith(prefix): - return path[len(prefix):] + return path[len(prefix) :] return path @@ -201,14 +226,14 @@ def main(args): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=False) d = parse_recipe(tinfoil, args.recipe) - nodes, recipe_file, inherit_cache, appends, common_prefix = discover_nodes( + nodes, recipe_file, inherit_cache, depends, appends, common_prefix = discover_nodes( tinfoil, d ) if not args.no_filter_base: nodes = filter_base_infrastructure(nodes) - edges = [] # TODO: discover_edges() + edges = discover_edges(nodes, inherit_cache, depends) if args.output: with open(args.output, "w") as f: From f7a240a4fa176630cf956c4e45ac9a865a2701e4 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:31:48 -0600 Subject: [PATCH 04/16] Add simple inherit/include resolution --- scripts/bbclasses | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/scripts/bbclasses b/scripts/bbclasses index 794cd1b..b375db8 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -116,6 +116,47 @@ def preprocess(text): return joined.replace("\\\n", " ") +def build_class_lookup(inherit_cache): + """Build a dict mapping bare class names to full paths. + + e.g. {"native": "/path/to/meta/classes/native.bbclass", ...} + """ + lookup = {} + for path in inherit_cache: + basename = os.path.basename(path) + name = basename.removesuffix(".bbclass") + lookup[name] = path + return lookup + + +def resolve_inherit(name, class_lookup): + """Resolve an inherit class name to its full path, or None.""" + if "$" in name: + # TODO: variable expansion + logging.debug("Skipping variable reference in inherit: %s", name) + return None + result = class_lookup.get(name) + if result is None: + logging.debug("Could not resolve inherit: %s", name) + return result + + +def resolve_include(ref, nodes): + """Resolve a require/include path to a node in the set by suffix match, or None.""" + if "$" in ref: + # TODO: variable expansion + logging.debug("Skipping variable reference in include: %s", ref) + return None + matches = [n for n in nodes if n.endswith("/" + ref) or n == ref] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + logging.debug("Ambiguous include ref '%s', matched %d nodes", ref, len(matches)) + else: + logging.debug("Could not resolve include: %s", ref) + return None + + def discover_edges(nodes, inherit_cache, depends): """Given a set of nodes, discover edges by parsing the files for directives.""" edges = [] From 11b4738b9f1b34a0c43a2745569561bb925d9301 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:37:51 -0600 Subject: [PATCH 05/16] Add basic edge discovery without variable expansion --- scripts/bbclasses | 53 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index b375db8..1261343 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -157,9 +157,56 @@ def resolve_include(ref, nodes): return None -def discover_edges(nodes, inherit_cache, depends): - """Given a set of nodes, discover edges by parsing the files for directives.""" +INHERIT_RE = re.compile(r"^(inherit(?:_defer)?)\s+(.+)", re.MULTILINE) +REQUIRE_INCLUDE_RE = re.compile(r"^(require|include)\s+(\S+)", re.MULTILINE) + + +def scan_file(source, nodes, class_lookup): + """Parse a single file for inherit/require/include directives, returning edges.""" edges = [] + try: + text = preprocess(open(source).read()) + except OSError: + logging.warning("Could not read file: %s", source) + return edges + + for match in INHERIT_RE.finditer(text): + edge_type = match.group(1) + for name in match.group(2).split(): + target = resolve_inherit(name, class_lookup) + if target and target in nodes: + edges.append((source, target, edge_type)) + + for match in REQUIRE_INCLUDE_RE.finditer(text): + edge_type = match.group(1) + ref = match.group(2) + target = resolve_include(ref, nodes) + if target: + edges.append((source, target, edge_type)) + + return edges + + +def discover_edges(nodes, recipe_file, inherit_cache, depends, appends): + """Discover edges by scanning the recipe, its inherited classes, and its dependencies.""" + class_lookup = build_class_lookup(inherit_cache) + edges = [] + + # Scan the recipe file itself + edges.extend(scan_file(recipe_file, nodes, class_lookup)) + + # Scan each inherited bbclass + for path in inherit_cache: + edges.extend(scan_file(path, nodes, class_lookup)) + + # Scan each file dependency (.bb, .inc, .bbappend) + for path in depends: + edges.extend(scan_file(path, nodes, class_lookup)) + + # Synthetic appends edges + for append in appends: + if append in nodes: + edges.append((append, recipe_file, "appends")) return edges @@ -274,7 +321,7 @@ def main(args): if not args.no_filter_base: nodes = filter_base_infrastructure(nodes) - edges = discover_edges(nodes, inherit_cache, depends) + edges = discover_edges(nodes, recipe_file, inherit_cache, depends, appends) if args.output: with open(args.output, "w") as f: From b7665a396499d1a7637bf062b2fa5cb9fd374aa9 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:45:21 -0600 Subject: [PATCH 06/16] Reverse edge direction for classic inheritance diagram --- scripts/bbclasses | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 1261343..ba69177 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -175,14 +175,14 @@ def scan_file(source, nodes, class_lookup): for name in match.group(2).split(): target = resolve_inherit(name, class_lookup) if target and target in nodes: - edges.append((source, target, edge_type)) + edges.append((target, source, edge_type)) for match in REQUIRE_INCLUDE_RE.finditer(text): edge_type = match.group(1) ref = match.group(2) target = resolve_include(ref, nodes) if target: - edges.append((source, target, edge_type)) + edges.append((target, source, edge_type)) return edges @@ -206,7 +206,7 @@ def discover_edges(nodes, recipe_file, inherit_cache, depends, appends): # Synthetic appends edges for append in appends: if append in nodes: - edges.append((append, recipe_file, "appends")) + edges.append((recipe_file, append, "appends")) return edges @@ -223,7 +223,7 @@ BASE_FILTER_BASENAMES = { "metadata_scm.bbclass", "mirrors.bbclass", "patch.bbclass", - "sanity.conf", + "sanity.bbclass", "sstate.bbclass", "staging.bbclass", "utility-tasks.bbclass", From d09932c1545237d5fe6cc566ee3d3c8e17254517 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 17:55:42 -0600 Subject: [PATCH 07/16] Add global inherits from INHERIT += in local.conf et al --- scripts/bbclasses | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index ba69177..74afde7 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -86,6 +86,11 @@ def discover_nodes(tinfoil, d): for path in appends: logging.debug(" append: %s", path) + global_inherits = (d.getVar("INHERIT") or "").split() + logging.info("Found %d global INHERIT classes", len(global_inherits)) + for name in global_inherits: + logging.debug(" INHERIT: %s", name) + bblayers = (d.getVar("BBLAYERS") or "").split() common_prefix = os.path.commonpath(bblayers) + "/" if bblayers else "/" logging.info("Common layer prefix: %s", common_prefix) @@ -95,7 +100,7 @@ def discover_nodes(tinfoil, d): nodes.update(dep_files) nodes.update(appends) logging.info("Total node count: %d", len(nodes)) - return nodes, recipe_file, inherit_cache, dep_files, appends, common_prefix + return nodes, recipe_file, inherit_cache, dep_files, appends, global_inherits, common_prefix # Edge discovery helpers @@ -187,11 +192,17 @@ def scan_file(source, nodes, class_lookup): return edges -def discover_edges(nodes, recipe_file, inherit_cache, depends, appends): +def discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits): """Discover edges by scanning the recipe, its inherited classes, and its dependencies.""" class_lookup = build_class_lookup(inherit_cache) edges = [] + # Global INHERIT classes (from local.conf / bitbake.conf / layer.conf) + for name in global_inherits: + target = resolve_inherit(name, class_lookup) + if target and target in nodes: + edges.append((target, recipe_file, "INHERIT")) + # Scan the recipe file itself edges.extend(scan_file(recipe_file, nodes, class_lookup)) @@ -314,14 +325,14 @@ def main(args): with bb.tinfoil.Tinfoil() as tinfoil: tinfoil.prepare(config_only=False) d = parse_recipe(tinfoil, args.recipe) - nodes, recipe_file, inherit_cache, depends, appends, common_prefix = discover_nodes( - tinfoil, d + nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( + discover_nodes(tinfoil, d) ) if not args.no_filter_base: nodes = filter_base_infrastructure(nodes) - edges = discover_edges(nodes, recipe_file, inherit_cache, depends, appends) + edges = discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits) if args.output: with open(args.output, "w") as f: From c110ba539614184f48082e052b943054e973b30c Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:09:28 -0600 Subject: [PATCH 08/16] Fix base infra filtering --- scripts/bbclasses | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 74afde7..ff7c2bc 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -226,7 +226,6 @@ BASE_FILTER_BASENAMES = { "base.bbclass", "bblayers.conf", "bitbake.conf", - "documentation.bbclass", "insane.bbclass", "license.bbclass", "local.conf", @@ -238,12 +237,13 @@ BASE_FILTER_BASENAMES = { "sstate.bbclass", "staging.bbclass", "utility-tasks.bbclass", + "utils.bbclass", } -def filter_base_infrastructure(nodes): - """Remove base BitBake infrastructure files that add noise to the graph.""" - filtered = set() +def filter_base_infrastructure(nodes, edges): + """Remove base BitBake infrastructure files and their edges.""" + filtered_nodes = set() for path in nodes: basename = os.path.basename(path) if basename in BASE_FILTER_BASENAMES: @@ -252,8 +252,13 @@ def filter_base_infrastructure(nodes): if path.endswith(".conf"): logging.debug("Filtering conf file: %s", path) continue - filtered.add(path) - return filtered + filtered_nodes.add(path) + filtered_edges = [ + (src, tgt, etype) + for src, tgt, etype in edges + if src in filtered_nodes and tgt in filtered_nodes + ] + return filtered_nodes, filtered_edges # Mermaid output helpers @@ -298,7 +303,8 @@ def node_shape(path, label): def emit_mermaid(nodes, edges, common_prefix, out): """Write a Mermaid flowchart to the given file object.""" - out.write("flowchart TB\n") + # Normal inheritance diagrams are TB, but LR diagrams render better when there's lots of nodes + out.write("flowchart LR\n") for path in sorted(nodes): path = strip_prefix(path, common_prefix) node_id = sanitize_mermaid_id(path) @@ -329,11 +335,11 @@ def main(args): discover_nodes(tinfoil, d) ) - if not args.no_filter_base: - nodes = filter_base_infrastructure(nodes) - edges = discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits) + if not args.no_filter_base: + nodes, edges = filter_base_infrastructure(nodes, edges) + if args.output: with open(args.output, "w") as f: emit_mermaid(nodes, edges, common_prefix, f) From 5de61a15270fae959db5c580495a0b16f0ba37ec Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:24:14 -0600 Subject: [PATCH 09/16] Add inherit_defer variable expansion --- scripts/bbclasses | 77 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index ff7c2bc..d04fb34 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -134,24 +134,28 @@ def build_class_lookup(inherit_cache): return lookup -def resolve_inherit(name, class_lookup): +def resolve_inherit(name, class_lookup, d): """Resolve an inherit class name to its full path, or None.""" if "$" in name: - # TODO: variable expansion - logging.debug("Skipping variable reference in inherit: %s", name) - return None + try: + name = d.expand(name) + except Exception: + logging.debug("Could not expand variable in inherit: %s", name) + return None result = class_lookup.get(name) if result is None: logging.debug("Could not resolve inherit: %s", name) return result -def resolve_include(ref, nodes): +def resolve_include(ref, nodes, d): """Resolve a require/include path to a node in the set by suffix match, or None.""" if "$" in ref: - # TODO: variable expansion - logging.debug("Skipping variable reference in include: %s", ref) - return None + try: + ref = d.expand(ref) + except Exception: + logging.debug("Could not expand variable in include: %s", ref) + return None matches = [n for n in nodes if n.endswith("/" + ref) or n == ref] if len(matches) == 1: return matches[0] @@ -163,10 +167,36 @@ def resolve_include(ref, nodes): INHERIT_RE = re.compile(r"^(inherit(?:_defer)?)\s+(.+)", re.MULTILINE) -REQUIRE_INCLUDE_RE = re.compile(r"^(require|include)\s+(\S+)", re.MULTILINE) - - -def scan_file(source, nodes, class_lookup): +REQUIRE_INCLUDE_RE = re.compile(r"^(require|include)\s+(.+)", re.MULTILINE) + + +def split_tokens(text): + """Split a string on whitespace, respecting ${...} brace nesting.""" + tokens = [] + current = [] + depth = 0 + for char in text: + if char == "$" or (depth > 0 and char == "{"): + if char == "{": + depth += 1 + current.append(char) + elif depth > 0 and char == "}": + depth -= 1 + current.append(char) + elif depth == 0 and char in (" ", "\t"): + if current: + tokens.append("".join(current)) + current = [] + else: + if char == "{" and current and current[-1] == "$": + depth += 1 + current.append(char) + if current: + tokens.append("".join(current)) + return tokens + + +def scan_file(source, nodes, class_lookup, d): """Parse a single file for inherit/require/include directives, returning edges.""" edges = [] try: @@ -177,42 +207,42 @@ def scan_file(source, nodes, class_lookup): for match in INHERIT_RE.finditer(text): edge_type = match.group(1) - for name in match.group(2).split(): - target = resolve_inherit(name, class_lookup) + for name in split_tokens(match.group(2)): + target = resolve_inherit(name, class_lookup, d) if target and target in nodes: edges.append((target, source, edge_type)) for match in REQUIRE_INCLUDE_RE.finditer(text): edge_type = match.group(1) - ref = match.group(2) - target = resolve_include(ref, nodes) + ref = match.group(2).strip() + target = resolve_include(ref, nodes, d) if target: edges.append((target, source, edge_type)) return edges -def discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits): +def discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits, d): """Discover edges by scanning the recipe, its inherited classes, and its dependencies.""" class_lookup = build_class_lookup(inherit_cache) edges = [] # Global INHERIT classes (from local.conf / bitbake.conf / layer.conf) for name in global_inherits: - target = resolve_inherit(name, class_lookup) + target = resolve_inherit(name, class_lookup, d) if target and target in nodes: edges.append((target, recipe_file, "INHERIT")) # Scan the recipe file itself - edges.extend(scan_file(recipe_file, nodes, class_lookup)) + edges.extend(scan_file(recipe_file, nodes, class_lookup, d)) # Scan each inherited bbclass for path in inherit_cache: - edges.extend(scan_file(path, nodes, class_lookup)) + edges.extend(scan_file(path, nodes, class_lookup, d)) # Scan each file dependency (.bb, .inc, .bbappend) for path in depends: - edges.extend(scan_file(path, nodes, class_lookup)) + edges.extend(scan_file(path, nodes, class_lookup, d)) # Synthetic appends edges for append in appends: @@ -334,8 +364,9 @@ def main(args): nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( discover_nodes(tinfoil, d) ) - - edges = discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_inherits) + edges = discover_edges( + nodes, recipe_file, inherit_cache, depends, appends, global_inherits, d + ) if not args.no_filter_base: nodes, edges = filter_base_infrastructure(nodes, edges) From 684dfea912a303737018351046f2c86f0546490c Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:37:27 -0600 Subject: [PATCH 10/16] Filter out nodes that don't exist on disk --- scripts/bbclasses | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index d04fb34..0aa1e79 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -95,12 +95,22 @@ def discover_nodes(tinfoil, d): common_prefix = os.path.commonpath(bblayers) + "/" if bblayers else "/" logging.info("Common layer prefix: %s", common_prefix) - nodes = {recipe_file} - nodes.update(inherit_cache) - nodes.update(dep_files) - nodes.update(appends) - logging.info("Total node count: %d", len(nodes)) - return nodes, recipe_file, inherit_cache, dep_files, appends, global_inherits, common_prefix + all_paths = {recipe_file} + all_paths.update(inherit_cache) + all_paths.update(dep_files) + all_paths.update(appends) + exists = {p for p in all_paths if os.path.isfile(p)} + skipped = all_paths - exists + if skipped: + logging.debug("Skipped %d non-existent files", len(skipped)) + for path in sorted(skipped): + logging.debug(" skipped: %s", path) + + inherit_cache = [p for p in inherit_cache if p in exists] + dep_files = [p for p in dep_files if p in exists] + appends = [p for p in appends if p in exists] + logging.info("Total node count: %d", len(exists)) + return exists, recipe_file, inherit_cache, dep_files, appends, global_inherits, common_prefix # Edge discovery helpers @@ -140,11 +150,11 @@ def resolve_inherit(name, class_lookup, d): try: name = d.expand(name) except Exception: - logging.debug("Could not expand variable in inherit: %s", name) + logging.warning("Could not expand variable in inherit: %s", name) return None result = class_lookup.get(name) if result is None: - logging.debug("Could not resolve inherit: %s", name) + logging.warning("Could not resolve inherit: %s", name) return result @@ -154,15 +164,15 @@ def resolve_include(ref, nodes, d): try: ref = d.expand(ref) except Exception: - logging.debug("Could not expand variable in include: %s", ref) + logging.warning("Could not expand variable in include: %s", ref) return None matches = [n for n in nodes if n.endswith("/" + ref) or n == ref] if len(matches) == 1: return matches[0] if len(matches) > 1: - logging.debug("Ambiguous include ref '%s', matched %d nodes", ref, len(matches)) + logging.warning("Ambiguous include ref '%s', matched %d nodes", ref, len(matches)) else: - logging.debug("Could not resolve include: %s", ref) + logging.warning("Could not resolve include: %s", ref) return None From 52664b7aef3d00e3ae60d725d5bb8a499395a3a9 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:47:08 -0600 Subject: [PATCH 11/16] Fix variable expansion resolving to multiple inherits --- scripts/bbclasses | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 0aa1e79..88de448 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -144,18 +144,26 @@ def build_class_lookup(inherit_cache): return lookup -def resolve_inherit(name, class_lookup, d): - """Resolve an inherit class name to its full path, or None.""" +def resolve_inherits(name, class_lookup, d): + """Resolve an inherit class name to a list of (name, path) pairs. + + A single name can expand to multiple classes if it contains a variable + reference like ${IMGCLASSES} or ${@bb.utils.contains(...)}. + """ if "$" in name: try: name = d.expand(name) except Exception: logging.warning("Could not expand variable in inherit: %s", name) - return None - result = class_lookup.get(name) - if result is None: - logging.warning("Could not resolve inherit: %s", name) - return result + return [] + results = [] + for n in name.split(): + path = class_lookup.get(n) + if path is None: + logging.warning("Could not resolve inherit: %s", n) + else: + results.append(path) + return results def resolve_include(ref, nodes, d): @@ -218,9 +226,9 @@ def scan_file(source, nodes, class_lookup, d): for match in INHERIT_RE.finditer(text): edge_type = match.group(1) for name in split_tokens(match.group(2)): - target = resolve_inherit(name, class_lookup, d) - if target and target in nodes: - edges.append((target, source, edge_type)) + for target in resolve_inherits(name, class_lookup, d): + if target in nodes: + edges.append((target, source, edge_type)) for match in REQUIRE_INCLUDE_RE.finditer(text): edge_type = match.group(1) @@ -239,9 +247,9 @@ def discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_i # Global INHERIT classes (from local.conf / bitbake.conf / layer.conf) for name in global_inherits: - target = resolve_inherit(name, class_lookup, d) - if target and target in nodes: - edges.append((target, recipe_file, "INHERIT")) + for target in resolve_inherits(name, class_lookup, d): + if target in nodes: + edges.append((target, recipe_file, "INHERIT")) # Scan the recipe file itself edges.extend(scan_file(recipe_file, nodes, class_lookup, d)) From 64e75ad6c0f27378842fffa10587d1c56cd4298f Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:51:18 -0600 Subject: [PATCH 12/16] Refactor variable expansion to happen before resolution --- scripts/bbclasses | 51 +++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 88de448..7e06142 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -144,26 +144,27 @@ def build_class_lookup(inherit_cache): return lookup -def resolve_inherits(name, class_lookup, d): - """Resolve an inherit class name to a list of (name, path) pairs. +def expand_names(token, d): + """Expand a token that may contain ${...} variable references. - A single name can expand to multiple classes if it contains a variable - reference like ${IMGCLASSES} or ${@bb.utils.contains(...)}. + Returns a list of individual names after expansion and splitting. """ - if "$" in name: - try: - name = d.expand(name) - except Exception: - logging.warning("Could not expand variable in inherit: %s", name) - return [] - results = [] - for n in name.split(): - path = class_lookup.get(n) - if path is None: - logging.warning("Could not resolve inherit: %s", n) - else: - results.append(path) - return results + if "$" not in token: + return [token] + try: + expanded = d.expand(token) + except Exception: + logging.warning("Could not expand variable: %s", token) + return [] + return expanded.split() + + +def resolve_inherit(name, class_lookup): + """Resolve a single inherit class name to its full path, or None.""" + result = class_lookup.get(name) + if result is None: + logging.warning("Could not resolve inherit: %s", name) + return result def resolve_include(ref, nodes, d): @@ -225,9 +226,10 @@ def scan_file(source, nodes, class_lookup, d): for match in INHERIT_RE.finditer(text): edge_type = match.group(1) - for name in split_tokens(match.group(2)): - for target in resolve_inherits(name, class_lookup, d): - if target in nodes: + for token in split_tokens(match.group(2)): + for name in expand_names(token, d): + target = resolve_inherit(name, class_lookup) + if target and target in nodes: edges.append((target, source, edge_type)) for match in REQUIRE_INCLUDE_RE.finditer(text): @@ -246,9 +248,10 @@ def discover_edges(nodes, recipe_file, inherit_cache, depends, appends, global_i edges = [] # Global INHERIT classes (from local.conf / bitbake.conf / layer.conf) - for name in global_inherits: - for target in resolve_inherits(name, class_lookup, d): - if target in nodes: + for token in global_inherits: + for name in expand_names(token, d): + target = resolve_inherit(name, class_lookup) + if target and target in nodes: edges.append((target, recipe_file, "INHERIT")) # Scan the recipe file itself From c48c0d4a44cec533c244b3b41ebcbf9067770106 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 18:56:10 -0600 Subject: [PATCH 13/16] Dump tinfoil logs to stderr --- scripts/bbclasses | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 7e06142..6892726 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -379,7 +379,7 @@ def main(args): "(e.g. 'source oe-init-build-env')." ) - with bb.tinfoil.Tinfoil() as tinfoil: + with bb.tinfoil.Tinfoil(output=sys.stderr) as tinfoil: tinfoil.prepare(config_only=False) d = parse_recipe(tinfoil, args.recipe) nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( From a3bff4731f2621d3f41be5bb5d60a6194001e3be Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 19:04:14 -0600 Subject: [PATCH 14/16] Redirect stdout from tinfoil to stderr --- scripts/bbclasses | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index 6892726..c3bfc49 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -379,15 +379,20 @@ def main(args): "(e.g. 'source oe-init-build-env')." ) - with bb.tinfoil.Tinfoil(output=sys.stderr) as tinfoil: - tinfoil.prepare(config_only=False) - d = parse_recipe(tinfoil, args.recipe) - nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( - discover_nodes(tinfoil, d) - ) - edges = discover_edges( - nodes, recipe_file, inherit_cache, depends, appends, global_inherits, d - ) + saved_stdout = sys.stdout + sys.stdout = sys.stderr + try: + with bb.tinfoil.Tinfoil(output=sys.stderr) as tinfoil: + tinfoil.prepare(config_only=False) + d = parse_recipe(tinfoil, args.recipe) + nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( + discover_nodes(tinfoil, d) + ) + edges = discover_edges( + nodes, recipe_file, inherit_cache, depends, appends, global_inherits, d + ) + finally: + sys.stdout = saved_stdout if not args.no_filter_base: nodes, edges = filter_base_infrastructure(nodes, edges) @@ -402,17 +407,10 @@ def main(args): if __name__ == "__main__": args = parse_args() - fmt = "%(asctime)s %(module)s %(levelname)s: %(message)s" logging.basicConfig( - format=fmt, + format="%(asctime)s %(module)s %(levelname)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z", level=args.log_level, stream=sys.stderr, ) - try: - import coloredlogs - - coloredlogs.install(fmt=fmt, level=args.log_level, datefmt="%Y-%m-%dT%H:%M:%S%z") - except ImportError: - pass main(args) From 67afc4ee3d456b8eefb6ee9eeac1a2deef0c23e6 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 19:15:39 -0600 Subject: [PATCH 15/16] Add group-by-layer --- scripts/bbclasses | 76 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/scripts/bbclasses b/scripts/bbclasses index c3bfc49..ade8114 100755 --- a/scripts/bbclasses +++ b/scripts/bbclasses @@ -22,11 +22,13 @@ def parse_args(): ) parser.add_argument( "--no-filter-base", + "-n", action="store_true", help="Disable filtering of base BitBake infrastructure noise", ) parser.add_argument( "--group-by-layer", + "-g", action="store_true", help="Group files into Mermaid subgraphs by meta-layer", ) @@ -34,7 +36,7 @@ def parse_args(): "--log-level", "-l", type=str, - default="DEBUG", + default="INFO", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the logging output level. Defaults to INFO.", ) @@ -110,7 +112,16 @@ def discover_nodes(tinfoil, d): dep_files = [p for p in dep_files if p in exists] appends = [p for p in appends if p in exists] logging.info("Total node count: %d", len(exists)) - return exists, recipe_file, inherit_cache, dep_files, appends, global_inherits, common_prefix + return ( + exists, + recipe_file, + inherit_cache, + dep_files, + appends, + global_inherits, + bblayers, + common_prefix, + ) # Edge discovery helpers @@ -352,15 +363,47 @@ def node_shape(path, label): return f'["{escaped}"]' -def emit_mermaid(nodes, edges, common_prefix, out): +def classify_layers(nodes, bblayers): + """Group nodes by layer using longest-prefix match against BBLAYERS.""" + layers = {} + for path in sorted(nodes): + best = None + for layer in bblayers: + if path.startswith(layer + "/") and (best is None or len(layer) > len(best)): + best = layer + layer_name = os.path.basename(best) if best else "other" + layers.setdefault(layer_name, []).append(path) + return layers + + +def emit_node(path, common_prefix, indent, out): + """Emit a single Mermaid node definition.""" + rel = strip_prefix(path, common_prefix) + node_id = sanitize_mermaid_id(rel) + shape = node_shape(rel, label=rel) + out.write(f"{indent}{node_id}{shape}\n") + + +def emit_mermaid(nodes, edges, common_prefix, bblayers, group_by_layer, out): """Write a Mermaid flowchart to the given file object.""" # Normal inheritance diagrams are TB, but LR diagrams render better when there's lots of nodes out.write("flowchart LR\n") - for path in sorted(nodes): - path = strip_prefix(path, common_prefix) - node_id = sanitize_mermaid_id(path) - shape = node_shape(path, label=path) - out.write(f" {node_id}{shape}\n") + + if group_by_layer: + layers = classify_layers(nodes, bblayers) + for layer_name in sorted(layers): + paths = layers[layer_name] + if len(paths) > 1: + layer_id = sanitize_mermaid_id(layer_name) + out.write(f" subgraph {layer_id}[{layer_name}]\n") + for path in paths: + emit_node(path, common_prefix, " ", out) + out.write(" end\n") + else: + emit_node(paths[0], common_prefix, " ", out) + else: + for path in sorted(nodes): + emit_node(path, common_prefix, " ", out) for source, target, edge_type in sorted(edges): src_id = sanitize_mermaid_id(strip_prefix(source, common_prefix)) @@ -385,9 +428,16 @@ def main(args): with bb.tinfoil.Tinfoil(output=sys.stderr) as tinfoil: tinfoil.prepare(config_only=False) d = parse_recipe(tinfoil, args.recipe) - nodes, recipe_file, inherit_cache, depends, appends, global_inherits, common_prefix = ( - discover_nodes(tinfoil, d) - ) + ( + nodes, + recipe_file, + inherit_cache, + depends, + appends, + global_inherits, + bblayers, + common_prefix, + ) = discover_nodes(tinfoil, d) edges = discover_edges( nodes, recipe_file, inherit_cache, depends, appends, global_inherits, d ) @@ -399,10 +449,10 @@ def main(args): if args.output: with open(args.output, "w") as f: - emit_mermaid(nodes, edges, common_prefix, f) + emit_mermaid(nodes, edges, common_prefix, bblayers, args.group_by_layer, f) logging.info("Wrote output to %s", args.output) else: - emit_mermaid(nodes, edges, common_prefix, sys.stdout) + emit_mermaid(nodes, edges, common_prefix, bblayers, args.group_by_layer, sys.stdout) if __name__ == "__main__": From ad256ab5bb5810739e5832cbf92b9dd70df61805 Mon Sep 17 00:00:00 2001 From: Austin Gill Date: Mon, 16 Feb 2026 19:30:55 -0600 Subject: [PATCH 16/16] Add bbclasses to README --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index ff06f31..e097dc0 100644 --- a/README.md +++ b/README.md @@ -342,3 +342,73 @@ $ canstruct data/abort-then-full.log 2025-06-28T15:36:19.051620Z WARN csvizmo::can::tp: TP.CM_ABRT 0x1C <- 0x2A reason ExistingTransportSession pgn 0xEF00 (1750963079.795905) can0 18EF2A1C#11111111111111222222222222223333333333333344 ``` + +## bbclasses + +The [bbclasses](./scripts/bbclasses) script can parse BitBake recipes to generate an inheritance +diagram. It tries to evaluate variable expansion, and needs to run in your BitBake environment to +work properly. + +```sh +bbclasses --group-by-layer curl >curl.dot +``` + +```mermaid +flowchart LR + subgraph meta[meta] + poky/meta/classes-global/debian.bbclass{{"poky/meta/classes-global/debian.bbclass"}} + poky/meta/classes-global/package.bbclass{{"poky/meta/classes-global/package.bbclass"}} + poky/meta/classes-global/package_pkgdata.bbclass{{"poky/meta/classes-global/package_pkgdata.bbclass"}} + poky/meta/classes-global/package_rpm.bbclass{{"poky/meta/classes-global/package_rpm.bbclass"}} + poky/meta/classes-global/packagedata.bbclass{{"poky/meta/classes-global/packagedata.bbclass"}} + poky/meta/classes-recipe/autotools.bbclass{{"poky/meta/classes-recipe/autotools.bbclass"}} + poky/meta/classes-recipe/binconfig.bbclass{{"poky/meta/classes-recipe/binconfig.bbclass"}} + poky/meta/classes-recipe/multilib_header.bbclass{{"poky/meta/classes-recipe/multilib_header.bbclass"}} + poky/meta/classes-recipe/multilib_script.bbclass{{"poky/meta/classes-recipe/multilib_script.bbclass"}} + poky/meta/classes-recipe/pkgconfig.bbclass{{"poky/meta/classes-recipe/pkgconfig.bbclass"}} + poky/meta/classes-recipe/ptest.bbclass{{"poky/meta/classes-recipe/ptest.bbclass"}} + poky/meta/classes-recipe/siteinfo.bbclass{{"poky/meta/classes-recipe/siteinfo.bbclass"}} + poky/meta/classes-recipe/update-alternatives.bbclass{{"poky/meta/classes-recipe/update-alternatives.bbclass"}} + poky/meta/classes/chrpath.bbclass{{"poky/meta/classes/chrpath.bbclass"}} + poky/meta/classes/image-buildinfo.bbclass{{"poky/meta/classes/image-buildinfo.bbclass"}} + poky/meta/classes/siteconfig.bbclass{{"poky/meta/classes/siteconfig.bbclass"}} + poky/meta/conf/distro/include/ptest-packagelists.inc[["poky/meta/conf/distro/include/ptest-packagelists.inc"]] + poky/meta/recipes-support/curl/curl_8.7.1.bb["poky/meta/recipes-support/curl/curl_8.7.1.bb"] + end + subgraph meta-oem[meta-oem] + meta-oem/classes/dynamic-packagearch.bbclass{{"meta-oem/classes/dynamic-packagearch.bbclass"}} + meta-oem/classes/machine-overrides-extender.bbclass{{"meta-oem/classes/machine-overrides-extender.bbclass"}} + end + poky/meta-poky/classes/poky-sanity.bbclass{{"poky/meta-poky/classes/poky-sanity.bbclass"}} + meta-work/recipes-support/curl/curl__.bbappend(["meta-work/recipes-support/curl/curl_%.bbappend"]) + meta-oem/classes/dynamic-packagearch.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + meta-oem/classes/machine-overrides-extender.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta-poky/classes/poky-sanity.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-global/debian.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-global/package.bbclass -->|"inherit"| poky/meta/classes-global/debian.bbclass + poky/meta/classes-global/package.bbclass -->|"inherit"| poky/meta/classes-global/package_rpm.bbclass + poky/meta/classes-global/package_pkgdata.bbclass -->|"inherit"| poky/meta/classes-global/package.bbclass + poky/meta/classes-global/package_rpm.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-global/package_rpm.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-global/packagedata.bbclass -->|"inherit"| poky/meta/classes-global/package.bbclass + poky/meta/classes-recipe/autotools.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/autotools.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/binconfig.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/binconfig.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/multilib_header.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/multilib_header.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/multilib_script.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/multilib_script.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/pkgconfig.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/pkgconfig.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/ptest.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/ptest.bbclass -->|"inherit"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes-recipe/siteinfo.bbclass -->|"inherit"| poky/meta/classes-recipe/autotools.bbclass + poky/meta/classes-recipe/siteinfo.bbclass -->|"inherit"| poky/meta/classes-recipe/multilib_header.bbclass + poky/meta/classes-recipe/update-alternatives.bbclass -->|"inherit"| poky/meta/classes-recipe/multilib_script.bbclass + poky/meta/classes/chrpath.bbclass -->|"inherit"| poky/meta/classes-global/package.bbclass + poky/meta/classes/image-buildinfo.bbclass -->|"INHERIT"| poky/meta/recipes-support/curl/curl_8.7.1.bb + poky/meta/classes/siteconfig.bbclass -->|"inherit"| poky/meta/classes-recipe/autotools.bbclass + poky/meta/conf/distro/include/ptest-packagelists.inc -->|"require"| poky/meta/classes-recipe/ptest.bbclass + poky/meta/recipes-support/curl/curl_8.7.1.bb -->|"appends"| meta-work/recipes-support/curl/curl__.bbappend +```