1 # Copyright (C) 2008 The Android Open Source Project 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 import copy 16 import errno 17 import getopt 18 import getpass 19 import imp 20 import os 21 import platform 22 import re 23 import shutil 24 import subprocess 25 import sys 26 import tempfile 27 import threading 28 import time 29 import zipfile 30 31 try: 32 from hashlib import sha1 as sha1 33 except ImportError: 34 from sha import sha as sha1 35 36 # missing in Python 2.4 and before 37 if not hasattr(os, "SEEK_SET"): 38 os.SEEK_SET = 0 39 40 class Options(object): pass 41 OPTIONS = Options() 42 OPTIONS.search_path = "out/host/linux-x86" 43 OPTIONS.verbose = False 44 OPTIONS.tempfiles = [] 45 OPTIONS.device_specific = None 46 OPTIONS.extras = {} 47 OPTIONS.info_dict = None 48 49 50 # Values for "certificate" in apkcerts that mean special things. 51 SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL") 52 53 54 class ExternalError(RuntimeError): pass 55 56 57 def Run(args, **kwargs): 58 """Create and return a subprocess.Popen object, printing the command 59 line on the terminal if -v was specified.""" 60 if OPTIONS.verbose: 61 print " running: ", " ".join(args) 62 return subprocess.Popen(args, **kwargs) 63 64 65 def CloseInheritedPipes(): 66 """ Gmake in MAC OS has file descriptor (PIPE) leak. We close those fds 67 before doing other work.""" 68 if platform.system() != "Darwin": 69 return 70 for d in range(3, 1025): 71 try: 72 stat = os.fstat(d) 73 if stat is not None: 74 pipebit = stat[0] & 0x1000 75 if pipebit != 0: 76 os.close(d) 77 except OSError: 78 pass 79 80 81 def LoadInfoDict(zip): 82 """Read and parse the META/misc_info.txt key/value pairs from the 83 input target files and return a dict.""" 84 85 d = {} 86 try: 87 for line in zip.read("META/misc_info.txt").split("\n"): 88 line = line.strip() 89 if not line or line.startswith("#"): continue 90 k, v = line.split("=", 1) 91 d[k] = v 92 except KeyError: 93 # ok if misc_info.txt doesn't exist 94 pass 95 96 # backwards compatibility: These values used to be in their own 97 # files. Look for them, in case we're processing an old 98 # target_files zip. 99 100 if "mkyaffs2_extra_flags" not in d: 101 try: 102 d["mkyaffs2_extra_flags"] = zip.read("META/mkyaffs2-extra-flags.txt").strip() 103 except KeyError: 104 # ok if flags don't exist 105 pass 106 107 if "recovery_api_version" not in d: 108 try: 109 d["recovery_api_version"] = zip.read("META/recovery-api-version.txt").strip() 110 except KeyError: 111 raise ValueError("can't find recovery API version in input target-files") 112 113 if "tool_extensions" not in d: 114 try: 115 d["tool_extensions"] = zip.read("META/tool-extensions.txt").strip() 116 except KeyError: 117 # ok if extensions don't exist 118 pass 119 120 try: 121 data = zip.read("META/imagesizes.txt") 122 for line in data.split("\n"): 123 if not line: continue 124 name, value = line.split(" ", 1) 125 if not value: continue 126 if name == "blocksize": 127 d[name] = value 128 else: 129 d[name + "_size"] = value 130 except KeyError: 131 pass 132 133 def makeint(key): 134 if key in d: 135 d[key] = int(d[key], 0) 136 137 makeint("recovery_api_version") 138 makeint("blocksize") 139 makeint("system_size") 140 makeint("userdata_size") 141 makeint("cache_size") 142 makeint("recovery_size") 143 makeint("boot_size") 144 145 d["fstab"] = LoadRecoveryFSTab(zip) 146 return d 147 148 def LoadRecoveryFSTab(zip): 149 class Partition(object): 150 pass 151 152 try: 153 data = zip.read("RECOVERY/RAMDISK/etc/recovery.fstab") 154 except KeyError: 155 print "Warning: could not find RECOVERY/RAMDISK/etc/recovery.fstab in %s." % zip 156 data = "" 157 158 d = {} 159 for line in data.split("\n"): 160 line = line.strip() 161 if not line or line.startswith("#"): continue 162 pieces = line.split() 163 if not (3 <= len(pieces) <= 4): 164 raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,)) 165 166 p = Partition() 167 p.mount_point = pieces[0] 168 p.fs_type = pieces[1] 169 p.device = pieces[2] 170 p.length = 0 171 options = None 172 if len(pieces) >= 4: 173 if pieces[3].startswith("/"): 174 p.device2 = pieces[3] 175 if len(pieces) >= 5: 176 options = pieces[4] 177 else: 178 p.device2 = None 179 options = pieces[3] 180 else: 181 p.device2 = None 182 183 if options: 184 options = options.split(",") 185 for i in options: 186 if i.startswith("length="): 187 p.length = int(i[7:]) 188 else: 189 print "%s: unknown option \"%s\"" % (p.mount_point, i) 190 191 d[p.mount_point] = p 192 return d 193 194 195 def DumpInfoDict(d): 196 for k, v in sorted(d.items()): 197 print "%-25s = (%s) %s" % (k, type(v).__name__, v) 198 199 def BuildBootableImage(sourcedir, fs_config_file): 200 """Take a kernel, cmdline, and ramdisk directory from the input (in 201 'sourcedir'), and turn them into a boot image. Return the image 202 data, or None if sourcedir does not appear to contains files for 203 building the requested image.""" 204 205 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or 206 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)): 207 return None 208 209 ramdisk_img = tempfile.NamedTemporaryFile() 210 img = tempfile.NamedTemporaryFile() 211 212 if os.access(fs_config_file, os.F_OK): 213 cmd = ["mkbootfs", "-f", fs_config_file, os.path.join(sourcedir, "RAMDISK")] 214 else: 215 cmd = ["mkbootfs", os.path.join(sourcedir, "RAMDISK")] 216 p1 = Run(cmd, stdout=subprocess.PIPE) 217 p2 = Run(["minigzip"], 218 stdin=p1.stdout, stdout=ramdisk_img.file.fileno()) 219 220 p2.wait() 221 p1.wait() 222 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,) 223 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,) 224 225 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")] 226 227 fn = os.path.join(sourcedir, "cmdline") 228 if os.access(fn, os.F_OK): 229 cmd.append("--cmdline") 230 cmd.append(open(fn).read().rstrip("\n")) 231 232 fn = os.path.join(sourcedir, "base") 233 if os.access(fn, os.F_OK): 234 cmd.append("--base") 235 cmd.append(open(fn).read().rstrip("\n")) 236 237 fn = os.path.join(sourcedir, "pagesize") 238 if os.access(fn, os.F_OK): 239 cmd.append("--pagesize") 240 cmd.append(open(fn).read().rstrip("\n")) 241 242 cmd.extend(["--ramdisk", ramdisk_img.name, 243 "--output", img.name]) 244 245 p = Run(cmd, stdout=subprocess.PIPE) 246 p.communicate() 247 assert p.returncode == 0, "mkbootimg of %s image failed" % ( 248 os.path.basename(sourcedir),) 249 250 img.seek(os.SEEK_SET, 0) 251 data = img.read() 252 253 ramdisk_img.close() 254 img.close() 255 256 return data 257 258 259 def GetBootableImage(name, prebuilt_name, unpack_dir, tree_subdir): 260 """Return a File object (with name 'name') with the desired bootable 261 image. Look for it in 'unpack_dir'/BOOTABLE_IMAGES under the name 262 'prebuilt_name', otherwise construct it from the source files in 263 'unpack_dir'/'tree_subdir'.""" 264 265 prebuilt_path = os.path.join(unpack_dir, "BOOTABLE_IMAGES", prebuilt_name) 266 if os.path.exists(prebuilt_path): 267 print "using prebuilt %s..." % (prebuilt_name,) 268 return File.FromLocalFile(name, prebuilt_path) 269 else: 270 print "building image from target_files %s..." % (tree_subdir,) 271 fs_config = "META/" + tree_subdir.lower() + "_filesystem_config.txt" 272 return File(name, BuildBootableImage(os.path.join(unpack_dir, tree_subdir), 273 os.path.join(unpack_dir, fs_config))) 274 275 276 def UnzipTemp(filename, pattern=None): 277 """Unzip the given archive into a temporary directory and return the name. 278 279 If filename is of the form "foo.zip+bar.zip", unzip foo.zip into a 280 temp dir, then unzip bar.zip into that_dir/BOOTABLE_IMAGES. 281 282 Returns (tempdir, zipobj) where zipobj is a zipfile.ZipFile (of the 283 main file), open for reading. 284 """ 285 286 tmp = tempfile.mkdtemp(prefix="targetfiles-") 287 OPTIONS.tempfiles.append(tmp) 288 289 def unzip_to_dir(filename, dirname): 290 cmd = ["unzip", "-o", "-q", filename, "-d", dirname] 291 if pattern is not None: 292 cmd.append(pattern) 293 p = Run(cmd, stdout=subprocess.PIPE) 294 p.communicate() 295 if p.returncode != 0: 296 raise ExternalError("failed to unzip input target-files \"%s\"" % 297 (filename,)) 298 299 m = re.match(r"^(.*[.]zip)\+(.*[.]zip)$", filename, re.IGNORECASE) 300 if m: 301 unzip_to_dir(m.group(1), tmp) 302 unzip_to_dir(m.group(2), os.path.join(tmp, "BOOTABLE_IMAGES")) 303 filename = m.group(1) 304 else: 305 unzip_to_dir(filename, tmp) 306 307 return tmp, zipfile.ZipFile(filename, "r") 308 309 310 def GetKeyPasswords(keylist): 311 """Given a list of keys, prompt the user to enter passwords for 312 those which require them. Return a {key: password} dict. password 313 will be None if the key has no password.""" 314 315 no_passwords = [] 316 need_passwords = [] 317 devnull = open("/dev/null", "w+b") 318 for k in sorted(keylist): 319 # We don't need a password for things that aren't really keys. 320 if k in SPECIAL_CERT_STRINGS: 321 no_passwords.append(k) 322 continue 323 324 p = Run(["openssl", "pkcs8", "-in", k+".pk8", 325 "-inform", "DER", "-nocrypt"], 326 stdin=devnull.fileno(), 327 stdout=devnull.fileno(), 328 stderr=subprocess.STDOUT) 329 p.communicate() 330 if p.returncode == 0: 331 no_passwords.append(k) 332 else: 333 need_passwords.append(k) 334 devnull.close() 335 336 key_passwords = PasswordManager().GetPasswords(need_passwords) 337 key_passwords.update(dict.fromkeys(no_passwords, None)) 338 return key_passwords 339 340 341 def SignFile(input_name, output_name, key, password, align=None, 342 whole_file=False): 343 """Sign the input_name zip/jar/apk, producing output_name. Use the 344 given key and password (the latter may be None if the key does not 345 have a password. 346 347 If align is an integer > 1, zipalign is run to align stored files in 348 the output zip on 'align'-byte boundaries. 349 350 If whole_file is true, use the "-w" option to SignApk to embed a 351 signature that covers the whole file in the archive comment of the 352 zip file. 353 """ 354 355 if align == 0 or align == 1: 356 align = None 357 358 if align: 359 temp = tempfile.NamedTemporaryFile() 360 sign_name = temp.name 361 else: 362 sign_name = output_name 363 364 cmd = ["java", "-Xmx2048m", "-jar", 365 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")] 366 if whole_file: 367 cmd.append("-w") 368 cmd.extend([key + ".x509.pem", key + ".pk8", 369 input_name, sign_name]) 370 371 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 372 if password is not None: 373 password += "\n" 374 p.communicate(password) 375 if p.returncode != 0: 376 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,)) 377 378 if align: 379 p = Run(["zipalign", "-f", str(align), sign_name, output_name]) 380 p.communicate() 381 if p.returncode != 0: 382 raise ExternalError("zipalign failed: return code %s" % (p.returncode,)) 383 temp.close() 384 385 386 def CheckSize(data, target, info_dict): 387 """Check the data string passed against the max size limit, if 388 any, for the given target. Raise exception if the data is too big. 389 Print a warning if the data is nearing the maximum size.""" 390 391 if target.endswith(".img"): target = target[:-4] 392 mount_point = "/" + target 393 394 if info_dict["fstab"]: 395 if mount_point == "/userdata": mount_point = "/data" 396 p = info_dict["fstab"][mount_point] 397 fs_type = p.fs_type 398 device = p.device 399 if "/" in device: 400 device = device[device.rfind("/")+1:] 401 limit = info_dict.get(device + "_size", None) 402 if not fs_type or not limit: return 403 404 if fs_type == "yaffs2": 405 # image size should be increased by 1/64th to account for the 406 # spare area (64 bytes per 2k page) 407 limit = limit / 2048 * (2048+64) 408 size = len(data) 409 pct = float(size) * 100.0 / limit 410 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit) 411 if pct >= 99.0: 412 raise ExternalError(msg) 413 elif pct >= 95.0: 414 print 415 print " WARNING: ", msg 416 print 417 elif OPTIONS.verbose: 418 print " ", msg 419 420 421 def ReadApkCerts(tf_zip): 422 """Given a target_files ZipFile, parse the META/apkcerts.txt file 423 and return a {package: cert} dict.""" 424 certmap = {} 425 for line in tf_zip.read("META/apkcerts.txt").split("\n"): 426 line = line.strip() 427 if not line: continue 428 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+' 429 r'private_key="(.*)"$', line) 430 if m: 431 name, cert, privkey = m.groups() 432 if cert in SPECIAL_CERT_STRINGS and not privkey: 433 certmap[name] = cert 434 elif (cert.endswith(".x509.pem") and 435 privkey.endswith(".pk8") and 436 cert[:-9] == privkey[:-4]): 437 certmap[name] = cert[:-9] 438 else: 439 raise ValueError("failed to parse line from apkcerts.txt:\n" + line) 440 return certmap 441 442 443 COMMON_DOCSTRING = """ 444 -p (--path) <dir> 445 Prepend <dir>/bin to the list of places to search for binaries 446 run by this script, and expect to find jars in <dir>/framework. 447 448 -s (--device_specific) <file> 449 Path to the python module containing device-specific 450 releasetools code. 451 452 -x (--extra) <key=value> 453 Add a key/value pair to the 'extras' dict, which device-specific 454 extension code may look at. 455 456 -v (--verbose) 457 Show command lines being executed. 458 459 -h (--help) 460 Display this usage message and exit. 461 """ 462 463 def Usage(docstring): 464 print docstring.rstrip("\n") 465 print COMMON_DOCSTRING 466 467 468 def ParseOptions(argv, 469 docstring, 470 extra_opts="", extra_long_opts=(), 471 extra_option_handler=None): 472 """Parse the options in argv and return any arguments that aren't 473 flags. docstring is the calling module's docstring, to be displayed 474 for errors and -h. extra_opts and extra_long_opts are for flags 475 defined by the caller, which are processed by passing them to 476 extra_option_handler.""" 477 478 try: 479 opts, args = getopt.getopt( 480 argv, "hvp:s:x:" + extra_opts, 481 ["help", "verbose", "path=", "device_specific=", "extra="] + 482 list(extra_long_opts)) 483 except getopt.GetoptError, err: 484 Usage(docstring) 485 print "**", str(err), "**" 486 sys.exit(2) 487 488 path_specified = False 489 490 for o, a in opts: 491 if o in ("-h", "--help"): 492 Usage(docstring) 493 sys.exit() 494 elif o in ("-v", "--verbose"): 495 OPTIONS.verbose = True 496 elif o in ("-p", "--path"): 497 OPTIONS.search_path = a 498 elif o in ("-s", "--device_specific"): 499 OPTIONS.device_specific = a 500 elif o in ("-x", "--extra"): 501 key, value = a.split("=", 1) 502 OPTIONS.extras[key] = value 503 else: 504 if extra_option_handler is None or not extra_option_handler(o, a): 505 assert False, "unknown option \"%s\"" % (o,) 506 507 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") + 508 os.pathsep + os.environ["PATH"]) 509 510 return args 511 512 513 def Cleanup(): 514 for i in OPTIONS.tempfiles: 515 if os.path.isdir(i): 516 shutil.rmtree(i) 517 else: 518 os.remove(i) 519 520 521 class PasswordManager(object): 522 def __init__(self): 523 self.editor = os.getenv("EDITOR", None) 524 self.pwfile = os.getenv("ANDROID_PW_FILE", None) 525 526 def GetPasswords(self, items): 527 """Get passwords corresponding to each string in 'items', 528 returning a dict. (The dict may have keys in addition to the 529 values in 'items'.) 530 531 Uses the passwords in $ANDROID_PW_FILE if available, letting the 532 user edit that file to add more needed passwords. If no editor is 533 available, or $ANDROID_PW_FILE isn't define, prompts the user 534 interactively in the ordinary way. 535 """ 536 537 current = self.ReadFile() 538 539 first = True 540 while True: 541 missing = [] 542 for i in items: 543 if i not in current or not current[i]: 544 missing.append(i) 545 # Are all the passwords already in the file? 546 if not missing: return current 547 548 for i in missing: 549 current[i] = "" 550 551 if not first: 552 print "key file %s still missing some passwords." % (self.pwfile,) 553 answer = raw_input("try to edit again? [y]> ").strip() 554 if answer and answer[0] not in 'yY': 555 raise RuntimeError("key passwords unavailable") 556 first = False 557 558 current = self.UpdateAndReadFile(current) 559 560 def PromptResult(self, current): 561 """Prompt the user to enter a value (password) for each key in 562 'current' whose value is fales. Returns a new dict with all the 563 values. 564 """ 565 result = {} 566 for k, v in sorted(current.iteritems()): 567 if v: 568 result[k] = v 569 else: 570 while True: 571 result[k] = getpass.getpass("Enter password for %s key> " 572 % (k,)).strip() 573 if result[k]: break 574 return result 575 576 def UpdateAndReadFile(self, current): 577 if not self.editor or not self.pwfile: 578 return self.PromptResult(current) 579 580 f = open(self.pwfile, "w") 581 os.chmod(self.pwfile, 0600) 582 f.write("# Enter key passwords between the [[[ ]]] brackets.\n") 583 f.write("# (Additional spaces are harmless.)\n\n") 584 585 first_line = None 586 sorted = [(not v, k, v) for (k, v) in current.iteritems()] 587 sorted.sort() 588 for i, (_, k, v) in enumerate(sorted): 589 f.write("[[[ %s ]]] %s\n" % (v, k)) 590 if not v and first_line is None: 591 # position cursor on first line with no password. 592 first_line = i + 4 593 f.close() 594 595 p = Run([self.editor, "+%d" % (first_line,), self.pwfile]) 596 _, _ = p.communicate() 597 598 return self.ReadFile() 599 600 def ReadFile(self): 601 result = {} 602 if self.pwfile is None: return result 603 try: 604 f = open(self.pwfile, "r") 605 for line in f: 606 line = line.strip() 607 if not line or line[0] == '#': continue 608 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line) 609 if not m: 610 print "failed to parse password file: ", line 611 else: 612 result[m.group(2)] = m.group(1) 613 f.close() 614 except IOError, e: 615 if e.errno != errno.ENOENT: 616 print "error reading password file: ", str(e) 617 return result 618 619 620 def ZipWriteStr(zip, filename, data, perms=0644): 621 # use a fixed timestamp so the output is repeatable. 622 zinfo = zipfile.ZipInfo(filename=filename, 623 date_time=(2009, 1, 1, 0, 0, 0)) 624 zinfo.compress_type = zip.compression 625 zinfo.external_attr = perms << 16 626 zip.writestr(zinfo, data) 627 628 629 class DeviceSpecificParams(object): 630 module = None 631 def __init__(self, **kwargs): 632 """Keyword arguments to the constructor become attributes of this 633 object, which is passed to all functions in the device-specific 634 module.""" 635 for k, v in kwargs.iteritems(): 636 setattr(self, k, v) 637 self.extras = OPTIONS.extras 638 639 if self.module is None: 640 path = OPTIONS.device_specific 641 if not path: return 642 try: 643 if os.path.isdir(path): 644 info = imp.find_module("releasetools", [path]) 645 else: 646 d, f = os.path.split(path) 647 b, x = os.path.splitext(f) 648 if x == ".py": 649 f = b 650 info = imp.find_module(f, [d]) 651 self.module = imp.load_module("device_specific", *info) 652 except ImportError: 653 print "unable to load device-specific module; assuming none" 654 655 def _DoCall(self, function_name, *args, **kwargs): 656 """Call the named function in the device-specific module, passing 657 the given args and kwargs. The first argument to the call will be 658 the DeviceSpecific object itself. If there is no module, or the 659 module does not define the function, return the value of the 660 'default' kwarg (which itself defaults to None).""" 661 if self.module is None or not hasattr(self.module, function_name): 662 return kwargs.get("default", None) 663 return getattr(self.module, function_name)(*((self,) + args), **kwargs) 664 665 def FullOTA_Assertions(self): 666 """Called after emitting the block of assertions at the top of a 667 full OTA package. Implementations can add whatever additional 668 assertions they like.""" 669 return self._DoCall("FullOTA_Assertions") 670 671 def FullOTA_InstallBegin(self): 672 """Called at the start of full OTA installation.""" 673 return self._DoCall("FullOTA_InstallBegin") 674 675 def FullOTA_InstallEnd(self): 676 """Called at the end of full OTA installation; typically this is 677 used to install the image for the device's baseband processor.""" 678 return self._DoCall("FullOTA_InstallEnd") 679 680 def IncrementalOTA_Assertions(self): 681 """Called after emitting the block of assertions at the top of an 682 incremental OTA package. Implementations can add whatever 683 additional assertions they like.""" 684 return self._DoCall("IncrementalOTA_Assertions") 685 686 def IncrementalOTA_VerifyBegin(self): 687 """Called at the start of the verification phase of incremental 688 OTA installation; additional checks can be placed here to abort 689 the script before any changes are made.""" 690 return self._DoCall("IncrementalOTA_VerifyBegin") 691 692 def IncrementalOTA_VerifyEnd(self): 693 """Called at the end of the verification phase of incremental OTA 694 installation; additional checks can be placed here to abort the 695 script before any changes are made.""" 696 return self._DoCall("IncrementalOTA_VerifyEnd") 697 698 def IncrementalOTA_InstallBegin(self): 699 """Called at the start of incremental OTA installation (after 700 verification is complete).""" 701 return self._DoCall("IncrementalOTA_InstallBegin") 702 703 def IncrementalOTA_InstallEnd(self): 704 """Called at the end of incremental OTA installation; typically 705 this is used to install the image for the device's baseband 706 processor.""" 707 return self._DoCall("IncrementalOTA_InstallEnd") 708 709 class File(object): 710 def __init__(self, name, data): 711 self.name = name 712 self.data = data 713 self.size = len(data) 714 self.sha1 = sha1(data).hexdigest() 715 716 @classmethod 717 def FromLocalFile(cls, name, diskname): 718 f = open(diskname, "rb") 719 data = f.read() 720 f.close() 721 return File(name, data) 722 723 def WriteToTemp(self): 724 t = tempfile.NamedTemporaryFile() 725 t.write(self.data) 726 t.flush() 727 return t 728 729 def AddToZip(self, z): 730 ZipWriteStr(z, self.name, self.data) 731 732 DIFF_PROGRAM_BY_EXT = { 733 ".gz" : "imgdiff", 734 ".zip" : ["imgdiff", "-z"], 735 ".jar" : ["imgdiff", "-z"], 736 ".apk" : ["imgdiff", "-z"], 737 ".img" : "imgdiff", 738 } 739 740 class Difference(object): 741 def __init__(self, tf, sf): 742 self.tf = tf 743 self.sf = sf 744 self.patch = None 745 746 def ComputePatch(self): 747 """Compute the patch (as a string of data) needed to turn sf into 748 tf. Returns the same tuple as GetPatch().""" 749 750 tf = self.tf 751 sf = self.sf 752 753 ext = os.path.splitext(tf.name)[1] 754 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 755 756 ttemp = tf.WriteToTemp() 757 stemp = sf.WriteToTemp() 758 759 ext = os.path.splitext(tf.name)[1] 760 761 try: 762 ptemp = tempfile.NamedTemporaryFile() 763 if isinstance(diff_program, list): 764 cmd = copy.copy(diff_program) 765 else: 766 cmd = [diff_program] 767 cmd.append(stemp.name) 768 cmd.append(ttemp.name) 769 cmd.append(ptemp.name) 770 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 771 _, err = p.communicate() 772 if err or p.returncode != 0: 773 print "WARNING: failure running %s:\n%s\n" % (diff_program, err) 774 return None 775 diff = ptemp.read() 776 finally: 777 ptemp.close() 778 stemp.close() 779 ttemp.close() 780 781 self.patch = diff 782 return self.tf, self.sf, self.patch 783 784 785 def GetPatch(self): 786 """Return a tuple (target_file, source_file, patch_data). 787 patch_data may be None if ComputePatch hasn't been called, or if 788 computing the patch failed.""" 789 return self.tf, self.sf, self.patch 790 791 792 def ComputeDifferences(diffs): 793 """Call ComputePatch on all the Difference objects in 'diffs'.""" 794 print len(diffs), "diffs to compute" 795 796 # Do the largest files first, to try and reduce the long-pole effect. 797 by_size = [(i.tf.size, i) for i in diffs] 798 by_size.sort(reverse=True) 799 by_size = [i[1] for i in by_size] 800 801 lock = threading.Lock() 802 diff_iter = iter(by_size) # accessed under lock 803 804 def worker(): 805 try: 806 lock.acquire() 807 for d in diff_iter: 808 lock.release() 809 start = time.time() 810 d.ComputePatch() 811 dur = time.time() - start 812 lock.acquire() 813 814 tf, sf, patch = d.GetPatch() 815 if sf.name == tf.name: 816 name = tf.name 817 else: 818 name = "%s (%s)" % (tf.name, sf.name) 819 if patch is None: 820 print "patching failed! %s" % (name,) 821 else: 822 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % ( 823 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name) 824 lock.release() 825 except Exception, e: 826 print e 827 raise 828 829 # start worker threads; wait for them all to finish. 830 threads = [threading.Thread(target=worker) 831 for i in range(OPTIONS.worker_threads)] 832 for th in threads: 833 th.start() 834 while threads: 835 threads.pop().join() 836 837 838 # map recovery.fstab's fs_types to mount/format "partition types" 839 PARTITION_TYPES = { "yaffs2": "MTD", "mtd": "MTD", 840 "ext4": "EMMC", "emmc": "EMMC" } 841 842 def GetTypeAndDevice(mount_point, info): 843 fstab = info["fstab"] 844 if fstab: 845 return PARTITION_TYPES[fstab[mount_point].fs_type], fstab[mount_point].device 846 else: 847 return None 848