1# SPDX-License-Identifier: GPL-2.0+ 2# 3# Copyright (c) 2016 Google, Inc 4# 5 6import glob 7import os 8import shlex 9import shutil 10import sys 11import tempfile 12import urllib.request 13 14from u_boot_pylib import command 15from u_boot_pylib import tout 16 17# Output directly (generally this is temporary) 18outdir = None 19 20# True to keep the output directory around after exiting 21preserve_outdir = False 22 23# Path to the Chrome OS chroot, if we know it 24chroot_path = None 25 26# Search paths to use for filename(), used to find files 27search_paths = [] 28 29tool_search_paths = [] 30 31# Tools and the packages that contain them, on debian 32packages = { 33 'lz4': 'liblz4-tool', 34 } 35 36# List of paths to use when looking for an input file 37indir = [] 38 39def prepare_output_dir(dirname, preserve=False): 40 """Select an output directory, ensuring it exists. 41 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. 45 46 Args: 47 dirname: a string, name of the output directory to use to store 48 intermediate and output files. If is None - create a temporary 49 directory. 50 preserve: a Boolean. If outdir above is None and preserve is False, the 51 created temporary directory will be destroyed on exit. 52 53 Raises: 54 OSError: If it cannot create the output directory. 55 """ 56 global outdir, preserve_outdir 57 58 preserve_outdir = dirname or preserve 59 if dirname: 60 outdir = dirname 61 if not os.path.isdir(outdir): 62 try: 63 os.makedirs(outdir) 64 except OSError as err: 65 raise ValueError( 66 f"Cannot make output directory 'outdir': 'err.strerror'") 67 tout.debug("Using output directory '%s'" % outdir) 68 else: 69 outdir = tempfile.mkdtemp(prefix='binman.') 70 tout.debug("Using temporary directory '%s'" % outdir) 71 72def _remove_output_dir(): 73 global outdir 74 75 shutil.rmtree(outdir) 76 tout.debug("Deleted temporary directory '%s'" % outdir) 77 outdir = None 78 79def finalise_output_dir(): 80 global outdir, preserve_outdir 81 82 """Tidy up: delete output directory if temporary and not preserved.""" 83 if outdir and not preserve_outdir: 84 _remove_output_dir() 85 outdir = None 86 87def get_output_filename(fname): 88 """Return a filename within the output directory. 89 90 Args: 91 fname: Filename to use for new file 92 93 Returns: 94 The full path of the filename, within the output directory 95 """ 96 return os.path.join(outdir, fname) 97 98def get_output_dir(): 99 """Return the current output directory 100 101 Returns: 102 str: The output directory 103 """ 104 return outdir 105 106def _finalise_for_test(): 107 """Remove the output directory (for use by tests)""" 108 global outdir 109 110 if outdir: 111 _remove_output_dir() 112 outdir = None 113 114def set_input_dirs(dirname): 115 """Add a list of input directories, where input files are kept. 116 117 Args: 118 dirname: a list of paths to input directories to use for obtaining 119 files needed by binman to place in the image. 120 """ 121 global indir 122 123 indir = dirname 124 tout.debug("Using input directories %s" % indir) 125 126def append_input_dirs(dirname): 127 """Append a list of input directories to the current list of input 128 directories 129 130 Args: 131 dirname: a list of paths to input directories to use for obtaining 132 files needed by binman to place in the image. 133 """ 134 global indir 135 136 for dir in dirname: 137 if dirname not in indir: 138 indir.append(dirname) 139 140 tout.debug("Updated input directories %s" % indir) 141 142def get_input_filename(fname, allow_missing=False): 143 """Return a filename for use as input. 144 145 Args: 146 fname: Filename to use for new file 147 allow_missing: True if the filename can be missing 148 149 Returns: 150 fname, if indir is None; 151 full path of the filename, within the input directory; 152 None, if file is missing and allow_missing is True 153 154 Raises: 155 ValueError if file is missing and allow_missing is False 156 """ 157 if not indir or fname[:1] == '/': 158 return fname 159 for dirname in indir: 160 pathname = os.path.join(dirname, fname) 161 if os.path.exists(pathname): 162 return pathname 163 164 if allow_missing: 165 return None 166 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" % 167 (fname, ','.join(indir), os.getcwd())) 168 169def get_input_filename_glob(pattern): 170 """Return a list of filenames for use as input. 171 172 Args: 173 pattern: Filename pattern to search for 174 175 Returns: 176 A list of matching files in all input directories 177 """ 178 if not indir: 179 return glob.glob(pattern) 180 files = [] 181 for dirname in indir: 182 pathname = os.path.join(dirname, pattern) 183 files += glob.glob(pathname) 184 return sorted(files) 185 186def align(pos, align): 187 if align: 188 mask = align - 1 189 pos = (pos + mask) & ~mask 190 return pos 191 192def not_power_of_two(num): 193 return num and (num & (num - 1)) 194 195def set_tool_paths(toolpaths): 196 """Set the path to search for tools 197 198 Args: 199 toolpaths: List of paths to search for tools executed by run() 200 """ 201 global tool_search_paths 202 203 tool_search_paths = toolpaths 204 205def path_has_file(path_spec, fname): 206 """Check if a given filename is in the PATH 207 208 Args: 209 path_spec: Value of PATH variable to check 210 fname: Filename to check 211 212 Returns: 213 True if found, False if not 214 """ 215 for dir in path_spec.split(':'): 216 if os.path.exists(os.path.join(dir, fname)): 217 return True 218 return False 219 220def get_host_compile_tool(env, name): 221 """Get the host-specific version for a compile tool 222 223 This checks the environment variables that specify which version of 224 the tool should be used (e.g. ${HOSTCC}). 225 226 The following table lists the host-specific versions of the tools 227 this function resolves to: 228 229 Compile Tool | Host version 230 --------------+---------------- 231 as | ${HOSTAS} 232 ld | ${HOSTLD} 233 cc | ${HOSTCC} 234 cpp | ${HOSTCPP} 235 c++ | ${HOSTCXX} 236 ar | ${HOSTAR} 237 nm | ${HOSTNM} 238 ldr | ${HOSTLDR} 239 strip | ${HOSTSTRIP} 240 objcopy | ${HOSTOBJCOPY} 241 objdump | ${HOSTOBJDUMP} 242 dtc | ${HOSTDTC} 243 244 Args: 245 name: Command name to run 246 247 Returns: 248 host_name: Exact command name to run instead 249 extra_args: List of extra arguments to pass 250 """ 251 host_name = None 252 extra_args = [] 253 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip', 254 'objcopy', 'objdump', 'dtc'): 255 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ') 256 elif name == 'c++': 257 host_name, *host_args = env.get('HOSTCXX', '').split(' ') 258 259 if host_name: 260 return host_name, extra_args 261 return name, [] 262 263def get_target_compile_tool(name, cross_compile=None): 264 """Get the target-specific version for a compile tool 265 266 This first checks the environment variables that specify which 267 version of the tool should be used (e.g. ${CC}). If those aren't 268 specified, it checks the CROSS_COMPILE variable as a prefix for the 269 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc). 270 271 The following table lists the target-specific versions of the tools 272 this function resolves to: 273 274 Compile Tool | First choice | Second choice 275 --------------+----------------+---------------------------- 276 as | ${AS} | ${CROSS_COMPILE}as 277 ld | ${LD} | ${CROSS_COMPILE}ld.bfd 278 | | or ${CROSS_COMPILE}ld 279 cc | ${CC} | ${CROSS_COMPILE}gcc 280 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E 281 c++ | ${CXX} | ${CROSS_COMPILE}g++ 282 ar | ${AR} | ${CROSS_COMPILE}ar 283 nm | ${NM} | ${CROSS_COMPILE}nm 284 ldr | ${LDR} | ${CROSS_COMPILE}ldr 285 strip | ${STRIP} | ${CROSS_COMPILE}strip 286 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy 287 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump 288 dtc | ${DTC} | (no CROSS_COMPILE version) 289 290 Args: 291 name: Command name to run 292 293 Returns: 294 target_name: Exact command name to run instead 295 extra_args: List of extra arguments to pass 296 """ 297 env = dict(os.environ) 298 299 target_name = None 300 extra_args = [] 301 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip', 302 'objcopy', 'objdump', 'dtc'): 303 target_name, *extra_args = env.get(name.upper(), '').split(' ') 304 elif name == 'c++': 305 target_name, *extra_args = env.get('CXX', '').split(' ') 306 307 if target_name: 308 return target_name, extra_args 309 310 if cross_compile is None: 311 cross_compile = env.get('CROSS_COMPILE', '') 312 313 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'): 314 target_name = cross_compile + name 315 elif name == 'ld': 316 try: 317 if run(cross_compile + 'ld.bfd', '-v'): 318 target_name = cross_compile + 'ld.bfd' 319 except: 320 target_name = cross_compile + 'ld' 321 elif name == 'cc': 322 target_name = cross_compile + 'gcc' 323 elif name == 'cpp': 324 target_name = cross_compile + 'gcc' 325 extra_args = ['-E'] 326 elif name == 'c++': 327 target_name = cross_compile + 'g++' 328 else: 329 target_name = name 330 return target_name, extra_args 331 332def get_env_with_path(): 333 """Get an updated environment with the PATH variable set correctly 334 335 If there are any search paths set, these need to come first in the PATH so 336 that these override any other version of the tools. 337 338 Returns: 339 dict: New environment with PATH updated, or None if there are not search 340 paths 341 """ 342 if tool_search_paths: 343 env = dict(os.environ) 344 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH'] 345 return env 346 347def run_result(name, *args, **kwargs): 348 """Run a tool with some arguments 349 350 This runs a 'tool', which is a program used by binman to process files and 351 perhaps produce some output. Tools can be located on the PATH or in a 352 search path. 353 354 Args: 355 name: Command name to run 356 args: Arguments to the tool 357 for_host: True to resolve the command to the version for the host 358 for_target: False to run the command as-is, without resolving it 359 to the version for the compile target 360 raise_on_error: Raise an error if the command fails (True by default) 361 362 Returns: 363 CommandResult object 364 """ 365 try: 366 binary = kwargs.get('binary') 367 for_host = kwargs.get('for_host', False) 368 for_target = kwargs.get('for_target', not for_host) 369 raise_on_error = kwargs.get('raise_on_error', True) 370 env = get_env_with_path() 371 if for_target: 372 name, extra_args = get_target_compile_tool(name) 373 args = tuple(extra_args) + args 374 elif for_host: 375 name, extra_args = get_host_compile_tool(env, name) 376 args = tuple(extra_args) + args 377 name = os.path.expanduser(name) # Expand paths containing ~ 378 all_args = (name,) + args 379 result = command.run_one(*all_args, capture=True, capture_stderr=True, 380 env=env, raise_on_error=False, binary=binary) 381 if result.return_code: 382 if raise_on_error: 383 raise ValueError("Error %d running '%s': %s" % 384 (result.return_code,' '.join(all_args), 385 result.stderr or result.stdout)) 386 return result 387 except ValueError: 388 if env and not path_has_file(env['PATH'], name): 389 msg = "Please install tool '%s'" % name 390 package = packages.get(name) 391 if package: 392 msg += " (e.g. from package '%s')" % package 393 raise ValueError(msg) 394 raise 395 396def tool_find(name): 397 """Search the current path for a tool 398 399 This uses both PATH and any value from set_tool_paths() to search for a tool 400 401 Args: 402 name (str): Name of tool to locate 403 404 Returns: 405 str: Full path to tool if found, else None 406 """ 407 name = os.path.expanduser(name) # Expand paths containing ~ 408 paths = [] 409 pathvar = os.environ.get('PATH') 410 if pathvar: 411 paths = pathvar.split(':') 412 if tool_search_paths: 413 paths += tool_search_paths 414 for path in paths: 415 fname = os.path.join(path, name) 416 if os.path.isfile(fname) and os.access(fname, os.X_OK): 417 return fname 418 419def run(name, *args, **kwargs): 420 """Run a tool with some arguments 421 422 This runs a 'tool', which is a program used by binman to process files and 423 perhaps produce some output. Tools can be located on the PATH or in a 424 search path. 425 426 Args: 427 name: Command name to run 428 args: Arguments to the tool 429 for_host: True to resolve the command to the version for the host 430 for_target: False to run the command as-is, without resolving it 431 to the version for the compile target 432 433 Returns: 434 CommandResult object 435 """ 436 result = run_result(name, *args, **kwargs) 437 if result is not None: 438 return result.stdout 439 440def filename(fname): 441 """Resolve a file path to an absolute path. 442 443 If fname starts with ##/ and chroot is available, ##/ gets replaced with 444 the chroot path. If chroot is not available, this file name can not be 445 resolved, `None' is returned. 446 447 If fname is not prepended with the above prefix, and is not an existing 448 file, the actual file name is retrieved from the passed in string and the 449 search_paths directories (if any) are searched to for the file. If found - 450 the path to the found file is returned, `None' is returned otherwise. 451 452 Args: 453 fname: a string, the path to resolve. 454 455 Returns: 456 Absolute path to the file or None if not found. 457 """ 458 if fname.startswith('##/'): 459 if chroot_path: 460 fname = os.path.join(chroot_path, fname[3:]) 461 else: 462 return None 463 464 # Search for a pathname that exists, and return it if found 465 if fname and not os.path.exists(fname): 466 for path in search_paths: 467 pathname = os.path.join(path, os.path.basename(fname)) 468 if os.path.exists(pathname): 469 return pathname 470 471 # If not found, just return the standard, unchanged path 472 return fname 473 474def read_file(fname, binary=True): 475 """Read and return the contents of a file. 476 477 Args: 478 fname: path to filename to read, where ## signifiies the chroot. 479 480 Returns: 481 data read from file, as a string. 482 """ 483 with open(filename(fname), binary and 'rb' or 'r') as fd: 484 data = fd.read() 485 #self._out.Info("Read file '%s' size %d (%#0x)" % 486 #(fname, len(data), len(data))) 487 return data 488 489def write_file(fname, data, binary=True): 490 """Write data into a file. 491 492 Args: 493 fname: path to filename to write 494 data: data to write to file, as a string 495 """ 496 #self._out.Info("Write file '%s' size %d (%#0x)" % 497 #(fname, len(data), len(data))) 498 with open(filename(fname), binary and 'wb' or 'w') as fd: 499 fd.write(data) 500 501def get_bytes(byte, size): 502 """Get a string of bytes of a given size 503 504 Args: 505 byte: Numeric byte value to use 506 size: Size of bytes/string to return 507 508 Returns: 509 A bytes type with 'byte' repeated 'size' times 510 """ 511 return bytes([byte]) * size 512 513def to_bytes(string): 514 """Convert a str type into a bytes type 515 516 Args: 517 string: string to convert 518 519 Returns: 520 A bytes type 521 """ 522 return string.encode('utf-8') 523 524def to_string(bval): 525 """Convert a bytes type into a str type 526 527 Args: 528 bval: bytes value to convert 529 530 Returns: 531 Python 3: A bytes type 532 Python 2: A string type 533 """ 534 return bval.decode('utf-8') 535 536def to_hex(val): 537 """Convert an integer value (or None) to a string 538 539 Returns: 540 hex value, or 'None' if the value is None 541 """ 542 return 'None' if val is None else '%#x' % val 543 544def to_hex_size(val): 545 """Return the size of an object in hex 546 547 Returns: 548 hex value of size, or 'None' if the value is None 549 """ 550 return 'None' if val is None else '%#x' % len(val) 551 552def print_full_help(fname): 553 """Print the full help message for a tool using an appropriate pager. 554 555 Args: 556 fname: Path to a file containing the full help message 557 """ 558 pager = shlex.split(os.getenv('PAGER', '')) 559 if not pager: 560 lesspath = shutil.which('less') 561 pager = [lesspath] if lesspath else None 562 if not pager: 563 pager = ['more'] 564 command.run(*pager, fname) 565 566def download(url, tmpdir_pattern='.patman'): 567 """Download a file to a temporary directory 568 569 Args: 570 url (str): URL to download 571 tmpdir_pattern (str): pattern to use for the temporary directory 572 573 Returns: 574 Tuple: 575 Full path to the downloaded archive file in that directory, 576 or None if there was an error while downloading 577 Temporary directory name 578 """ 579 print('- downloading: %s' % url) 580 leaf = url.split('/')[-1] 581 tmpdir = tempfile.mkdtemp(tmpdir_pattern) 582 response = urllib.request.urlopen(url) 583 fname = os.path.join(tmpdir, leaf) 584 fd = open(fname, 'wb') 585 meta = response.info() 586 size = int(meta.get('Content-Length')) 587 done = 0 588 block_size = 1 << 16 589 status = '' 590 591 # Read the file in chunks and show progress as we go 592 while True: 593 buffer = response.read(block_size) 594 if not buffer: 595 print(chr(8) * (len(status) + 1), '\r', end=' ') 596 break 597 598 done += len(buffer) 599 fd.write(buffer) 600 status = r'%10d MiB [%3d%%]' % (done // 1024 // 1024, 601 done * 100 // size) 602 status = status + chr(8) * (len(status) + 1) 603 print(status, end=' ') 604 sys.stdout.flush() 605 print('\r', end='') 606 sys.stdout.flush() 607 fd.close() 608 if done != size: 609 print('Error, failed to download') 610 os.remove(fname) 611 fname = None 612 return fname, tmpdir 613