1#!/usr/bin/env python3
2
3# Copyright (c) 2021 Nordic Semiconductor ASA
4# SPDX-License-Identifier: Apache-2.0
5
6'''
7This script uses edtlib and the devicetree data in the build directory
8to generate a CMake file which contains devicetree data.
9
10That data can then be used in the rest of the build system.
11
12The generated CMake file looks like this:
13
14  add_custom_target(devicetree_target)
15  set_target_properties(devicetree_target PROPERTIES
16                        "DT_PROP|/soc|compatible" "vnd,soc;")
17  ...
18
19It defines a special CMake target, and saves various values in the
20devicetree as CMake target properties.
21
22Be careful:
23
24  "Property" here can refer to a CMake target property or a
25  DTS property. DTS property values are stored inside
26  CMake target properties, along with other devicetree data.
27
28The build system includes this generated file early on, so
29devicetree values can be used at CMake processing time.
30
31Access is not done directly, but with Zephyr CMake extension APIs,
32like this:
33
34  # sets 'compat' to "vnd,soc" in CMake
35  dt_prop(compat PATH "/soc" PROPERTY compatible INDEX 0)
36
37This is analogous to how DTS values are encoded as C macros,
38which can be read in source code using C APIs like
39DT_PROP(node_id, foo) from devicetree.h.
40'''
41
42import argparse
43import os
44import pickle
45import sys
46from collections import defaultdict
47
48sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'python-devicetree',
49                                'src'))
50
51ESCAPE_TABLE = str.maketrans(
52    {
53        "\n": "\\n",
54        "\r": "\\r",
55        '\"': '\\"',
56        "\\": "\\\\",
57    }
58)
59
60
61def escape(value):
62    if isinstance(value, str):
63        return value.translate(ESCAPE_TABLE)
64
65    return value
66
67
68def parse_args():
69    # Returns parsed command-line arguments
70
71    parser = argparse.ArgumentParser(allow_abbrev=False)
72    parser.add_argument("--cmake-out", required=True,
73                        help="path to write the CMake property file")
74    parser.add_argument("--edt-pickle", required=True,
75                        help="path to read the pickled edtlib.EDT object from")
76
77    return parser.parse_args()
78
79
80def main():
81    args = parse_args()
82
83    with open(args.edt_pickle, 'rb') as f:
84        edt = pickle.load(f)
85
86    # In what looks like an undocumented implementation detail, CMake
87    # target properties are stored in a C++ standard library map whose
88    # keys and values are each arbitrary strings, so we can use
89    # whatever we want as target property names.
90    #
91    # We therefore use '|' as a field separator character below within
92    # because it's not a valid character in DTS node paths or property
93    # names. This lets us store the "real" paths and property names
94    # without conversion to lowercase-and-underscores like we have to
95    # do in C.
96    #
97    # If CMake adds restrictions on target property names later, we
98    # can just tweak the generated file to use a more restrictive
99    # property encoding, perhaps reusing the same style documented in
100    # macros.bnf for C macros.
101
102    cmake_props = []
103    chosen_nodes = edt.chosen_nodes
104    for node in chosen_nodes:
105        path = chosen_nodes[node].path
106        cmake_props.append(f'"DT_CHOSEN|{node}" "{path}"')
107
108    # The separate loop over edt.nodes here is meant to keep
109    # all of the alias-related properties in one place.
110    for node in edt.nodes:
111        path = node.path
112        for alias in node.aliases:
113            cmake_props.append(f'"DT_ALIAS|{alias}" "{path}"')
114
115    compatible2paths = defaultdict(list)
116    for node in edt.nodes:
117        cmake_props.append(f'"DT_NODE|{node.path}" TRUE')
118
119        for label in node.labels:
120            cmake_props.append(f'"DT_NODELABEL|{label}" "{node.path}"')
121
122        for item in node.props:
123            # We currently do not support phandles for edt -> cmake conversion.
124            if "phandle" not in node.props[item].type:
125                if "array" in node.props[item].type:
126                    # Convert array to CMake list
127                    cmake_value = ''
128                    for val in node.props[item].val:
129                        cmake_value = f'{cmake_value}{val};'
130                else:
131                    cmake_value = node.props[item].val
132
133                # Encode node's property 'item' as a CMake target property
134                # with a name like 'DT_PROP|<path>|<property>'.
135                cmake_prop = f'DT_PROP|{node.path}|{item}'
136                cmake_props.append(f'"{cmake_prop}" "{escape(cmake_value)}"')
137
138                if item == 'compatible':
139                    # compatibles is always an array
140                    for comp in node.props[item].val:
141                        compatible2paths[comp].append(node.path)
142
143        if node.regs is not None:
144            cmake_props.append(f'"DT_REG|{node.path}|NUM" "{len(node.regs)}"')
145            cmake_addr = ''
146            cmake_size = ''
147
148            for reg in node.regs:
149                if reg.addr is None:
150                    cmake_addr = f'{cmake_addr}NONE;'
151                else:
152                    cmake_addr = f'{cmake_addr}{hex(reg.addr)};'
153
154                if reg.size is None:
155                    cmake_size = f'{cmake_size}NONE;'
156                else:
157                    cmake_size = f'{cmake_size}{hex(reg.size)};'
158
159            cmake_props.append(f'"DT_REG|{node.path}|ADDR" "{cmake_addr}"')
160            cmake_props.append(f'"DT_REG|{node.path}|SIZE" "{cmake_size}"')
161
162    for comp in compatible2paths.keys():
163        cmake_path = ''
164        for path in compatible2paths[comp]:
165            cmake_path = f'{cmake_path}{path};'
166
167        # Remove the last ';'
168        cmake_path = cmake_path[:-1]
169
170        cmake_comp = f'DT_COMP|{comp}'
171        cmake_props.append(f'"{cmake_comp}" "{cmake_path}"')
172
173    with open(args.cmake_out, "w", encoding="utf-8") as cmake_file:
174        print('add_custom_target(devicetree_target)', file=cmake_file)
175        print(file=cmake_file)
176
177        for prop in cmake_props:
178            print(
179                f'set_target_properties(devicetree_target PROPERTIES {prop})',
180                file=cmake_file
181            )
182
183
184if __name__ == "__main__":
185    main()
186