From 6330585664f23499abb3109b8590a4b954e7134a Mon Sep 17 00:00:00 2001
From: Michele Calgaro <michele.calgaro@yahoo.it>
Date: Sat, 29 Aug 2026 14:54:22 +0900
Subject: Add script to generate module dependency graph.

The graph is generated from the Debian packaging dependency information and saved in the TDE_ROOT/main/.metadata/tde_module_dependencies.json file.

Co-authored-by: Opencode/Big Pickle
Signed-off-by: Michele Calgaro <michele.calgaro@yahoo.it>
---
 metadata/generate_tde_module_deps.py | 255 +++++++++++++++++++++++++++++++++++
 1 file changed, 255 insertions(+)
 create mode 100755 metadata/generate_tde_module_deps.py

diff --git a/metadata/generate_tde_module_deps.py b/metadata/generate_tde_module_deps.py
new file mode 100755
index 0000000..ae5d4bf
--- /dev/null
+++ b/metadata/generate_tde_module_deps.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python
+
+"""
+Generate tde_module_dependencies.json from the TDE source tree and
+Debian packaging information.
+
+This script is executed from the TDE `scripts/metadata` folder:
+    <tde>/scripts/metadata/generate_tde_module_deps.py
+
+The TDE layout is fixed, so the paths below are hard-coded relative to the
+TDE root (the parent of scripts/):
+    - main repo:        <root>/main
+    - debian packaging: <root>/packaging/debian/_base
+    - output file:      <root>/.metadata/tde_module_dependencies.json
+
+It enumerates every TDE source module that ships a debian/control file under
+packaging/debian/_base (categories: applications, core, dependencies,
+libraries) and derives the 1-step direct inter-module build-dependency graph
+from each module's Build-Depends / Build-Depends-Indep fields.
+
+Excluded on purpose (no inter-module build dependency edges):
+    - metapackages/ : (packaging aggregates, not source modules)
+    - common/       : (admin, libltdl, libtdevnc -- build support)
+    - build tools / system libs
+Any module whose name is in EXCLUDE_MODULES is dropped from the output.
+
+Output format:
+    - valid JSON, one entry "module : [deps]" per line, in alphabetical order
+    - only 1-step direct dependencies
+    - guaranteed acyclic DAG (verified at the end)
+
+Usage:
+    python3 generate_tde_module_deps.py
+"""
+
+import json
+import os
+import re
+import sys
+
+# ---------------------------------------------------------------------------
+# Paths (hard-coded; the TDE layout is fixed)
+# ---------------------------------------------------------------------------
+# This script lives in <tde>/scripts/, so the TDE root is its parent folder.
+THIS_SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
+SCRIPT_DIR = os.path.dirname(THIS_SCRIPT_DIR)
+TDE_ROOT = os.path.dirname(SCRIPT_DIR)
+
+# Every path below is derived from the (fixed) TDE root.
+MAIN_REPO = os.path.join(TDE_ROOT, "main")
+DEBIAN_PKG_REPO = os.path.join(TDE_ROOT, "packaging", "debian", "_base")
+OUTPUT_DIR = os.path.join(MAIN_REPO, ".metadata")
+OUTPUT_JSON = os.path.join(OUTPUT_DIR, "tde_module_dependencies.json")
+
+CATEGORIES = ["applications", "core", "dependencies", "libraries"]
+
+# Modules explicitly removed from the output.
+EXCLUDE_MODULES = { "common", "admin", "libltdl", "libtdevnc", "qt4-tqt-theme-engine" }
+
+# TDE package name -> owning module.
+PACKAGE_TO_MODULE = {
+    # --- dependencies ---
+    "libtqt3-mt-dev": "tqt", "libtqt3-mt-sqlite3": "tqt",
+    "tqt3-apps-dev": "tqt", "tqt3-dev-tools": "tqt", "tqt3-doc": "tqt",
+    "libarts1-trinity-dev": "arts", "libakode-dev": "akode",
+    "libart-2.0-dev": "libart-lgpl", "libr-dev": "libr",
+    "libcaldav-dev": "libcaldav", "libcarddav-dev": "libcarddav",
+    "libavahi-tqt-dev": "avahi-tqt", "libdbus-1-tqt-dev": "dbus-1-tqt",
+    "libpolkit-tqt-dev": "polkit-tqt", "libtqca-dev": "tqca",
+    "libtqscintilla-dev": "tqscintilla",
+    "python3-sip-tqt": "sip4-tqt", "python3-sip-tqt-dev": "sip4-tqt",
+    "python3-sip-tqt-dbg": "sip4-tqt", "libsip-tqt-dev": "sip4-tqt",
+    "pytqt-dev": "pytqt", "pytqt-tools": "pytqt",
+    "python3-pytqt-dbg": "pytqt", "libpythonize0-trinity-dev": "pytqt",
+    # --- core ---
+    "tdelibs-trinity-dev": "tdelibs", "tdelibs14-trinity-dev": "tdelibs",
+    "tdelibs-trinity": "tdelibs", "tdelibs14-trinity-doc": "tdelibs",
+    "tdebase-trinity-dev": "tdebase", "libkonq4-trinity-dev": "tdebase",
+    "ksplash-trinity": "tdebase",
+    "tdemultimedia-trinity-dev": "tdemultimedia",
+    "tdegames-trinity-dev": "tdegames", "libtdegames-trinity-dev": "tdegames",
+    "atlantik-trinity-dev": "tdegames",
+    "tdenetwork-trinity-dev": "tdenetwork",
+    "tdepim-trinity-dev": "tdepim", "libtdepim1-trinity-dev": "tdepim",
+    "libktnef1-trinity-dev": "tdepim", "libmimelib1-trinity-dev": "tdepim",
+    "libcvsservice-trinity-dev": "tdesdk",
+    "libsmoketqt-trinity-dev": "tdebindings",
+    "libpoppler-tqt-dev": "tdegraphics",
+    # --- libraries ---
+    "libkdcraw3-trinity-dev": "libkdcraw", "libkdcraw-trinity-dev": "libkdcraw",
+    "libkexiv2-3-trinity-dev": "libkexiv2", "libkexiv2-trinity-dev": "libkexiv2",
+    "libkipi0-trinity-dev": "libkipi", "libkipi-trinity-dev": "libkipi",
+    "libksquirrel-trinity-dev": "libksquirrel",
+    "libtdeldap-trinity-dev": "libtdeldap",
+    "pytde-dev": "pytde", "python3-pytde": "pytde",
+    "kipi-plugins-trinity": "kipi-plugins",
+    # --- applications ---
+    "koffice-trinity-dev": "koffice",
+    "libk3b-trinity-dev": "k3b",
+}
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+def read_module_build_deps(package_dir):
+    """Return dict 'Build-Depends'/'Build-Depends-Indep' raw strings for a
+    module's <pkg>/debian/control source paragraph, merging continuations."""
+    control = os.path.join(package_dir, "debian", "control")
+    if not os.path.isfile(control):
+        return {}
+    txt = open(control, encoding="utf-8", errors="replace").read()
+    source_para = txt.split("\n\n")[0]
+    lines = source_para.split("\n")
+    fields = {}
+    idx = 0
+    while idx < len(lines):
+        line = lines[idx]
+        if not line.startswith(" ") and ":" in line:
+            key, _, rest = line.partition(":")
+            value = rest.strip()
+            j = idx + 1
+            while j < len(lines) and lines[j].startswith(" "):
+                value += " " + lines[j].strip()
+                j += 1
+            fields[key] = value
+            idx = j
+        else:
+            idx += 1
+    return {
+        k: v for k, v in fields.items() if k in ("Build-Depends", "Build-Depends-Indep")
+    }
+
+
+def package_tokens(build_dep_str):
+    """Split a Build-Depends value into its package names (strip versions,
+    alternatives & arch restrictions)."""
+    tokens = set()
+    for clause in build_dep_str.split(","):
+        clause = clause.strip()
+        if not clause:
+            continue
+        name = re.split(r"[\s(<|]", clause)[0]
+        if name:
+            tokens.add(name)
+    return tokens
+
+
+def resolve_module(pkg, known_modules):
+    """Map a TDE package name to a source module, or None if it is a build
+    tool / system library with no inter-module edge."""
+    pkg = pkg.strip()
+    # explicit mapping, most specific first
+    if pkg in PACKAGE_TO_MODULE:
+        return PACKAGE_TO_MODULE[pkg]
+    # direct module-name match (e.g. Build-Depends literally on the module)
+    if pkg in known_modules:
+        return pkg
+    # generic trinity package whose module shares its name
+    for suffix in ("-trinity-dev", "-trinity"):
+        if pkg.endswith(suffix):
+            mname = pkg[: -len(suffix)]
+            if mname in known_modules:
+                return mname
+    # python trinity packages
+    m = re.match(r"python3-(tde.*)$", pkg)
+    if m and m.group(1) in known_modules:
+        return m.group(1)
+    return None  # system library / build tool -> drop
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+def main():
+    root_for_main = MAIN_REPO
+
+    # 1) enumerate modules from _base control files
+    control_dirs = {}  # module -> <base>/<category>/<module>
+    for cat in CATEGORIES:
+        cdir = os.path.join(DEBIAN_PKG_REPO, cat)
+        if not os.path.isdir(cdir):
+            continue
+        for root, _dirs, _files in os.walk(cdir):
+            files = os.listdir(root)
+            if "control" in files:
+                module = os.path.basename(os.path.dirname(root))
+                control_dirs[module] = os.path.dirname(root)
+
+    known_modules = set(control_dirs.keys())
+
+    # 2) build dependency edges
+    graph = {}
+    for module in sorted(known_modules):
+        if module in EXCLUDE_MODULES:
+            continue
+        pkg_dir = control_dirs[module]
+        bd = read_module_build_deps(pkg_dir)
+        deps = set()
+        for field in ("Build-Depends", "Build-Depends-Indep"):
+            if field in bd:
+                for pkg in package_tokens(bd[field]):
+                    dep_mod = resolve_module(pkg, known_modules)
+                    if dep_mod and dep_mod != module and dep_mod not in EXCLUDE_MODULES:
+                        deps.add(dep_mod)
+        graph[module] = sorted(deps)
+
+    # 3) validate graph acyclicity (iterative DFS)
+    def is_acyclic(g):
+        WHITE, GRAY, BLACK = 0, 1, 2
+        color = {k: WHITE for k in g}
+        for start in g:
+            if color[start] != WHITE:
+                continue
+            stack = [(start, iter(g[start]))]
+            color[start] = GRAY
+            while stack:
+                node, it = stack[-1]
+                advanced = False
+                for nxt in it:
+                    if color[nxt] == GRAY:
+                        return False, (node, nxt)
+                    if color[nxt] == WHITE:
+                        color[nxt] = GRAY
+                        stack.append((nxt, iter(g[nxt])))
+                        advanced = True
+                        break
+                if not advanced:
+                    color[node] = BLACK
+                    stack.pop()
+        return True, None
+
+    ok, cycle = is_acyclic(graph)
+    if not ok:
+        raise SystemExit("ERROR: dependency cycle detected: %s" % (cycle,))
+
+    # 4) write JSON, one entry per line, alphabetical
+    os.makedirs(OUTPUT_DIR, exist_ok=True) # create output folder if needed
+    keys = sorted(graph.keys())
+    lines = ["{"]
+    for i, k in enumerate(keys):
+        comma = "," if i < len(keys) - 1 else ""
+        lines.append("  {}: {}{}".format(json.dumps(k), json.dumps(graph[k]), comma))
+    lines.append("}")
+    out = "\n".join(lines) + "\n"
+    with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
+        f.write(out)
+
+    print("TDE root  : %s" % TDE_ROOT)
+    print("Main repo : %s" % MAIN_REPO)
+    print("Output    : %s" % OUTPUT_JSON)
+    print("Written %d modules -> %s" % (len(graph), OUTPUT_JSON))
+
+
+if __name__ == "__main__":
+    main()
-- 
cgit v1.2.3

