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