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