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 get_input_filename(fname, allow_missing=False): 127 """Return a filename for use as input. 128 129 Args: 130 fname: Filename to use for new file 131 allow_missing: True if the filename can be missing 132 133 Returns: 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 137 138 Raises: 139 ValueError if file is missing and allow_missing is False 140 """ 141 if not indir or fname[:1] == '/': 142 return fname 143 for dirname in indir: 144 pathname = os.path.join(dirname, fname) 145 if os.path.exists(pathname): 146 return pathname 147 148 if allow_missing: 149 return None 150 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" % 151 (fname, ','.join(indir), os.getcwd())) 152 153def get_input_filename_glob(pattern): 154 """Return a list of filenames for use as input. 155 156 Args: 157 pattern: Filename pattern to search for 158 159 Returns: 160 A list of matching files in all input directories 161 """ 162 if not indir: 163 return glob.glob(pattern) 164 files = [] 165 for dirname in indir: 166 pathname = os.path.join(dirname, pattern) 167 files += glob.glob(pathname) 168 return sorted(files) 169 170def align(pos, align): 171 if align: 172 mask = align - 1 173 pos = (pos + mask) & ~mask 174 return pos 175 176def not_power_of_two(num): 177 return num and (num & (num - 1)) 178 179def set_tool_paths(toolpaths): 180 """Set the path to search for tools 181 182 Args: 183 toolpaths: List of paths to search for tools executed by run() 184 """ 185 global tool_search_paths 186 187 tool_search_paths = toolpaths 188 189def path_has_file(path_spec, fname): 190 """Check if a given filename is in the PATH 191 192 Args: 193 path_spec: Value of PATH variable to check 194 fname: Filename to check 195 196 Returns: 197 True if found, False if not 198 """ 199 for dir in path_spec.split(':'): 200 if os.path.exists(os.path.join(dir, fname)): 201 return True 202 return False 203 204def get_host_compile_tool(env, name): 205 """Get the host-specific version for a compile tool 206 207 This checks the environment variables that specify which version of 208 the tool should be used (e.g. ${HOSTCC}). 209 210 The following table lists the host-specific versions of the tools 211 this function resolves to: 212 213 Compile Tool | Host version 214 --------------+---------------- 215 as | ${HOSTAS} 216 ld | ${HOSTLD} 217 cc | ${HOSTCC} 218 cpp | ${HOSTCPP} 219 c++ | ${HOSTCXX} 220 ar | ${HOSTAR} 221 nm | ${HOSTNM} 222 ldr | ${HOSTLDR} 223 strip | ${HOSTSTRIP} 224 objcopy | ${HOSTOBJCOPY} 225 objdump | ${HOSTOBJDUMP} 226 dtc | ${HOSTDTC} 227 228 Args: 229 name: Command name to run 230 231 Returns: 232 host_name: Exact command name to run instead 233 extra_args: List of extra arguments to pass 234 """ 235 host_name = None 236 extra_args = [] 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(' ') 240 elif name == 'c++': 241 host_name, *host_args = env.get('HOSTCXX', '').split(' ') 242 243 if host_name: 244 return host_name, extra_args 245 return name, [] 246 247def get_target_compile_tool(name, cross_compile=None): 248 """Get the target-specific version for a compile tool 249 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). 254 255 The following table lists the target-specific versions of the tools 256 this function resolves to: 257 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) 273 274 Args: 275 name: Command name to run 276 277 Returns: 278 target_name: Exact command name to run instead 279 extra_args: List of extra arguments to pass 280 """ 281 env = dict(os.environ) 282 283 target_name = None 284 extra_args = [] 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(' ') 288 elif name == 'c++': 289 target_name, *extra_args = env.get('CXX', '').split(' ') 290 291 if target_name: 292 return target_name, extra_args 293 294 if cross_compile is None: 295 cross_compile = env.get('CROSS_COMPILE', '') 296 297 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'): 298 target_name = cross_compile + name 299 elif name == 'ld': 300 try: 301 if run(cross_compile + 'ld.bfd', '-v'): 302 target_name = cross_compile + 'ld.bfd' 303 except: 304 target_name = cross_compile + 'ld' 305 elif name == 'cc': 306 target_name = cross_compile + 'gcc' 307 elif name == 'cpp': 308 target_name = cross_compile + 'gcc' 309 extra_args = ['-E'] 310 elif name == 'c++': 311 target_name = cross_compile + 'g++' 312 else: 313 target_name = name 314 return target_name, extra_args 315 316def get_env_with_path(): 317 """Get an updated environment with the PATH variable set correctly 318 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. 321 322 Returns: 323 dict: New environment with PATH updated, or None if there are not search 324 paths 325 """ 326 if tool_search_paths: 327 env = dict(os.environ) 328 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH'] 329 return env 330 331def run_result(name, *args, **kwargs): 332 """Run a tool with some arguments 333 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 336 search path. 337 338 Args: 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) 345 346 Returns: 347 CommandResult object 348 """ 349 try: 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() 355 if for_target: 356 name, extra_args = get_target_compile_tool(name) 357 args = tuple(extra_args) + args 358 elif for_host: 359 name, extra_args = get_host_compile_tool(env, name) 360 args = tuple(extra_args) + args 361 name = os.path.expanduser(name) # Expand paths containing ~ 362 all_args = (name,) + args 363 result = command.run_pipe([all_args], capture=True, capture_stderr=True, 364 env=env, raise_on_error=False, binary=binary) 365 if result.return_code: 366 if raise_on_error: 367 raise ValueError("Error %d running '%s': %s" % 368 (result.return_code,' '.join(all_args), 369 result.stderr or result.stdout)) 370 return result 371 except ValueError: 372 if env and not path_has_file(env['PATH'], name): 373 msg = "Please install tool '%s'" % name 374 package = packages.get(name) 375 if package: 376 msg += " (e.g. from package '%s')" % package 377 raise ValueError(msg) 378 raise 379 380def tool_find(name): 381 """Search the current path for a tool 382 383 This uses both PATH and any value from set_tool_paths() to search for a tool 384 385 Args: 386 name (str): Name of tool to locate 387 388 Returns: 389 str: Full path to tool if found, else None 390 """ 391 name = os.path.expanduser(name) # Expand paths containing ~ 392 paths = [] 393 pathvar = os.environ.get('PATH') 394 if pathvar: 395 paths = pathvar.split(':') 396 if tool_search_paths: 397 paths += tool_search_paths 398 for path in paths: 399 fname = os.path.join(path, name) 400 if os.path.isfile(fname) and os.access(fname, os.X_OK): 401 return fname 402 403def run(name, *args, **kwargs): 404 """Run a tool with some arguments 405 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 408 search path. 409 410 Args: 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 416 417 Returns: 418 CommandResult object 419 """ 420 result = run_result(name, *args, **kwargs) 421 if result is not None: 422 return result.stdout 423 424def filename(fname): 425 """Resolve a file path to an absolute path. 426 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. 430 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. 435 436 Args: 437 fname: a string, the path to resolve. 438 439 Returns: 440 Absolute path to the file or None if not found. 441 """ 442 if fname.startswith('##/'): 443 if chroot_path: 444 fname = os.path.join(chroot_path, fname[3:]) 445 else: 446 return None 447 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): 453 return pathname 454 455 # If not found, just return the standard, unchanged path 456 return fname 457 458def read_file(fname, binary=True): 459 """Read and return the contents of a file. 460 461 Args: 462 fname: path to filename to read, where ## signifiies the chroot. 463 464 Returns: 465 data read from file, as a string. 466 """ 467 with open(filename(fname), binary and 'rb' or 'r') as fd: 468 data = fd.read() 469 #self._out.Info("Read file '%s' size %d (%#0x)" % 470 #(fname, len(data), len(data))) 471 return data 472 473def write_file(fname, data, binary=True): 474 """Write data into a file. 475 476 Args: 477 fname: path to filename to write 478 data: data to write to file, as a string 479 """ 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: 483 fd.write(data) 484 485def get_bytes(byte, size): 486 """Get a string of bytes of a given size 487 488 Args: 489 byte: Numeric byte value to use 490 size: Size of bytes/string to return 491 492 Returns: 493 A bytes type with 'byte' repeated 'size' times 494 """ 495 return bytes([byte]) * size 496 497def to_bytes(string): 498 """Convert a str type into a bytes type 499 500 Args: 501 string: string to convert 502 503 Returns: 504 A bytes type 505 """ 506 return string.encode('utf-8') 507 508def to_string(bval): 509 """Convert a bytes type into a str type 510 511 Args: 512 bval: bytes value to convert 513 514 Returns: 515 Python 3: A bytes type 516 Python 2: A string type 517 """ 518 return bval.decode('utf-8') 519 520def to_hex(val): 521 """Convert an integer value (or None) to a string 522 523 Returns: 524 hex value, or 'None' if the value is None 525 """ 526 return 'None' if val is None else '%#x' % val 527 528def to_hex_size(val): 529 """Return the size of an object in hex 530 531 Returns: 532 hex value of size, or 'None' if the value is None 533 """ 534 return 'None' if val is None else '%#x' % len(val) 535 536def print_full_help(fname): 537 """Print the full help message for a tool using an appropriate pager. 538 539 Args: 540 fname: Path to a file containing the full help message 541 """ 542 pager = shlex.split(os.getenv('PAGER', '')) 543 if not pager: 544 lesspath = shutil.which('less') 545 pager = [lesspath] if lesspath else None 546 if not pager: 547 pager = ['more'] 548 command.run(*pager, fname) 549 550def download(url, tmpdir_pattern='.patman'): 551 """Download a file to a temporary directory 552 553 Args: 554 url (str): URL to download 555 tmpdir_pattern (str): pattern to use for the temporary directory 556 557 Returns: 558 Tuple: 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 562 """ 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')) 571 done = 0 572 block_size = 1 << 16 573 status = '' 574 575 # Read the file in chunks and show progress as we go 576 while True: 577 buffer = response.read(block_size) 578 if not buffer: 579 print(chr(8) * (len(status) + 1), '\r', end=' ') 580 break 581 582 done += len(buffer) 583 fd.write(buffer) 584 status = r'%10d MiB [%3d%%]' % (done // 1024 // 1024, 585 done * 100 // size) 586 status = status + chr(8) * (len(status) + 1) 587 print(status, end=' ') 588 sys.stdout.flush() 589 print('\r', end='') 590 sys.stdout.flush() 591 fd.close() 592 if done != size: 593 print('Error, failed to download') 594 os.remove(fname) 595 fname = None 596 return fname, tmpdir 597