1 # SPDX-License-Identifier: GPL-2.0+
3 # Copyright (c) 2016 Google, Inc
13 from patman import command
14 from patman import tout
16 # Output directly (generally this is temporary)
19 # True to keep the output directory around after exiting
20 preserve_outdir = False
22 # Path to the Chrome OS chroot, if we know it
25 # Search paths to use for Filename(), used to find files
28 tool_search_paths = []
30 # Tools and the packages that contain them, on debian
35 # List of paths to use when looking for an input file
38 def PrepareOutputDir(dirname, preserve=False):
39 """Select an output directory, ensuring it exists.
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.
46 dirname: a string, name of the output directory to use to store
47 intermediate and output files. If is None - create a temporary
49 preserve: a Boolean. If outdir above is None and preserve is False, the
50 created temporary directory will be destroyed on exit.
53 OSError: If it cannot create the output directory.
55 global outdir, preserve_outdir
57 preserve_outdir = dirname or preserve
60 if not os.path.isdir(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)
68 outdir = tempfile.mkdtemp(prefix='binman.')
69 tout.Debug("Using temporary directory '%s'" % outdir)
71 def _RemoveOutputDir():
75 tout.Debug("Deleted temporary directory '%s'" % outdir)
78 def FinaliseOutputDir():
79 global outdir, preserve_outdir
81 """Tidy up: delete output directory if temporary and not preserved."""
82 if outdir and not preserve_outdir:
86 def GetOutputFilename(fname):
87 """Return a filename within the output directory.
90 fname: Filename to use for new file
93 The full path of the filename, within the output directory
95 return os.path.join(outdir, fname)
97 def _FinaliseForTest():
98 """Remove the output directory (for use by tests)"""
105 def SetInputDirs(dirname):
106 """Add a list of input directories, where input files are kept.
109 dirname: a list of paths to input directories to use for obtaining
110 files needed by binman to place in the image.
115 tout.Debug("Using input directories %s" % indir)
117 def GetInputFilename(fname, allow_missing=False):
118 """Return a filename for use as input.
121 fname: Filename to use for new file
122 allow_missing: True if the filename can be missing
125 The full path of the filename, within the input directory, or
128 if not indir or fname[:1] == '/':
130 for dirname in indir:
131 pathname = os.path.join(dirname, fname)
132 if os.path.exists(pathname):
137 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
138 (fname, ','.join(indir), os.getcwd()))
140 def GetInputFilenameGlob(pattern):
141 """Return a list of filenames for use as input.
144 pattern: Filename pattern to search for
147 A list of matching files in all input directories
150 return glob.glob(fname)
152 for dirname in indir:
153 pathname = os.path.join(dirname, pattern)
154 files += glob.glob(pathname)
157 def Align(pos, align):
160 pos = (pos + mask) & ~mask
163 def NotPowerOfTwo(num):
164 return num and (num & (num - 1))
166 def SetToolPaths(toolpaths):
167 """Set the path to search for tools
170 toolpaths: List of paths to search for tools executed by Run()
172 global tool_search_paths
174 tool_search_paths = toolpaths
176 def PathHasFile(path_spec, fname):
177 """Check if a given filename is in the PATH
180 path_spec: Value of PATH variable to check
181 fname: Filename to check
184 True if found, False if not
186 for dir in path_spec.split(':'):
187 if os.path.exists(os.path.join(dir, fname)):
191 def GetHostCompileTool(name):
192 """Get the host-specific version for a compile tool
194 This checks the environment variables that specify which version of
195 the tool should be used (e.g. ${HOSTCC}).
197 The following table lists the host-specific versions of the tools
198 this function resolves to:
200 Compile Tool | Host version
201 --------------+----------------
211 objcopy | ${HOSTOBJCOPY}
212 objdump | ${HOSTOBJDUMP}
216 name: Command name to run
219 host_name: Exact command name to run instead
220 extra_args: List of extra arguments to pass
224 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
225 'objcopy', 'objdump', 'dtc'):
226 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
228 host_name, *host_args = env.get('HOSTCXX', '').split(' ')
231 return host_name, extra_args
234 def GetTargetCompileTool(name, cross_compile=None):
235 """Get the target-specific version for a compile tool
237 This first checks the environment variables that specify which
238 version of the tool should be used (e.g. ${CC}). If those aren't
239 specified, it checks the CROSS_COMPILE variable as a prefix for the
240 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
242 The following table lists the target-specific versions of the tools
243 this function resolves to:
245 Compile Tool | First choice | Second choice
246 --------------+----------------+----------------------------
247 as | ${AS} | ${CROSS_COMPILE}as
248 ld | ${LD} | ${CROSS_COMPILE}ld.bfd
249 | | or ${CROSS_COMPILE}ld
250 cc | ${CC} | ${CROSS_COMPILE}gcc
251 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
252 c++ | ${CXX} | ${CROSS_COMPILE}g++
253 ar | ${AR} | ${CROSS_COMPILE}ar
254 nm | ${NM} | ${CROSS_COMPILE}nm
255 ldr | ${LDR} | ${CROSS_COMPILE}ldr
256 strip | ${STRIP} | ${CROSS_COMPILE}strip
257 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
258 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
259 dtc | ${DTC} | (no CROSS_COMPILE version)
262 name: Command name to run
265 target_name: Exact command name to run instead
266 extra_args: List of extra arguments to pass
268 env = dict(os.environ)
272 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
273 'objcopy', 'objdump', 'dtc'):
274 target_name, *extra_args = env.get(name.upper(), '').split(' ')
276 target_name, *extra_args = env.get('CXX', '').split(' ')
279 return target_name, extra_args
281 if cross_compile is None:
282 cross_compile = env.get('CROSS_COMPILE', '')
283 if not cross_compile:
286 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
287 target_name = cross_compile + name
290 if Run(cross_compile + 'ld.bfd', '-v'):
291 target_name = cross_compile + 'ld.bfd'
293 target_name = cross_compile + 'ld'
295 target_name = cross_compile + 'gcc'
297 target_name = cross_compile + 'gcc'
300 target_name = cross_compile + 'g++'
303 return target_name, extra_args
305 def Run(name, *args, **kwargs):
306 """Run a tool with some arguments
308 This runs a 'tool', which is a program used by binman to process files and
309 perhaps produce some output. Tools can be located on the PATH or in a
313 name: Command name to run
314 args: Arguments to the tool
315 for_host: True to resolve the command to the version for the host
316 for_target: False to run the command as-is, without resolving it
317 to the version for the compile target
323 binary = kwargs.get('binary')
324 for_host = kwargs.get('for_host', False)
325 for_target = kwargs.get('for_target', not for_host)
327 if tool_search_paths:
328 env = dict(os.environ)
329 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
331 name, extra_args = GetTargetCompileTool(name)
332 args = tuple(extra_args) + args
334 name, extra_args = GetHostCompileTool(name)
335 args = tuple(extra_args) + args
336 name = os.path.expanduser(name) # Expand paths containing ~
337 all_args = (name,) + args
338 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
339 env=env, raise_on_error=False, binary=binary)
340 if result.return_code:
341 raise Exception("Error %d running '%s': %s" %
342 (result.return_code,' '.join(all_args),
346 if env and not PathHasFile(env['PATH'], name):
347 msg = "Please install tool '%s'" % name
348 package = packages.get(name)
350 msg += " (e.g. from package '%s')" % package
351 raise ValueError(msg)
355 """Resolve a file path to an absolute path.
357 If fname starts with ##/ and chroot is available, ##/ gets replaced with
358 the chroot path. If chroot is not available, this file name can not be
359 resolved, `None' is returned.
361 If fname is not prepended with the above prefix, and is not an existing
362 file, the actual file name is retrieved from the passed in string and the
363 search_paths directories (if any) are searched to for the file. If found -
364 the path to the found file is returned, `None' is returned otherwise.
367 fname: a string, the path to resolve.
370 Absolute path to the file or None if not found.
372 if fname.startswith('##/'):
374 fname = os.path.join(chroot_path, fname[3:])
378 # Search for a pathname that exists, and return it if found
379 if fname and not os.path.exists(fname):
380 for path in search_paths:
381 pathname = os.path.join(path, os.path.basename(fname))
382 if os.path.exists(pathname):
385 # If not found, just return the standard, unchanged path
388 def ReadFile(fname, binary=True):
389 """Read and return the contents of a file.
392 fname: path to filename to read, where ## signifiies the chroot.
395 data read from file, as a string.
397 with open(Filename(fname), binary and 'rb' or 'r') as fd:
399 #self._out.Info("Read file '%s' size %d (%#0x)" %
400 #(fname, len(data), len(data)))
403 def WriteFile(fname, data, binary=True):
404 """Write data into a file.
407 fname: path to filename to write
408 data: data to write to file, as a string
410 #self._out.Info("Write file '%s' size %d (%#0x)" %
411 #(fname, len(data), len(data)))
412 with open(Filename(fname), binary and 'wb' or 'w') as fd:
415 def GetBytes(byte, size):
416 """Get a string of bytes of a given size
418 This handles the unfortunate different between Python 2 and Python 2.
421 byte: Numeric byte value to use
422 size: Size of bytes/string to return
425 A bytes type with 'byte' repeated 'size' times
427 if sys.version_info[0] >= 3:
428 data = bytes([byte]) * size
430 data = chr(byte) * size
434 """Make sure a value is a unicode string
436 This allows some amount of compatibility between Python 2 and Python3. For
437 the former, it returns a unicode object.
440 val: string or unicode object
443 unicode version of val
445 if sys.version_info[0] >= 3:
447 return val if isinstance(val, unicode) else val.decode('utf-8')
449 def FromUnicode(val):
450 """Make sure a value is a non-unicode string
452 This allows some amount of compatibility between Python 2 and Python3. For
453 the former, it converts a unicode object to a string.
456 val: string or unicode object
459 non-unicode version of val
461 if sys.version_info[0] >= 3:
463 return val if isinstance(val, str) else val.encode('utf-8')
466 """Convert a character to an ASCII value
468 This is useful because in Python 2 bytes is an alias for str, but in
469 Python 3 they are separate types. This function converts the argument to
470 an ASCII value in either case.
473 ch: A string (Python 2) or byte (Python 3) value
476 integer ASCII value for ch
478 return ord(ch) if type(ch) == str else ch
481 """Convert a byte to a character
483 This is useful because in Python 2 bytes is an alias for str, but in
484 Python 3 they are separate types. This function converts an ASCII value to
485 a value with the appropriate type in either case.
488 byte: A byte or str value
490 return chr(byte) if type(byte) != str else byte
492 def ToChars(byte_list):
493 """Convert a list of bytes to a str/bytes type
496 byte_list: List of ASCII values representing the string
499 string made by concatenating all the ASCII values
501 return ''.join([chr(byte) for byte in byte_list])
504 """Convert a str type into a bytes type
507 string: string to convert
510 Python 3: A bytes type
511 Python 2: A string type
513 if sys.version_info[0] >= 3:
514 return string.encode('utf-8')
518 """Convert a bytes type into a str type
521 bval: bytes value to convert
524 Python 3: A bytes type
525 Python 2: A string type
527 return bval.decode('utf-8')
529 def Compress(indata, algo, with_header=True):
530 """Compress some data using a given algorithm
532 Note that for lzma this uses an old version of the algorithm, not that
535 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
536 directory to be previously set up, by calling PrepareOutputDir().
539 indata: Input data to compress
540 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
547 fname = GetOutputFilename('%s.comp.tmp' % algo)
548 WriteFile(fname, indata)
550 data = Run('lz4', '--no-frame-crc', '-c', fname, binary=True)
551 # cbfstool uses a very old version of lzma
553 outfname = GetOutputFilename('%s.comp.otmp' % algo)
554 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
555 data = ReadFile(outfname)
557 data = Run('gzip', '-c', fname, binary=True)
559 raise ValueError("Unknown algorithm '%s'" % algo)
561 hdr = struct.pack('<I', len(data))
565 def Decompress(indata, algo, with_header=True):
566 """Decompress some data using a given algorithm
568 Note that for lzma this uses an old version of the algorithm, not that
571 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
572 directory to be previously set up, by calling PrepareOutputDir().
575 indata: Input data to decompress
576 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
584 data_len = struct.unpack('<I', indata[:4])[0]
585 indata = indata[4:4 + data_len]
586 fname = GetOutputFilename('%s.decomp.tmp' % algo)
587 with open(fname, 'wb') as fd:
590 data = Run('lz4', '-dc', fname, binary=True)
592 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
593 Run('lzma_alone', 'd', fname, outfname)
594 data = ReadFile(outfname, binary=True)
596 data = Run('gzip', '-cd', fname, binary=True)
598 raise ValueError("Unknown algorithm '%s'" % algo)
601 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
604 CMD_CREATE: 'create',
605 CMD_DELETE: 'delete',
607 CMD_REPLACE: 'replace',
608 CMD_EXTRACT: 'extract',
611 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
612 """Run ifwitool with the given arguments:
615 ifwi_file: IFWI file to operation on
616 cmd: Command to execute (CMD_...)
617 fname: Filename of file to add/replace/extract/create (None for
619 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
620 entry_name: Name of directory entry to operate on, or None if none
622 args = ['ifwitool', ifwi_file]
623 args.append(IFWITOOL_CMDS[cmd])
625 args += ['-f', fname]
627 args += ['-n', subpart]
629 args += ['-d', '-e', entry_name]
633 """Convert an integer value (or None) to a string
636 hex value, or 'None' if the value is None
638 return 'None' if val is None else '%#x' % val
641 """Return the size of an object in hex
644 hex value of size, or 'None' if the value is None
646 return 'None' if val is None else '%#x' % len(val)