xref: /linux/scripts/make_fit.py (revision 4b132aacb0768ac1e652cf517097ea6f237214b9)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0+
3#
4# Copyright 2024 Google LLC
5# Written by Simon Glass <sjg@chromium.org>
6#
7
8"""Build a FIT containing a lot of devicetree files
9
10Usage:
11    make_fit.py -A arm64 -n 'Linux-6.6' -O linux
12        -o arch/arm64/boot/image.fit -k /tmp/kern/arch/arm64/boot/image.itk
13        @arch/arm64/boot/dts/dtbs-list -E -c gzip
14
15Creates a FIT containing the supplied kernel and a set of devicetree files,
16either specified individually or listed in a file (with an '@' prefix).
17
18Use -E to generate an external FIT (where the data is placed after the
19FIT data structure). This allows parsing of the data without loading
20the entire FIT.
21
22Use -c to compress the data, using bzip2, gzip, lz4, lzma, lzo and
23zstd algorithms.
24
25Use -D to decompose "composite" DTBs into their base components and
26deduplicate the resulting base DTBs and DTB overlays. This requires the
27DTBs to be sourced from the kernel build directory, as the implementation
28looks at the .cmd files produced by the kernel build.
29
30The resulting FIT can be booted by bootloaders which support FIT, such
31as U-Boot, Linuxboot, Tianocore, etc.
32
33Note that this tool does not yet support adding a ramdisk / initrd.
34"""
35
36import argparse
37import collections
38import os
39import subprocess
40import sys
41import tempfile
42import time
43
44import libfdt
45
46
47# Tool extension and the name of the command-line tools
48CompTool = collections.namedtuple('CompTool', 'ext,tools')
49
50COMP_TOOLS = {
51    'bzip2': CompTool('.bz2', 'bzip2'),
52    'gzip': CompTool('.gz', 'pigz,gzip'),
53    'lz4': CompTool('.lz4', 'lz4'),
54    'lzma': CompTool('.lzma', 'lzma'),
55    'lzo': CompTool('.lzo', 'lzop'),
56    'zstd': CompTool('.zstd', 'zstd'),
57}
58
59
60def parse_args():
61    """Parse the program ArgumentParser
62
63    Returns:
64        Namespace object containing the arguments
65    """
66    epilog = 'Build a FIT from a directory tree containing .dtb files'
67    parser = argparse.ArgumentParser(epilog=epilog, fromfile_prefix_chars='@')
68    parser.add_argument('-A', '--arch', type=str, required=True,
69          help='Specifies the architecture')
70    parser.add_argument('-c', '--compress', type=str, default='none',
71          help='Specifies the compression')
72    parser.add_argument('-D', '--decompose-dtbs', action='store_true',
73          help='Decompose composite DTBs into base DTB and overlays')
74    parser.add_argument('-E', '--external', action='store_true',
75          help='Convert the FIT to use external data')
76    parser.add_argument('-n', '--name', type=str, required=True,
77          help='Specifies the name')
78    parser.add_argument('-o', '--output', type=str, required=True,
79          help='Specifies the output file (.fit)')
80    parser.add_argument('-O', '--os', type=str, required=True,
81          help='Specifies the operating system')
82    parser.add_argument('-k', '--kernel', type=str, required=True,
83          help='Specifies the (uncompressed) kernel input file (.itk)')
84    parser.add_argument('-v', '--verbose', action='store_true',
85                        help='Enable verbose output')
86    parser.add_argument('dtbs', type=str, nargs='*',
87          help='Specifies the devicetree files to process')
88
89    return parser.parse_args()
90
91
92def setup_fit(fsw, name):
93    """Make a start on writing the FIT
94
95    Outputs the root properties and the 'images' node
96
97    Args:
98        fsw (libfdt.FdtSw): Object to use for writing
99        name (str): Name of kernel image
100    """
101    fsw.INC_SIZE = 65536
102    fsw.finish_reservemap()
103    fsw.begin_node('')
104    fsw.property_string('description', f'{name} with devicetree set')
105    fsw.property_u32('#address-cells', 1)
106
107    fsw.property_u32('timestamp', int(time.time()))
108    fsw.begin_node('images')
109
110
111def write_kernel(fsw, data, args):
112    """Write out the kernel image
113
114    Writes a kernel node along with the required properties
115
116    Args:
117        fsw (libfdt.FdtSw): Object to use for writing
118        data (bytes): Data to write (possibly compressed)
119        args (Namespace): Contains necessary strings:
120            arch: FIT architecture, e.g. 'arm64'
121            fit_os: Operating Systems, e.g. 'linux'
122            name: Name of OS, e.g. 'Linux-6.6.0-rc7'
123            compress: Compression algorithm to use, e.g. 'gzip'
124    """
125    with fsw.add_node('kernel'):
126        fsw.property_string('description', args.name)
127        fsw.property_string('type', 'kernel_noload')
128        fsw.property_string('arch', args.arch)
129        fsw.property_string('os', args.os)
130        fsw.property_string('compression', args.compress)
131        fsw.property('data', data)
132        fsw.property_u32('load', 0)
133        fsw.property_u32('entry', 0)
134
135
136def finish_fit(fsw, entries):
137    """Finish the FIT ready for use
138
139    Writes the /configurations node and subnodes
140
141    Args:
142        fsw (libfdt.FdtSw): Object to use for writing
143        entries (list of tuple): List of configurations:
144            str: Description of model
145            str: Compatible stringlist
146    """
147    fsw.end_node()
148    seq = 0
149    with fsw.add_node('configurations'):
150        for model, compat, files in entries:
151            seq += 1
152            with fsw.add_node(f'conf-{seq}'):
153                fsw.property('compatible', bytes(compat))
154                fsw.property_string('description', model)
155                fsw.property('fdt', bytes(''.join(f'fdt-{x}\x00' for x in files), "ascii"))
156                fsw.property_string('kernel', 'kernel')
157    fsw.end_node()
158
159
160def compress_data(inf, compress):
161    """Compress data using a selected algorithm
162
163    Args:
164        inf (IOBase): Filename containing the data to compress
165        compress (str): Compression algorithm, e.g. 'gzip'
166
167    Return:
168        bytes: Compressed data
169    """
170    if compress == 'none':
171        return inf.read()
172
173    comp = COMP_TOOLS.get(compress)
174    if not comp:
175        raise ValueError(f"Unknown compression algorithm '{compress}'")
176
177    with tempfile.NamedTemporaryFile() as comp_fname:
178        with open(comp_fname.name, 'wb') as outf:
179            done = False
180            for tool in comp.tools.split(','):
181                try:
182                    subprocess.call([tool, '-c'], stdin=inf, stdout=outf)
183                    done = True
184                    break
185                except FileNotFoundError:
186                    pass
187            if not done:
188                raise ValueError(f'Missing tool(s): {comp.tools}\n')
189            with open(comp_fname.name, 'rb') as compf:
190                comp_data = compf.read()
191    return comp_data
192
193
194def output_dtb(fsw, seq, fname, arch, compress):
195    """Write out a single devicetree to the FIT
196
197    Args:
198        fsw (libfdt.FdtSw): Object to use for writing
199        seq (int): Sequence number (1 for first)
200        fname (str): Filename containing the DTB
201        arch: FIT architecture, e.g. 'arm64'
202        compress (str): Compressed algorithm, e.g. 'gzip'
203    """
204    with fsw.add_node(f'fdt-{seq}'):
205        fsw.property_string('description', os.path.basename(fname))
206        fsw.property_string('type', 'flat_dt')
207        fsw.property_string('arch', arch)
208        fsw.property_string('compression', compress)
209
210        with open(fname, 'rb') as inf:
211            compressed = compress_data(inf, compress)
212        fsw.property('data', compressed)
213
214
215def process_dtb(fname, args):
216    """Process an input DTB, decomposing it if requested and is possible
217
218    Args:
219        fname (str): Filename containing the DTB
220        args (Namespace): Program arguments
221    Returns:
222        tuple:
223            str: Model name string
224            str: Root compatible string
225            files: list of filenames corresponding to the DTB
226    """
227    # Get the compatible / model information
228    with open(fname, 'rb') as inf:
229        data = inf.read()
230    fdt = libfdt.FdtRo(data)
231    model = fdt.getprop(0, 'model').as_str()
232    compat = fdt.getprop(0, 'compatible')
233
234    if args.decompose_dtbs:
235        # Check if the DTB needs to be decomposed
236        path, basename = os.path.split(fname)
237        cmd_fname = os.path.join(path, f'.{basename}.cmd')
238        with open(cmd_fname, 'r', encoding='ascii') as inf:
239            cmd = inf.read()
240
241        if 'scripts/dtc/fdtoverlay' in cmd:
242            # This depends on the structure of the composite DTB command
243            files = cmd.split()
244            files = files[files.index('-i') + 1:]
245        else:
246            files = [fname]
247    else:
248        files = [fname]
249
250    return (model, compat, files)
251
252def build_fit(args):
253    """Build the FIT from the provided files and arguments
254
255    Args:
256        args (Namespace): Program arguments
257
258    Returns:
259        tuple:
260            bytes: FIT data
261            int: Number of configurations generated
262            size: Total uncompressed size of data
263    """
264    seq = 0
265    size = 0
266    fsw = libfdt.FdtSw()
267    setup_fit(fsw, args.name)
268    entries = []
269    fdts = {}
270
271    # Handle the kernel
272    with open(args.kernel, 'rb') as inf:
273        comp_data = compress_data(inf, args.compress)
274    size += os.path.getsize(args.kernel)
275    write_kernel(fsw, comp_data, args)
276
277    for fname in args.dtbs:
278        # Ignore non-DTB (*.dtb) files
279        if os.path.splitext(fname)[1] != '.dtb':
280            continue
281
282        (model, compat, files) = process_dtb(fname, args)
283
284        for fn in files:
285            if fn not in fdts:
286                seq += 1
287                size += os.path.getsize(fn)
288                output_dtb(fsw, seq, fn, args.arch, args.compress)
289                fdts[fn] = seq
290
291        files_seq = [fdts[fn] for fn in files]
292
293        entries.append([model, compat, files_seq])
294
295    finish_fit(fsw, entries)
296
297    # Include the kernel itself in the returned file count
298    return fsw.as_fdt().as_bytearray(), seq + 1, size
299
300
301def run_make_fit():
302    """Run the tool's main logic"""
303    args = parse_args()
304
305    out_data, count, size = build_fit(args)
306    with open(args.output, 'wb') as outf:
307        outf.write(out_data)
308
309    ext_fit_size = None
310    if args.external:
311        mkimage = os.environ.get('MKIMAGE', 'mkimage')
312        subprocess.check_call([mkimage, '-E', '-F', args.output],
313                              stdout=subprocess.DEVNULL)
314
315        with open(args.output, 'rb') as inf:
316            data = inf.read()
317        ext_fit = libfdt.FdtRo(data)
318        ext_fit_size = ext_fit.totalsize()
319
320    if args.verbose:
321        comp_size = len(out_data)
322        print(f'FIT size {comp_size:#x}/{comp_size / 1024 / 1024:.1f} MB',
323              end='')
324        if ext_fit_size:
325            print(f', header {ext_fit_size:#x}/{ext_fit_size / 1024:.1f} KB',
326                  end='')
327        print(f', {count} files, uncompressed {size / 1024 / 1024:.1f} MB')
328
329
330if __name__ == "__main__":
331    sys.exit(run_make_fit())
332