xref: /linux/scripts/make_fit.py (revision 37a93dd5c49b5fda807fd204edf2547c3493319c)
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        -r /boot/initrd.img-6.14.0-27-generic @arch/arm64/boot/dts/dtbs-list
14        -E -c gzip
15
16Creates a FIT containing the supplied kernel, an optional ramdisk, and a set of
17devicetree files, either specified individually or listed in a file (with an
18'@' prefix).
19
20Use -r to specify an existing ramdisk/initrd file.
21
22Use -E to generate an external FIT (where the data is placed after the
23FIT data structure). This allows parsing of the data without loading
24the entire FIT.
25
26Use -c to compress the data, using bzip2, gzip, lz4, lzma, lzo and
27zstd algorithms.
28
29Use -D to decompose "composite" DTBs into their base components and
30deduplicate the resulting base DTBs and DTB overlays. This requires the
31DTBs to be sourced from the kernel build directory, as the implementation
32looks at the .cmd files produced by the kernel build.
33
34The resulting FIT can be booted by bootloaders which support FIT, such
35as U-Boot, Linuxboot, Tianocore, etc.
36"""
37
38import argparse
39import collections
40import multiprocessing
41import os
42import subprocess
43import sys
44import tempfile
45import time
46
47import libfdt
48
49
50# Tool extension and the name of the command-line tools
51CompTool = collections.namedtuple('CompTool', 'ext,tools')
52
53COMP_TOOLS = {
54    'bzip2': CompTool('.bz2', 'pbzip2,bzip2'),
55    'gzip': CompTool('.gz', 'pigz,gzip'),
56    'lz4': CompTool('.lz4', 'lz4'),
57    'lzma': CompTool('.lzma', 'plzip,lzma'),
58    'lzo': CompTool('.lzo', 'lzop'),
59    'xz': CompTool('.xz', 'xz'),
60    'zstd': CompTool('.zstd', 'zstd'),
61}
62
63
64def parse_args():
65    """Parse the program ArgumentParser
66
67    Returns:
68        Namespace object containing the arguments
69    """
70    epilog = 'Build a FIT from a directory tree containing .dtb files'
71    parser = argparse.ArgumentParser(epilog=epilog, fromfile_prefix_chars='@')
72    parser.add_argument('-A', '--arch', type=str, required=True,
73          help='Specifies the architecture')
74    parser.add_argument('-c', '--compress', type=str, default='none',
75          help='Specifies the compression')
76    parser.add_argument('-D', '--decompose-dtbs', action='store_true',
77          help='Decompose composite DTBs into base DTB and overlays')
78    parser.add_argument('-E', '--external', action='store_true',
79          help='Convert the FIT to use external data')
80    parser.add_argument('-n', '--name', type=str, required=True,
81          help='Specifies the name')
82    parser.add_argument('-o', '--output', type=str, required=True,
83          help='Specifies the output file (.fit)')
84    parser.add_argument('-O', '--os', type=str, required=True,
85          help='Specifies the operating system')
86    parser.add_argument('-k', '--kernel', type=str, required=True,
87          help='Specifies the (uncompressed) kernel input file (.itk)')
88    parser.add_argument('-r', '--ramdisk', type=str,
89          help='Specifies the ramdisk/initrd input file')
90    parser.add_argument('-v', '--verbose', action='store_true',
91                        help='Enable verbose output')
92    parser.add_argument('dtbs', type=str, nargs='*',
93          help='Specifies the devicetree files to process')
94
95    return parser.parse_args()
96
97
98def setup_fit(fsw, name):
99    """Make a start on writing the FIT
100
101    Outputs the root properties and the 'images' node
102
103    Args:
104        fsw (libfdt.FdtSw): Object to use for writing
105        name (str): Name of kernel image
106    """
107    fsw.INC_SIZE = 16 << 20
108    fsw.finish_reservemap()
109    fsw.begin_node('')
110    fsw.property_string('description', f'{name} with devicetree set')
111    fsw.property_u32('#address-cells', 1)
112
113    fsw.property_u32('timestamp', int(time.time()))
114    fsw.begin_node('images')
115
116
117def write_kernel(fsw, data, args):
118    """Write out the kernel image
119
120    Writes a kernel node along with the required properties
121
122    Args:
123        fsw (libfdt.FdtSw): Object to use for writing
124        data (bytes): Data to write (possibly compressed)
125        args (Namespace): Contains necessary strings:
126            arch: FIT architecture, e.g. 'arm64'
127            fit_os: Operating Systems, e.g. 'linux'
128            name: Name of OS, e.g. 'Linux-6.6.0-rc7'
129            compress: Compression algorithm to use, e.g. 'gzip'
130    """
131    with fsw.add_node('kernel'):
132        fsw.property_string('description', args.name)
133        fsw.property_string('type', 'kernel_noload')
134        fsw.property_string('arch', args.arch)
135        fsw.property_string('os', args.os)
136        fsw.property_string('compression', args.compress)
137        fsw.property('data', data)
138        fsw.property_u32('load', 0)
139        fsw.property_u32('entry', 0)
140
141
142def write_ramdisk(fsw, data, args):
143    """Write out the ramdisk image
144
145    Writes a ramdisk node along with the required properties
146
147    Args:
148        fsw (libfdt.FdtSw): Object to use for writing
149        data (bytes): Data to write (possibly compressed)
150        args (Namespace): Contains necessary strings:
151            arch: FIT architecture, e.g. 'arm64'
152            fit_os: Operating Systems, e.g. 'linux'
153    """
154    with fsw.add_node('ramdisk'):
155        fsw.property_string('description', 'Ramdisk')
156        fsw.property_string('type', 'ramdisk')
157        fsw.property_string('arch', args.arch)
158        fsw.property_string('compression', 'none')
159        fsw.property_string('os', args.os)
160        fsw.property('data', data)
161
162
163def finish_fit(fsw, entries, has_ramdisk=False):
164    """Finish the FIT ready for use
165
166    Writes the /configurations node and subnodes
167
168    Args:
169        fsw (libfdt.FdtSw): Object to use for writing
170        entries (list of tuple): List of configurations:
171            str: Description of model
172            str: Compatible stringlist
173        has_ramdisk (bool): True if a ramdisk is included in the FIT
174    """
175    fsw.end_node()
176    seq = 0
177    with fsw.add_node('configurations'):
178        for model, compat, files in entries:
179            seq += 1
180            with fsw.add_node(f'conf-{seq}'):
181                fsw.property('compatible', bytes(compat))
182                fsw.property_string('description', model)
183                fsw.property('fdt', bytes(''.join(f'fdt-{x}\x00' for x in files), "ascii"))
184                fsw.property_string('kernel', 'kernel')
185                if has_ramdisk:
186                    fsw.property_string('ramdisk', 'ramdisk')
187    fsw.end_node()
188
189
190def compress_data(inf, compress):
191    """Compress data using a selected algorithm
192
193    Args:
194        inf (IOBase): Filename containing the data to compress
195        compress (str): Compression algorithm, e.g. 'gzip'
196
197    Return:
198        bytes: Compressed data
199    """
200    if compress == 'none':
201        return inf.read()
202
203    comp = COMP_TOOLS.get(compress)
204    if not comp:
205        raise ValueError(f"Unknown compression algorithm '{compress}'")
206
207    with tempfile.NamedTemporaryFile() as comp_fname:
208        with open(comp_fname.name, 'wb') as outf:
209            done = False
210            for tool in comp.tools.split(','):
211                try:
212                    # Add parallel flags for tools that support them
213                    cmd = [tool]
214                    if tool in ('zstd', 'xz'):
215                        cmd.extend(['-T0'])  # Use all available cores
216                    cmd.append('-c')
217                    subprocess.call(cmd, stdin=inf, stdout=outf)
218                    done = True
219                    break
220                except FileNotFoundError:
221                    pass
222            if not done:
223                raise ValueError(f'Missing tool(s): {comp.tools}\n')
224            with open(comp_fname.name, 'rb') as compf:
225                comp_data = compf.read()
226    return comp_data
227
228
229def compress_dtb(fname, compress):
230    """Compress a single DTB file
231
232    Args:
233        fname (str): Filename containing the DTB
234        compress (str): Compression algorithm, e.g. 'gzip'
235
236    Returns:
237        tuple: (str: fname, bytes: compressed_data)
238    """
239    with open(fname, 'rb') as inf:
240        compressed = compress_data(inf, compress)
241    return fname, compressed
242
243
244def output_dtb(fsw, seq, fname, arch, compress, data=None):
245    """Write out a single devicetree to the FIT
246
247    Args:
248        fsw (libfdt.FdtSw): Object to use for writing
249        seq (int): Sequence number (1 for first)
250        fname (str): Filename containing the DTB
251        arch (str): FIT architecture, e.g. 'arm64'
252        compress (str): Compressed algorithm, e.g. 'gzip'
253        data (bytes): Pre-compressed data (optional)
254    """
255    with fsw.add_node(f'fdt-{seq}'):
256        fsw.property_string('description', os.path.basename(fname))
257        fsw.property_string('type', 'flat_dt')
258        fsw.property_string('arch', arch)
259        fsw.property_string('compression', compress)
260
261        if data is None:
262            with open(fname, 'rb') as inf:
263                data = compress_data(inf, compress)
264        fsw.property('data', data)
265
266
267def process_dtb(fname, args):
268    """Process an input DTB, decomposing it if requested and is possible
269
270    Args:
271        fname (str): Filename containing the DTB
272        args (Namespace): Program arguments
273    Returns:
274        tuple:
275            str: Model name string
276            str: Root compatible string
277            files: list of filenames corresponding to the DTB
278    """
279    # Get the compatible / model information
280    with open(fname, 'rb') as inf:
281        data = inf.read()
282    fdt = libfdt.FdtRo(data)
283    model = fdt.getprop(0, 'model').as_str()
284    compat = fdt.getprop(0, 'compatible')
285
286    if args.decompose_dtbs:
287        # Check if the DTB needs to be decomposed
288        path, basename = os.path.split(fname)
289        cmd_fname = os.path.join(path, f'.{basename}.cmd')
290        with open(cmd_fname, 'r', encoding='ascii') as inf:
291            cmd = inf.read()
292
293        if 'scripts/dtc/fdtoverlay' in cmd:
294            # This depends on the structure of the composite DTB command
295            files = cmd.split()
296            files = files[files.index('-i') + 1:]
297        else:
298            files = [fname]
299    else:
300        files = [fname]
301
302    return (model, compat, files)
303
304
305def _process_dtbs(args, fsw, entries, fdts):
306    """Process all DTB files and add them to the FIT
307
308    Args:
309        args: Program arguments
310        fsw: FIT writer object
311        entries: List to append entries to
312        fdts: Dictionary of processed DTBs
313
314    Returns:
315        tuple:
316            Number of files processed
317            Total size of files processed
318    """
319    seq = 0
320    size = 0
321
322    # First figure out the unique DTB files that need compression
323    todo = []
324    file_info = []  # List of (fname, model, compat, files) tuples
325
326    for fname in args.dtbs:
327        # Ignore non-DTB (*.dtb) files
328        if os.path.splitext(fname)[1] != '.dtb':
329            continue
330
331        try:
332            (model, compat, files) = process_dtb(fname, args)
333        except Exception as e:
334            sys.stderr.write(f'Error processing {fname}:\n')
335            raise e
336
337        file_info.append((fname, model, compat, files))
338        for fn in files:
339            if fn not in fdts and fn not in todo:
340                todo.append(fn)
341
342    # Compress all DTBs in parallel
343    cache = {}
344    if todo and args.compress != 'none':
345        if args.verbose:
346            print(f'Compressing {len(todo)} DTBs...')
347
348        with multiprocessing.Pool() as pool:
349            compress_args = [(fn, args.compress) for fn in todo]
350            # unpacks each tuple, calls compress_dtb(fn, compress) in parallel
351            results = pool.starmap(compress_dtb, compress_args)
352
353        cache = dict(results)
354
355    # Now write all DTBs to the FIT using pre-compressed data
356    for fname, model, compat, files in file_info:
357        for fn in files:
358            if fn not in fdts:
359                seq += 1
360                size += os.path.getsize(fn)
361                output_dtb(fsw, seq, fn, args.arch, args.compress,
362                           cache.get(fn))
363                fdts[fn] = seq
364
365        files_seq = [fdts[fn] for fn in files]
366        entries.append([model, compat, files_seq])
367
368    return seq, size
369
370
371def build_fit(args):
372    """Build the FIT from the provided files and arguments
373
374    Args:
375        args (Namespace): Program arguments
376
377    Returns:
378        tuple:
379            bytes: FIT data
380            int: Number of configurations generated
381            size: Total uncompressed size of data
382    """
383    size = 0
384    fsw = libfdt.FdtSw()
385    setup_fit(fsw, args.name)
386    entries = []
387    fdts = {}
388
389    # Handle the kernel
390    with open(args.kernel, 'rb') as inf:
391        comp_data = compress_data(inf, args.compress)
392    size += os.path.getsize(args.kernel)
393    write_kernel(fsw, comp_data, args)
394
395    # Handle the ramdisk if provided. Compression is not supported as it is
396    # already compressed.
397    if args.ramdisk:
398        with open(args.ramdisk, 'rb') as inf:
399            data = inf.read()
400        size += len(data)
401        write_ramdisk(fsw, data, args)
402
403    count, fdt_size = _process_dtbs(args, fsw, entries, fdts)
404    size += fdt_size
405
406    finish_fit(fsw, entries, bool(args.ramdisk))
407
408    # Include the kernel itself in the returned file count
409    fdt = fsw.as_fdt()
410    fdt.pack()
411    return fdt.as_bytearray(), count + 1 + bool(args.ramdisk), size
412
413
414def run_make_fit():
415    """Run the tool's main logic"""
416    args = parse_args()
417
418    out_data, count, size = build_fit(args)
419    with open(args.output, 'wb') as outf:
420        outf.write(out_data)
421
422    ext_fit_size = None
423    if args.external:
424        mkimage = os.environ.get('MKIMAGE', 'mkimage')
425        subprocess.check_call([mkimage, '-E', '-F', args.output],
426                              stdout=subprocess.DEVNULL)
427
428        with open(args.output, 'rb') as inf:
429            data = inf.read()
430        ext_fit = libfdt.FdtRo(data)
431        ext_fit_size = ext_fit.totalsize()
432
433    if args.verbose:
434        comp_size = len(out_data)
435        print(f'FIT size {comp_size:#x}/{comp_size / 1024 / 1024:.1f} MB',
436              end='')
437        if ext_fit_size:
438            print(f', header {ext_fit_size:#x}/{ext_fit_size / 1024:.1f} KB',
439                  end='')
440        print(f', {count} files, uncompressed {size / 1024 / 1024:.1f} MB')
441
442
443if __name__ == "__main__":
444    sys.exit(run_make_fit())
445