1# SPDX-License-Identifier: GPL-2.0+ 2# 3# Copyright (c) 2016 Google, Inc 4# 5 6import glob 7import os 8import shutil 9import struct 10import sys 11import tempfile 12 13from patman import command 14from patman import tout 15 16# Output directly (generally this is temporary) 17outdir = None 18 19# True to keep the output directory around after exiting 20preserve_outdir = False 21 22# Path to the Chrome OS chroot, if we know it 23chroot_path = None 24 25# Search paths to use for Filename(), used to find files 26search_paths = [] 27 28tool_search_paths = [] 29 30# Tools and the packages that contain them, on debian 31packages = { 32 'lz4': 'liblz4-tool', 33 } 34 35# List of paths to use when looking for an input file 36indir = [] 37 38def PrepareOutputDir(dirname, preserve=False): 39 """Select an output directory, ensuring it exists. 40 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. 44 45 Args: 46 dirname: a string, name of the output directory to use to store 47 intermediate and output files. If is None - create a temporary 48 directory. 49 preserve: a Boolean. If outdir above is None and preserve is False, the 50 created temporary directory will be destroyed on exit. 51 52 Raises: 53 OSError: If it cannot create the output directory. 54 """ 55 global outdir, preserve_outdir 56 57 preserve_outdir = dirname or preserve 58 if dirname: 59 outdir = dirname 60 if not os.path.isdir(outdir): 61 try: 62 os.makedirs(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) 67 else: 68 outdir = tempfile.mkdtemp(prefix='binman.') 69 tout.Debug("Using temporary directory '%s'" % outdir) 70 71def _RemoveOutputDir(): 72 global outdir 73 74 shutil.rmtree(outdir) 75 tout.Debug("Deleted temporary directory '%s'" % outdir) 76 outdir = None 77 78def FinaliseOutputDir(): 79 global outdir, preserve_outdir 80 81 """Tidy up: delete output directory if temporary and not preserved.""" 82 if outdir and not preserve_outdir: 83 _RemoveOutputDir() 84 outdir = None 85 86def GetOutputFilename(fname): 87 """Return a filename within the output directory. 88 89 Args: 90 fname: Filename to use for new file 91 92 Returns: 93 The full path of the filename, within the output directory 94 """ 95 return os.path.join(outdir, fname) 96 97def GetOutputDir(): 98 """Return the current output directory 99 100 Returns: 101 str: The output directory 102 """ 103 return outdir 104 105def _FinaliseForTest(): 106 """Remove the output directory (for use by tests)""" 107 global outdir 108 109 if outdir: 110 _RemoveOutputDir() 111 outdir = None 112 113def SetInputDirs(dirname): 114 """Add a list of input directories, where input files are kept. 115 116 Args: 117 dirname: a list of paths to input directories to use for obtaining 118 files needed by binman to place in the image. 119 """ 120 global indir 121 122 indir = dirname 123 tout.Debug("Using input directories %s" % indir) 124 125def GetInputFilename(fname, allow_missing=False): 126 """Return a filename for use as input. 127 128 Args: 129 fname: Filename to use for new file 130 allow_missing: True if the filename can be missing 131 132 Returns: 133 The full path of the filename, within the input directory, or 134 None on error 135 """ 136 if not indir or fname[:1] == '/': 137 return fname 138 for dirname in indir: 139 pathname = os.path.join(dirname, fname) 140 if os.path.exists(pathname): 141 return pathname 142 143 if allow_missing: 144 return None 145 raise ValueError("Filename '%s' not found in input path (%s) (cwd='%s')" % 146 (fname, ','.join(indir), os.getcwd())) 147 148def GetInputFilenameGlob(pattern): 149 """Return a list of filenames for use as input. 150 151 Args: 152 pattern: Filename pattern to search for 153 154 Returns: 155 A list of matching files in all input directories 156 """ 157 if not indir: 158 return glob.glob(fname) 159 files = [] 160 for dirname in indir: 161 pathname = os.path.join(dirname, pattern) 162 files += glob.glob(pathname) 163 return sorted(files) 164 165def Align(pos, align): 166 if align: 167 mask = align - 1 168 pos = (pos + mask) & ~mask 169 return pos 170 171def NotPowerOfTwo(num): 172 return num and (num & (num - 1)) 173 174def SetToolPaths(toolpaths): 175 """Set the path to search for tools 176 177 Args: 178 toolpaths: List of paths to search for tools executed by Run() 179 """ 180 global tool_search_paths 181 182 tool_search_paths = toolpaths 183 184def PathHasFile(path_spec, fname): 185 """Check if a given filename is in the PATH 186 187 Args: 188 path_spec: Value of PATH variable to check 189 fname: Filename to check 190 191 Returns: 192 True if found, False if not 193 """ 194 for dir in path_spec.split(':'): 195 if os.path.exists(os.path.join(dir, fname)): 196 return True 197 return False 198 199def GetHostCompileTool(name): 200 """Get the host-specific version for a compile tool 201 202 This checks the environment variables that specify which version of 203 the tool should be used (e.g. ${HOSTCC}). 204 205 The following table lists the host-specific versions of the tools 206 this function resolves to: 207 208 Compile Tool | Host version 209 --------------+---------------- 210 as | ${HOSTAS} 211 ld | ${HOSTLD} 212 cc | ${HOSTCC} 213 cpp | ${HOSTCPP} 214 c++ | ${HOSTCXX} 215 ar | ${HOSTAR} 216 nm | ${HOSTNM} 217 ldr | ${HOSTLDR} 218 strip | ${HOSTSTRIP} 219 objcopy | ${HOSTOBJCOPY} 220 objdump | ${HOSTOBJDUMP} 221 dtc | ${HOSTDTC} 222 223 Args: 224 name: Command name to run 225 226 Returns: 227 host_name: Exact command name to run instead 228 extra_args: List of extra arguments to pass 229 """ 230 host_name = None 231 extra_args = [] 232 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip', 233 'objcopy', 'objdump', 'dtc'): 234 host_name, *host_args = env.get('HOST' + name.upper(), '').split(' ') 235 elif name == 'c++': 236 host_name, *host_args = env.get('HOSTCXX', '').split(' ') 237 238 if host_name: 239 return host_name, extra_args 240 return name, [] 241 242def GetTargetCompileTool(name, cross_compile=None): 243 """Get the target-specific version for a compile tool 244 245 This first checks the environment variables that specify which 246 version of the tool should be used (e.g. ${CC}). If those aren't 247 specified, it checks the CROSS_COMPILE variable as a prefix for the 248 tool with some substitutions (e.g. "${CROSS_COMPILE}gcc" for cc). 249 250 The following table lists the target-specific versions of the tools 251 this function resolves to: 252 253 Compile Tool | First choice | Second choice 254 --------------+----------------+---------------------------- 255 as | ${AS} | ${CROSS_COMPILE}as 256 ld | ${LD} | ${CROSS_COMPILE}ld.bfd 257 | | or ${CROSS_COMPILE}ld 258 cc | ${CC} | ${CROSS_COMPILE}gcc 259 cpp | ${CPP} | ${CROSS_COMPILE}gcc -E 260 c++ | ${CXX} | ${CROSS_COMPILE}g++ 261 ar | ${AR} | ${CROSS_COMPILE}ar 262 nm | ${NM} | ${CROSS_COMPILE}nm 263 ldr | ${LDR} | ${CROSS_COMPILE}ldr 264 strip | ${STRIP} | ${CROSS_COMPILE}strip 265 objcopy | ${OBJCOPY} | ${CROSS_COMPILE}objcopy 266 objdump | ${OBJDUMP} | ${CROSS_COMPILE}objdump 267 dtc | ${DTC} | (no CROSS_COMPILE version) 268 269 Args: 270 name: Command name to run 271 272 Returns: 273 target_name: Exact command name to run instead 274 extra_args: List of extra arguments to pass 275 """ 276 env = dict(os.environ) 277 278 target_name = None 279 extra_args = [] 280 if name in ('as', 'ld', 'cc', 'cpp', 'ar', 'nm', 'ldr', 'strip', 281 'objcopy', 'objdump', 'dtc'): 282 target_name, *extra_args = env.get(name.upper(), '').split(' ') 283 elif name == 'c++': 284 target_name, *extra_args = env.get('CXX', '').split(' ') 285 286 if target_name: 287 return target_name, extra_args 288 289 if cross_compile is None: 290 cross_compile = env.get('CROSS_COMPILE', '') 291 if not cross_compile: 292 return name, [] 293 294 if name in ('as', 'ar', 'nm', 'ldr', 'strip', 'objcopy', 'objdump'): 295 target_name = cross_compile + name 296 elif name == 'ld': 297 try: 298 if Run(cross_compile + 'ld.bfd', '-v'): 299 target_name = cross_compile + 'ld.bfd' 300 except: 301 target_name = cross_compile + 'ld' 302 elif name == 'cc': 303 target_name = cross_compile + 'gcc' 304 elif name == 'cpp': 305 target_name = cross_compile + 'gcc' 306 extra_args = ['-E'] 307 elif name == 'c++': 308 target_name = cross_compile + 'g++' 309 else: 310 target_name = name 311 return target_name, extra_args 312 313def Run(name, *args, **kwargs): 314 """Run a tool with some arguments 315 316 This runs a 'tool', which is a program used by binman to process files and 317 perhaps produce some output. Tools can be located on the PATH or in a 318 search path. 319 320 Args: 321 name: Command name to run 322 args: Arguments to the tool 323 for_host: True to resolve the command to the version for the host 324 for_target: False to run the command as-is, without resolving it 325 to the version for the compile target 326 327 Returns: 328 CommandResult object 329 """ 330 try: 331 binary = kwargs.get('binary') 332 for_host = kwargs.get('for_host', False) 333 for_target = kwargs.get('for_target', not for_host) 334 env = None 335 if tool_search_paths: 336 env = dict(os.environ) 337 env['PATH'] = ':'.join(tool_search_paths) + ':' + env['PATH'] 338 if for_target: 339 name, extra_args = GetTargetCompileTool(name) 340 args = tuple(extra_args) + args 341 elif for_host: 342 name, extra_args = GetHostCompileTool(name) 343 args = tuple(extra_args) + args 344 name = os.path.expanduser(name) # Expand paths containing ~ 345 all_args = (name,) + args 346 result = command.RunPipe([all_args], capture=True, capture_stderr=True, 347 env=env, raise_on_error=False, binary=binary) 348 if result.return_code: 349 raise Exception("Error %d running '%s': %s" % 350 (result.return_code,' '.join(all_args), 351 result.stderr)) 352 return result.stdout 353 except: 354 if env and not PathHasFile(env['PATH'], name): 355 msg = "Please install tool '%s'" % name 356 package = packages.get(name) 357 if package: 358 msg += " (e.g. from package '%s')" % package 359 raise ValueError(msg) 360 raise 361 362def Filename(fname): 363 """Resolve a file path to an absolute path. 364 365 If fname starts with ##/ and chroot is available, ##/ gets replaced with 366 the chroot path. If chroot is not available, this file name can not be 367 resolved, `None' is returned. 368 369 If fname is not prepended with the above prefix, and is not an existing 370 file, the actual file name is retrieved from the passed in string and the 371 search_paths directories (if any) are searched to for the file. If found - 372 the path to the found file is returned, `None' is returned otherwise. 373 374 Args: 375 fname: a string, the path to resolve. 376 377 Returns: 378 Absolute path to the file or None if not found. 379 """ 380 if fname.startswith('##/'): 381 if chroot_path: 382 fname = os.path.join(chroot_path, fname[3:]) 383 else: 384 return None 385 386 # Search for a pathname that exists, and return it if found 387 if fname and not os.path.exists(fname): 388 for path in search_paths: 389 pathname = os.path.join(path, os.path.basename(fname)) 390 if os.path.exists(pathname): 391 return pathname 392 393 # If not found, just return the standard, unchanged path 394 return fname 395 396def ReadFile(fname, binary=True): 397 """Read and return the contents of a file. 398 399 Args: 400 fname: path to filename to read, where ## signifiies the chroot. 401 402 Returns: 403 data read from file, as a string. 404 """ 405 with open(Filename(fname), binary and 'rb' or 'r') as fd: 406 data = fd.read() 407 #self._out.Info("Read file '%s' size %d (%#0x)" % 408 #(fname, len(data), len(data))) 409 return data 410 411def WriteFile(fname, data, binary=True): 412 """Write data into a file. 413 414 Args: 415 fname: path to filename to write 416 data: data to write to file, as a string 417 """ 418 #self._out.Info("Write file '%s' size %d (%#0x)" % 419 #(fname, len(data), len(data))) 420 with open(Filename(fname), binary and 'wb' or 'w') as fd: 421 fd.write(data) 422 423def GetBytes(byte, size): 424 """Get a string of bytes of a given size 425 426 Args: 427 byte: Numeric byte value to use 428 size: Size of bytes/string to return 429 430 Returns: 431 A bytes type with 'byte' repeated 'size' times 432 """ 433 return bytes([byte]) * size 434 435def ToBytes(string): 436 """Convert a str type into a bytes type 437 438 Args: 439 string: string to convert 440 441 Returns: 442 A bytes type 443 """ 444 return string.encode('utf-8') 445 446def ToString(bval): 447 """Convert a bytes type into a str type 448 449 Args: 450 bval: bytes value to convert 451 452 Returns: 453 Python 3: A bytes type 454 Python 2: A string type 455 """ 456 return bval.decode('utf-8') 457 458def Compress(indata, algo, with_header=True): 459 """Compress some data using a given algorithm 460 461 Note that for lzma this uses an old version of the algorithm, not that 462 provided by xz. 463 464 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 465 directory to be previously set up, by calling PrepareOutputDir(). 466 467 Args: 468 indata: Input data to compress 469 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 470 471 Returns: 472 Compressed data 473 """ 474 if algo == 'none': 475 return indata 476 fname = GetOutputFilename('%s.comp.tmp' % algo) 477 WriteFile(fname, indata) 478 if algo == 'lz4': 479 data = Run('lz4', '--no-frame-crc', '-B4', '-5', '-c', fname, 480 binary=True) 481 # cbfstool uses a very old version of lzma 482 elif algo == 'lzma': 483 outfname = GetOutputFilename('%s.comp.otmp' % algo) 484 Run('lzma_alone', 'e', fname, outfname, '-lc1', '-lp0', '-pb0', '-d8') 485 data = ReadFile(outfname) 486 elif algo == 'gzip': 487 data = Run('gzip', '-c', fname, binary=True) 488 else: 489 raise ValueError("Unknown algorithm '%s'" % algo) 490 if with_header: 491 hdr = struct.pack('<I', len(data)) 492 data = hdr + data 493 return data 494 495def Decompress(indata, algo, with_header=True): 496 """Decompress some data using a given algorithm 497 498 Note that for lzma this uses an old version of the algorithm, not that 499 provided by xz. 500 501 This requires 'lz4' and 'lzma_alone' tools. It also requires an output 502 directory to be previously set up, by calling PrepareOutputDir(). 503 504 Args: 505 indata: Input data to decompress 506 algo: Algorithm to use ('none', 'gzip', 'lz4' or 'lzma') 507 508 Returns: 509 Compressed data 510 """ 511 if algo == 'none': 512 return indata 513 if with_header: 514 data_len = struct.unpack('<I', indata[:4])[0] 515 indata = indata[4:4 + data_len] 516 fname = GetOutputFilename('%s.decomp.tmp' % algo) 517 with open(fname, 'wb') as fd: 518 fd.write(indata) 519 if algo == 'lz4': 520 data = Run('lz4', '-dc', fname, binary=True) 521 elif algo == 'lzma': 522 outfname = GetOutputFilename('%s.decomp.otmp' % algo) 523 Run('lzma_alone', 'd', fname, outfname) 524 data = ReadFile(outfname, binary=True) 525 elif algo == 'gzip': 526 data = Run('gzip', '-cd', fname, binary=True) 527 else: 528 raise ValueError("Unknown algorithm '%s'" % algo) 529 return data 530 531CMD_CREATE, CMD_DELETE, CMD_ADD, CMD_REPLACE, CMD_EXTRACT = range(5) 532 533IFWITOOL_CMDS = { 534 CMD_CREATE: 'create', 535 CMD_DELETE: 'delete', 536 CMD_ADD: 'add', 537 CMD_REPLACE: 'replace', 538 CMD_EXTRACT: 'extract', 539 } 540 541def RunIfwiTool(ifwi_file, cmd, fname=None, subpart=None, entry_name=None): 542 """Run ifwitool with the given arguments: 543 544 Args: 545 ifwi_file: IFWI file to operation on 546 cmd: Command to execute (CMD_...) 547 fname: Filename of file to add/replace/extract/create (None for 548 CMD_DELETE) 549 subpart: Name of sub-partition to operation on (None for CMD_CREATE) 550 entry_name: Name of directory entry to operate on, or None if none 551 """ 552 args = ['ifwitool', ifwi_file] 553 args.append(IFWITOOL_CMDS[cmd]) 554 if fname: 555 args += ['-f', fname] 556 if subpart: 557 args += ['-n', subpart] 558 if entry_name: 559 args += ['-d', '-e', entry_name] 560 Run(*args) 561 562def ToHex(val): 563 """Convert an integer value (or None) to a string 564 565 Returns: 566 hex value, or 'None' if the value is None 567 """ 568 return 'None' if val is None else '%#x' % val 569 570def ToHexSize(val): 571 """Return the size of an object in hex 572 573 Returns: 574 hex value of size, or 'None' if the value is None 575 """ 576 return 'None' if val is None else '%#x' % len(val) 577