blob: 66f6ab7af0636bc6baac4177887169e234a27313 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glass1f1864b2016-07-25 18:59:08 -06002#
3# Copyright (c) 2016 Google, Inc
4#
Simon Glass1f1864b2016-07-25 18:59:08 -06005
Simon Glass0a98b282018-09-14 04:57:28 -06006import glob
Simon Glass1f1864b2016-07-25 18:59:08 -06007import os
8import shutil
Simon Glasseb0f4a42019-07-20 12:24:06 -06009import struct
Simon Glasse6d85ff2019-05-14 15:53:47 -060010import sys
Simon Glass1f1864b2016-07-25 18:59:08 -060011import tempfile
12
Simon Glassbf776672020-04-17 18:09:04 -060013from patman import command
14from patman import tout
Simon Glass1f1864b2016-07-25 18:59:08 -060015
Simon Glassaeffc5e2018-07-17 13:25:43 -060016# Output directly (generally this is temporary)
Simon Glass1f1864b2016-07-25 18:59:08 -060017outdir = None
Simon Glassaeffc5e2018-07-17 13:25:43 -060018
19# True to keep the output directory around after exiting
Simon Glass1f1864b2016-07-25 18:59:08 -060020preserve_outdir = False
21
Simon Glassaeffc5e2018-07-17 13:25:43 -060022# Path to the Chrome OS chroot, if we know it
23chroot_path = None
24
25# Search paths to use for Filename(), used to find files
26search_paths = []
27
Simon Glassc22b8cf2019-07-08 13:18:27 -060028tool_search_paths = []
29
Simon Glass04187a82018-09-14 04:57:25 -060030# Tools and the packages that contain them, on debian
31packages = {
32 'lz4': 'liblz4-tool',
33 }
Simon Glassaeffc5e2018-07-17 13:25:43 -060034
Simon Glass1fda1822018-10-01 21:12:44 -060035# List of paths to use when looking for an input file
36indir = []
37
Simon Glass1f1864b2016-07-25 18:59:08 -060038def PrepareOutputDir(dirname, preserve=False):
39 """Select an output directory, ensuring it exists.
40
41 This either creates a temporary directory or checks that the one supplied
42 by the user is valid. For a temporary directory, it makes a note to
43 remove it later if required.
44
45 Args:
46 dirname: a string, name of the output directory to use to store
47 intermediate and output files. If is None - create a temporary
48 directory.
49 preserve: a Boolean. If outdir above is None and preserve is False, the
50 created temporary directory will be destroyed on exit.
51
52 Raises:
53 OSError: If it cannot create the output directory.
54 """
55 global outdir, preserve_outdir
56
57 preserve_outdir = dirname or preserve
58 if dirname:
59 outdir = dirname
60 if not os.path.isdir(outdir):
61 try:
62 os.makedirs(outdir)
63 except OSError as err:
64 raise CmdError("Cannot make output directory '%s': '%s'" %
65 (outdir, err.strerror))
66 tout.Debug("Using output directory '%s'" % outdir)
67 else:
68 outdir = tempfile.mkdtemp(prefix='binman.')
69 tout.Debug("Using temporary directory '%s'" % outdir)
70
71def _RemoveOutputDir():
72 global outdir
73
74 shutil.rmtree(outdir)
75 tout.Debug("Deleted temporary directory '%s'" % outdir)
76 outdir = None
77
78def FinaliseOutputDir():
79 global outdir, preserve_outdir
80
81 """Tidy up: delete output directory if temporary and not preserved."""
82 if outdir and not preserve_outdir:
83 _RemoveOutputDir()
Simon Glass31353302019-07-20 12:24:07 -060084 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -060085
86def GetOutputFilename(fname):
87 """Return a filename within the output directory.
88
89 Args:
90 fname: Filename to use for new file
91
92 Returns:
93 The full path of the filename, within the output directory
94 """
95 return os.path.join(outdir, fname)
96
97def _FinaliseForTest():
98 """Remove the output directory (for use by tests)"""
99 global outdir
100
101 if outdir:
102 _RemoveOutputDir()
Simon Glass31353302019-07-20 12:24:07 -0600103 outdir = None
Simon Glass1f1864b2016-07-25 18:59:08 -0600104
105def SetInputDirs(dirname):
106 """Add a list of input directories, where input files are kept.
107
108 Args:
109 dirname: a list of paths to input directories to use for obtaining
110 files needed by binman to place in the image.
111 """
112 global indir
113
114 indir = dirname
115 tout.Debug("Using input directories %s" % indir)
116
Simon Glass4f9f1052020-07-09 18:39:38 -0600117def GetInputFilename(fname, allow_missing=False):
Simon Glass1f1864b2016-07-25 18:59:08 -0600118 """Return a filename for use as input.
119
120 Args:
121 fname: Filename to use for new file
Simon Glass4f9f1052020-07-09 18:39:38 -0600122 allow_missing: True if the filename can be missing
Simon Glass1f1864b2016-07-25 18:59:08 -0600123
124 Returns:
Simon Glass4f9f1052020-07-09 18:39:38 -0600125 The full path of the filename, within the input directory, or
126 None on error
Simon Glass1f1864b2016-07-25 18:59:08 -0600127 """
Simon Glassf514d8f2019-08-24 07:22:54 -0600128 if not indir or fname[:1] == '/':
Simon Glass1f1864b2016-07-25 18:59:08 -0600129 return fname
130 for dirname in indir:
131 pathname = os.path.join(dirname, fname)
132 if os.path.exists(pathname):
133 return pathname
134
Simon Glass4f9f1052020-07-09 18:39:38 -0600135 if allow_missing:
136 return None
Simon Glass4f5dea42018-07-17 13:25:45 -0600137 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
138 (fname, ','.join(indir), os.getcwd()))
Simon Glass1f1864b2016-07-25 18:59:08 -0600139
Simon Glass0a98b282018-09-14 04:57:28 -0600140def GetInputFilenameGlob(pattern):
141 """Return a list of filenames for use as input.
142
143 Args:
144 pattern: Filename pattern to search for
145
146 Returns:
147 A list of matching files in all input directories
148 """
149 if not indir:
150 return glob.glob(fname)
151 files = []
152 for dirname in indir:
153 pathname = os.path.join(dirname, pattern)
154 files += glob.glob(pathname)
155 return sorted(files)
156
Simon Glass1f1864b2016-07-25 18:59:08 -0600157def Align(pos, align):
158 if align:
159 mask = align - 1
160 pos = (pos + mask) & ~mask
161 return pos
162
163def NotPowerOfTwo(num):
164 return num and (num & (num - 1))
Simon Glassaeffc5e2018-07-17 13:25:43 -0600165
Simon Glassc22b8cf2019-07-08 13:18:27 -0600166def SetToolPaths(toolpaths):
167 """Set the path to search for tools
168
169 Args:
170 toolpaths: List of paths to search for tools executed by Run()
171 """
172 global tool_search_paths
173
174 tool_search_paths = toolpaths
175
176def PathHasFile(path_spec, fname):
Simon Glass04187a82018-09-14 04:57:25 -0600177 """Check if a given filename is in the PATH
178
179 Args:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600180 path_spec: Value of PATH variable to check
Simon Glass04187a82018-09-14 04:57:25 -0600181 fname: Filename to check
182
183 Returns:
184 True if found, False if not
185 """
Simon Glassc22b8cf2019-07-08 13:18:27 -0600186 for dir in path_spec.split(':'):
Simon Glass04187a82018-09-14 04:57:25 -0600187 if os.path.exists(os.path.join(dir, fname)):
188 return True
189 return False
190
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300191def GetTargetCompileTool(name, cross_compile=None):
192 """Get the target-specific version for a compile tool
193
194 This first checks the environment variables that specify which
195 version of the tool should be used (e.g. ${CC}). If those aren't
196 specified, it checks the CROSS_COMPILE variable as a prefix for the
197 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
198
199 The following table lists the target-specific versions of the tools
200 this function resolves to:
201
202 Compile Tool | First choice | Second choice
203 --------------+----------------+----------------------------
204 as | ${AS} | ${CROSS_COMPILE}as
205 ld | ${LD} | ${CROSS_COMPILE}ld.bfd
206 | | or ${CROSS_COMPILE}ld
207 cc | ${CC} | ${CROSS_COMPILE}gcc
208 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
209 c++ | ${CXX} | ${CROSS_COMPILE}g++
210 ar | ${AR} | ${CROSS_COMPILE}ar
211 nm | ${NM} | ${CROSS_COMPILE}nm
212 ldr | ${LDR} | ${CROSS_COMPILE}ldr
213 strip | ${STRIP} | ${CROSS_COMPILE}strip
214 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
215 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
216 dtc | ${DTC} | (no CROSS_COMPILE version)
217
218 Args:
219 name: Command name to run
220
221 Returns:
222 target_name: Exact command name to run instead
223 extra_args: List of extra arguments to pass
224 """
225 env = dict(os.environ)
226
227 target_name = None
228 extra_args = []
229 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
230 'objcopy', 'objdump', 'dtc'):
231 target_name, *extra_args = env.get(name.upper(), '').split(' ')
232 elif name == 'c++':
233 target_name, *extra_args = env.get('CXX', '').split(' ')
234
235 if target_name:
236 return target_name, extra_args
237
238 if cross_compile is None:
239 cross_compile = env.get('CROSS_COMPILE', '')
240 if not cross_compile:
241 return name, []
242
243 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
244 target_name = cross_compile + name
245 elif name == 'ld':
246 try:
247 if Run(cross_compile + 'ld.bfd', '-v'):
248 target_name = cross_compile + 'ld.bfd'
249 except:
250 target_name = cross_compile + 'ld'
251 elif name == 'cc':
252 target_name = cross_compile + 'gcc'
253 elif name == 'cpp':
254 target_name = cross_compile + 'gcc'
255 extra_args = ['-E']
256 elif name == 'c++':
257 target_name = cross_compile + 'g++'
258 else:
259 target_name = name
260 return target_name, extra_args
261
Simon Glass3b3e3c02019-10-31 07:42:50 -0600262def Run(name, *args, **kwargs):
Simon Glassc22b8cf2019-07-08 13:18:27 -0600263 """Run a tool with some arguments
264
265 This runs a 'tool', which is a program used by binman to process files and
266 perhaps produce some output. Tools can be located on the PATH or in a
267 search path.
268
269 Args:
270 name: Command name to run
271 args: Arguments to the tool
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300272 for_target: False to run the command as-is, without resolving it
273 to the version for the compile target
Simon Glassc22b8cf2019-07-08 13:18:27 -0600274
275 Returns:
276 CommandResult object
277 """
Simon Glass04187a82018-09-14 04:57:25 -0600278 try:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600279 binary = kwargs.get('binary')
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300280 for_target = kwargs.get('for_target', True)
Simon Glassc22b8cf2019-07-08 13:18:27 -0600281 env = None
282 if tool_search_paths:
283 env = dict(os.environ)
284 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
Alper Nebi Yasak1e4687a2020-09-06 14:46:05 +0300285 if for_target:
286 name, extra_args = GetTargetCompileTool(name)
287 args = tuple(extra_args) + args
Simon Glass6eace392019-08-24 07:22:42 -0600288 all_args = (name,) + args
289 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
Simon Glass3b3e3c02019-10-31 07:42:50 -0600290 env=env, raise_on_error=False, binary=binary)
Simon Glass6eace392019-08-24 07:22:42 -0600291 if result.return_code:
292 raise Exception("Error %d running '%s': %s" %
293 (result.return_code,' '.join(all_args),
294 result.stderr))
295 return result.stdout
Simon Glass04187a82018-09-14 04:57:25 -0600296 except:
Simon Glassc22b8cf2019-07-08 13:18:27 -0600297 if env and not PathHasFile(env['PATH'], name):
298 msg = "Please install tool '%s'" % name
Simon Glass04187a82018-09-14 04:57:25 -0600299 package = packages.get(name)
300 if package:
301 msg += " (e.g. from package '%s')" % package
302 raise ValueError(msg)
303 raise
Simon Glassaeffc5e2018-07-17 13:25:43 -0600304
305def Filename(fname):
306 """Resolve a file path to an absolute path.
307
308 If fname starts with ##/ and chroot is available, ##/ gets replaced with
309 the chroot path. If chroot is not available, this file name can not be
310 resolved, `None' is returned.
311
312 If fname is not prepended with the above prefix, and is not an existing
313 file, the actual file name is retrieved from the passed in string and the
314 search_paths directories (if any) are searched to for the file. If found -
315 the path to the found file is returned, `None' is returned otherwise.
316
317 Args:
318 fname: a string, the path to resolve.
319
320 Returns:
321 Absolute path to the file or None if not found.
322 """
323 if fname.startswith('##/'):
324 if chroot_path:
325 fname = os.path.join(chroot_path, fname[3:])
326 else:
327 return None
328
329 # Search for a pathname that exists, and return it if found
330 if fname and not os.path.exists(fname):
331 for path in search_paths:
332 pathname = os.path.join(path, os.path.basename(fname))
333 if os.path.exists(pathname):
334 return pathname
335
336 # If not found, just return the standard, unchanged path
337 return fname
338
Simon Glass3c47e412019-05-17 22:00:44 -0600339def ReadFile(fname, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600340 """Read and return the contents of a file.
341
342 Args:
343 fname: path to filename to read, where ## signifiies the chroot.
344
345 Returns:
346 data read from file, as a string.
347 """
Simon Glass3c47e412019-05-17 22:00:44 -0600348 with open(Filename(fname), binary and 'rb' or 'r') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600349 data = fd.read()
350 #self._out.Info("Read file '%s' size %d (%#0x)" %
351 #(fname, len(data), len(data)))
352 return data
353
Simon Glassfd709862020-07-05 21:41:50 -0600354def WriteFile(fname, data, binary=True):
Simon Glassaeffc5e2018-07-17 13:25:43 -0600355 """Write data into a file.
356
357 Args:
358 fname: path to filename to write
359 data: data to write to file, as a string
360 """
361 #self._out.Info("Write file '%s' size %d (%#0x)" %
362 #(fname, len(data), len(data)))
Simon Glassfd709862020-07-05 21:41:50 -0600363 with open(Filename(fname), binary and 'wb' or 'w') as fd:
Simon Glassaeffc5e2018-07-17 13:25:43 -0600364 fd.write(data)
Simon Glasse6d85ff2019-05-14 15:53:47 -0600365
366def GetBytes(byte, size):
367 """Get a string of bytes of a given size
368
369 This handles the unfortunate different between Python 2 and Python 2.
370
371 Args:
372 byte: Numeric byte value to use
373 size: Size of bytes/string to return
374
375 Returns:
376 A bytes type with 'byte' repeated 'size' times
377 """
378 if sys.version_info[0] >= 3:
379 data = bytes([byte]) * size
380 else:
381 data = chr(byte) * size
382 return data
Simon Glass513eace2019-05-14 15:53:50 -0600383
384def ToUnicode(val):
385 """Make sure a value is a unicode string
386
387 This allows some amount of compatibility between Python 2 and Python3. For
388 the former, it returns a unicode object.
389
390 Args:
391 val: string or unicode object
392
393 Returns:
394 unicode version of val
395 """
396 if sys.version_info[0] >= 3:
397 return val
398 return val if isinstance(val, unicode) else val.decode('utf-8')
399
400def FromUnicode(val):
401 """Make sure a value is a non-unicode string
402
403 This allows some amount of compatibility between Python 2 and Python3. For
404 the former, it converts a unicode object to a string.
405
406 Args:
407 val: string or unicode object
408
409 Returns:
410 non-unicode version of val
411 """
412 if sys.version_info[0] >= 3:
413 return val
414 return val if isinstance(val, str) else val.encode('utf-8')
Simon Glass2b6ed5e2019-05-17 22:00:35 -0600415
416def ToByte(ch):
417 """Convert a character to an ASCII value
418
419 This is useful because in Python 2 bytes is an alias for str, but in
420 Python 3 they are separate types. This function converts the argument to
421 an ASCII value in either case.
422
423 Args:
424 ch: A string (Python 2) or byte (Python 3) value
425
426 Returns:
427 integer ASCII value for ch
428 """
429 return ord(ch) if type(ch) == str else ch
430
431def ToChar(byte):
432 """Convert a byte to a character
433
434 This is useful because in Python 2 bytes is an alias for str, but in
435 Python 3 they are separate types. This function converts an ASCII value to
436 a value with the appropriate type in either case.
437
438 Args:
439 byte: A byte or str value
440 """
441 return chr(byte) if type(byte) != str else byte
Simon Glassf6b64812019-05-17 22:00:36 -0600442
443def ToChars(byte_list):
444 """Convert a list of bytes to a str/bytes type
445
446 Args:
447 byte_list: List of ASCII values representing the string
448
449 Returns:
450 string made by concatenating all the ASCII values
451 """
452 return ''.join([chr(byte) for byte in byte_list])
453
454def ToBytes(string):
455 """Convert a str type into a bytes type
456
457 Args:
Simon Glass3b3e3c02019-10-31 07:42:50 -0600458 string: string to convert
Simon Glassf6b64812019-05-17 22:00:36 -0600459
460 Returns:
461 Python 3: A bytes type
462 Python 2: A string type
463 """
464 if sys.version_info[0] >= 3:
465 return string.encode('utf-8')
466 return string
Simon Glass07d9e702019-07-08 13:18:41 -0600467
Simon Glass3b3e3c02019-10-31 07:42:50 -0600468def ToString(bval):
469 """Convert a bytes type into a str type
470
471 Args:
472 bval: bytes value to convert
473
474 Returns:
475 Python 3: A bytes type
476 Python 2: A string type
477 """
478 return bval.decode('utf-8')
479
Simon Glasseb0f4a42019-07-20 12:24:06 -0600480def Compress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600481 """Compress some data using a given algorithm
482
483 Note that for lzma this uses an old version of the algorithm, not that
484 provided by xz.
485
486 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
487 directory to be previously set up, by calling PrepareOutputDir().
488
489 Args:
490 indata: Input data to compress
491 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
492
493 Returns:
494 Compressed data
495 """
496 if algo == 'none':
497 return indata
498 fname = GetOutputFilename('%s.comp.tmp' % algo)
499 WriteFile(fname, indata)
500 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600501 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600502 # cbfstool uses a very old version of lzma
503 elif algo == 'lzma':
504 outfname = GetOutputFilename('%s.comp.otmp' % algo)
505 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
506 data = ReadFile(outfname)
507 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600508 data = Run('gzip', '-c', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600509 else:
510 raise ValueError("Unknown algorithm '%s'" % algo)
Simon Glasseb0f4a42019-07-20 12:24:06 -0600511 if with_header:
512 hdr = struct.pack('<I', len(data))
513 data = hdr + data
Simon Glass07d9e702019-07-08 13:18:41 -0600514 return data
515
Simon Glasseb0f4a42019-07-20 12:24:06 -0600516def Decompress(indata, algo, with_header=True):
Simon Glass07d9e702019-07-08 13:18:41 -0600517 """Decompress some data using a given algorithm
518
519 Note that for lzma this uses an old version of the algorithm, not that
520 provided by xz.
521
522 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
523 directory to be previously set up, by calling PrepareOutputDir().
524
525 Args:
526 indata: Input data to decompress
527 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
528
529 Returns:
530 Compressed data
531 """
532 if algo == 'none':
533 return indata
Simon Glasseb0f4a42019-07-20 12:24:06 -0600534 if with_header:
535 data_len = struct.unpack('<I', indata[:4])[0]
536 indata = indata[4:4 + data_len]
Simon Glass07d9e702019-07-08 13:18:41 -0600537 fname = GetOutputFilename('%s.decomp.tmp' % algo)
538 with open(fname, 'wb') as fd:
539 fd.write(indata)
540 if algo == 'lz4':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600541 data = Run('lz4', '-dc', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600542 elif algo == 'lzma':
543 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
544 Run('lzma_alone', 'd', fname, outfname)
Simon Glass3b3e3c02019-10-31 07:42:50 -0600545 data = ReadFile(outfname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600546 elif algo == 'gzip':
Simon Glass3b3e3c02019-10-31 07:42:50 -0600547 data = Run('gzip', '-cd', fname, binary=True)
Simon Glass07d9e702019-07-08 13:18:41 -0600548 else:
549 raise ValueError("Unknown algorithm '%s'" % algo)
550 return data
Simon Glass1cfdfc02019-07-08 13:18:51 -0600551
552CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
553
554IFWITOOL_CMDS = {
555 CMD_CREATE: 'create',
556 CMD_DELETE: 'delete',
557 CMD_ADD: 'add',
558 CMD_REPLACE: 'replace',
559 CMD_EXTRACT: 'extract',
560 }
561
562def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
563 """Run ifwitool with the given arguments:
564
565 Args:
566 ifwi_file: IFWI file to operation on
567 cmd: Command to execute (CMD_...)
568 fname: Filename of file to add/replace/extract/create (None for
569 CMD_DELETE)
570 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
571 entry_name: Name of directory entry to operate on, or None if none
572 """
573 args = ['ifwitool', ifwi_file]
574 args.append(IFWITOOL_CMDS[cmd])
575 if fname:
576 args += ['-f', fname]
577 if subpart:
578 args += ['-n', subpart]
579 if entry_name:
580 args += ['-d', '-e', entry_name]
581 Run(*args)
Simon Glass9f297b02019-07-20 12:23:36 -0600582
583def ToHex(val):
584 """Convert an integer value (or None) to a string
585
586 Returns:
587 hex value, or 'None' if the value is None
588 """
589 return 'None' if val is None else '%#x' % val
590
591def ToHexSize(val):
592 """Return the size of an object in hex
593
594 Returns:
595 hex value of size, or 'None' if the value is None
596 """
597 return 'None' if val is None else '%#x' % len(val)