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