make_fit.py 10 KB

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