dtoc: Support copying the contents of a node into another

This permits implementation of a simple templating system, where a node
can be reused as a base for others.

For now this adds new subnodes after any existing ones.

Signed-off-by: Simon Glass <sjg@chromium.org>
This commit is contained in:
Simon Glass 2023-07-18 07:24:02 -06:00
parent e1ad57e7ef
commit 4df457b657
3 changed files with 266 additions and 3 deletions

View file

@ -13,6 +13,7 @@ from dtoc import fdt_util
import libfdt
from libfdt import QUIET_NOTFOUND
from u_boot_pylib import tools
from u_boot_pylib import tout
# This deals with a device tree, presenting it as an assortment of Node and
# Prop objects, representing nodes and properties, respectively. This file
@ -264,6 +265,13 @@ class Prop:
fdt_obj.setprop(node.Offset(), self.name, self.bytes)
self.dirty = False
def purge(self):
"""Set a property offset to None
The property remains in the tree structure and will be recreated when
the FDT is synced
"""
self._offset = None
class Node:
"""A device tree node
@ -534,8 +542,8 @@ class Node:
"""
return self.AddData(prop_name, struct.pack('>I', val))
def AddSubnode(self, name):
"""Add a new subnode to the node
def Subnode(self, name):
"""Create new subnode for the node
Args:
name: name of node to add
@ -544,10 +552,72 @@ class Node:
New subnode that was created
"""
path = self.path + '/' + name
subnode = Node(self._fdt, self, None, name, path)
return Node(self._fdt, self, None, name, path)
def AddSubnode(self, name):
"""Add a new subnode to the node, after all other subnodes
Args:
name: name of node to add
Returns:
New subnode that was created
"""
subnode = self.Subnode(name)
self.subnodes.append(subnode)
return subnode
def insert_subnode(self, name):
"""Add a new subnode to the node, before all other subnodes
This deletes other subnodes and sets their offset to None, so that they
will be recreated after this one.
Args:
name: name of node to add
Returns:
New subnode that was created
"""
# Deleting a node invalidates the offsets of all following nodes, so
# process in reverse order so that the offset of each node remains valid
# until deletion.
for subnode in reversed(self.subnodes):
subnode.purge(True)
subnode = self.Subnode(name)
self.subnodes.insert(0, subnode)
return subnode
def purge(self, delete_it=False):
"""Purge this node, setting offset to None and deleting from FDT"""
if self._offset is not None:
if delete_it:
CheckErr(self._fdt._fdt_obj.del_node(self.Offset()),
"Node '%s': delete" % self.path)
self._offset = None
self._fdt.Invalidate()
for prop in self.props.values():
prop.purge()
for subnode in self.subnodes:
subnode.purge(False)
def move_to_first(self):
"""Move the current node to first in its parent's node list"""
parent = self.parent
if parent.subnodes and parent.subnodes[0] == self:
return
for subnode in reversed(parent.subnodes):
subnode.purge(True)
new_subnodes = [self]
for subnode in parent.subnodes:
#subnode.purge(False)
if subnode != self:
new_subnodes.append(subnode)
parent.subnodes = new_subnodes
def Delete(self):
"""Delete a node
@ -635,6 +705,49 @@ class Node:
prop.Sync(auto_resize)
return added
def merge_props(self, src):
"""Copy missing properties (except 'phandle') from another node
Args:
src (Node): Node containing properties to copy
Adds properties which are present in src but not in this node. Any
'phandle' property is not copied since this might result in two nodes
with the same phandle, thus making phandle references ambiguous.
"""
for name, src_prop in src.props.items():
if name != 'phandle' and name not in self.props:
self.props[name] = Prop(self, None, name, src_prop.bytes)
def copy_node(self, src):
"""Copy a node and all its subnodes into this node
Args:
src (Node): Node to copy
Returns:
Node: Resulting destination node
This works recursively.
The new node is put before all other nodes. If the node already
exists, just its subnodes and properties are copied, placing them before
any existing subnodes. Properties which exist in the destination node
already are not copied.
"""
dst = self.FindNode(src.name)
if dst:
dst.move_to_first()
else:
dst = self.insert_subnode(src.name)
dst.merge_props(src)
# Process in reverse order so that they appear correctly in the result,
# since copy_node() puts the node first in the list
for node in reversed(src.subnodes):
dst.copy_node(node)
return dst
class Fdt:
"""Provides simple access to a flat device tree blob using libfdts.

View file

@ -0,0 +1,76 @@
// SPDX-License-Identifier: GPL-2.0+
/*
* Test device tree file for dtoc
*
* Copyright 2017 Google, Inc
*/
/dts-v1/;
/ {
#address-cells = <1>;
#size-cells = <1>;
reference = <&over>; /* nake sure that the 'over' phandle exists */
dest {
bootph-all;
compatible = "sandbox,spl-test";
stringarray = "one";
longbytearray = [09 0a 0b 0c 0d 0e 0f 10];
maybe-empty-int = <1>;
first@0 {
a-prop = <456>;
b-prop = <1>;
};
existing {
};
base {
second {
second3 {
};
second2 {
new-prop;
};
second1 {
new-prop;
};
second4 {
};
};
};
};
base {
compatible = "sandbox,i2c";
bootph-all;
#address-cells = <1>;
#size-cells = <0>;
over: over {
compatible = "sandbox,pmic";
bootph-all;
reg = <9>;
low-power;
};
first@0 {
reg = <0>;
a-prop = <123>;
};
second: second {
second1 {
some-prop;
};
second2 {
some-prop;
};
};
};
};

View file

@ -306,6 +306,80 @@ class TestNode(unittest.TestCase):
self.assertIn("Internal error, node '/spl-test' name mismatch 'i2c@0'",
str(exc.exception))
def test_copy_node(self):
"""Test copy_node() function"""
def do_copy_checks(dtb, dst, expect_none):
self.assertEqual(
['/dest/base', '/dest/first@0', '/dest/existing'],
[n.path for n in dst.subnodes])
chk = dtb.GetNode('/dest/base')
self.assertTrue(chk)
self.assertEqual(
{'compatible', 'bootph-all', '#address-cells', '#size-cells'},
chk.props.keys())
# Check the first property
prop = chk.props['bootph-all']
self.assertEqual('bootph-all', prop.name)
self.assertEqual(True, prop.value)
self.assertEqual(chk.path, prop._node.path)
# Check the second property
prop2 = chk.props['compatible']
self.assertEqual('compatible', prop2.name)
self.assertEqual('sandbox,i2c', prop2.value)
self.assertEqual(chk.path, prop2._node.path)
base = chk.FindNode('base')
self.assertTrue(chk)
first = dtb.GetNode('/dest/base/first@0')
self.assertTrue(first)
over = dtb.GetNode('/dest/base/over')
self.assertTrue(over)
# Make sure that the phandle for 'over' is not copied
self.assertNotIn('phandle', over.props.keys())
second = dtb.GetNode('/dest/base/second')
self.assertTrue(second)
self.assertEqual([over.name, first.name, second.name],
[n.name for n in chk.subnodes])
self.assertEqual(chk, over.parent)
self.assertEqual(
{'bootph-all', 'compatible', 'reg', 'low-power'},
over.props.keys())
if expect_none:
self.assertIsNone(prop._offset)
self.assertIsNone(prop2._offset)
self.assertIsNone(over._offset)
else:
self.assertTrue(prop._offset)
self.assertTrue(prop2._offset)
self.assertTrue(over._offset)
# Now check ordering of the subnodes
self.assertEqual(
['second1', 'second2', 'second3', 'second4'],
[n.name for n in second.subnodes])
dtb = fdt.FdtScan(find_dtb_file('dtoc_test_copy.dts'))
tmpl = dtb.GetNode('/base')
dst = dtb.GetNode('/dest')
dst.copy_node(tmpl)
do_copy_checks(dtb, dst, expect_none=True)
dtb.Sync(auto_resize=True)
# Now check that the FDT looks correct
new_dtb = fdt.Fdt.FromData(dtb.GetContents())
new_dtb.Scan()
dst = new_dtb.GetNode('/dest')
do_copy_checks(new_dtb, dst, expect_none=False)
class TestProp(unittest.TestCase):
"""Test operation of the Prop class"""