1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2016 Google, Inc
3# Written by Simon Glass <sjg@chromium.org>
4#
5# Entry-type module for producing a FIT
6#
7
8from collections import defaultdict, OrderedDict
9import libfdt
10
11from binman.entry import Entry, EntryArg
12from dtoc import fdt_util
13from dtoc.fdt import Fdt
14from patman import tools
15
16class Entry_fit(Entry):
17    """Flat Image Tree (FIT)
18
19    This calls mkimage to create a FIT (U-Boot Flat Image Tree) based on the
20    input provided.
21
22    Nodes for the FIT should be written out in the binman configuration just as
23    they would be in a file passed to mkimage.
24
25    For example, this creates an image containing a FIT with U-Boot SPL::
26
27        binman {
28            fit {
29                description = "Test FIT";
30                fit,fdt-list = "of-list";
31
32                images {
33                    kernel@1 {
34                        description = "SPL";
35                        os = "u-boot";
36                        type = "rkspi";
37                        arch = "arm";
38                        compression = "none";
39                        load = <0>;
40                        entry = <0>;
41
42                        u-boot-spl {
43                        };
44                    };
45                };
46            };
47        };
48
49    U-Boot supports creating fdt and config nodes automatically. To do this,
50    pass an of-list property (e.g. -a of-list=file1 file2). This tells binman
51    that you want to generates nodes for two files: file1.dtb and file2.dtb
52    The fit,fdt-list property (see above) indicates that of-list should be used.
53    If the property is missing you will get an error.
54
55    Then add a 'generator node', a node with a name starting with '@'::
56
57        images {
58            @fdt-SEQ {
59                description = "fdt-NAME";
60                type = "flat_dt";
61                compression = "none";
62            };
63        };
64
65    This tells binman to create nodes fdt-1 and fdt-2 for each of your two
66    files. All the properties you specify will be included in the node. This
67    node acts like a template to generate the nodes. The generator node itself
68    does not appear in the output - it is replaced with what binman generates.
69
70    You can create config nodes in a similar way::
71
72        configurations {
73            default = "@config-DEFAULT-SEQ";
74            @config-SEQ {
75                description = "NAME";
76                firmware = "atf";
77                loadables = "uboot";
78                fdt = "fdt-SEQ";
79            };
80        };
81
82    This tells binman to create nodes config-1 and config-2, i.e. a config for
83    each of your two files.
84
85    Available substitutions for '@' nodes are:
86
87    SEQ:
88        Sequence number of the generated fdt (1, 2, ...)
89    NAME
90        Name of the dtb as provided (i.e. without adding '.dtb')
91
92    Note that if no devicetree files are provided (with '-a of-list' as above)
93    then no nodes will be generated.
94
95    The 'default' property, if present, will be automatically set to the name
96    if of configuration whose devicetree matches the 'default-dt' entry
97    argument, e.g. with '-a default-dt=sun50i-a64-pine64-lts'.
98
99    Available substitutions for '@' property values are
100
101    DEFAULT-SEQ:
102        Sequence number of the default fdt,as provided by the 'default-dt' entry
103        argument
104
105    Properties (in the 'fit' node itself):
106        fit,external-offset: Indicates that the contents of the FIT are external
107            and provides the external offset. This is passsed to mkimage via
108            the -E and -p flags.
109
110    """
111    def __init__(self, section, etype, node):
112        """
113        Members:
114            _fit: FIT file being built
115            _fit_sections: dict:
116                key: relative path to entry Node (from the base of the FIT)
117                value: Entry_section object comprising the contents of this
118                    node
119        """
120        super().__init__(section, etype, node)
121        self._fit = None
122        self._fit_sections = {}
123        self._fit_props = {}
124        for pname, prop in self._node.props.items():
125            if pname.startswith('fit,'):
126                self._fit_props[pname] = prop
127
128        self._fdts = None
129        self._fit_list_prop = self._fit_props.get('fit,fdt-list')
130        if self._fit_list_prop:
131            fdts, = self.GetEntryArgsOrProps(
132                [EntryArg(self._fit_list_prop.value, str)])
133            if fdts is not None:
134                self._fdts = fdts.split()
135        self._fit_default_dt = self.GetEntryArgsOrProps([EntryArg('default-dt',
136                                                                  str)])[0]
137
138    def ReadNode(self):
139        self._ReadSubnodes()
140        super().ReadNode()
141
142    def _ReadSubnodes(self):
143        def _AddNode(base_node, depth, node):
144            """Add a node to the FIT
145
146            Args:
147                base_node: Base Node of the FIT (with 'description' property)
148                depth: Current node depth (0 is the base node)
149                node: Current node to process
150
151            There are two cases to deal with:
152                - hash and signature nodes which become part of the FIT
153                - binman entries which are used to define the 'data' for each
154                  image
155            """
156            for pname, prop in node.props.items():
157                if not pname.startswith('fit,'):
158                    if pname == 'default':
159                        val = prop.value
160                        # Handle the 'default' property
161                        if val.startswith('@'):
162                            if not self._fdts:
163                                continue
164                            if not self._fit_default_dt:
165                                self.Raise("Generated 'default' node requires default-dt entry argument")
166                            if self._fit_default_dt not in self._fdts:
167                                self.Raise("default-dt entry argument '%s' not found in fdt list: %s" %
168                                           (self._fit_default_dt,
169                                            ', '.join(self._fdts)))
170                            seq = self._fdts.index(self._fit_default_dt)
171                            val = val[1:].replace('DEFAULT-SEQ', str(seq + 1))
172                            fsw.property_string(pname, val)
173                            continue
174                    fsw.property(pname, prop.bytes)
175
176            rel_path = node.path[len(base_node.path):]
177            in_images = rel_path.startswith('/images')
178            has_images = depth == 2 and in_images
179            if has_images:
180                # This node is a FIT subimage node (e.g. "/images/kernel")
181                # containing content nodes. We collect the subimage nodes and
182                # section entries for them here to merge the content subnodes
183                # together and put the merged contents in the subimage node's
184                # 'data' property later.
185                entry = Entry.Create(self.section, node, etype='section')
186                entry.ReadNode()
187                self._fit_sections[rel_path] = entry
188
189            for subnode in node.subnodes:
190                if has_images and not (subnode.name.startswith('hash') or
191                                       subnode.name.startswith('signature')):
192                    # This subnode is a content node not meant to appear in
193                    # the FIT (e.g. "/images/kernel/u-boot"), so don't call
194                    # fsw.add_node() or _AddNode() for it.
195                    pass
196                elif subnode.name.startswith('@'):
197                    if self._fdts:
198                        # Generate notes for each FDT
199                        for seq, fdt_fname in enumerate(self._fdts):
200                            node_name = subnode.name[1:].replace('SEQ',
201                                                                 str(seq + 1))
202                            fname = tools.GetInputFilename(fdt_fname + '.dtb')
203                            with fsw.add_node(node_name):
204                                for pname, prop in subnode.props.items():
205                                    val = prop.bytes.replace(
206                                        b'NAME', tools.ToBytes(fdt_fname))
207                                    val = val.replace(
208                                        b'SEQ', tools.ToBytes(str(seq + 1)))
209                                    fsw.property(pname, val)
210
211                                # Add data for 'fdt' nodes (but not 'config')
212                                if depth == 1 and in_images:
213                                    fsw.property('data',
214                                                 tools.ReadFile(fname))
215                    else:
216                        if self._fdts is None:
217                            if self._fit_list_prop:
218                                self.Raise("Generator node requires '%s' entry argument" %
219                                           self._fit_list_prop.value)
220                            else:
221                                self.Raise("Generator node requires 'fit,fdt-list' property")
222                else:
223                    with fsw.add_node(subnode.name):
224                        _AddNode(base_node, depth + 1, subnode)
225
226        # Build a new tree with all nodes and properties starting from the
227        # entry node
228        fsw = libfdt.FdtSw()
229        fsw.finish_reservemap()
230        with fsw.add_node(''):
231            _AddNode(self._node, 0, self._node)
232        fdt = fsw.as_fdt()
233
234        # Pack this new FDT and scan it so we can add the data later
235        fdt.pack()
236        self._fdt = Fdt.FromData(fdt.as_bytearray())
237        self._fdt.Scan()
238
239    def ObtainContents(self):
240        """Obtain the contents of the FIT
241
242        This adds the 'data' properties to the input ITB (Image-tree Binary)
243        then runs mkimage to process it.
244        """
245        # self._BuildInput() either returns bytes or raises an exception.
246        data = self._BuildInput(self._fdt)
247        uniq = self.GetUniqueName()
248        input_fname = tools.GetOutputFilename('%s.itb' % uniq)
249        output_fname = tools.GetOutputFilename('%s.fit' % uniq)
250        tools.WriteFile(input_fname, data)
251        tools.WriteFile(output_fname, data)
252
253        args = []
254        ext_offset = self._fit_props.get('fit,external-offset')
255        if ext_offset is not None:
256            args += ['-E', '-p', '%x' % fdt_util.fdt32_to_cpu(ext_offset.value)]
257        tools.Run('mkimage', '-t', '-F', output_fname, *args)
258
259        self.SetContents(tools.ReadFile(output_fname))
260        return True
261
262    def _BuildInput(self, fdt):
263        """Finish the FIT by adding the 'data' properties to it
264
265        Arguments:
266            fdt: FIT to update
267
268        Returns:
269            New fdt contents (bytes)
270        """
271        for path, section in self._fit_sections.items():
272            node = fdt.GetNode(path)
273            # Entry_section.ObtainContents() either returns True or
274            # raises an exception.
275            section.ObtainContents()
276            section.Pack(0)
277            data = section.GetData()
278            node.AddData('data', data)
279
280        fdt.Sync(auto_resize=True)
281        data = fdt.GetContents()
282        return data
283
284    def CheckMissing(self, missing_list):
285        """Check if any entries in this FIT have missing external blobs
286
287        If there are missing blobs, the entries are added to the list
288
289        Args:
290            missing_list: List of Entry objects to be added to
291        """
292        for path, section in self._fit_sections.items():
293            section.CheckMissing(missing_list)
294
295    def SetAllowMissing(self, allow_missing):
296        for section in self._fit_sections.values():
297            section.SetAllowMissing(allow_missing)
298