scripts: dts: Improve syntax and code for including binding files

Have

    include: foo.dts
    include: [foo.dts, bar.dts]

instead of

    inherits:
        !include foo.dts

    inherits:
        !include [foo.dts, bar.dts]

This is a nicer and shorter and less cryptic syntax, and will make it
possible to get rid of the custom PyYAML constructor for '!include'
later.

'inherits: !include ...' is still supported for backwards compatibility
for now. Later on, I'm planning to mass-replace it, add a deprecation
warning if it's used, and document 'include:'. Then the '!include'
implementation can be removed a bit later.

'!include' has caused issues in the past (see the comment above the
add_constructor() call), gets iffy with multiple EDT instances, and
makes the code harder to follow.

I'm guessing '!include' might've been intended to be useful outside of
'inherits:' originally, but that's the only place where it's used. It's
undocumented that it's possible to put it elsewhere.

To implement the backwards compatibility, the code just transforms

    inherits:
        !include foo.dts

into

    inherits:
        - foo.dts

and treats 'inherits:' similarly to 'include:'. Previously, !include
inserted the contents of the included file instead.

Some more sanity checks for 'include:'/'inherits:' are included as well.

Signed-off-by: Ulf Magnusson <Ulf.Magnusson@nordicsemi.no>
This commit is contained in:
Ulf Magnusson 2019-08-21 16:41:03 +02:00 committed by Kumar Gala
commit d834b69bd9
9 changed files with 103 additions and 61 deletions

View file

@ -5,15 +5,14 @@ description: >
Can go over multiple lines.
# Many bindings have common fields. These are stored in files given in
# 'inherits', which are merged into the binding (with a recursive dictionary
# 'include', which are merged into the binding (with a recursive dictionary
# merge).
#
# If a field appears both in the binding and in a file it inherits, then the
# If a field appears both in the binding and in a file it includes, then the
# value in the binding takes precedence. This can be used e.g. to change a
# 'required: false' from an inherited file to a 'required: true' (see the
# 'properties' description below).
inherits:
!include other.yaml # or [other1.yaml, other2.yaml]
include: other.yaml # or [other1.yaml, other2.yaml]
# If the node describes a bus, then the bus type should be given, like below
child:

View file

@ -157,13 +157,9 @@ class EDT:
# Only bindings for 'compatible' strings that appear in the device tree
# are loaded.
# Add '!include foo.yaml' handling.
#
# Do yaml.Loader.add_constructor() instead of yaml.add_constructor() to be
# Add legacy '!include foo.yaml' handling. Do
# yaml.Loader.add_constructor() instead of yaml.add_constructor() to be
# compatible with both version 3.13 and version 5.1 of PyYAML.
#
# TODO: Is there some 3.13/5.1-compatible way to only do this once, even
# if multiple EDT objects are created?
yaml.Loader.add_constructor("!include", self._binding_include)
dt_compats = _dt_compats(self._dt)
@ -203,48 +199,90 @@ class EDT:
if compat not in dt_compats:
continue
# It's a match. Merge in the !included bindings, do sanity checks,
# It's a match. Merge in the included bindings, do sanity checks,
# and register the binding.
binding = _merge_included_bindings(binding, binding_path)
binding = self._merge_included_bindings(binding, binding_path)
_check_binding(binding, binding_path)
self._compat2binding[compat, _binding_bus(binding)] = \
(binding, binding_path)
def _binding_include(self, loader, node):
# Implements !include. Returns a list with the YAML structures for the
# included files (a single-element list if the !include is for a single
# file).
def _merge_included_bindings(self, binding, binding_path):
# Merges any bindings listed in the 'include:' section of the binding
# into the top level of 'binding'. Also supports the legacy
# 'inherits: !include ...' syntax for including bindings.
#
# Properties in 'binding' take precedence over properties from included
# bindings.
if isinstance(node, yaml.ScalarNode):
# !include foo.yaml
return [self._binding_include_file(loader.construct_scalar(node))]
fnames = []
if isinstance(node, yaml.SequenceNode):
# !include [foo.yaml, bar.yaml]
return [self._binding_include_file(filename)
for filename in loader.construct_sequence(node)]
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))
_binding_inc_error("unrecognised node type in !include statement")
# Legacy syntax
if "inherits" in binding:
_warn("the 'inherits:' syntax in {} is deprecated and will be "
"removed - please use 'include: foo.yaml' or "
"'include: [foo.yaml, bar.yaml]' instead"
.format(binding_path))
def _binding_include_file(self, filename):
# _binding_include() helper for loading an !include'd file. !include
# takes just the basename of the file, so we need to make sure there
# aren't multiple candidates.
inherits = binding.pop("inherits")
if not isinstance(inherits, list) or \
not all(isinstance(elm, str) for elm in inherits):
_err("malformed 'inherits:' in " + binding_path)
fnames += inherits
for fname in fnames:
included = self._file_yaml(fname)
_merge_props(
binding, self._merge_included_bindings(included, binding_path),
None, binding_path)
return binding
def _file_yaml(self, filename):
# _merge_included_bindings() helper for loading an included file.
# 'include:' lists just the basenames of the files, so we check that
# there aren't multiple candidates.
paths = [path for path in self._binding_paths
if os.path.basename(path) == filename]
if not paths:
_binding_inc_error("'{}' not found".format(filename))
_err("'{}' not found".format(filename))
if len(paths) > 1:
_binding_inc_error("multiple candidates for '{}' in !include: {}"
.format(filename, ", ".join(paths)))
_err("multiple candidates for included file '{}': {}"
.format(filename, ", ".join(paths)))
with open(paths[0], encoding="utf-8") as f:
return yaml.load(f, Loader=yaml.Loader)
def _binding_include(self, loader, node):
# Implements !include, for backwards compatibility.
# '!include [foo, bar]' just becomes [foo, bar].
if isinstance(node, yaml.ScalarNode):
# !include foo.yaml
return [loader.construct_scalar(node)]
if isinstance(node, yaml.SequenceNode):
# !include [foo.yaml, bar.yaml]
return loader.construct_sequence(node)
_binding_inc_error("unrecognised node type in !include statement")
def _init_devices(self):
# Creates a list of devices (Device objects) from the DT nodes, in
# self.devices
@ -1174,26 +1212,9 @@ def _binding_inc_error(msg):
raise yaml.constructor.ConstructorError(None, None, "error: " + msg)
def _merge_included_bindings(binding, binding_path):
# Merges any bindings in the 'inherits:' section of 'binding' into the top
# level of 'binding'. !includes have already been processed at this point,
# and leave the data for the included binding(s) in 'inherits:'.
#
# Properties in 'binding' take precedence over properties from included
# bindings.
if "inherits" in binding:
for inherited in binding.pop("inherits"):
_merge_props(
binding, _merge_included_bindings(inherited, binding_path),
None, binding_path)
return binding
def _merge_props(to_dict, from_dict, parent, binding_path):
# Recursively merges 'from_dict' into 'to_dict', to implement !include. If
# a key exists in both 'from_dict' and 'to_dict', then the value in
# Recursively merges 'from_dict' into 'to_dict', to implement 'include:'.
# If a key exists in both 'from_dict' and 'to_dict', then the value in
# 'to_dict' takes precedence.
#
# 'parent' is the name of the parent key containing 'to_dict' and
@ -1207,7 +1228,7 @@ def _merge_props(to_dict, from_dict, parent, binding_path):
elif prop not in to_dict:
to_dict[prop] = from_dict[prop]
elif _bad_overwrite(to_dict, from_dict, prop):
_err("{} (in '{}'): '{}' from !included file overwritten "
_err("{} (in '{}'): '{}' from included file overwritten "
"('{}' replaced with '{}')".format(
binding_path, parent, prop, from_dict[prop],
to_dict[prop]))

View file

@ -211,13 +211,26 @@ def merge_included_bindings(fname, node):
# section of the binding. 'fname' is the path to the top-level binding
# file, and 'node' the current top-level YAML node being processed.
res = node
if "include" in node:
fnames = node.pop("include")
if isinstance(fnames, str):
fnames = [fnames]
for fname in fnames:
binding = load_binding_file(fname)
inherited = merge_included_bindings(fname, binding)
merge_properties(None, fname, inherited, res)
res = inherited
if 'inherits' in node:
for inherited in node.pop('inherits'):
inherited = merge_included_bindings(fname, inherited)
merge_properties(None, fname, inherited, node)
node = inherited
merge_properties(None, fname, inherited, res)
res = inherited
return node
return res
def define_str(name, value, value_tabs, is_deprecated=False):

View file

@ -1,7 +1,10 @@
# SPDX-License-Identifier: BSD-3-Clause
include: [grandchild-1.yaml, grandchild-2.yaml]
# Legacy include
inherits:
!include grandchild.yaml
!include grandchild-3.yaml
properties:
bar:

View file

@ -0,0 +1,6 @@
# SPDX-License-Identifier: BSD-3-Clause
properties:
baz:
required: true
type: int

View file

@ -3,8 +3,7 @@
title: Parent binding
description: Parent binding
inherits:
!include child.yaml
include: child.yaml
properties:
compatible:
@ -12,6 +11,6 @@ properties:
type: string-array
foo:
# Changed from not being required in grandchild.yaml
# Changed from not being required in grandchild-1.yaml
required: true
# Type set in grandchild

View file

@ -274,7 +274,7 @@
};
//
// !include in bindings
// For testing 'include:' and the legacy 'inherits: !include ...'
//
binding-include {
@ -282,6 +282,7 @@
foo = <0>;
bar = <1>;
baz = <2>;
qaz = <3>;
};
//

View file

@ -96,14 +96,14 @@ def run():
"[<Register, addr: 0x30000000200000001, size: 0x1>]")
#
# Test !include in bindings
# Test 'include:' and the legacy 'inherits: !include ...'
#
verify_streq(edt.get_dev("/binding-include").description,
"Parent binding")
verify_streq(edt.get_dev("/binding-include").props,
"{'compatible': <Property, name: compatible, type: string-array, value: ['binding-include-test']>, 'foo': <Property, name: foo, type: int, value: 0>, 'bar': <Property, name: bar, type: int, value: 1>, 'baz': <Property, name: baz, type: int, value: 2>}")
"{'compatible': <Property, name: compatible, type: string-array, value: ['binding-include-test']>, 'foo': <Property, name: foo, type: int, value: 0>, 'bar': <Property, name: bar, type: int, value: 1>, 'baz': <Property, name: baz, type: int, value: 2>, 'qaz': <Property, name: qaz, type: int, value: 3>}")
#
# Test 'sub-node:' in binding