blob: 4036d8984b5e31fcc45a31d1f6c3e674d49ae1d9 [file] [log] [blame]
Fabio Baltierief1bd082023-03-08 17:50:56 +00001#!/usr/bin/env python3
2
3# Copyright 2023 Google LLC
4# SPDX-License-Identifier: Apache-2.0
5
6"""
7Checks the initialization priorities
8
9This script parses the object files in the specified build directory, creates a
10list of known devices and their effective initialization priorities and
11compares that with the device dependencies inferred from the devicetree
12hierarchy.
13
14This can be used to detect devices that are initialized in the incorrect order,
15but also devices that are initialized at the same priority but depends on each
16other, which can potentially break if the linking order is changed.
17"""
18
19import argparse
20import logging
21import os
22import pathlib
23import pickle
24import sys
25
26from elftools.elf.elffile import ELFFile
27from elftools.elf.relocation import RelocationSection
28from elftools.elf.sections import SymbolTableSection
29
30# This is needed to load edt.pickle files.
31sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..",
32 "dts", "python-devicetree", "src"))
33from devicetree import edtlib # pylint: disable=unused-import
34
35# Prefix used for relocation sections containing initialization data, as in
36# sequence of "struct init_entry".
37_INIT_SECTION_PREFIX = (".rel.z_init_", ".rela.z_init_")
38
39# Prefix used for "struct device" reference initialized based on devicetree
40# entries with a known ordinal.
41_DEVICE_ORD_PREFIX = "__device_dts_ord_"
42
43# File name suffix for object files to be scanned.
44_OBJ_FILE_SUFFIX = ".c.obj"
45
46# Defined init level in order of priority.
47_DEVICE_INIT_LEVELS = ["EARLY", "PRE_KERNEL_1", "PRE_KERNEL_2", "POST_KERNEL",
48 "APPLICATION", "SMP"]
49
50# File name to check for detecting and skiping nested build directories.
51_BUILD_DIR_DETECT_FILE = "CMakeCache.txt"
52
53# List of compatibles for node where the initialization priority should be the
54# opposite of the device tree inferred dependency.
55_INVERTED_PRIORITY_COMPATIBLES = frozenset()
56
Fabio Baltieri6d99e382023-07-21 15:20:43 +000057# List of compatibles for nodes where we don't check the priority.
58_IGNORE_COMPATIBLES = frozenset([
59 # There is no direct dependency between the CDC ACM UART and the USB
60 # device controller, the logical connection is established after USB
61 # device support is enabled.
62 "zephyr,cdc-acm-uart",
63 ])
64
Fabio Baltierief1bd082023-03-08 17:50:56 +000065class Priority:
66 """Parses and holds a device initialization priority.
67
68 Parses an ELF section name for the corresponding initialization level and
69 priority, for example ".rel.z_init_PRE_KERNEL_155_" for "PRE_KERNEL_1 55".
70
71 The object can be used for comparing levels with one another.
72
73 Attributes:
74 name: the section name
75 """
76 def __init__(self, name):
77 for idx, level in enumerate(_DEVICE_INIT_LEVELS):
78 if level in name:
Jordan Yates23280f42023-07-19 21:20:18 +100079 _, priority_str = name.strip("_").split(level)
80 priority, sub_priority = priority_str.split("_")
Fabio Baltierief1bd082023-03-08 17:50:56 +000081 self._level = idx
82 self._priority = int(priority)
Jordan Yates23280f42023-07-19 21:20:18 +100083 self._sub_priority = int(sub_priority)
84 # Tuples compare elementwise in order
85 self._level_priority = (self._level, self._priority, self._sub_priority)
Fabio Baltierief1bd082023-03-08 17:50:56 +000086 return
87
88 raise ValueError("Unknown level in %s" % name)
89
90 def __repr__(self):
Jordan Yates23280f42023-07-19 21:20:18 +100091 return "<%s %s %d %d>" % (self.__class__.__name__,
92 _DEVICE_INIT_LEVELS[self._level], self._priority, self._sub_priority)
Fabio Baltierief1bd082023-03-08 17:50:56 +000093
94 def __str__(self):
Jordan Yates23280f42023-07-19 21:20:18 +100095 return "%s %d %d" % (_DEVICE_INIT_LEVELS[self._level], self._priority, self._sub_priority)
Fabio Baltierief1bd082023-03-08 17:50:56 +000096
97 def __lt__(self, other):
98 return self._level_priority < other._level_priority
99
100 def __eq__(self, other):
101 return self._level_priority == other._level_priority
102
103 def __hash__(self):
104 return self._level_priority
105
106
107class ZephyrObjectFile:
108 """Load an object file and finds the device defined within it.
109
110 Load an object file and scans the relocation sections looking for the known
111 ones containing initialization callbacks. Then finds what device ordinals
112 are being initialized at which priority and stores the list internally.
113
114 A dictionary of {ordinal: Priority} is available in the defined_devices
115 class variable.
116
117 Attributes:
118 file_path: path of the file to be loaded.
119 """
120 def __init__(self, file_path):
121 self.file_path = file_path
122 self._elf = ELFFile(open(file_path, "rb"))
123 self._load_symbols()
124 self._find_defined_devices()
125
126 def _load_symbols(self):
127 """Initialize the symbols table."""
128 self._symbols = {}
129
130 for section in self._elf.iter_sections():
131 if not isinstance(section, SymbolTableSection):
132 continue
133
134 for num, sym in enumerate(section.iter_symbols()):
135 if sym.name:
136 self._symbols[num] = sym.name
137
138 def _device_ord_from_rel(self, rel):
139 """Find a device ordinal from a device symbol name."""
140 sym_id = rel["r_info_sym"]
141 sym_name = self._symbols.get(sym_id, None)
142
143 if not sym_name:
144 return None
145
146 if not sym_name.startswith(_DEVICE_ORD_PREFIX):
147 return None
148
149 _, device_ord = sym_name.split(_DEVICE_ORD_PREFIX)
150 return int(device_ord)
151
152 def _find_defined_devices(self):
153 """Find the device structures defined in the object file."""
154 self.defined_devices = {}
155
156 for section in self._elf.iter_sections():
157 if not isinstance(section, RelocationSection):
158 continue
159
160 if not section.name.startswith(_INIT_SECTION_PREFIX):
161 continue
162
163 prio = Priority(section.name)
164
165 for rel in section.iter_relocations():
166 device_ord = self._device_ord_from_rel(rel)
167 if not device_ord:
168 continue
169
170 if device_ord in self.defined_devices:
171 raise ValueError(
172 f"Device {device_ord} already defined, stale "
173 "object files in the build directory? "
174 "Try running a clean build.")
175
176 self.defined_devices[device_ord] = prio
177
178 def __repr__(self):
179 return (f"<{self.__class__.__name__} {self.file_path} "
180 f"defined_devices: {self.defined_devices}>")
181
182class Validator():
183 """Validates the initialization priorities.
184
185 Scans through a build folder for object files and list all the device
186 initialization priorities. Then compares that against the EDT derived
187 dependency list and log any found priority issue.
188
189 Attributes:
190 build_dir: the build directory to scan
191 edt_pickle_path: path of the EDT pickle file
192 log: a logging.Logger object
193 """
194 def __init__(self, build_dir, edt_pickle_path, log):
195 self.log = log
196
197 edtser = pathlib.Path(build_dir, edt_pickle_path)
198 with open(edtser, "rb") as f:
199 edt = pickle.load(f)
200
201 self._ord2node = edt.dep_ord2node
202
203 self._objs = []
204 for file in self._find_build_objfiles(build_dir, is_root=True):
205 obj = ZephyrObjectFile(file)
206 if obj.defined_devices:
207 self._objs.append(obj)
Fabio Baltieri44e691e2023-06-01 15:11:19 +0000208 for dev in obj.defined_devices:
209 dev_path = self._ord2node[dev].path
210 self.log.debug(f"{file}: {dev_path}")
Fabio Baltierief1bd082023-03-08 17:50:56 +0000211
212 self._dev_priorities = {}
213 for obj in self._objs:
214 for dev, prio in obj.defined_devices.items():
215 if dev in self._dev_priorities:
216 dev_path = self._ord2node[dev].path
217 raise ValueError(
218 f"ERROR: device {dev} ({dev_path}) already defined")
219 self._dev_priorities[dev] = prio
220
221 self.warnings = 0
222 self.errors = 0
223
224 def _find_build_objfiles(self, build_dir, is_root=False):
225 """Find all project object files, skip sub-build directories."""
226 if not is_root and pathlib.Path(build_dir, _BUILD_DIR_DETECT_FILE).exists():
227 return
228
229 for file in pathlib.Path(build_dir).iterdir():
230 if file.is_file() and file.name.endswith(_OBJ_FILE_SUFFIX):
231 yield file
232 if file.is_dir():
233 for file in self._find_build_objfiles(file.resolve()):
234 yield file
235
236 def _check_dep(self, dev_ord, dep_ord):
237 """Validate the priority between two devices."""
238 if dev_ord == dep_ord:
239 return
240
241 dev_node = self._ord2node[dev_ord]
242 dep_node = self._ord2node[dep_ord]
243
Fabio Baltieri6d99e382023-07-21 15:20:43 +0000244 if dev_node._binding:
245 dev_compat = dev_node._binding.compatible
246 if dev_compat in _IGNORE_COMPATIBLES:
247 self.log.info(f"Ignoring priority: {dev_node._binding.compatible}")
248 return
249
Fabio Baltierief1bd082023-03-08 17:50:56 +0000250 if dev_node._binding and dep_node._binding:
251 dev_compat = dev_node._binding.compatible
252 dep_compat = dep_node._binding.compatible
253 if (dev_compat, dep_compat) in _INVERTED_PRIORITY_COMPATIBLES:
254 self.log.info(f"Swapped priority: {dev_compat}, {dep_compat}")
255 dev_ord, dep_ord = dep_ord, dev_ord
256
257 dev_prio = self._dev_priorities.get(dev_ord, None)
258 dep_prio = self._dev_priorities.get(dep_ord, None)
259
260 if not dev_prio or not dep_prio:
261 return
262
263 if dev_prio == dep_prio:
264 self.warnings += 1
265 self.log.warning(
266 f"{dev_node.path} {dev_prio} == {dep_node.path} {dep_prio}")
267 elif dev_prio < dep_prio:
268 self.errors += 1
269 self.log.error(
270 f"{dev_node.path} {dev_prio} < {dep_node.path} {dep_prio}")
271 else:
272 self.log.info(
273 f"{dev_node.path} {dev_prio} > {dep_node.path} {dep_prio}")
274
275 def _check_edt_r(self, dev_ord, dev):
276 """Recursively check for dependencies of a device."""
277 for dep in dev.depends_on:
278 self._check_dep(dev_ord, dep.dep_ordinal)
279 if dev._binding and dev._binding.child_binding:
280 for child in dev.children.values():
281 if "compatible" in child.props:
282 continue
283 if dev._binding.path != child._binding.path:
284 continue
285 self._check_edt_r(dev_ord, child)
286
287 def check_edt(self):
288 """Scan through all known devices and validate the init priorities."""
289 for dev_ord in self._dev_priorities:
290 dev = self._ord2node[dev_ord]
291 self._check_edt_r(dev_ord, dev)
292
293def _parse_args(argv):
294 """Parse the command line arguments."""
295 parser = argparse.ArgumentParser(
296 description=__doc__,
297 formatter_class=argparse.RawDescriptionHelpFormatter,
298 allow_abbrev=False)
299
300 parser.add_argument("-d", "--build-dir", default="build",
301 help="build directory to use")
Fabio Baltieri44e691e2023-06-01 15:11:19 +0000302 parser.add_argument("-v", "--verbose", action="count",
303 help=("enable verbose output, can be used multiple times "
304 "to increase verbosity level"))
Fabio Baltierief1bd082023-03-08 17:50:56 +0000305 parser.add_argument("-w", "--fail-on-warning", action="store_true",
306 help="fail on both warnings and errors")
307 parser.add_argument("--always-succeed", action="store_true",
308 help="always exit with a return code of 0, used for testing")
309 parser.add_argument("-o", "--output",
310 help="write the output to a file in addition to stdout")
311 parser.add_argument("--edt-pickle", default=pathlib.Path("zephyr", "edt.pickle"),
312 help="path to read the pickled edtlib.EDT object from",
313 type=pathlib.Path)
314
315 return parser.parse_args(argv)
316
317def _init_log(verbose, output):
318 """Initialize a logger object."""
319 log = logging.getLogger(__file__)
320
321 console = logging.StreamHandler()
322 console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
323 log.addHandler(console)
324
325 if output:
326 file = logging.FileHandler(output)
327 file.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
328 log.addHandler(file)
329
Fabio Baltieri44e691e2023-06-01 15:11:19 +0000330 if verbose and verbose > 1:
331 log.setLevel(logging.DEBUG)
332 elif verbose and verbose > 0:
Fabio Baltierief1bd082023-03-08 17:50:56 +0000333 log.setLevel(logging.INFO)
334 else:
335 log.setLevel(logging.WARNING)
336
337 return log
338
339def main(argv=None):
340 args = _parse_args(argv)
341
342 log = _init_log(args.verbose, args.output)
343
344 log.info(f"check_init_priorities build_dir: {args.build_dir}")
345
346 validator = Validator(args.build_dir, args.edt_pickle, log)
347 validator.check_edt()
348
349 if args.always_succeed:
350 return 0
351
352 if args.fail_on_warning and validator.warnings:
353 return 1
354
355 if validator.errors:
356 return 1
357
358 return 0
359
360if __name__ == "__main__":
361 sys.exit(main(sys.argv[1:]))