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