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