1#!/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
57class Priority:
58    """Parses and holds a device initialization priority.
59
60    Parses an ELF section name for the corresponding initialization level and
61    priority, for example ".rel.z_init_PRE_KERNEL_155_" for "PRE_KERNEL_1 55".
62
63    The object can be used for comparing levels with one another.
64
65    Attributes:
66        name: the section name
67    """
68    def __init__(self, name):
69        for idx, level in enumerate(_DEVICE_INIT_LEVELS):
70            if level in name:
71                _, priority = name.strip("_").split(level)
72                self._level = idx
73                self._priority = int(priority)
74                self._level_priority = self._level * 100 + self._priority
75                return
76
77        raise ValueError("Unknown level in %s" % name)
78
79    def __repr__(self):
80        return "<%s %s %d>" % (self.__class__.__name__,
81                               _DEVICE_INIT_LEVELS[self._level], self._priority)
82
83    def __str__(self):
84        return "%s %d" % (_DEVICE_INIT_LEVELS[self._level], self._priority)
85
86    def __lt__(self, other):
87        return self._level_priority < other._level_priority
88
89    def __eq__(self, other):
90        return self._level_priority == other._level_priority
91
92    def __hash__(self):
93        return self._level_priority
94
95
96class ZephyrObjectFile:
97    """Load an object file and finds the device defined within it.
98
99    Load an object file and scans the relocation sections looking for the known
100    ones containing initialization callbacks. Then finds what device ordinals
101    are being initialized at which priority and stores the list internally.
102
103    A dictionary of {ordinal: Priority} is available in the defined_devices
104    class variable.
105
106    Attributes:
107        file_path: path of the file to be loaded.
108    """
109    def __init__(self, file_path):
110        self.file_path = file_path
111        self._elf = ELFFile(open(file_path, "rb"))
112        self._load_symbols()
113        self._find_defined_devices()
114
115    def _load_symbols(self):
116        """Initialize the symbols table."""
117        self._symbols = {}
118
119        for section in self._elf.iter_sections():
120            if not isinstance(section, SymbolTableSection):
121                continue
122
123            for num, sym in enumerate(section.iter_symbols()):
124                if sym.name:
125                    self._symbols[num] = sym.name
126
127    def _device_ord_from_rel(self, rel):
128        """Find a device ordinal from a device symbol name."""
129        sym_id = rel["r_info_sym"]
130        sym_name = self._symbols.get(sym_id, None)
131
132        if not sym_name:
133            return None
134
135        if not sym_name.startswith(_DEVICE_ORD_PREFIX):
136            return None
137
138        _, device_ord = sym_name.split(_DEVICE_ORD_PREFIX)
139        return int(device_ord)
140
141    def _find_defined_devices(self):
142        """Find the device structures defined in the object file."""
143        self.defined_devices = {}
144
145        for section in self._elf.iter_sections():
146            if not isinstance(section, RelocationSection):
147                continue
148
149            if not section.name.startswith(_INIT_SECTION_PREFIX):
150                continue
151
152            prio = Priority(section.name)
153
154            for rel in section.iter_relocations():
155                device_ord = self._device_ord_from_rel(rel)
156                if not device_ord:
157                    continue
158
159                if device_ord in self.defined_devices:
160                    raise ValueError(
161                            f"Device {device_ord} already defined, stale "
162                            "object files in the build directory? "
163                            "Try running a clean build.")
164
165                self.defined_devices[device_ord] = prio
166
167    def __repr__(self):
168        return (f"<{self.__class__.__name__} {self.file_path} "
169                f"defined_devices: {self.defined_devices}>")
170
171class Validator():
172    """Validates the initialization priorities.
173
174    Scans through a build folder for object files and list all the device
175    initialization priorities. Then compares that against the EDT derived
176    dependency list and log any found priority issue.
177
178    Attributes:
179        build_dir: the build directory to scan
180        edt_pickle_path: path of the EDT pickle file
181        log: a logging.Logger object
182    """
183    def __init__(self, build_dir, edt_pickle_path, log):
184        self.log = log
185
186        edtser = pathlib.Path(build_dir, edt_pickle_path)
187        with open(edtser, "rb") as f:
188            edt = pickle.load(f)
189
190        self._ord2node = edt.dep_ord2node
191
192        self._objs = []
193        for file in self._find_build_objfiles(build_dir, is_root=True):
194            obj = ZephyrObjectFile(file)
195            if obj.defined_devices:
196                self._objs.append(obj)
197                for dev in obj.defined_devices:
198                    dev_path = self._ord2node[dev].path
199                    self.log.debug(f"{file}: {dev_path}")
200
201        self._dev_priorities = {}
202        for obj in self._objs:
203            for dev, prio in obj.defined_devices.items():
204                if dev in self._dev_priorities:
205                    dev_path = self._ord2node[dev].path
206                    raise ValueError(
207                            f"ERROR: device {dev} ({dev_path}) already defined")
208                self._dev_priorities[dev] = prio
209
210        self.warnings = 0
211        self.errors = 0
212
213    def _find_build_objfiles(self, build_dir, is_root=False):
214        """Find all project object files, skip sub-build directories."""
215        if not is_root and pathlib.Path(build_dir, _BUILD_DIR_DETECT_FILE).exists():
216            return
217
218        for file in pathlib.Path(build_dir).iterdir():
219            if file.is_file() and file.name.endswith(_OBJ_FILE_SUFFIX):
220                yield file
221            if file.is_dir():
222                for file in self._find_build_objfiles(file.resolve()):
223                    yield file
224
225    def _check_dep(self, dev_ord, dep_ord):
226        """Validate the priority between two devices."""
227        if dev_ord == dep_ord:
228            return
229
230        dev_node = self._ord2node[dev_ord]
231        dep_node = self._ord2node[dep_ord]
232
233        if dev_node._binding and dep_node._binding:
234            dev_compat = dev_node._binding.compatible
235            dep_compat = dep_node._binding.compatible
236            if (dev_compat, dep_compat) in _INVERTED_PRIORITY_COMPATIBLES:
237                self.log.info(f"Swapped priority: {dev_compat}, {dep_compat}")
238                dev_ord, dep_ord = dep_ord, dev_ord
239
240        dev_prio = self._dev_priorities.get(dev_ord, None)
241        dep_prio = self._dev_priorities.get(dep_ord, None)
242
243        if not dev_prio or not dep_prio:
244            return
245
246        if dev_prio == dep_prio:
247            self.warnings += 1
248            self.log.warning(
249                    f"{dev_node.path} {dev_prio} == {dep_node.path} {dep_prio}")
250        elif dev_prio < dep_prio:
251            self.errors += 1
252            self.log.error(
253                    f"{dev_node.path} {dev_prio} < {dep_node.path} {dep_prio}")
254        else:
255            self.log.info(
256                    f"{dev_node.path} {dev_prio} > {dep_node.path} {dep_prio}")
257
258    def _check_edt_r(self, dev_ord, dev):
259        """Recursively check for dependencies of a device."""
260        for dep in dev.depends_on:
261            self._check_dep(dev_ord, dep.dep_ordinal)
262        if dev._binding and dev._binding.child_binding:
263            for child in dev.children.values():
264                if "compatible" in child.props:
265                    continue
266                if dev._binding.path != child._binding.path:
267                    continue
268                self._check_edt_r(dev_ord, child)
269
270    def check_edt(self):
271        """Scan through all known devices and validate the init priorities."""
272        for dev_ord in self._dev_priorities:
273            dev = self._ord2node[dev_ord]
274            self._check_edt_r(dev_ord, dev)
275
276def _parse_args(argv):
277    """Parse the command line arguments."""
278    parser = argparse.ArgumentParser(
279        description=__doc__,
280        formatter_class=argparse.RawDescriptionHelpFormatter,
281        allow_abbrev=False)
282
283    parser.add_argument("-d", "--build-dir", default="build",
284                        help="build directory to use")
285    parser.add_argument("-v", "--verbose", action="count",
286                        help=("enable verbose output, can be used multiple times "
287                              "to increase verbosity level"))
288    parser.add_argument("-w", "--fail-on-warning", action="store_true",
289                        help="fail on both warnings and errors")
290    parser.add_argument("--always-succeed", action="store_true",
291                        help="always exit with a return code of 0, used for testing")
292    parser.add_argument("-o", "--output",
293                        help="write the output to a file in addition to stdout")
294    parser.add_argument("--edt-pickle", default=pathlib.Path("zephyr", "edt.pickle"),
295                        help="path to read the pickled edtlib.EDT object from",
296                        type=pathlib.Path)
297
298    return parser.parse_args(argv)
299
300def _init_log(verbose, output):
301    """Initialize a logger object."""
302    log = logging.getLogger(__file__)
303
304    console = logging.StreamHandler()
305    console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
306    log.addHandler(console)
307
308    if output:
309        file = logging.FileHandler(output)
310        file.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
311        log.addHandler(file)
312
313    if verbose and verbose > 1:
314        log.setLevel(logging.DEBUG)
315    elif verbose and verbose > 0:
316        log.setLevel(logging.INFO)
317    else:
318        log.setLevel(logging.WARNING)
319
320    return log
321
322def main(argv=None):
323    args = _parse_args(argv)
324
325    log = _init_log(args.verbose, args.output)
326
327    log.info(f"check_init_priorities build_dir: {args.build_dir}")
328
329    validator = Validator(args.build_dir, args.edt_pickle, log)
330    validator.check_edt()
331
332    if args.always_succeed:
333        return 0
334
335    if args.fail_on_warning and validator.warnings:
336        return 1
337
338    if validator.errors:
339        return 1
340
341    return 0
342
343if __name__ == "__main__":
344    sys.exit(main(sys.argv[1:]))
345