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 struct 11import sys 12import tempfile 13 14from patman import command 15from patman 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 PrepareOutputDir(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 CmdError("Cannot make output directory '%s': '%s'" % 66 (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 _RemoveOutputDir(): 73 global outdir 74 75 shutil.rmtree(outdir) 76 tout.Debug("Deleted temporary directory '%s'" % outdir) 77 outdir = None 78 79def FinaliseOutputDir(): 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 _RemoveOutputDir() 85 outdir = None 86 87def GetOutputFilename(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 GetOutputDir(): 99 """Return the current output directory 100 101 Returns: 102 str: The output directory 103 """ 104 return outdir 105 106def _FinaliseForTest(): 107 """Remove the output directory (for use by tests)""" 108 global outdir 109 110 if outdir: 111 _RemoveOutputDir() 112 outdir = None 113 114def SetInputDirs(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 GetInputFilename(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 GetInputFilenameGlob(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(fname) 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 NotPowerOfTwo(num): 177 return num and (num & (num - 1)) 178 179def SetToolPaths(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 PathHasFile(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 GetHostCompileTool(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 GetTargetCompileTool(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 Run(name, *args, **kwargs): 317 """Run a tool with some arguments 318 319 This runs a 'tool', which is a program used by binman to process files and 320 perhaps produce some output. Tools can be located on the PATH or in a 321 search path. 322 323 Args: 324 name: Command name to run 325 args: Arguments to the tool 326 for_host: True to resolve the command to the version for the host 327 for_target: False to run the command as-is, without resolving it 328 to the version for the compile target 329 330 Returns: 331 CommandResult object 332 """ 333 try: 334 binary = kwargs.get('binary') 335 for_host = kwargs.get('for_host', False) 336 for_target = kwargs.get('for_target', not for_host) 337 env = None 338 if tool_search_paths: 339 env = dict(os.environ) 340 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH'] 341 if for_target: 342 name, extra_args = GetTargetCompileTool(name) 343 args = tuple(extra_args) + args 344 elif for_host: 345 name, extra_args = GetHostCompileTool(name) 346 args = tuple(extra_args) + args 347 name = os.path.expanduser(name) # Expand paths containing ~ 348 all_args = (name,) + args 349 result = command.RunPipe([all_args], capture=True, capture_stderr=True, 350 env=env, raise_on_error=False, binary=binary) 351 if result.return_code: 352 raise Exception("Error %d running '%s': %s" % 353 (result.return_code,' '.join(all_args), 354 result.stderr)) 355 return result.stdout 356 except: 357 if env and not PathHasFile(env['PATH'], name): 358 msg = "Please install tool '%s'" % name 359 package = packages.get(name) 360 if package: 361 msg += " (e.g. from package '%s')" % package 362 raise ValueError(msg) 363 raise 364 365def Filename(fname): 366 """Resolve a file path to an absolute path. 367 368 If fname starts with ##/ and chroot is available, ##/ gets replaced with 369 the chroot path. If chroot is not available, this file name can not be 370 resolved, `None' is returned. 371 372 If fname is not prepended with the above prefix, and is not an existing 373 file, the actual file name is retrieved from the passed in string and the 374 search_paths directories (if any) are searched to for the file. If found - 375 the path to the found file is returned, `None' is returned otherwise. 376 377 Args: 378 fname: a string, the path to resolve. 379 380 Returns: 381 Absolute path to the file or None if not found. 382 """ 383 if fname.startswith('##/'): 384 if chroot_path: 385 fname = os.path.join(chroot_path, fname[3:]) 386 else: 387 return None 388 389 # Search for a pathname that exists, and return it if found 390 if fname and not os.path.exists(fname): 391 for path in search_paths: 392 pathname = os.path.join(path, os.path.basename(fname)) 393 if os.path.exists(pathname): 394 return pathname 395 396 # If not found, just return the standard, unchanged path 397 return fname 398 399def ReadFile(fname, binary=True): 400 """Read and return the contents of a file. 401 402 Args: 403 fname: path to filename to read, where ## signifiies the chroot. 404 405 Returns: 406 data read from file, as a string. 407 """ 408 with open(Filename(fname), binary and 'rb' or 'r') as fd: 409 data = fd.read() 410 #self._out.Info("Read file '%s' size %d (%#0x)" % 411 #(fname, len(data), len(data))) 412 return data 413 414def WriteFile(fname, data, binary=True): 415 """Write data into a file. 416 417 Args: 418 fname: path to filename to write 419 data: data to write to file, as a string 420 """ 421 #self._out.Info("Write file '%s' size %d (%#0x)" % 422 #(fname, len(data), len(data))) 423 with open(Filename(fname), binary and 'wb' or 'w') as fd: 424 fd.write(data) 425 426def GetBytes(byte, size): 427 """Get a string of bytes of a given size 428 429 Args: 430 byte: Numeric byte value to use 431 size: Size of bytes/string to return 432 433 Returns: 434 A bytes type with 'byte' repeated 'size' times 435 """ 436 return bytes([byte]) * size 437 438def ToBytes(string): 439 """Convert a str type into a bytes type 440 441 Args: 442 string: string to convert 443 444 Returns: 445 A bytes type 446 """ 447 return string.encode('utf-8') 448 449def ToString(bval): 450 """Convert a bytes type into a str type 451 452 Args: 453 bval: bytes value to convert 454 455 Returns: 456 Python 3: A bytes type 457 Python 2: A string type 458 """ 459 return bval.decode('utf-8') 460 461def Compress(indata, algo, with_header=True): 462 """Compress some data using a given algorithm 463 464 Note that for lzma this uses an old version of the algorithm, not that 465 provided by xz. 466 467 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 468 directory to be previously set up, by calling PrepareOutputDir(). 469 470 Care is taken to use unique temporary files so that this function can be 471 called from multiple threads. 472 473 Args: 474 indata: Input data to compress 475 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 476 477 Returns: 478 Compressed data 479 """ 480 if algo == 'none': 481 return indata 482 fname = tempfile.NamedTemporaryFile(prefix='%s.comp.tmp' % algo, 483 dir=outdir).name 484 WriteFile(fname, indata) 485 if algo == 'lz4': 486 data = Run('lz4', '--no-frame-crc', '-B4', '-5', '-c', fname, 487 binary=True) 488 # cbfstool uses a very old version of lzma 489 elif algo == 'lzma': 490 outfname = tempfile.NamedTemporaryFile(prefix='%s.comp.otmp' % algo, 491 dir=outdir).name 492 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8') 493 data = ReadFile(outfname) 494 elif algo == 'gzip': 495 data = Run('gzip', '-c', fname, binary=True) 496 else: 497 raise ValueError("Unknown algorithm '%s'" % algo) 498 if with_header: 499 hdr = struct.pack('<I', len(data)) 500 data = hdr + data 501 return data 502 503def Decompress(indata, algo, with_header=True): 504 """Decompress some data using a given algorithm 505 506 Note that for lzma this uses an old version of the algorithm, not that 507 provided by xz. 508 509 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 510 directory to be previously set up, by calling PrepareOutputDir(). 511 512 Args: 513 indata: Input data to decompress 514 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 515 516 Returns: 517 Compressed data 518 """ 519 if algo == 'none': 520 return indata 521 if with_header: 522 data_len = struct.unpack('<I', indata[:4])[0] 523 indata = indata[4:4 + data_len] 524 fname = GetOutputFilename('%s.decomp.tmp' % algo) 525 with open(fname, 'wb') as fd: 526 fd.write(indata) 527 if algo == 'lz4': 528 data = Run('lz4', '-dc', fname, binary=True) 529 elif algo == 'lzma': 530 outfname = GetOutputFilename('%s.decomp.otmp' % algo) 531 Run('lzma_alone', 'd', fname, outfname) 532 data = ReadFile(outfname, binary=True) 533 elif algo == 'gzip': 534 data = Run('gzip', '-cd', fname, binary=True) 535 else: 536 raise ValueError("Unknown algorithm '%s'" % algo) 537 return data 538 539CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5) 540 541IFWITOOL_CMDS = { 542 CMD_CREATE: 'create', 543 CMD_DELETE: 'delete', 544 CMD_ADD: 'add', 545 CMD_REPLACE: 'replace', 546 CMD_EXTRACT: 'extract', 547 } 548 549def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None): 550 """Run ifwitool with the given arguments: 551 552 Args: 553 ifwi_file: IFWI file to operation on 554 cmd: Command to execute (CMD_...) 555 fname: Filename of file to add/replace/extract/create (None for 556 CMD_DELETE) 557 subpart: Name of sub-partition to operation on (None for CMD_CREATE) 558 entry_name: Name of directory entry to operate on, or None if none 559 """ 560 args = ['ifwitool', ifwi_file] 561 args.append(IFWITOOL_CMDS[cmd]) 562 if fname: 563 args += ['-f', fname] 564 if subpart: 565 args += ['-n', subpart] 566 if entry_name: 567 args += ['-d', '-e', entry_name] 568 Run(*args) 569 570def ToHex(val): 571 """Convert an integer value (or None) to a string 572 573 Returns: 574 hex value, or 'None' if the value is None 575 """ 576 return 'None' if val is None else '%#x' % val 577 578def ToHexSize(val): 579 """Return the size of an object in hex 580 581 Returns: 582 hex value of size, or 'None' if the value is None 583 """ 584 return 'None' if val is None else '%#x' % len(val) 585 586def PrintFullHelp(fname): 587 """Print the full help message for a tool using an appropriate pager. 588 589 Args: 590 fname: Path to a file containing the full help message 591 """ 592 pager = shlex.split(os.getenv('PAGER', '')) 593 if not pager: 594 lesspath = shutil.which('less') 595 pager = [lesspath] if lesspath else None 596 if not pager: 597 pager = ['more'] 598 command.Run(*pager, fname) 599