diff --git a/src/devicetree/dtlib.py b/src/devicetree/dtlib.py new file mode 100644 index 0000000..37f7ade --- /dev/null +++ b/src/devicetree/dtlib.py @@ -0,0 +1,1977 @@ +# 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. + +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 (