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)
98 """Return the current output directory
101 str: The output directory
105 def _FinaliseForTest():
106 """Remove the output directory (for use by tests)"""
113 def SetInputDirs(dirname):
114 """Add a list of input directories, where input files are kept.
117 dirname: a list of paths to input directories to use for obtaining
118 files needed by binman to place in the image.
123 tout.Debug("Using input directories %s" % indir)
125 def GetInputFilename(fname, allow_missing=False):
126 """Return a filename for use as input.
129 fname: Filename to use for new file
130 allow_missing: True if the filename can be missing
133 fname, if indir is None;
134 full path of the filename, within the input directory;
135 None, if file is missing and allow_missing is True
138 ValueError if file is missing and allow_missing is False
140 if not indir or fname[:1] == '/':
142 for dirname in indir:
143 pathname = os.path.join(dirname, fname)
144 if os.path.exists(pathname):
149 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
150 (fname, ','.join(indir), os.getcwd()))
152 def GetInputFilenameGlob(pattern):
153 """Return a list of filenames for use as input.
156 pattern: Filename pattern to search for
159 A list of matching files in all input directories
162 return glob.glob(fname)
164 for dirname in indir:
165 pathname = os.path.join(dirname, pattern)
166 files += glob.glob(pathname)
169 def Align(pos, align):
172 pos = (pos + mask) & ~mask
175 def NotPowerOfTwo(num):
176 return num and (num & (num - 1))
178 def SetToolPaths(toolpaths):
179 """Set the path to search for tools
182 toolpaths: List of paths to search for tools executed by Run()
184 global tool_search_paths
186 tool_search_paths = toolpaths
188 def PathHasFile(path_spec, fname):
189 """Check if a given filename is in the PATH
192 path_spec: Value of PATH variable to check
193 fname: Filename to check
196 True if found, False if not
198 for dir in path_spec.split(':'):
199 if os.path.exists(os.path.join(dir, fname)):
203 def GetHostCompileTool(name):
204 """Get the host-specific version for a compile tool
206 This checks the environment variables that specify which version of
207 the tool should be used (e.g. ${HOSTCC}).
209 The following table lists the host-specific versions of the tools
210 this function resolves to:
212 Compile Tool | Host version
213 --------------+----------------
223 objcopy | ${HOSTOBJCOPY}
224 objdump | ${HOSTOBJDUMP}
228 name: Command name to run
231 host_name: Exact command name to run instead
232 extra_args: List of extra arguments to pass
236 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
237 'objcopy', 'objdump', 'dtc'):
238 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
240 host_name, *host_args = env.get('HOSTCXX', '').split(' ')
243 return host_name, extra_args
246 def GetTargetCompileTool(name, cross_compile=None):
247 """Get the target-specific version for a compile tool
249 This first checks the environment variables that specify which
250 version of the tool should be used (e.g. ${CC}). If those aren't
251 specified, it checks the CROSS_COMPILE variable as a prefix for the
252 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
254 The following table lists the target-specific versions of the tools
255 this function resolves to:
257 Compile Tool | First choice | Second choice
258 --------------+----------------+----------------------------
259 as | ${AS} | ${CROSS_COMPILE}as
260 ld | ${LD} | ${CROSS_COMPILE}ld.bfd
261 | | or ${CROSS_COMPILE}ld
262 cc | ${CC} | ${CROSS_COMPILE}gcc
263 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
264 c++ | ${CXX} | ${CROSS_COMPILE}g++
265 ar | ${AR} | ${CROSS_COMPILE}ar
266 nm | ${NM} | ${CROSS_COMPILE}nm
267 ldr | ${LDR} | ${CROSS_COMPILE}ldr
268 strip | ${STRIP} | ${CROSS_COMPILE}strip
269 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
270 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
271 dtc | ${DTC} | (no CROSS_COMPILE version)
274 name: Command name to run
277 target_name: Exact command name to run instead
278 extra_args: List of extra arguments to pass
280 env = dict(os.environ)
284 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
285 'objcopy', 'objdump', 'dtc'):
286 target_name, *extra_args = env.get(name.upper(), '').split(' ')
288 target_name, *extra_args = env.get('CXX', '').split(' ')
291 return target_name, extra_args
293 if cross_compile is None:
294 cross_compile = env.get('CROSS_COMPILE', '')
296 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
297 target_name = cross_compile + name
300 if Run(cross_compile + 'ld.bfd', '-v'):
301 target_name = cross_compile + 'ld.bfd'
303 target_name = cross_compile + 'ld'
305 target_name = cross_compile + 'gcc'
307 target_name = cross_compile + 'gcc'
310 target_name = cross_compile + 'g++'
313 return target_name, extra_args
315 def Run(name, *args, **kwargs):
316 """Run a tool with some arguments
318 This runs a 'tool', which is a program used by binman to process files and
319 perhaps produce some output. Tools can be located on the PATH or in a
323 name: Command name to run
324 args: Arguments to the tool
325 for_host: True to resolve the command to the version for the host
326 for_target: False to run the command as-is, without resolving it
327 to the version for the compile target
333 binary = kwargs.get('binary')
334 for_host = kwargs.get('for_host', False)
335 for_target = kwargs.get('for_target', not for_host)
337 if tool_search_paths:
338 env = dict(os.environ)
339 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
341 name, extra_args = GetTargetCompileTool(name)
342 args = tuple(extra_args) + args
344 name, extra_args = GetHostCompileTool(name)
345 args = tuple(extra_args) + args
346 name = os.path.expanduser(name) # Expand paths containing ~
347 all_args = (name,) + args
348 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
349 env=env, raise_on_error=False, binary=binary)
350 if result.return_code:
351 raise Exception("Error %d running '%s': %s" %
352 (result.return_code,' '.join(all_args),
356 if env and not PathHasFile(env['PATH'], name):
357 msg = "Please install tool '%s'" % name
358 package = packages.get(name)
360 msg += " (e.g. from package '%s')" % package
361 raise ValueError(msg)
365 """Resolve a file path to an absolute path.
367 If fname starts with ##/ and chroot is available, ##/ gets replaced with
368 the chroot path. If chroot is not available, this file name can not be
369 resolved, `None' is returned.
371 If fname is not prepended with the above prefix, and is not an existing
372 file, the actual file name is retrieved from the passed in string and the
373 search_paths directories (if any) are searched to for the file. If found -
374 the path to the found file is returned, `None' is returned otherwise.
377 fname: a string, the path to resolve.
380 Absolute path to the file or None if not found.
382 if fname.startswith('##/'):
384 fname = os.path.join(chroot_path, fname[3:])
388 # Search for a pathname that exists, and return it if found
389 if fname and not os.path.exists(fname):
390 for path in search_paths:
391 pathname = os.path.join(path, os.path.basename(fname))
392 if os.path.exists(pathname):
395 # If not found, just return the standard, unchanged path
398 def ReadFile(fname, binary=True):
399 """Read and return the contents of a file.
402 fname: path to filename to read, where ## signifiies the chroot.
405 data read from file, as a string.
407 with open(Filename(fname), binary and 'rb' or 'r') as fd:
409 #self._out.Info("Read file '%s' size %d (%#0x)" %
410 #(fname, len(data), len(data)))
413 def WriteFile(fname, data, binary=True):
414 """Write data into a file.
417 fname: path to filename to write
418 data: data to write to file, as a string
420 #self._out.Info("Write file '%s' size %d (%#0x)" %
421 #(fname, len(data), len(data)))
422 with open(Filename(fname), binary and 'wb' or 'w') as fd:
425 def GetBytes(byte, size):
426 """Get a string of bytes of a given size
429 byte: Numeric byte value to use
430 size: Size of bytes/string to return
433 A bytes type with 'byte' repeated 'size' times
435 return bytes([byte]) * size
438 """Convert a str type into a bytes type
441 string: string to convert
446 return string.encode('utf-8')
449 """Convert a bytes type into a str type
452 bval: bytes value to convert
455 Python 3: A bytes type
456 Python 2: A string type
458 return bval.decode('utf-8')
460 def Compress(indata, algo, with_header=True):
461 """Compress some data using a given algorithm
463 Note that for lzma this uses an old version of the algorithm, not that
466 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
467 directory to be previously set up, by calling PrepareOutputDir().
469 Care is taken to use unique temporary files so that this function can be
470 called from multiple threads.
473 indata: Input data to compress
474 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
481 fname = tempfile.NamedTemporaryFile(prefix='%s.comp.tmp' % algo,
483 WriteFile(fname, indata)
485 data = Run('lz4', '--no-frame-crc', '-B4', '-5', '-c', fname,
487 # cbfstool uses a very old version of lzma
489 outfname = tempfile.NamedTemporaryFile(prefix='%s.comp.otmp' % algo,
491 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8')
492 data = ReadFile(outfname)
494 data = Run('gzip', '-c', fname, binary=True)
496 raise ValueError("Unknown algorithm '%s'" % algo)
498 hdr = struct.pack('<I', len(data))
502 def Decompress(indata, algo, with_header=True):
503 """Decompress some data using a given algorithm
505 Note that for lzma this uses an old version of the algorithm, not that
508 This requires 'lz4' and 'lzma_alone' tools. It also requires an output
509 directory to be previously set up, by calling PrepareOutputDir().
512 indata: Input data to decompress
513 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma')
521 data_len = struct.unpack('<I', indata[:4])[0]
522 indata = indata[4:4 + data_len]
523 fname = GetOutputFilename('%s.decomp.tmp' % algo)
524 with open(fname, 'wb') as fd:
527 data = Run('lz4', '-dc', fname, binary=True)
529 outfname = GetOutputFilename('%s.decomp.otmp' % algo)
530 Run('lzma_alone', 'd', fname, outfname)
531 data = ReadFile(outfname, binary=True)
533 data = Run('gzip', '-cd', fname, binary=True)
535 raise ValueError("Unknown algorithm '%s'" % algo)
538 CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5)
541 CMD_CREATE: 'create',
542 CMD_DELETE: 'delete',
544 CMD_REPLACE: 'replace',
545 CMD_EXTRACT: 'extract',
548 def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None):
549 """Run ifwitool with the given arguments:
552 ifwi_file: IFWI file to operation on
553 cmd: Command to execute (CMD_...)
554 fname: Filename of file to add/replace/extract/create (None for
556 subpart: Name of sub-partition to operation on (None for CMD_CREATE)
557 entry_name: Name of directory entry to operate on, or None if none
559 args = ['ifwitool', ifwi_file]
560 args.append(IFWITOOL_CMDS[cmd])
562 args += ['-f', fname]
564 args += ['-n', subpart]
566 args += ['-d', '-e', entry_name]
570 """Convert an integer value (or None) to a string
573 hex value, or 'None' if the value is None
575 return 'None' if val is None else '%#x' % val
578 """Return the size of an object in hex
581 hex value of size, or 'None' if the value is None
583 return 'None' if val is None else '%#x' % len(val)