blob: 4a1bb2f55861adfce07959528e175260bc503593 [file] [log] [blame]
Simon Glass7a23b022024-03-29 16:28:36 +13001#!/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
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +080025Use -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
Simon Glass7a23b022024-03-29 16:28:36 +130030The 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')
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +080072 parser.add_argument('-D', '--decompose-dtbs', action='store_true',
73 help='Decompose composite DTBs into base DTB and overlays')
Simon Glass7a23b022024-03-29 16:28:36 +130074 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'):
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800150 for model, compat, files in entries:
Simon Glass7a23b022024-03-29 16:28:36 +1300151 seq += 1
152 with fsw.add_node(f'conf-{seq}'):
153 fsw.property('compatible', bytes(compat))
154 fsw.property_string('description', model)
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800155 fsw.property('fdt', bytes(''.join(f'fdt-{x}\x00' for x in files), "ascii"))
Simon Glass7a23b022024-03-29 16:28:36 +1300156 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)
Chen-Yu Tsaie06a6982024-05-28 16:52:18 +0800200 fname (str): Filename containing the DTB
Simon Glass7a23b022024-03-29 16:28:36 +1300201 arch: FIT architecture, e.g. 'arm64'
202 compress (str): Compressed algorithm, e.g. 'gzip'
Simon Glass7a23b022024-03-29 16:28:36 +1300203 """
204 with fsw.add_node(f'fdt-{seq}'):
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800205 fsw.property_string('description', os.path.basename(fname))
Simon Glass7a23b022024-03-29 16:28:36 +1300206 fsw.property_string('type', 'flat_dt')
207 fsw.property_string('arch', arch)
208 fsw.property_string('compression', compress)
Simon Glass7a23b022024-03-29 16:28:36 +1300209
210 with open(fname, 'rb') as inf:
211 compressed = compress_data(inf, compress)
212 fsw.property('data', compressed)
Simon Glass7a23b022024-03-29 16:28:36 +1300213
214
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800215def 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
Simon Glass7a23b022024-03-29 16:28:36 +1300252def 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 = []
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800269 fdts = {}
Simon Glass7a23b022024-03-29 16:28:36 +1300270
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:
Chen-Yu Tsai17c31ad2024-06-13 17:34:32 +0800278 # 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])
Simon Glass7a23b022024-03-29 16:28:36 +1300294
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())