# Copyright (c) 2019, Nordic Semiconductor # SPDX-License-Identifier: BSD-3-Clause # Tip: You can view just the documentation with 'pydoc3 dtlib' # _init_tokens() builds names dynamically. # # pylint: disable=undefined-variable """ A library for extracting information from .dts (devicetree) files. See the documentation for the DT and Node classes for more information. The top-level entry point of the library is the DT class. DT.__init__() takes a .dts file to parse and a list of directories to search for any /include/d files. """ import collections import errno import os import re import sys import textwrap # NOTE: testdtlib.py is the test suite for this library. It can be run directly # as a script: # # ./testdtlib.py class DT: """ Represents a devicetree parsed from a .dts file (or from many files, if the .dts file /include/s other files). Creating many instances of this class is fine. The library has no global state. These attributes are available on DT instances: root: A Node instance representing the root (/) node. alias2node: A dictionary that maps maps alias strings (from /aliases) to Node instances label2node: A dictionary that maps each node label (a string) to the Node instance for the node. label2prop: A dictionary that maps each property label (a string) to a Property instance. label2prop_offset: A dictionary that maps each label (a string) within a property value (e.g., 'x = label_1: < 1 label2: 2 >;') to a (prop, offset) tuple, where 'prop' is a Property instance and 'offset' the byte offset (0 for label_1 and 4 for label_2 in the example). phandle2node: A dictionary that maps each phandle (a number) to a Node instance. memreserves: A list of (labels, address, length) tuples for the /memreserve/s in the .dts file, in the same order as they appear in the file. 'labels' is a possibly empty set with all labels preceding the memreserve (e.g., 'label1: label2: /memreserve/ ...'). 'address' and 'length' are numbers. filename: The filename passed to the DT constructor. """ # # Public interface # def __init__(self, filename, include_path=()): """ Parses a DTS file to create a DT instance. Raises OSError if 'filename' can't be opened, and DTError for any parse errors. filename: Path to the .dts file to parse. include_path: An iterable (e.g. list or tuple) containing paths to search for /include/d and /incbin/'d files. By default, files are only looked up relative to the .dts file that contains the /include/ or /incbin/. """ self.filename = filename self._include_path = include_path with open(filename, encoding="utf-8") as f: self._file_contents = f.read() self._tok_i = self._tok_end_i = 0 self._filestack = [] self.alias2node = {} self._lexer_state = _DEFAULT self._saved_token = None self._lineno = 1 self._parse_dt() self._register_phandles() self._fixup_props() self._register_aliases() self._remove_unreferenced() self._register_labels() def get_node(self, path): """ Returns the Node instance for the node with path or alias 'path' (a string). Raises DTError if the path or alias doesn't exist. For example, both dt.get_node("/foo/bar") and dt.get_node("bar-alias") will return the 'bar' node below: /dts-v1/; / { foo { bar_label: bar { baz { }; }; }; aliases { bar-alias = &bar-label; }; }; Fetching subnodes via aliases is supported: dt.get_node("bar-alias/baz") returns the 'baz' node. """ if path.startswith("/"): return _root_and_path_to_node(self.root, path, path) # Path does not start with '/'. First component must be an alias. alias, _, rest = path.partition("/") if alias not in self.alias2node: _err("no alias '{}' found -- did you forget the leading '/' in " "the node path?".format(alias)) return _root_and_path_to_node(self.alias2node[alias], rest, path) def has_node(self, path): """ Returns True if the path or alias 'path' exists. See Node.get_node(). """ try: self.get_node(path) return True except DTError: return False def node_iter(self): """ Returns a generator for iterating over all nodes in the devicetree. For example, this will print the name of each node that has a property called 'foo': for node in dt.node_iter(): if "foo" in node.props: print(node.name) """ yield from self.root.node_iter() def __str__(self): """ Returns a DTS representation of the devicetree. Called automatically if the DT instance is print()ed. """ s = "/dts-v1/;\n\n" if self.memreserves: for labels, address, offset in self.memreserves: # List the labels in a consistent order to help with testing for label in labels: s += label + ": " s += "/memreserve/ {:#018x} {:#018x};\n" \ .format(address, offset) s += "\n" return s + str(self.root) def __repr__(self): """ Returns some information about the DT instance. Called automatically if the DT instance is evaluated. """ return "DT(filename='{}', include_path={})" \ .format(self.filename, self._include_path) # # Parsing # def _parse_dt(self): # Top-level parsing loop self._parse_header() self._parse_memreserves() self.root = None while True: tok = self._next_token() if tok.val == "/": # '/ { ... };', the root node if not self.root: self.root = Node(name="/", parent=None, dt=self) self._parse_node(self.root) elif tok.id in (_T_LABEL, _T_REF): # '&foo { ... };' or 'label: &foo { ... };'. The C tools only # support a single label here too. if tok.id is _T_LABEL: label = tok.val tok = self._next_token() if tok.id is not _T_REF: self._parse_error("expected label reference (&foo)") else: label = None try: node = self._ref2node(tok.val) except DTError as e: self._parse_error(e) node = self._parse_node(node) if label: _append_no_dup(node.labels, label) elif tok.id is _T_DEL_NODE: self._next_ref2node()._del() self._expect_token(";") elif tok.id is _T_OMIT_IF_NO_REF: self._next_ref2node()._omit_if_no_ref = True self._expect_token(";") elif tok.id is _T_EOF: if not self.root: self._parse_error("no root node defined") return else: self._parse_error("expected '/' or label reference (&foo)") def _parse_header(self): # Parses /dts-v1/ (expected) and /plugin/ (unsupported) at the start of # files. There may be multiple /dts-v1/ at the start of a file. has_dts_v1 = False while self._peek_token().id is _T_DTS_V1: has_dts_v1 = True self._next_token() self._expect_token(";") # /plugin/ always comes after /dts-v1/ if self._peek_token().id is _T_PLUGIN: self._parse_error("/plugin/ is not supported") if not has_dts_v1: self._parse_error("expected '/dts-v1/;' at start of file") def _parse_memreserves(self): # Parses /memreserve/, which appears after /dts-v1/ self.memreserves = [] while True: # Labels before /memreserve/ labels = [] while self._peek_token().id is _T_LABEL: _append_no_dup(labels, self._next_token().val) if self._peek_token().id is _T_MEMRESERVE: self._next_token() self.memreserves.append( (labels, self._eval_prim(), self._eval_prim())) self._expect_token(";") elif labels: self._parse_error("expected /memreserve/ after labels at " "beginning of file") else: return def _parse_node(self, node): # Parses the '{ ... };' part of 'node-name { ... };'. Returns the new # Node. self._expect_token("{") while True: labels, omit_if_no_ref = self._parse_propnode_labels() tok = self._next_token() if tok.id is _T_PROPNODENAME: if self._peek_token().val == "{": # ' { ...', expect node if tok.val.count("@") > 1: self._parse_error("multiple '@' in node name") # Fetch the existing node if it already exists. This # happens when overriding nodes. child = node.nodes.get(tok.val) or \ Node(name=tok.val, parent=node, dt=self) for label in labels: _append_no_dup(child.labels, label) if omit_if_no_ref: child._omit_if_no_ref = True node.nodes[child.name] = child self._parse_node(child) else: # Not ' { ...', expect property assignment if omit_if_no_ref: self._parse_error( "/omit-if-no-ref/ can only be used on nodes") prop = node._get_prop(tok.val) if self._check_token("="): self._parse_assignment(prop) elif not self._check_token(";"): # ';' is for an empty property, like 'foo;' self._parse_error("expected '{', '=', or ';'") for label in labels: _append_no_dup(prop.labels, label) elif tok.id is _T_DEL_NODE: tok2 = self._next_token() if tok2.id is not _T_PROPNODENAME: self._parse_error("expected node name") if tok2.val in node.nodes: node.nodes[tok2.val]._del() self._expect_token(";") elif tok.id is _T_DEL_PROP: tok2 = self._next_token() if tok2.id is not _T_PROPNODENAME: self._parse_error("expected property name") node.props.pop(tok2.val, None) self._expect_token(";") elif tok.val == "}": self._expect_token(";") return node else: self._parse_error("expected node name, property name, or '}'") def _parse_propnode_labels(self): # _parse_node() helpers for parsing labels and /omit-if-no-ref/s before # nodes and properties. Returns a (