| #!/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() |