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