blob: a615a9a6f88bcc76a6c72539793633c109c41502 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2024 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Compare two set of DTBs and output the different nodes
Accept shell-like wildcard pattern to compare multiple DTBs at the same time,
and output the following informations:
- Compatibles only in one set of DTBs
- Nodes that only in one set of DTBs, and the compatible strings that contains
this node.
Use the address of the nodes as the identifier to compare two set of DTBs
because nodes often get renamed during the upstream process. Also, nodes without
`reg` or `compatible` property are ignored.
A sample usage of this script:
```
# pylint: disable=line-too-long
./dtb_compare.py \
"${HOME}/chromiumos/out/build/asurada-kernelnext/var/cache/portage/sys-kernel/chromeos-kernel-5_4/arch/arm64/boot/dts/mediatek/mt8192*.dtb" \
"${HOME}/chromiumos/out/build/asurada/var/cache/portage/sys-kernel/chromeos-kernel-6_1/arch/arm64/boot/dts/mediatek/mt8192*.dtb"
```
"""
import glob
import sys
# pylint: disable=import-error
import libfdt
def get_compatibles(fdt):
"""Return a set of compatible strings in `fdt1`."""
return set(fdt.getprop(0, "compatible").as_stringlist())
def get_property(fdt, offset, name, default=None):
"""A .getprop wrapper that returns `default` on FdtException."""
try:
ret = fdt.getprop(offset, name)
except libfdt.FdtException as e:
if e.err == -1:
return default
raise e
return ret
def get_address_cells(fdt, offset):
return get_property(
fdt, fdt.parent_offset(offset), "#address-cells", 1
).as_int32()
def node2id(fdt, offset):
"""Build an ID of the DT node.
The ID is generated by concatenating the node address and the parent's ID.
Use path instead when the node doesn't have a reg proprety.
"""
reg = get_property(fdt, offset, "reg")
if reg is None:
return fdt.get_path(offset)
addr_len = 8 * get_address_cells(fdt, offset)
return node2id(fdt, fdt.parent_offset(offset)) + "/" + reg.hex()[:addr_len]
def traverse_dt(fdt, id_path, id_compatible):
"""Traverse the tree and update id_path and id_compatible."""
compats = get_compatibles(fdt)
offset = 0
depth = 1
while True:
offset, depth = fdt.next_node(offset, depth)
if offset == -1:
break
# Skipping all nodes without reg or compatible.
if (
get_property(fdt, offset, "reg") is None
or get_property(fdt, offset, "compatible") is None
):
continue
node_id = node2id(fdt, offset)
if node_id not in id_compatible:
id_compatible[node_id] = set()
id_path[node_id] = fdt.get_path(offset)
id_compatible[node_id] |= compats
def main():
if len(sys.argv) != 3:
print("usage: %s [DTBs 1 pattern] [DTBs 2 pattern]" % sys.argv[0])
sys.exit(1)
skipped_compatibles = set()
# Set of compatibles appear in each set of DTB files.
compats = [set(), set()]
# Mapping from ID to DT path.
id_path = [{}, {}]
# Mapping from ID to compatibles that containing this ID.
id_compatible = [{}, {}]
for i in range(2):
for dtb_file in glob.glob(sys.argv[i + 1]):
with open(dtb_file, "rb") as fp:
fdt = libfdt.Fdt(fp.read())
c = get_compatibles(fdt)
# Skip the device compatibles (e.g., "google,spherion") that
# appears in multiple DTB files.
skipped_compatibles |= c & compats[i]
compats[i] |= c
traverse_dt(fdt, id_path[i], id_compatible[i])
if skipped_compatibles:
print(
"The following device compatible(s) appears in multiple DTB files. "
"Assume they are back-up compatibles and skip them."
)
for c in skipped_compatibles:
print(c)
print("")
def _print_diff_compatibles(c1, c2):
"""Print the compatibles only in c1"""
for i in c1 - c2:
print(i)
print("")
print("Compatibles only in DTBs 1:")
_print_diff_compatibles(compats[0], compats[1])
print("Compatibles only in DTBs 2:")
_print_diff_compatibles(compats[1], compats[0])
# Skip compatibles that only appear in one DTBs.
skipped_compatibles |= compats[0] - compats[1]
skipped_compatibles |= compats[1] - compats[0]
def _print_diff_nodes(id_path1, id_compat1, id_path2, id_compat2):
"""Print nodes that only appears in id_path1/id_compat1"""
for node_id in set(id_path1) | set(id_path2):
c1 = id_compat1.get(node_id, set())
c2 = id_compat2.get(node_id, set())
compats_only_in_c1 = (c1 - c2) - skipped_compatibles
if compats_only_in_c1:
print("-", id_path1[node_id])
for c in sorted(compats_only_in_c1):
print(" -", c)
print("")
print("Nodes only in DTBs 1:")
_print_diff_nodes(
id_path[0], id_compatible[0], id_path[1], id_compatible[1]
)
print("Nodes only in DTBs 2:")
_print_diff_nodes(
id_path[1], id_compatible[1], id_path[0], id_compatible[0]
)
if __name__ == "__main__":
main()