1#!/usr/bin/env python3
2
3# SPDX-License-Identifier: Apache-2.0
4# SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors
5
6"""
7A script to help diagnose build errors related to Devicetree.
8
9To use this script as a standalone tool, provide the path to an edt.pickle file
10(e.g ./build/zephyr/edt.pickle) and a symbol that appeared in the build error
11message (e.g. __device_dts_ord_123).
12
13Example usage:
14
15./scripts/dts/dtdoctor_analyzer.py \\
16    --edt-pickle ./build/zephyr/edt.pickle \\
17    --symbol __device_dts_ord_123
18
19"""
20
21import argparse
22import os
23import pickle
24import re
25import sys
26from pathlib import Path
27
28sys.path.insert(0, str(Path(__file__).parent / "python-devicetree" / "src"))
29sys.path.insert(0, str(Path(__file__).parents[1] / "kconfig"))
30
31import kconfiglib
32from devicetree import edtlib
33from tabulate import tabulate
34
35
36def parse_args() -> argparse.Namespace:
37    parser = argparse.ArgumentParser(
38        description=__doc__,
39        formatter_class=argparse.RawDescriptionHelpFormatter,
40        allow_abbrev=False,
41    )
42    parser.add_argument(
43        "--edt-pickle",
44        required=True,
45        help="path to edt.pickle file corresponding to the build to analyze",
46    )
47    parser.add_argument(
48        "--symbol", required=True, help="symbol for which to obtain troubleshooting information"
49    )
50    return parser.parse_args()
51
52
53def load_edt(path: str) -> edtlib.EDT:
54    with open(path, "rb") as f:
55        return pickle.load(f)
56
57
58def setup_kconfig() -> kconfiglib.Kconfig:
59    kconf = kconfiglib.Kconfig(os.path.join(os.environ.get("ZEPHYR_BASE"), "Kconfig"), warn=False)
60    return kconf
61
62
63def format_node(node: edtlib.Node) -> str:
64    return f"{node.labels[0]}: {node.path}" if node.labels else node.path
65
66
67def find_kconfig_deps(kconf: kconfiglib.Kconfig, dt_has_symbol: str) -> set[str]:
68    """
69    Find all Kconfig symbols that depend on the provided DT_HAS symbol.
70    """
71    prefix = os.environ.get("CONFIG_", "CONFIG_")
72    target = f"{prefix}{dt_has_symbol}"
73    deps = set()
74
75    def collect_syms(expr):
76        # Recursively collect all symbol names in the expression tree except the target
77        for item in kconfiglib.expr_items(expr):
78            if not isinstance(item, kconfiglib.Symbol):
79                continue
80            sym_name = f"{prefix}{item.name}"
81            if sym_name != target:
82                deps.add(sym_name)
83
84    for sym in getattr(kconf, "unique_defined_syms", []):
85        for node in sym.nodes:
86            # Check dependencies
87            if node.dep is None:
88                continue
89            dep_str = kconfiglib.expr_str(
90                node.dep,
91                lambda sc: f"{prefix}{sc.name}" if hasattr(sc, 'name') and sc.name else str(sc),
92            )
93            if target in dep_str:
94                collect_syms(node.dep)
95
96            # Check selects/implies
97            for attr in ["orig_selects", "orig_implies"]:
98                for value, _ in getattr(node, attr, []) or []:
99                    value_str = kconfiglib.expr_str(value, str)
100                    if target in value_str:
101                        collect_syms(value)
102
103    return deps
104
105
106def handle_enabled_node(node: edtlib.Node) -> list[str]:
107    """
108    Handle diagnosis for an enabled DT node (linker error, one or more Kconfigs might be gating
109    the device driver).
110    """
111    lines = [f"'{format_node(node)}' is enabled but no driver appears to be available for it.\n"]
112
113    compats = list(getattr(node, "compats", []))
114    if compats:
115        kconf = setup_kconfig()
116        deps = set()
117        for compat in compats:
118            dt_has = f"DT_HAS_{edtlib.str_as_token(compat.upper())}_ENABLED"
119            deps.update(find_kconfig_deps(kconf, dt_has))
120
121        if deps:
122            lines.append("Try enabling these Kconfig options:\n")
123            lines.extend(f" - {dep}=y" for dep in sorted(deps))
124    else:
125        lines.append("Could not determine compatible; check driver Kconfig manually.")
126
127    return lines
128
129
130def handle_disabled_node(node: edtlib.Node) -> list[str]:
131    """
132    Handle diagnosis for a disabled DT node.
133    """
134    edt = node.edt
135    status_prop = node._node.props.get('status')
136    lines = [f"'{format_node(node)}' is disabled in {status_prop.filename}:{status_prop.lineno}"]
137
138    # Show dependency
139    users = getattr(node, "required_by", [])
140    if users:
141        lines.append("The following nodes depend on it:")
142        lines.extend(f" - {u.path}" for u in users)
143
144    # Show chosen/alias references
145    chosen_refs = [
146        name
147        for name, n in (getattr(edt, "chosen_nodes", {}) or getattr(edt, "chosen", {})).items()
148        if n is node
149    ]
150    alias_refs = [name for name, n in getattr(edt, "aliases", {}).items() if n is node]
151
152    if chosen_refs or alias_refs:
153        lines.append("")
154
155    if chosen_refs:
156        lines.append(
157            "It is referenced as a \"chosen\" in "
158            f"""{', '.join([f"'{ref}'" for ref in sorted(chosen_refs)])}"""
159        )
160    if alias_refs:
161        lines.append(
162            "It is referenced by the following aliases: "
163            f"""{', '.join([f"'{ref}'" for ref in sorted(alias_refs)])}"""
164        )
165
166    lines.append("\nTry enabling the node by setting its 'status' property to 'okay'.")
167
168    return lines
169
170
171def main() -> int:
172    args = parse_args()
173
174    m = re.search(r"__device_dts_ord_(\d+)", args.symbol)
175    if not m:
176        return 1
177
178    # Find node by ordinal amongst all nodes
179    edt = load_edt(args.edt_pickle)
180    node = next((n for n in edt.nodes if n.dep_ordinal == int(m.group(1))), None)
181    if not node:
182        print(f"Ordinal {m.group(1)} not found in edt.pickle", file=sys.stderr)
183        return 1
184
185    if node.status == "okay":
186        lines = handle_enabled_node(node)
187    else:
188        lines = handle_disabled_node(node)
189
190    print(tabulate([["\n".join(lines)]], headers=["DT Doctor"], tablefmt="grid"))
191    return 0
192
193
194if __name__ == "__main__":
195    sys.exit(main())
196