1 # SPDX-License-Identifier: GPL-2.0+
3 # Copyright (c) 2016 Google, Inc
14 from patman import command
15 from patman import tout
17 # Output directly (generally this is temporary)
20 # True to keep the output directory around after exiting
21 preserve_outdir = False
23 # Path to the Chrome OS chroot, if we know it
26 # Search paths to use for filename(), used to find files
29 tool_search_paths = []
31 # Tools and the packages that contain them, on debian
36 # List of paths to use when looking for an input file
39 def prepare_output_dir(dirname, preserve=False):
40 """Select an output directory, ensuring it exists.
42 This either creates a temporary directory or checks that the one supplied
43 by the user is valid. For a temporary directory, it makes a note to
44 remove it later if required.
47 dirname: a string, name of the output directory to use to store
48 intermediate and output files. If is None - create a temporary
50 preserve: a Boolean. If outdir above is None and preserve is False, the
51 created temporary directory will be destroyed on exit.
54 OSError: If it cannot create the output directory.
56 global outdir, preserve_outdir
58 preserve_outdir = dirname or preserve
61 if not os.path.isdir(outdir):
64 except OSError as err:
65 raise CmdError("Cannot make output directory '%s': '%s'" %
66 (outdir, err.strerror))
67 tout.Debug("Using output directory '%s'" % outdir)
69 outdir = tempfile.mkdtemp(prefix='binman.')
70 tout.Debug("Using temporary directory '%s'" % outdir)
72 def _remove_output_dir():
76 tout.Debug("Deleted temporary directory '%s'" % outdir)
79 def finalise_output_dir():
80 global outdir, preserve_outdir
82 """Tidy up: delete output directory if temporary and not preserved."""
83 if outdir and not preserve_outdir:
87 def get_output_filename(fname):
88 """Return a filename within the output directory.
91 fname: Filename to use for new file
94 The full path of the filename, within the output directory
96 return os.path.join(outdir, fname)
99 """Return the current output directory
102 str: The output directory
106 def _finalise_for_test():
107 """Remove the output directory (for use by tests)"""
114 def set_input_dirs(dirname):
115 """Add a list of input directories, where input files are kept.
118 dirname: a list of paths to input directories to use for obtaining
119 files needed by binman to place in the image.
124 tout.Debug("Using input directories %s" % indir)
126 def get_input_filename(fname, allow_missing=False):
127 """Return a filename for use as input.
130 fname: Filename to use for new file
131 allow_missing: True if the filename can be missing
134 fname, if indir is None;
135 full path of the filename, within the input directory;
136 None, if file is missing and allow_missing is True
139 ValueError if file is missing and allow_missing is False
141 if not indir or fname[:1] == '/':
143 for dirname in indir:
144 pathname = os.path.join(dirname, fname)
145 if os.path.exists(pathname):
150 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" %
151 (fname, ','.join(indir), os.getcwd()))
153 def get_input_filename_glob(pattern):
154 """Return a list of filenames for use as input.
157 pattern: Filename pattern to search for
160 A list of matching files in all input directories
163 return glob.glob(fname)
165 for dirname in indir:
166 pathname = os.path.join(dirname, pattern)
167 files += glob.glob(pathname)
170 def align(pos, align):
173 pos = (pos + mask) & ~mask
176 def not_power_of_two(num):
177 return num and (num & (num - 1))
179 def set_tool_paths(toolpaths):
180 """Set the path to search for tools
183 toolpaths: List of paths to search for tools executed by run()
185 global tool_search_paths
187 tool_search_paths = toolpaths
189 def path_has_file(path_spec, fname):
190 """Check if a given filename is in the PATH
193 path_spec: Value of PATH variable to check
194 fname: Filename to check
197 True if found, False if not
199 for dir in path_spec.split(':'):
200 if os.path.exists(os.path.join(dir, fname)):
204 def get_host_compile_tool(name):
205 """Get the host-specific version for a compile tool
207 This checks the environment variables that specify which version of
208 the tool should be used (e.g. ${HOSTCC}).
210 The following table lists the host-specific versions of the tools
211 this function resolves to:
213 Compile Tool | Host version
214 --------------+----------------
224 objcopy | ${HOSTOBJCOPY}
225 objdump | ${HOSTOBJDUMP}
229 name: Command name to run
232 host_name: Exact command name to run instead
233 extra_args: List of extra arguments to pass
237 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
238 'objcopy', 'objdump', 'dtc'):
239 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ')
241 host_name, *host_args = env.get('HOSTCXX', '').split(' ')
244 return host_name, extra_args
247 def get_target_compile_tool(name, cross_compile=None):
248 """Get the target-specific version for a compile tool
250 This first checks the environment variables that specify which
251 version of the tool should be used (e.g. ${CC}). If those aren't
252 specified, it checks the CROSS_COMPILE variable as a prefix for the
253 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc).
255 The following table lists the target-specific versions of the tools
256 this function resolves to:
258 Compile Tool | First choice | Second choice
259 --------------+----------------+----------------------------
260 as | ${AS} | ${CROSS_COMPILE}as
261 ld | ${LD} | ${CROSS_COMPILE}ld.bfd
262 | | or ${CROSS_COMPILE}ld
263 cc | ${CC} | ${CROSS_COMPILE}gcc
264 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E
265 c++ | ${CXX} | ${CROSS_COMPILE}g++
266 ar | ${AR} | ${CROSS_COMPILE}ar
267 nm | ${NM} | ${CROSS_COMPILE}nm
268 ldr | ${LDR} | ${CROSS_COMPILE}ldr
269 strip | ${STRIP} | ${CROSS_COMPILE}strip
270 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy
271 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump
272 dtc | ${DTC} | (no CROSS_COMPILE version)
275 name: Command name to run
278 target_name: Exact command name to run instead
279 extra_args: List of extra arguments to pass
281 env = dict(os.environ)
285 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip',
286 'objcopy', 'objdump', 'dtc'):
287 target_name, *extra_args = env.get(name.upper(), '').split(' ')
289 target_name, *extra_args = env.get('CXX', '').split(' ')
292 return target_name, extra_args
294 if cross_compile is None:
295 cross_compile = env.get('CROSS_COMPILE', '')
297 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'):
298 target_name = cross_compile + name
301 if run(cross_compile + 'ld.bfd', '-v'):
302 target_name = cross_compile + 'ld.bfd'
304 target_name = cross_compile + 'ld'
306 target_name = cross_compile + 'gcc'
308 target_name = cross_compile + 'gcc'
311 target_name = cross_compile + 'g++'
314 return target_name, extra_args
316 def get_env_with_path():
317 """Get an updated environment with the PATH variable set correctly
319 If there are any search paths set, these need to come first in the PATH so
320 that these override any other version of the tools.
323 dict: New environment with PATH updated, or None if there are not search
326 if tool_search_paths:
327 env = dict(os.environ)
328 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH']
331 def run_result(name, *args, **kwargs):
332 """Run a tool with some arguments
334 This runs a 'tool', which is a program used by binman to process files and
335 perhaps produce some output. Tools can be located on the PATH or in a
339 name: Command name to run
340 args: Arguments to the tool
341 for_host: True to resolve the command to the version for the host
342 for_target: False to run the command as-is, without resolving it
343 to the version for the compile target
344 raise_on_error: Raise an error if the command fails (True by default)
350 binary = kwargs.get('binary')
351 for_host = kwargs.get('for_host', False)
352 for_target = kwargs.get('for_target', not for_host)
353 raise_on_error = kwargs.get('raise_on_error', True)
354 env = get_env_with_path()
356 name, extra_args = get_target_compile_tool(name)
357 args = tuple(extra_args) + args
359 name, extra_args = get_host_compile_tool(name)
360 args = tuple(extra_args) + args
361 name = os.path.expanduser(name) # Expand paths containing ~
362 all_args = (name,) + args
363 result = command.RunPipe([all_args], capture=True, capture_stderr=True,
364 env=env, raise_on_error=False, binary=binary)
365 if result.return_code:
367 raise ValueError("Error %d running '%s': %s" %
368 (result.return_code,' '.join(all_args),
369 result.stderr or result.stdout))
372 if env and not path_has_file(env['PATH'], name):
373 msg = "Please install tool '%s'" % name
374 package = packages.get(name)
376 msg += " (e.g. from package '%s')" % package
377 raise ValueError(msg)
381 """Search the current path for a tool
383 This uses both PATH and any value from set_tool_paths() to search for a tool
386 name (str): Name of tool to locate
389 str: Full path to tool if found, else None
391 name = os.path.expanduser(name) # Expand paths containing ~
393 pathvar = os.environ.get('PATH')
395 paths = pathvar.split(':')
396 if tool_search_paths:
397 paths += tool_search_paths
399 fname = os.path.join(path, name)
400 if os.path.isfile(fname) and os.access(fname, os.X_OK):
403 def run(name, *args, **kwargs):
404 """Run a tool with some arguments
406 This runs a 'tool', which is a program used by binman to process files and
407 perhaps produce some output. Tools can be located on the PATH or in a
411 name: Command name to run
412 args: Arguments to the tool
413 for_host: True to resolve the command to the version for the host
414 for_target: False to run the command as-is, without resolving it
415 to the version for the compile target
420 result = run_result(name, *args, **kwargs)
421 if result is not None:
425 """Resolve a file path to an absolute path.
427 If fname starts with ##/ and chroot is available, ##/ gets replaced with
428 the chroot path. If chroot is not available, this file name can not be
429 resolved, `None' is returned.
431 If fname is not prepended with the above prefix, and is not an existing
432 file, the actual file name is retrieved from the passed in string and the
433 search_paths directories (if any) are searched to for the file. If found -
434 the path to the found file is returned, `None' is returned otherwise.
437 fname: a string, the path to resolve.
440 Absolute path to the file or None if not found.
442 if fname.startswith('##/'):
444 fname = os.path.join(chroot_path, fname[3:])
448 # Search for a pathname that exists, and return it if found
449 if fname and not os.path.exists(fname):
450 for path in search_paths:
451 pathname = os.path.join(path, os.path.basename(fname))
452 if os.path.exists(pathname):
455 # If not found, just return the standard, unchanged path
458 def read_file(fname, binary=True):
459 """Read and return the contents of a file.
462 fname: path to filename to read, where ## signifiies the chroot.
465 data read from file, as a string.
467 with open(filename(fname), binary and 'rb' or 'r') as fd:
469 #self._out.Info("Read file '%s' size %d (%#0x)" %
470 #(fname, len(data), len(data)))
473 def write_file(fname, data, binary=True):
474 """Write data into a file.
477 fname: path to filename to write
478 data: data to write to file, as a string
480 #self._out.Info("Write file '%s' size %d (%#0x)" %
481 #(fname, len(data), len(data)))
482 with open(filename(fname), binary and 'wb' or 'w') as fd:
485 def get_bytes(byte, size):
486 """Get a string of bytes of a given size
489 byte: Numeric byte value to use
490 size: Size of bytes/string to return
493 A bytes type with 'byte' repeated 'size' times
495 return bytes([byte]) * size
497 def to_bytes(string):
498 """Convert a str type into a bytes type
501 string: string to convert
506 return string.encode('utf-8')
509 """Convert a bytes type into a str type
512 bval: bytes value to convert
515 Python 3: A bytes type
516 Python 2: A string type
518 return bval.decode('utf-8')
521 """Convert an integer value (or None) to a string
524 hex value, or 'None' if the value is None
526 return 'None' if val is None else '%#x' % val
528 def to_hex_size(val):
529 """Return the size of an object in hex
532 hex value of size, or 'None' if the value is None
534 return 'None' if val is None else '%#x' % len(val)
536 def print_full_help(fname):
537 """Print the full help message for a tool using an appropriate pager.
540 fname: Path to a file containing the full help message
542 pager = shlex.split(os.getenv('PAGER', ''))
544 lesspath = shutil.which('less')
545 pager = [lesspath] if lesspath else None
548 command.Run(*pager, fname)
550 def download(url, tmpdir_pattern='.patman'):
551 """Download a file to a temporary directory
554 url (str): URL to download
555 tmpdir_pattern (str): pattern to use for the temporary directory
559 Full path to the downloaded archive file in that directory,
560 or None if there was an error while downloading
561 Temporary directory name
563 print('- downloading: %s' % url)
564 leaf = url.split('/')[-1]
565 tmpdir = tempfile.mkdtemp(tmpdir_pattern)
566 response = urllib.request.urlopen(url)
567 fname = os.path.join(tmpdir, leaf)
568 fd = open(fname, 'wb')
569 meta = response.info()
570 size = int(meta.get('Content-Length'))
575 # Read the file in chunks and show progress as we go
577 buffer = response.read(block_size)
579 print(chr(8) * (len(status) + 1), '\r', end=' ')
584 status = r'%10d MiB [%3d%%]' % (done // 1024 // 1024,
586 status = status + chr(8) * (len(status) + 1)
587 print(status, end=' ')
593 print('Error, failed to download')