make_fit.py 14 KB

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