From 6973b377a80b62ba601ef65b0046866f203f1743 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sat, 22 Aug 2026 12:04:53 +1000 Subject: [PATCH] include-dep-report: bucket by what the build links, not by directory depth The six libnml subdirectories are in SUBDIRS and each has an empty Submakefile, present only to satisfy SUBMAKEFILES. Every source is listed in libnml/Submakefile and links into one libnml.so, so the five-way cycle the report showed between them was a directory layout, not a dependency. Fold a subdirectory into its parent when the parent's Submakefile lists its sources. emc/tp keeps its own bucket, the top-level Makefile builds it into tpmod. --- scripts/include-dep-report.py | 56 ++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/scripts/include-dep-report.py b/scripts/include-dep-report.py index 35c29e54b1a..efd54752900 100755 --- a/scripts/include-dep-report.py +++ b/scripts/include-dep-report.py @@ -2,7 +2,7 @@ """Directory-level include dependency report for the LinuxCNC src tree. Resolves every #include the way the compiler does, aggregates the result to the -directory granularity of SUBDIRS, finds the strongly connected components, and +directory that builds each file, finds the strongly connected components, and writes a markdown report. The exported-header list is read out of SRCHEADERS in src/Makefile, so the report follows what the build actually installs. @@ -103,13 +103,52 @@ def include_lines(path): yield lineno, m.group(1), m.group(2) +def folded_dirs(): + """Subdirectories the build does not treat as units of their own. + + A directory whose sources are listed by its parent's Submakefile compiles and + links as part of the parent, so an include crossing that boundary crosses + nothing. libnml is the case in the tree: SUBDIRS names its six + subdirectories and each has an empty Submakefile purely to satisfy + SUBMAKEFILES, while libnml/Submakefile lists every source and links them into + one libnml.so. Reporting those as separate nodes invents a cycle out of a + directory layout. Subdirectories the top-level Makefile builds on their own, + emc/tp into tpmod for one, are not folded. + """ + fold = {} + for cur, dirs, files in os.walk(SRC): + dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith(".")] + if "Submakefile" not in files: + continue + rel = os.path.relpath(cur, SRC) + if rel == ".": + continue + parent = os.path.dirname(rel) + pmake = os.path.join(SRC, parent, "Submakefile") + if not os.path.exists(pmake): + continue + with open(pmake, errors="replace") as f: + text = f.read() + pat = r"(^|[\s/])" + re.escape(os.path.basename(rel)) + r"/[\w.-]+\.(c|cc|cpp)\b" + if re.search(pat, text, re.M): + fold[rel] = parent + return fold + + +FOLD = folded_dirs() + + def module_of(rel): parts = rel.split("/") if len(parts) == 1: return "src" if parts[0] in ("emc", "hal", "libnml", "rtapi") and len(parts) > 2: - return "/".join(parts[:2]) - return parts[0] + mod = "/".join(parts[:2]) + else: + return parts[0] + while mod in FOLD: + mod = FOLD[mod] + return mod def sccs(adj, nodes): @@ -253,9 +292,11 @@ def main(): w("# src/ directory dependency report\n\n") w("Generated by `scripts/include-dep-report.py`, which resolves every `#include` " - "in the tree the way the compiler that sees it would, buckets each file into a " - "directory at the granularity of `SUBDIRS`, and reports the edges between those " - "buckets.\n\n") + "in the tree the way the compiler that sees it would, buckets each file into the " + "directory that builds it, and reports the edges between those buckets. A " + "subdirectory whose sources the parent's `Submakefile` lists compiles and links " + "as part of the parent, so it is folded into it; a subdirectory the top-level " + "`Makefile` builds on its own is not.\n\n") w("The two compiles do not get the same `-I`. Userspace gets `INCLUDE` from " "`src/Makefile`, which is `.` plus the `emc` added by `src/emc/Submakefile`. " "Realtime gets only what `EXTRA_CFLAGS` carries, `$(BASEPWD)` and the exported " @@ -276,7 +317,8 @@ def main(): f"{len(incycle)} directories, {len(intra)} edges, {intra_sites} include sites:\n") for c in comps: w(f" - {', '.join('`%s`' % m for m in c)}\n") - w(f"- {len(thin)} of those {len(intra)} cycle edges are **a single `#include`**.\n") + w(f"- {len(thin)} of those {len(intra)} cycle edges " + f"{'is' if len(thin) == 1 else 'are'} **a single `#include`**.\n") else: w("- No dependency cycles.\n") w(f"- {len(exported)} exported headers.\n\n")