diff --git a/scripts/dts/edtlib.py b/scripts/dts/edtlib.py index 67a3009246f..6e0eb58568f 100644 --- a/scripts/dts/edtlib.py +++ b/scripts/dts/edtlib.py @@ -12,16 +12,16 @@ properties. Some of this interpretation is based on conventions established by the Linux kernel, so the Documentation/devicetree/bindings in the Linux source code is sometimes good reference material. -In Zephyr, bindings are YAML files that describe devicetree nodes. Devicetree +Bindings are YAML files that describe devicetree nodes. Devicetree nodes are usually mapped to bindings via their 'compatible = "..."' property, but a binding can also come from a 'child-binding:' key in the binding for the -parent devicetree node. See binding-template.yaml for details. +parent devicetree node. Each devicetree node (dtlib.Node) gets a corresponding edtlib.Node instance, which has all the information related to the node. -The top-level entry point of the library is the EDT class. EDT.__init__() takes -a .dts file to parse and a list of paths to directories containing bindings. +The top-level entry points for the library are the EDT and Binding classes. +See their constructor docstrings for details. """ # NOTE: testedtlib.py is the test suite for this library. @@ -65,9 +65,6 @@ a .dts file to parse and a list of paths to directories containing bindings. # # @properties are documented in the class docstring, as if they were # variables. See the existing @properties for a template. -# -# - Please use ""-quoted strings instead of ''-quoted strings, just to make -# things consistent (''-quoting is more common otherwise in Python) from collections import OrderedDict, defaultdict import os @@ -152,7 +149,7 @@ class EDT: default_prop_types=True, support_fixed_partitions_on_any_bus=True, infer_binding_for_paths=None): - """EDT constructor. This is the top-level entry point to the library. + """EDT constructor. dts: Path to devicetree .dts file @@ -200,7 +197,7 @@ class EDT: self._dt = DT(dts) _check_dt(self._dt) - self._init_compat2binding(bindings_dirs) + self._init_compat2binding() self._init_nodes() self._init_graph() self._init_luts() @@ -294,18 +291,19 @@ class EDT: for intr in node.interrupts: self._graph.add_edge(node, intr.controller) - def _init_compat2binding(self, bindings_dirs): - # Creates self._compat2binding. This is a dictionary that maps - # (, ) tuples (both strings) to (, ) - # tuples. is the binding in parsed PyYAML format, and - # the path to the binding (nice for binding-related error messages). + def _init_compat2binding(self): + # Creates self._compat2binding, a dictionary that maps + # (, ) tuples (both strings) to Binding objects. # - # For example, self._compat2binding["company,dev", "can"] contains the - # binding/path for the 'company,dev' device, when it appears on the CAN - # bus. + # The Binding objects are created from YAML files discovered + # in self.bindings_dirs as needed. + # + # For example, self._compat2binding["company,dev", "can"] + # contains the Binding for the 'company,dev' device, when it + # appears on the CAN bus. # # For bindings that don't specify a bus, is None, so that e.g. - # self._compat2binding["company,notonbus", None] contains the binding. + # self._compat2binding["company,notonbus", None] is the Binding. # # Only bindings for 'compatible' strings that appear in the devicetree # are loaded. @@ -317,7 +315,9 @@ class EDT: "|".join(re.escape(compat) for compat in dt_compats) ).search - self._binding_paths = _binding_paths(bindings_dirs) + self._binding_paths = _binding_paths(self.bindings_dirs) + self._binding_fname2path = {os.path.basename(path): path + for path in self._binding_paths} self._compat2binding = {} for binding_path in self._binding_paths: @@ -336,127 +336,59 @@ class EDT: try: # Parsed PyYAML output (Python lists/dictionaries/strings/etc., # representing the file) - binding = yaml.load(contents, Loader=_BindingLoader) + raw = yaml.load(contents, Loader=_BindingLoader) except yaml.YAMLError as e: self._warn("'{}' appears in binding directories but isn't " "valid YAML: {}".format(binding_path, e)) continue + # Convert the raw data to a Binding object, erroring out + # if necessary. + binding = self._binding(raw, binding_path, dt_compats) - # Returns the string listed in 'compatible:' in 'binding', or None if - # no compatible is found. - - if binding is None or "compatible" not in binding: - # Empty file, binding fragment, spurious file, or old-style - # compat - binding_compat = None - else: - binding_compat = binding["compatible"] - if not isinstance(binding_compat, str): - _err("malformed 'compatible: {}' field in {} - " - "should be a string, not {}" - .format(binding_compat, binding_path, - type(binding_compat).__name__)) - - - if binding_compat not in dt_compats: - # Either not a binding (binding_compat is None -- might be a - # binding fragment or a spurious file), or a binding whose - # compatible does not appear in the devicetree (picked up via - # some unrelated text in the binding file that happened to - # match a compatible) + if binding is None: + # Either the file is not a binding or it's a binding + # whose compatible does not appear in the devicetree + # (picked up via some unrelated text in the binding + # file that happened to match a compatible). continue - # It's a match. Merge in the included bindings, do sanity checks, - # and register the binding. - - binding = self._merge_included_bindings(binding, binding_path) - self._check_binding(binding, binding_path) - - on_bus = _on_bus_from_binding(binding) - # Do not allow two different bindings to have the same # 'compatible:'/'on-bus:' combo - old_binding = self._compat2binding.get((binding_compat, on_bus)) + old_binding = self._compat2binding.get((binding.compatible, + binding.on_bus)) if old_binding: - msg = "both {} and {} have 'compatible: {}'".format( - old_binding[1], binding_path, binding_compat) - if on_bus is not None: - msg += " and 'on-bus: {}'".format(on_bus) + msg = (f"both {old_binding.path} and {binding_path} have " + f"'compatible: {binding.compatible}'") + if binding.on_bus is not None: + msg += f" and 'on-bus: {binding.on_bus}'" _err(msg) - self._compat2binding[binding_compat, on_bus] = (binding, binding_path) + # Register the binding. + self._compat2binding[binding.compatible, binding.on_bus] = binding - def _merge_included_bindings(self, binding, binding_path): - # Merges any bindings listed in the 'include:' section of 'binding' - # into the top level of 'binding'. + def _binding(self, raw, binding_path, dt_compats): + # Convert a 'raw' binding from YAML to a Binding object and return it. # - # Properties in 'binding' take precedence over properties from included - # bindings. + # Error out if the raw data looks like an invalid binding. + # + # Return None if the file doesn't contain a binding or the + # binding's compatible isn't in dt_compats. - fnames = [] + # Get the 'compatible:' string. + if raw is None or "compatible" not in raw: + # Empty file, binding fragment, spurious file, etc. + return None - if "include" in binding: - include = binding.pop("include") - if isinstance(include, str): - fnames.append(include) - elif isinstance(include, list): - if not all(isinstance(elm, str) for elm in include): - _err("all elements in 'include:' in {} should be strings" - .format(binding_path)) - fnames += include - else: - _err("'include:' in {} should be a string or a list of strings" - .format(binding_path)) + compatible = raw["compatible"] - if "child-binding" in binding and "include" in binding["child-binding"]: - self._merge_included_bindings(binding["child-binding"], binding_path) + if compatible not in dt_compats: + # Not a compatible we care about. + return None - if not fnames: - return binding - - # Got a list of included files in 'fnames'. Now we need to merge them - # together and then merge them into 'binding'. - - # First, merge the included files together. If more than one included - # file has a 'required:' for a particular property, OR the values - # together, so that 'required: true' wins. - - merged_included = self._load_binding(fnames[0]) - for fname in fnames[1:]: - included = self._load_binding(fname) - _merge_props(merged_included, included, None, binding_path, - check_required=False) - - # Next, merge the merged included files into 'binding'. Error out if - # 'binding' has 'required: false' while the merged included files have - # 'required: true'. - - _merge_props(binding, merged_included, None, binding_path, - check_required=True) - - return binding - - def _load_binding(self, fname): - # Returns the contents of the binding given by 'fname' after merging - # any bindings it lists in 'include:' into it. 'fname' is just the - # basename of the file, so we check that there aren't multiple - # candidates. - - paths = [path for path in self._binding_paths - if os.path.basename(path) == fname] - - if not paths: - _err("'{}' not found".format(fname)) - - if len(paths) > 1: - _err("multiple candidates for included file '{}': {}" - .format(fname, ", ".join(paths))) - - with open(paths[0], encoding="utf-8") as f: - return self._merge_included_bindings( - yaml.load(f, Loader=_BindingLoader), - paths[0]) + # Initialize and return the Binding object. + return Binding(binding_path, self._binding_fname2path, raw=raw, + warn_file=self._warn_file) def _init_nodes(self): # Creates a list of edtlib.Node objects from the dtlib.Node objects, in @@ -478,7 +410,7 @@ class EDT: else: node.compats = [] node.bus_node = node._bus_node(self._fixed_partitions_no_bus) - node._init_binding() + node._init_binding(warn_file=self._warn_file) node._init_regs() self.nodes.append(node) @@ -522,106 +454,6 @@ class EDT: node = nodeset[0] self.dep_ord2node[node.dep_ordinal] = node - def _check_binding(self, binding, binding_path): - # Does sanity checking on 'binding'. Only takes 'self' for the sake of - # self._warn(). - - if "description" not in binding: - _err("missing 'description' property in " + binding_path) - - for prop in "title", "description": - if prop in binding and (not isinstance(binding[prop], str) or - not binding[prop]): - _err("malformed or empty '{}' in {}" - .format(prop, binding_path)) - - ok_top = {"title", "description", "compatible", "properties", "#cells", - "bus", "on-bus", "parent-bus", "child-bus", "parent", "child", - "child-binding", "sub-node"} - - for prop in binding: - if prop not in ok_top and not prop.endswith("-cells"): - _err("unknown key '{}' in {}, expected one of {}, or *-cells" - .format(prop, binding_path, ", ".join(ok_top))) - - for bus_key in "bus", "on-bus": - if bus_key in binding and \ - not isinstance(binding[bus_key], str): - _err("malformed '{}:' value in {}, expected string" - .format(bus_key, binding_path)) - - self._check_binding_properties(binding, binding_path) - - if "child-binding" in binding: - if not isinstance(binding["child-binding"], dict): - _err("malformed 'child-binding:' in {}, expected a binding " - "(dictionary with keys/values)".format(binding_path)) - - self._check_binding(binding["child-binding"], binding_path) - - def ok_cells_val(val): - # Returns True if 'val' is an okay value for '*-cells:' (or the - # legacy '#cells:') - - return isinstance(val, list) and \ - all(isinstance(elm, str) for elm in val) - - for key, val in binding.items(): - if key.endswith("-cells") or key == "#cells": - if not ok_cells_val(val): - _err("malformed '{}:' in {}, expected a list of strings" - .format(key, binding_path)) - - def _check_binding_properties(self, binding, binding_path): - # _check_binding() helper for checking the contents of 'properties:'. - # Only takes 'self' for the sake of self._warn(). - - if "properties" not in binding: - return - - ok_prop_keys = {"description", "type", "required", "category", - "enum", "const", "default"} - - for prop_name, options in binding["properties"].items(): - for key in options: - if key == "category": - self._warn( - "please put 'required: {}' instead of 'category: {}' " - "in properties: {}: ...' in {} - 'category' will be " - "removed".format( - "true" if options["category"] == "required" - else "false", - options["category"], prop_name, binding_path)) - - if key not in ok_prop_keys: - _err("unknown setting '{}' in 'properties: {}: ...' in {}, " - "expected one of {}".format( - key, prop_name, binding_path, - ", ".join(ok_prop_keys))) - - _check_prop_type_and_default( - prop_name, options.get("type"), - options.get("required") or options.get("category") == "required", - options.get("default"), binding_path) - - if "required" in options and not isinstance(options["required"], bool): - _err("malformed 'required:' setting '{}' for '{}' in 'properties' " - "in {}, expected true/false" - .format(options["required"], prop_name, binding_path)) - - if "description" in options and \ - not isinstance(options["description"], str): - _err("missing, malformed, or empty 'description' for '{}' in " - "'properties' in {}".format(prop_name, binding_path)) - - if "enum" in options and not isinstance(options["enum"], list): - _err("enum in {} for property '{}' is not a list" - .format(binding_path, prop_name)) - - if "const" in options and not isinstance(options["const"], (int, str)): - _err("const in {} for property '{}' is not a scalar" - .format(binding_path, prop_name)) - def _warn(self, msg): if self._warn_file is not None: print("warning: " + msg, file=self._warn_file) @@ -777,8 +609,8 @@ class Node: @property def description(self): "See the class docstring." - if self._binding and "description" in self._binding: - return self._binding["description"].strip() + if self._binding: + return self._binding.description return None @property @@ -851,22 +683,8 @@ class Node: @property def bus(self): "See the class docstring" - binding = self._binding - if not binding: - return None - - if "bus" in binding: - return binding["bus"] - - # Legacy key - if "child-bus" in binding: - return binding["child-bus"] - - # Legacy key - if "child" in binding: - # _check_binding() has checked that the "bus" key exists - return binding["child"]["bus"] - + if self._binding: + return self._binding.bus return None @property @@ -923,7 +741,7 @@ class Node: "binding " + self.binding_path if self.binding_path else "no binding") - def _init_binding(self): + def _init_binding(self, warn_file=None): # Initializes Node.matching_compat, Node._binding, and # Node.binding_path. # @@ -936,7 +754,7 @@ class Node: # node_iter() order. if self.path in self.edt._infer_binding_for_paths: - self._binding_from_properties() + self._binding_from_properties(warn_file=warn_file) return if self.compats: @@ -945,43 +763,40 @@ class Node: for compat in self.compats: if (compat, on_bus) in self.edt._compat2binding: # Binding found + binding = self.edt._compat2binding[compat, on_bus] + self.binding_path = binding.path self.matching_compat = compat - self._binding, self.binding_path = \ - self.edt._compat2binding[compat, on_bus] - + self._binding = binding return else: - # No 'compatible' property. See if the parent binding has a - # 'child-binding:' key that gives the binding (or a legacy - # 'sub-node:' key). + # No 'compatible' property. See if the parent binding has + # a compatible. This can come from one or more levels of + # nesting with 'child-binding:'. binding_from_parent = self._binding_from_parent() if binding_from_parent: self._binding = binding_from_parent - self.binding_path = self.parent.binding_path - self.matching_compat = self.parent.matching_compat + self.binding_path = self._binding.path + self.matching_compat = self._binding.compatible return # No binding found self._binding = self.binding_path = self.matching_compat = None - def _binding_from_properties(self): - # Returns a binding synthesized from the properties in the node. + def _binding_from_properties(self, warn_file): + # Sets up a Binding object synthesized from the properties in the node. if self.compats: _err(f"compatible in node with inferred binding: {self.path}") - self._binding = OrderedDict() - self.matching_compat = self.path.split('/')[-1] - self.compats = [self.matching_compat] - self.binding_path = None - - properties = OrderedDict() - self._binding["properties"] = properties + # Synthesize a 'raw' binding as if it had been parsed from YAML. + raw = { + 'description': 'Inferred binding from properties, via edtlib.', + 'properties': {}, + } for name, prop in self._node.props.items(): pp = OrderedDict() - properties[name] = pp if prop.type == TYPE_EMPTY: pp["type"] = "boolean" elif prop.type == TYPE_BYTES: @@ -1002,10 +817,18 @@ class Node: pp["type"] = "phandle-array" else: _err(f"cannot infer binding from property: {prop}") + raw['properties'][name] = pp + + # Set up Node state. + self.binding_path = None + self.matching_compat = None + self.compats = [] + self._binding = Binding(None, {}, raw=raw, require_compatible=False, + warn_file=warn_file) def _binding_from_parent(self): # Returns the binding from 'child-binding:' in the parent node's - # binding (or from the legacy 'sub-node:' key), or None if missing + # binding. if not self.parent: return None @@ -1014,14 +837,8 @@ class Node: if not pbinding: return None - if "child-binding" in pbinding: - return pbinding["child-binding"] - - # Backwards compatibility - if "sub-node" in pbinding: - return {"title": pbinding["title"], - "description": pbinding["description"], - "properties": pbinding["sub-node"]["properties"]} + if pbinding.child_binding: + return pbinding.child_binding return None @@ -1056,14 +873,14 @@ class Node: node = self._node if self._binding: - binding_props = self._binding.get("properties") + prop2specs = self._binding.prop2specs else: - binding_props = None + prop2specs = None # Initialize self.props - if binding_props: - for name, options in binding_props.items(): - self._init_prop(name, options) + if prop2specs: + for prop_spec in prop2specs.values(): + self._init_prop(prop_spec) self._check_undeclared_props() elif default_prop_types: for name in node.props: @@ -1080,31 +897,31 @@ class Node: prop.enum_index = None self.props[name] = prop - def _init_prop(self, name, options): - # _init_props() helper for initializing a single property + def _init_prop(self, prop_spec): + # _init_props() helper for initializing a single property. + # 'prop_spec' is a PropertySpec object from the node's binding. - prop_type = options.get("type") + name = prop_spec.name + prop_type = prop_spec.type if not prop_type: _err("'{}' in {} lacks 'type'".format(name, self.binding_path)) - val = self._prop_val( - name, prop_type, - options.get("required") or options.get("category") == "required", - options.get("default")) + val = self._prop_val(name, prop_type, prop_spec.required, + prop_spec.default) if val is None: # 'required: false' property that wasn't there, or a property type # for which we store no data. return - enum = options.get("enum") + enum = prop_spec.enum if enum and val not in enum: _err("value of property '{}' on {} in {} ({!r}) is not in 'enum' " "list in {} ({!r})" .format(name, self.path, self.edt.dts_path, val, self.binding_path, enum)) - const = options.get("const") + const = prop_spec.const if const is not None and val != const: _err("value of property '{}' on {} in {} ({!r}) is different from " "the 'const' value specified in {} ({!r})" @@ -1119,7 +936,7 @@ class Node: prop = Property() prop.node = self prop.name = name - prop.description = options.get("description") + prop.description = prop_spec.description if prop.description: prop.description = prop.description.strip() prop.val = val @@ -1207,8 +1024,8 @@ class Node: if prop_type == "path": return self.edt._node2enode[prop.to_path()] - # prop_type == "compound". We have already checked that the 'type:' - # value is valid, in _check_binding(). + # prop_type == "compound". Checking that the 'type:' + # value is valid is done in _check_prop_type_and_default(). # # 'compound' is a dummy type for properties that don't fit any of the # patterns above, so that we can require all entries in 'properties:' @@ -1218,11 +1035,6 @@ class Node: def _check_undeclared_props(self): # Checks that all properties are declared in the binding - if "properties" in self._binding: - declared_props = self._binding["properties"].keys() - else: - declared_props = set() - for prop_name in self._node.props: # Allow a few special properties to not be declared in the binding if prop_name.endswith("-controller") or \ @@ -1233,7 +1045,7 @@ class Node: "interrupt-parent", "interrupts-extended", "device_type"}: continue - if prop_name not in declared_props: + if prop_name not in self._binding.prop2specs: _err("'{}' appears in {} in {}, but is not declared in " "'properties:' in {}" .format(prop_name, self._node.path, self.edt.dts_path, @@ -1347,7 +1159,7 @@ class Node: basename = "gpio" else: # Strip -s. We've already checked that the property names end in -s - # in _check_binding(). + # in _check_prop_type_and_default(). basename = prop.name[:-1] res = [] @@ -1378,11 +1190,8 @@ class Node: _err("{} controller {!r} for {!r} lacks binding" .format(basename, controller._node, self._node)) - if basename + "-cells" in controller._binding: - cell_names = controller._binding[basename + "-cells"] - elif "#cells" in controller._binding: - # Backwards compatibility - cell_names = controller._binding["#cells"] + if basename in controller._binding.specifier2cells: + cell_names = controller._binding.specifier2cells[basename] else: # Treat no *-cells in the binding the same as an empty *-cells, so # that bindings don't have to have e.g. an empty 'clock-cells:' for @@ -1560,6 +1369,418 @@ class Property: return "".format(", ".join(fields)) +class Binding: + """ + Represents a parsed binding. + + These attributes are available on Binding objects: + + path: + The absolute path to the file defining the binding. + + description: + The free-form description of the binding. + + compatible: + The compatible string the binding matches. This may be None if the + Binding object is a child binding or if it's inferred from node + properties. + + prop2specs: + A collections.OrderedDict mapping property names to PropertySpec objects + describing those properties' values. + + specifier2cells: + A collections.OrderedDict that maps specifier space names (like "gpio", + "clock", "pwm", etc.) to lists of cell names. + + For example, if the binding YAML contains 'pin' and 'flags' cell names + for the 'gpio' specifier space, like this: + + gpio-cells: + - pin + - flags + + Then the Binding object will have a 'specifier2cells' attribute mapping + "gpio" to ["pin", "flags"]. A missing key should be interpreted as zero + cells. + + raw: + The binding as an object parsed from YAML. + + bus: + If nodes with this binding's 'compatible' describe a bus, a string + describing the bus type (like "i2c"). None otherwise. + + on_bus: + If nodes with this binding's 'compatible' appear on a bus, a string + describing the bus type (like "i2c"). None otherwise. + + child_binding: + If this binding describes the properties of child nodes, then + this is a Binding object for those children; it is None otherwise. + A Binding object's 'child_binding.child_binding' is not None if there + are multiple levels of 'child-binding' descriptions in the binding. + """ + + def __init__(self, path, fname2path, raw=None, + require_compatible=True, require_description=True, + warn_file=None): + """ + Binding constructor. + + path: + Path to binding YAML file. May be None. + + fname2path: + Map from include files to their absolute paths. Must + not be None, but may be empty. + + raw: + Optional raw content in the binding. + This does not have to have any "include:" lines resolved. + May be left out, in which case 'path' is opened and read. + This can be used to resolve child bindings, for example. + + require_compatible: + If True, it is an error if the binding does not contain a + "compatible:" line. If False, a missing "compatible:" is + not an error. Either way, "compatible:" must be a string + if it is present in the binding. + + require_description: + If True, it is an error if the binding does not contain a + "description:" line. If False, a missing "description:" is + not an error. Either way, "description:" must be a string + if it is present in the binding. + + warn_file (default: None): + 'file' object to write warnings to. If None, sys.stderr is used. + """ + # Do this indirection with None in case sys.stderr is + # deliberately overridden. We'll only hold on to this file + # while we're initializing. + self._warn_file = sys.stderr if warn_file is None else warn_file + + self.path = path + self._fname2path = fname2path + + if raw is None: + with open(path, encoding="utf-8") as f: + raw = yaml.load(f, Loader=_BindingLoader) + + # Merge any included files into self.raw. This also pulls in + # inherited child binding definitions, so it has to be done + # before initializing those. + self.raw = self._merge_includes(raw, self.path) + + # Recursively initialize any child bindings. These don't + # require a 'compatible' or 'description' to be well defined, + # but they must be dicts. + if "child-binding" in raw: + if not isinstance(raw["child-binding"], dict): + _err(f"malformed 'child-binding:' in {self.path}, " + "expected a binding (dictionary with keys/values)") + self.child_binding = Binding(path, fname2path, + raw=raw["child-binding"], + require_compatible=False, + require_description=False) + else: + self.child_binding = None + + # Make sure this is a well defined object. + self._check(require_compatible, require_description) + + # Initialize look up tables. + self.prop2specs = OrderedDict() + for prop_name in self.raw.get("properties", {}).keys(): + self.prop2specs[prop_name] = PropertySpec(prop_name, self) + self.specifier2cells = OrderedDict() + for key, val in self.raw.items(): + if key.endswith("-cells"): + self.specifier2cells[key[:-len("-cells")]] = val + + # Drop the reference to the open warn file. This is necessary + # to make this object pickleable, but also allows it to get + # garbage collected and closed if nobody else is using it. + self._warn_file = None + + def __repr__(self): + if self.compatible: + compat = f" for compatible '{self.compatible}'" + else: + compat = "" + return f"" + + @property + def description(self): + "See the class docstring" + return self.raw['description'] + + @property + def compatible(self): + "See the class docstring" + return self.raw.get('compatible') + + @property + def bus(self): + "See the class docstring" + return self.raw.get('bus') + + @property + def on_bus(self): + "See the class docstring" + return self.raw.get('on-bus') + + def _merge_includes(self, raw, binding_path): + # Constructor helper. Merges included files in + # 'raw["include"]' into 'raw' using 'self._include_paths' as a + # source of include files, removing the "include" key while + # doing so. + # + # This treats 'binding_path' as the binding file being built up + # and uses it for error messages. + + if "include" not in raw: + return raw + + include = raw.pop("include") + fnames = [] + if isinstance(include, str): + fnames.append(include) + elif isinstance(include, list): + if not all(isinstance(elm, str) for elm in include): + _err(f"all elements in 'include:' in {binding_path} " + "should be strings") + fnames += include + else: + _err(f"'include:' in {binding_path} " + "should be a string or a list of strings") + + # First, merge the included files together. If more than one included + # file has a 'required:' for a particular property, OR the values + # together, so that 'required: true' wins. + + merged = {} + for fname in fnames: + _merge_props(merged, self._load_raw(fname), None, binding_path, + check_required=False) + + # Next, merge the merged included files into 'raw'. Error out if + # 'raw' has 'required: false' while the merged included files have + # 'required: true'. + + _merge_props(raw, merged, None, binding_path, check_required=True) + + return raw + + def _load_raw(self, fname): + # Returns the contents of the binding given by 'fname' after merging + # any bindings it lists in 'include:' into it. 'fname' is just the + # basename of the file, so we check that there aren't multiple + # candidates. + + path = self._fname2path.get(fname) + + if not path: + _err(f"'{fname}' not found") + + with open(path, encoding="utf-8") as f: + contents = yaml.load(f, Loader=_BindingLoader) + + return self._merge_includes(contents, path) + + def _check(self, require_compatible, require_description): + # Does sanity checking on the binding. + + raw = self.raw + + if "compatible" in raw: + compatible = raw["compatible"] + if not isinstance(compatible, str): + _err(f"malformed 'compatible: {compatible}' " + f"field in {self.path} - " + f"should be a string, not {type(compatible).__name__}") + elif require_compatible: + _err(f"missing 'compatible' property in {self.path}") + + if "description" not in raw and require_description: + _err(f"missing 'description' property in {self.path}") + + for prop in "title", "description": + if prop in raw and (not isinstance(raw[prop], str) or + not raw[prop]): + _err(f"malformed or empty '{prop}' in {self.path}") + + ok_top = {"title", "description", "compatible", "properties", + "bus", "on-bus", "parent-bus", "child-bus", "parent", "child", + "child-binding", "sub-node"} + + for prop in raw: + if prop == "#cells": # clean error for users of legacy syntax + _err(f"malformed '{prop}:' in {self.path}, " + "expected *-cells syntax") + if prop not in ok_top and not prop.endswith("-cells"): + _err(f"unknown key '{prop}' in {self.path}, " + "expected one of {', '.join(ok_top)}, or *-cells") + + for bus_key in "bus", "on-bus": + if bus_key in raw and \ + not isinstance(raw[bus_key], str): + _err(f"malformed '{bus_key}:' value in {self.path}, " + "expected string") + + self._check_properties() + + for key, val in raw.items(): + if key.endswith("-cells"): + if not isinstance(val, list) or \ + not all(isinstance(elm, str) for elm in val): + _err(f"malformed '{key}:' in {self.path}, " + "expected a list of strings") + + def _check_properties(self): + # _check() helper for checking the contents of 'properties:'. + + raw = self.raw + + if "properties" not in raw: + return + + ok_prop_keys = {"description", "type", "required", "category", + "enum", "const", "default"} + + for prop_name, options in raw["properties"].items(): + for key in options: + if key == "category": + self._warn( + "please put 'required: {}' instead of 'category: {}' " + "in properties: {}: ...' in {} - 'category' will be " + "removed".format( + "true" if options["category"] == "required" + else "false", + options["category"], prop_name, self.path)) + + if key not in ok_prop_keys: + _err(f"unknown setting '{key}' in " + f"'properties: {prop_name}: ...' in {self.path}, " + f"expected one of {', '.join(ok_prop_keys)}") + + _check_prop_type_and_default( + prop_name, options.get("type"), + options.get("required") or + options.get("category") == "required", + options.get("default"), + self.path) + + if "required" in options: + required = options["required"] + if not isinstance(required, bool): + _err(f"malformed 'required:' setting '{required}' " + f"for '{prop_name}' in 'properties' in {self.path}, " + "expected true/false") + + if "description" in options and \ + not isinstance(options["description"], str): + _err("missing, malformed, or empty 'description' for " + f"'{prop_name}' in 'properties' in {self.path}") + + if "enum" in options and not isinstance(options["enum"], list): + _err(f"enum in {self.path} for property '{prop_name}' " + "is not a list") + + if "const" in options and not isinstance(options["const"], + (int, str)): + _err(f"const in {self.path} for property '{prop_name}' " + "is not a scalar") + + def _warn(self, msg): + if self._warn_file is not None: + print("warning: " + msg, file=self._warn_file) + else: + raise _err("can't _warn() outside of Binding.__init__") + + +class PropertySpec: + """ + Represents a "property specification", i.e. the description of a + property provided by a binding file, like its type and description. + + These attributes are available on PropertySpec objects: + + binding: + The Binding object which defined this property. + + name: + The property's name. + + path: + The file where this property was defined. In case a binding includes + other bindings, this is the file where the property was last modified. + + type: + The type of the property as a string, as given in the binding. + + description: + The free-form description of the property as a string, or None. + + enum: + A list of values the property may take as given in the binding, or None. + + const: + The property's constant value as given in the binding, or None. + + default: + The property's default value as given in the binding, or None. + + required: + True if the property is marked required; False otherwise. + """ + + def __init__(self, name, binding): + self.binding = binding + self.name = name + self._raw = self.binding.raw["properties"][name] + + def __repr__(self): + return f"" + + @property + def path(self): + "See the class docstring" + return self.binding.path + + @property + def type(self): + "See the class docstring" + return self._raw["type"] + + @property + def description(self): + "See the class docstring" + return self._raw.get("description") + + @property + def enum(self): + "See the class docstring" + return self._raw.get("enum") + + @property + def const(self): + "See the class docstring" + return self._raw.get("const") + + @property + def default(self): + "See the class docstring" + return self._raw.get("default") + + @property + def required(self): + "See the class docstring" + return self._raw.get("required", False) + + class EDTError(Exception): "Exception raised for devicetree- and binding-related errors" @@ -1594,28 +1815,6 @@ def _binding_paths(bindings_dirs): return binding_paths -def _on_bus_from_binding(binding): - # Returns the bus specified by 'on-bus:' in the binding (or the - # legacy 'parent-bus:' and 'parent: bus:'), or None if missing - - if not binding: - return None - - if "on-bus" in binding: - return binding["on-bus"] - - # Legacy key - if "parent-bus" in binding: - return binding["parent-bus"] - - # Legacy key - if "parent" in binding: - # _check_binding() has checked that the "bus" key exists - return binding["parent"]["bus"] - - return None - - def _binding_inc_error(msg): # Helper for reporting errors in the !include implementation @@ -1655,7 +1854,7 @@ def _merge_props(to_dict, from_dict, parent, binding_path, check_required): to_dict[prop])) elif prop == "required": # Need a separate check here, because this code runs before - # _check_binding() + # Binding._check() if not (isinstance(from_dict["required"], bool) and isinstance(to_dict["required"], bool)): _err("malformed 'required:' setting for '{}' in 'properties' " @@ -1711,8 +1910,8 @@ def _binding_include(loader, node): def _check_prop_type_and_default(prop_name, prop_type, required, default, binding_path): - # _check_binding() helper. Checks 'type:' and 'default:' for the property - # named 'prop_name' + # Binding._check_properties() helper. Checks 'type:' and 'default:' for the + # property named 'prop_name' if prop_type is None: _err("missing 'type:' for '{}' in 'properties' in {}"