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