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")