common.py revision 258bf46ea6bb4f25d01fab1b783238589e5bbec4
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 363 size = len(data) 364 pct = float(size) * 100.0 / limit 365 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit) 366 if pct >= 99.0: 367 raise ExternalError(msg) 368 elif pct >= 95.0: 369 print 370 print " WARNING: ", msg 371 print 372 elif OPTIONS.verbose: 373 print " ", msg 374 375 376def ReadApkCerts(tf_zip): 377 """Given a target_files ZipFile, parse the META/apkcerts.txt file 378 and return a {package: cert} dict.""" 379 certmap = {} 380 for line in tf_zip.read("META/apkcerts.txt").split("\n"): 381 line = line.strip() 382 if not line: continue 383 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+' 384 r'private_key="(.*)"$', line) 385 if m: 386 name, cert, privkey = m.groups() 387 if cert in SPECIAL_CERT_STRINGS and not privkey: 388 certmap[name] = cert 389 elif (cert.endswith(".x509.pem") and 390 privkey.endswith(".pk8") and 391 cert[:-9] == privkey[:-4]): 392 certmap[name] = cert[:-9] 393 else: 394 raise ValueError("failed to parse line from apkcerts.txt:\n" + line) 395 return certmap 396 397 398COMMON_DOCSTRING = """ 399 -p (--path) <dir> 400 Prepend <dir>/bin to the list of places to search for binaries 401 run by this script, and expect to find jars in <dir>/framework. 402 403 -s (--device_specific) <file> 404 Path to the python module containing device-specific 405 releasetools code. 406 407 -x (--extra) <key=value> 408 Add a key/value pair to the 'extras' dict, which device-specific 409 extension code may look at. 410 411 -v (--verbose) 412 Show command lines being executed. 413 414 -h (--help) 415 Display this usage message and exit. 416""" 417 418def Usage(docstring): 419 print docstring.rstrip("\n") 420 print COMMON_DOCSTRING 421 422 423def ParseOptions(argv, 424 docstring, 425 extra_opts="", extra_long_opts=(), 426 extra_option_handler=None): 427 """Parse the options in argv and return any arguments that aren't 428 flags. docstring is the calling module's docstring, to be displayed 429 for errors and -h. extra_opts and extra_long_opts are for flags 430 defined by the caller, which are processed by passing them to 431 extra_option_handler.""" 432 433 try: 434 opts, args = getopt.getopt( 435 argv, "hvp:s:x:" + extra_opts, 436 ["help", "verbose", "path=", "device_specific=", "extra="] + 437 list(extra_long_opts)) 438 except getopt.GetoptError, err: 439 Usage(docstring) 440 print "**", str(err), "**" 441 sys.exit(2) 442 443 path_specified = False 444 445 for o, a in opts: 446 if o in ("-h", "--help"): 447 Usage(docstring) 448 sys.exit() 449 elif o in ("-v", "--verbose"): 450 OPTIONS.verbose = True 451 elif o in ("-p", "--path"): 452 OPTIONS.search_path = a 453 elif o in ("-s", "--device_specific"): 454 OPTIONS.device_specific = a 455 elif o in ("-x", "--extra"): 456 key, value = a.split("=", 1) 457 OPTIONS.extras[key] = value 458 else: 459 if extra_option_handler is None or not extra_option_handler(o, a): 460 assert False, "unknown option \"%s\"" % (o,) 461 462 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") + 463 os.pathsep + os.environ["PATH"]) 464 465 return args 466 467 468def Cleanup(): 469 for i in OPTIONS.tempfiles: 470 if os.path.isdir(i): 471 shutil.rmtree(i) 472 else: 473 os.remove(i) 474 475 476class PasswordManager(object): 477 def __init__(self): 478 self.editor = os.getenv("EDITOR", None) 479 self.pwfile = os.getenv("ANDROID_PW_FILE", None) 480 481 def GetPasswords(self, items): 482 """Get passwords corresponding to each string in 'items', 483 returning a dict. (The dict may have keys in addition to the 484 values in 'items'.) 485 486 Uses the passwords in $ANDROID_PW_FILE if available, letting the 487 user edit that file to add more needed passwords. If no editor is 488 available, or $ANDROID_PW_FILE isn't define, prompts the user 489 interactively in the ordinary way. 490 """ 491 492 current = self.ReadFile() 493 494 first = True 495 while True: 496 missing = [] 497 for i in items: 498 if i not in current or not current[i]: 499 missing.append(i) 500 # Are all the passwords already in the file? 501 if not missing: return current 502 503 for i in missing: 504 current[i] = "" 505 506 if not first: 507 print "key file %s still missing some passwords." % (self.pwfile,) 508 answer = raw_input("try to edit again? [y]> ").strip() 509 if answer and answer[0] not in 'yY': 510 raise RuntimeError("key passwords unavailable") 511 first = False 512 513 current = self.UpdateAndReadFile(current) 514 515 def PromptResult(self, current): 516 """Prompt the user to enter a value (password) for each key in 517 'current' whose value is fales. Returns a new dict with all the 518 values. 519 """ 520 result = {} 521 for k, v in sorted(current.iteritems()): 522 if v: 523 result[k] = v 524 else: 525 while True: 526 result[k] = getpass.getpass("Enter password for %s key> " 527 % (k,)).strip() 528 if result[k]: break 529 return result 530 531 def UpdateAndReadFile(self, current): 532 if not self.editor or not self.pwfile: 533 return self.PromptResult(current) 534 535 f = open(self.pwfile, "w") 536 os.chmod(self.pwfile, 0600) 537 f.write("# Enter key passwords between the [[[ ]]] brackets.\n") 538 f.write("# (Additional spaces are harmless.)\n\n") 539 540 first_line = None 541 sorted = [(not v, k, v) for (k, v) in current.iteritems()] 542 sorted.sort() 543 for i, (_, k, v) in enumerate(sorted): 544 f.write("[[[ %s ]]] %s\n" % (v, k)) 545 if not v and first_line is None: 546 # position cursor on first line with no password. 547 first_line = i + 4 548 f.close() 549 550 p = Run([self.editor, "+%d" % (first_line,), self.pwfile]) 551 _, _ = p.communicate() 552 553 return self.ReadFile() 554 555 def ReadFile(self): 556 result = {} 557 if self.pwfile is None: return result 558 try: 559 f = open(self.pwfile, "r") 560 for line in f: 561 line = line.strip() 562 if not line or line[0] == '#': continue 563 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line) 564 if not m: 565 print "failed to parse password file: ", line 566 else: 567 result[m.group(2)] = m.group(1) 568 f.close() 569 except IOError, e: 570 if e.errno != errno.ENOENT: 571 print "error reading password file: ", str(e) 572 return result 573 574 575def ZipWriteStr(zip, filename, data, perms=0644): 576 # use a fixed timestamp so the output is repeatable. 577 zinfo = zipfile.ZipInfo(filename=filename, 578 date_time=(2009, 1, 1, 0, 0, 0)) 579 zinfo.compress_type = zip.compression 580 zinfo.external_attr = perms << 16 581 zip.writestr(zinfo, data) 582 583 584class DeviceSpecificParams(object): 585 module = None 586 def __init__(self, **kwargs): 587 """Keyword arguments to the constructor become attributes of this 588 object, which is passed to all functions in the device-specific 589 module.""" 590 for k, v in kwargs.iteritems(): 591 setattr(self, k, v) 592 self.extras = OPTIONS.extras 593 594 if self.module is None: 595 path = OPTIONS.device_specific 596 if not path: return 597 try: 598 if os.path.isdir(path): 599 info = imp.find_module("releasetools", [path]) 600 else: 601 d, f = os.path.split(path) 602 b, x = os.path.splitext(f) 603 if x == ".py": 604 f = b 605 info = imp.find_module(f, [d]) 606 self.module = imp.load_module("device_specific", *info) 607 except ImportError: 608 print "unable to load device-specific module; assuming none" 609 610 def _DoCall(self, function_name, *args, **kwargs): 611 """Call the named function in the device-specific module, passing 612 the given args and kwargs. The first argument to the call will be 613 the DeviceSpecific object itself. If there is no module, or the 614 module does not define the function, return the value of the 615 'default' kwarg (which itself defaults to None).""" 616 if self.module is None or not hasattr(self.module, function_name): 617 return kwargs.get("default", None) 618 return getattr(self.module, function_name)(*((self,) + args), **kwargs) 619 620 def FullOTA_Assertions(self): 621 """Called after emitting the block of assertions at the top of a 622 full OTA package. Implementations can add whatever additional 623 assertions they like.""" 624 return self._DoCall("FullOTA_Assertions") 625 626 def FullOTA_InstallEnd(self): 627 """Called at the end of full OTA installation; typically this is 628 used to install the image for the device's baseband processor.""" 629 return self._DoCall("FullOTA_InstallEnd") 630 631 def IncrementalOTA_Assertions(self): 632 """Called after emitting the block of assertions at the top of an 633 incremental OTA package. Implementations can add whatever 634 additional assertions they like.""" 635 return self._DoCall("IncrementalOTA_Assertions") 636 637 def IncrementalOTA_VerifyEnd(self): 638 """Called at the end of the verification phase of incremental OTA 639 installation; additional checks can be placed here to abort the 640 script before any changes are made.""" 641 return self._DoCall("IncrementalOTA_VerifyEnd") 642 643 def IncrementalOTA_InstallEnd(self): 644 """Called at the end of incremental OTA installation; typically 645 this is used to install the image for the device's baseband 646 processor.""" 647 return self._DoCall("IncrementalOTA_InstallEnd") 648 649class File(object): 650 def __init__(self, name, data): 651 self.name = name 652 self.data = data 653 self.size = len(data) 654 self.sha1 = sha.sha(data).hexdigest() 655 656 def WriteToTemp(self): 657 t = tempfile.NamedTemporaryFile() 658 t.write(self.data) 659 t.flush() 660 return t 661 662 def AddToZip(self, z): 663 ZipWriteStr(z, self.name, self.data) 664 665DIFF_PROGRAM_BY_EXT = { 666 ".gz" : "imgdiff", 667 ".zip" : ["imgdiff", "-z"], 668 ".jar" : ["imgdiff", "-z"], 669 ".apk" : ["imgdiff", "-z"], 670 ".img" : "imgdiff", 671 } 672 673class Difference(object): 674 def __init__(self, tf, sf): 675 self.tf = tf 676 self.sf = sf 677 self.patch = None 678 679 def ComputePatch(self): 680 """Compute the patch (as a string of data) needed to turn sf into 681 tf. Returns the same tuple as GetPatch().""" 682 683 tf = self.tf 684 sf = self.sf 685 686 ext = os.path.splitext(tf.name)[1] 687 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 688 689 ttemp = tf.WriteToTemp() 690 stemp = sf.WriteToTemp() 691 692 ext = os.path.splitext(tf.name)[1] 693 694 try: 695 ptemp = tempfile.NamedTemporaryFile() 696 if isinstance(diff_program, list): 697 cmd = copy.copy(diff_program) 698 else: 699 cmd = [diff_program] 700 cmd.append(stemp.name) 701 cmd.append(ttemp.name) 702 cmd.append(ptemp.name) 703 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 704 _, err = p.communicate() 705 if err or p.returncode != 0: 706 print "WARNING: failure running %s:\n%s\n" % (diff_program, err) 707 return None 708 diff = ptemp.read() 709 finally: 710 ptemp.close() 711 stemp.close() 712 ttemp.close() 713 714 self.patch = diff 715 return self.tf, self.sf, self.patch 716 717 718 def GetPatch(self): 719 """Return a tuple (target_file, source_file, patch_data). 720 patch_data may be None if ComputePatch hasn't been called, or if 721 computing the patch failed.""" 722 return self.tf, self.sf, self.patch 723 724 725def ComputeDifferences(diffs): 726 """Call ComputePatch on all the Difference objects in 'diffs'.""" 727 print len(diffs), "diffs to compute" 728 729 # Do the largest files first, to try and reduce the long-pole effect. 730 by_size = [(i.tf.size, i) for i in diffs] 731 by_size.sort(reverse=True) 732 by_size = [i[1] for i in by_size] 733 734 lock = threading.Lock() 735 diff_iter = iter(by_size) # accessed under lock 736 737 def worker(): 738 try: 739 lock.acquire() 740 for d in diff_iter: 741 lock.release() 742 start = time.time() 743 d.ComputePatch() 744 dur = time.time() - start 745 lock.acquire() 746 747 tf, sf, patch = d.GetPatch() 748 if sf.name == tf.name: 749 name = tf.name 750 else: 751 name = "%s (%s)" % (tf.name, sf.name) 752 if patch is None: 753 print "patching failed! %s" % (name,) 754 else: 755 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % ( 756 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name) 757 lock.release() 758 except Exception, e: 759 print e 760 raise 761 762 # start worker threads; wait for them all to finish. 763 threads = [threading.Thread(target=worker) 764 for i in range(OPTIONS.worker_threads)] 765 for th in threads: 766 th.start() 767 while threads: 768 threads.pop().join() 769