Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 49 additions & 7 deletions scripts/include-dep-report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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 "
Expand All @@ -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")
Expand Down