common.py revision 1eb74dd9a01ec14a2e41309986ef7efba790be8f
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("cache_size")
142  makeint("recovery_size")
143  makeint("boot_size")
144
145  d["fstab"] = LoadRecoveryFSTab(zip)
146  d["build.prop"] = LoadBuildProp(zip)
147  return d
148
149def LoadBuildProp(zip):
150  try:
151    data = zip.read("SYSTEM/build.prop")
152  except KeyError:
153    print "Warning: could not find SYSTEM/build.prop in %s" % zip
154    data = ""
155
156  d = {}
157  for line in data.split("\n"):
158    line = line.strip()
159    if not line or line.startswith("#"): continue
160    name, value = line.split("=", 1)
161    d[name] = value
162  return d
163
164def LoadRecoveryFSTab(zip):
165  class Partition(object):
166    pass
167
168  try:
169    data = zip.read("RECOVERY/RAMDISK/etc/recovery.fstab")
170  except KeyError:
171    print "Warning: could not find RECOVERY/RAMDISK/etc/recovery.fstab in %s." % zip
172    data = ""
173
174  d = {}
175  for line in data.split("\n"):
176    line = line.strip()
177    if not line or line.startswith("#"): continue
178    pieces = line.split()
179    if not (3 <= len(pieces) <= 4):
180      raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,))
181
182    p = Partition()
183    p.mount_point = pieces[0]
184    p.fs_type = pieces[1]
185    p.device = pieces[2]
186    p.length = 0
187    options = None
188    if len(pieces) >= 4:
189      if pieces[3].startswith("/"):
190        p.device2 = pieces[3]
191        if len(pieces) >= 5:
192          options = pieces[4]
193      else:
194        p.device2 = None
195        options = pieces[3]
196    else:
197      p.device2 = None
198
199    if options:
200      options = options.split(",")
201      for i in options:
202        if i.startswith("length="):
203          p.length = int(i[7:])
204        else:
205          print "%s: unknown option \"%s\"" % (p.mount_point, i)
206
207    d[p.mount_point] = p
208  return d
209
210
211def DumpInfoDict(d):
212  for k, v in sorted(d.items()):
213    print "%-25s = (%s) %s" % (k, type(v).__name__, v)
214
215def BuildBootableImage(sourcedir, fs_config_file, info_dict=None):
216  """Take a kernel, cmdline, and ramdisk directory from the input (in
217  'sourcedir'), and turn them into a boot image.  Return the image
218  data, or None if sourcedir does not appear to contains files for
219  building the requested image."""
220
221  if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
222      not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
223    return None
224
225  if info_dict is None:
226    info_dict = OPTIONS.info_dict
227
228  ramdisk_img = tempfile.NamedTemporaryFile()
229  img = tempfile.NamedTemporaryFile()
230
231  if os.access(fs_config_file, os.F_OK):
232    cmd = ["mkbootfs", "-f", fs_config_file, os.path.join(sourcedir, "RAMDISK")]
233  else:
234    cmd = ["mkbootfs", os.path.join(sourcedir, "RAMDISK")]
235  p1 = Run(cmd, stdout=subprocess.PIPE)
236  p2 = Run(["minigzip"],
237           stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
238
239  p2.wait()
240  p1.wait()
241  assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
242  assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
243
244  cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
245
246  fn = os.path.join(sourcedir, "cmdline")
247  if os.access(fn, os.F_OK):
248    cmd.append("--cmdline")
249    cmd.append(open(fn).read().rstrip("\n"))
250
251  fn = os.path.join(sourcedir, "base")
252  if os.access(fn, os.F_OK):
253    cmd.append("--base")
254    cmd.append(open(fn).read().rstrip("\n"))
255
256  fn = os.path.join(sourcedir, "pagesize")
257  if os.access(fn, os.F_OK):
258    cmd.append("--pagesize")
259    cmd.append(open(fn).read().rstrip("\n"))
260
261  args = info_dict.get("mkbootimg_args", None)
262  if args and args.strip():
263    cmd.extend(args.split())
264
265  cmd.extend(["--ramdisk", ramdisk_img.name,
266              "--output", img.name])
267
268  p = Run(cmd, stdout=subprocess.PIPE)
269  p.communicate()
270  assert p.returncode == 0, "mkbootimg of %s image failed" % (
271      os.path.basename(sourcedir),)
272
273  img.seek(os.SEEK_SET, 0)
274  data = img.read()
275
276  ramdisk_img.close()
277  img.close()
278
279  return data
280
281
282def GetBootableImage(name, prebuilt_name, unpack_dir, tree_subdir,
283                     info_dict=None):
284  """Return a File object (with name 'name') with the desired bootable
285  image.  Look for it in 'unpack_dir'/BOOTABLE_IMAGES under the name
286  'prebuilt_name', otherwise construct it from the source files in
287  'unpack_dir'/'tree_subdir'."""
288
289  prebuilt_path = os.path.join(unpack_dir, "BOOTABLE_IMAGES", prebuilt_name)
290  if os.path.exists(prebuilt_path):
291    print "using prebuilt %s..." % (prebuilt_name,)
292    return File.FromLocalFile(name, prebuilt_path)
293  else:
294    print "building image from target_files %s..." % (tree_subdir,)
295    fs_config = "META/" + tree_subdir.lower() + "_filesystem_config.txt"
296    return File(name, BuildBootableImage(os.path.join(unpack_dir, tree_subdir),
297                                         os.path.join(unpack_dir, fs_config),
298                                         info_dict))
299
300
301def UnzipTemp(filename, pattern=None):
302  """Unzip the given archive into a temporary directory and return the name.
303
304  If filename is of the form "foo.zip+bar.zip", unzip foo.zip into a
305  temp dir, then unzip bar.zip into that_dir/BOOTABLE_IMAGES.
306
307  Returns (tempdir, zipobj) where zipobj is a zipfile.ZipFile (of the
308  main file), open for reading.
309  """
310
311  tmp = tempfile.mkdtemp(prefix="targetfiles-")
312  OPTIONS.tempfiles.append(tmp)
313
314  def unzip_to_dir(filename, dirname):
315    cmd = ["unzip", "-o", "-q", filename, "-d", dirname]
316    if pattern is not None:
317      cmd.append(pattern)
318    p = Run(cmd, stdout=subprocess.PIPE)
319    p.communicate()
320    if p.returncode != 0:
321      raise ExternalError("failed to unzip input target-files \"%s\"" %
322                          (filename,))
323
324  m = re.match(r"^(.*[.]zip)\+(.*[.]zip)$", filename, re.IGNORECASE)
325  if m:
326    unzip_to_dir(m.group(1), tmp)
327    unzip_to_dir(m.group(2), os.path.join(tmp, "BOOTABLE_IMAGES"))
328    filename = m.group(1)
329  else:
330    unzip_to_dir(filename, tmp)
331
332  return tmp, zipfile.ZipFile(filename, "r")
333
334
335def GetKeyPasswords(keylist):
336  """Given a list of keys, prompt the user to enter passwords for
337  those which require them.  Return a {key: password} dict.  password
338  will be None if the key has no password."""
339
340  no_passwords = []
341  need_passwords = []
342  devnull = open("/dev/null", "w+b")
343  for k in sorted(keylist):
344    # We don't need a password for things that aren't really keys.
345    if k in SPECIAL_CERT_STRINGS:
346      no_passwords.append(k)
347      continue
348
349    p = Run(["openssl", "pkcs8", "-in", k+".pk8",
350             "-inform", "DER", "-nocrypt"],
351            stdin=devnull.fileno(),
352            stdout=devnull.fileno(),
353            stderr=subprocess.STDOUT)
354    p.communicate()
355    if p.returncode == 0:
356      no_passwords.append(k)
357    else:
358      need_passwords.append(k)
359  devnull.close()
360
361  key_passwords = PasswordManager().GetPasswords(need_passwords)
362  key_passwords.update(dict.fromkeys(no_passwords, None))
363  return key_passwords
364
365
366def SignFile(input_name, output_name, key, password, align=None,
367             whole_file=False):
368  """Sign the input_name zip/jar/apk, producing output_name.  Use the
369  given key and password (the latter may be None if the key does not
370  have a password.
371
372  If align is an integer > 1, zipalign is run to align stored files in
373  the output zip on 'align'-byte boundaries.
374
375  If whole_file is true, use the "-w" option to SignApk to embed a
376  signature that covers the whole file in the archive comment of the
377  zip file.
378  """
379
380  if align == 0 or align == 1:
381    align = None
382
383  if align:
384    temp = tempfile.NamedTemporaryFile()
385    sign_name = temp.name
386  else:
387    sign_name = output_name
388
389  cmd = ["java", "-Xmx2048m", "-jar",
390           os.path.join(OPTIONS.search_path, "framework", "signapk.jar")]
391  if whole_file:
392    cmd.append("-w")
393  cmd.extend([key + ".x509.pem", key + ".pk8",
394              input_name, sign_name])
395
396  p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
397  if password is not None:
398    password += "\n"
399  p.communicate(password)
400  if p.returncode != 0:
401    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
402
403  if align:
404    p = Run(["zipalign", "-f", str(align), sign_name, output_name])
405    p.communicate()
406    if p.returncode != 0:
407      raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
408    temp.close()
409
410
411def CheckSize(data, target, info_dict):
412  """Check the data string passed against the max size limit, if
413  any, for the given target.  Raise exception if the data is too big.
414  Print a warning if the data is nearing the maximum size."""
415
416  if target.endswith(".img"): target = target[:-4]
417  mount_point = "/" + target
418
419  if info_dict["fstab"]:
420    if mount_point == "/userdata": mount_point = "/data"
421    p = info_dict["fstab"][mount_point]
422    fs_type = p.fs_type
423    device = p.device
424    if "/" in device:
425      device = device[device.rfind("/")+1:]
426    limit = info_dict.get(device + "_size", None)
427  if not fs_type or not limit: return
428
429  if fs_type == "yaffs2":
430    # image size should be increased by 1/64th to account for the
431    # spare area (64 bytes per 2k page)
432    limit = limit / 2048 * (2048+64)
433  size = len(data)
434  pct = float(size) * 100.0 / limit
435  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
436  if pct >= 99.0:
437    raise ExternalError(msg)
438  elif pct >= 95.0:
439    print
440    print "  WARNING: ", msg
441    print
442  elif OPTIONS.verbose:
443    print "  ", msg
444
445
446def ReadApkCerts(tf_zip):
447  """Given a target_files ZipFile, parse the META/apkcerts.txt file
448  and return a {package: cert} dict."""
449  certmap = {}
450  for line in tf_zip.read("META/apkcerts.txt").split("\n"):
451    line = line.strip()
452    if not line: continue
453    m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
454                 r'private_key="(.*)"$', line)
455    if m:
456      name, cert, privkey = m.groups()
457      if cert in SPECIAL_CERT_STRINGS and not privkey:
458        certmap[name] = cert
459      elif (cert.endswith(".x509.pem") and
460            privkey.endswith(".pk8") and
461            cert[:-9] == privkey[:-4]):
462        certmap[name] = cert[:-9]
463      else:
464        raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
465  return certmap
466
467
468COMMON_DOCSTRING = """
469  -p  (--path)  <dir>
470      Prepend <dir>/bin to the list of places to search for binaries
471      run by this script, and expect to find jars in <dir>/framework.
472
473  -s  (--device_specific) <file>
474      Path to the python module containing device-specific
475      releasetools code.
476
477  -x  (--extra)  <key=value>
478      Add a key/value pair to the 'extras' dict, which device-specific
479      extension code may look at.
480
481  -v  (--verbose)
482      Show command lines being executed.
483
484  -h  (--help)
485      Display this usage message and exit.
486"""
487
488def Usage(docstring):
489  print docstring.rstrip("\n")
490  print COMMON_DOCSTRING
491
492
493def ParseOptions(argv,
494                 docstring,
495                 extra_opts="", extra_long_opts=(),
496                 extra_option_handler=None):
497  """Parse the options in argv and return any arguments that aren't
498  flags.  docstring is the calling module's docstring, to be displayed
499  for errors and -h.  extra_opts and extra_long_opts are for flags
500  defined by the caller, which are processed by passing them to
501  extra_option_handler."""
502
503  try:
504    opts, args = getopt.getopt(
505        argv, "hvp:s:x:" + extra_opts,
506        ["help", "verbose", "path=", "device_specific=", "extra="] +
507          list(extra_long_opts))
508  except getopt.GetoptError, err:
509    Usage(docstring)
510    print "**", str(err), "**"
511    sys.exit(2)
512
513  path_specified = False
514
515  for o, a in opts:
516    if o in ("-h", "--help"):
517      Usage(docstring)
518      sys.exit()
519    elif o in ("-v", "--verbose"):
520      OPTIONS.verbose = True
521    elif o in ("-p", "--path"):
522      OPTIONS.search_path = a
523    elif o in ("-s", "--device_specific"):
524      OPTIONS.device_specific = a
525    elif o in ("-x", "--extra"):
526      key, value = a.split("=", 1)
527      OPTIONS.extras[key] = value
528    else:
529      if extra_option_handler is None or not extra_option_handler(o, a):
530        assert False, "unknown option \"%s\"" % (o,)
531
532  os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
533                        os.pathsep + os.environ["PATH"])
534
535  return args
536
537
538def Cleanup():
539  for i in OPTIONS.tempfiles:
540    if os.path.isdir(i):
541      shutil.rmtree(i)
542    else:
543      os.remove(i)
544
545
546class PasswordManager(object):
547  def __init__(self):
548    self.editor = os.getenv("EDITOR", None)
549    self.pwfile = os.getenv("ANDROID_PW_FILE", None)
550
551  def GetPasswords(self, items):
552    """Get passwords corresponding to each string in 'items',
553    returning a dict.  (The dict may have keys in addition to the
554    values in 'items'.)
555
556    Uses the passwords in $ANDROID_PW_FILE if available, letting the
557    user edit that file to add more needed passwords.  If no editor is
558    available, or $ANDROID_PW_FILE isn't define, prompts the user
559    interactively in the ordinary way.
560    """
561
562    current = self.ReadFile()
563
564    first = True
565    while True:
566      missing = []
567      for i in items:
568        if i not in current or not current[i]:
569          missing.append(i)
570      # Are all the passwords already in the file?
571      if not missing: return current
572
573      for i in missing:
574        current[i] = ""
575
576      if not first:
577        print "key file %s still missing some passwords." % (self.pwfile,)
578        answer = raw_input("try to edit again? [y]> ").strip()
579        if answer and answer[0] not in 'yY':
580          raise RuntimeError("key passwords unavailable")
581      first = False
582
583      current = self.UpdateAndReadFile(current)
584
585  def PromptResult(self, current):
586    """Prompt the user to enter a value (password) for each key in
587    'current' whose value is fales.  Returns a new dict with all the
588    values.
589    """
590    result = {}
591    for k, v in sorted(current.iteritems()):
592      if v:
593        result[k] = v
594      else:
595        while True:
596          result[k] = getpass.getpass("Enter password for %s key> "
597                                      % (k,)).strip()
598          if result[k]: break
599    return result
600
601  def UpdateAndReadFile(self, current):
602    if not self.editor or not self.pwfile:
603      return self.PromptResult(current)
604
605    f = open(self.pwfile, "w")
606    os.chmod(self.pwfile, 0600)
607    f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
608    f.write("# (Additional spaces are harmless.)\n\n")
609
610    first_line = None
611    sorted = [(not v, k, v) for (k, v) in current.iteritems()]
612    sorted.sort()
613    for i, (_, k, v) in enumerate(sorted):
614      f.write("[[[  %s  ]]] %s\n" % (v, k))
615      if not v and first_line is None:
616        # position cursor on first line with no password.
617        first_line = i + 4
618    f.close()
619
620    p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
621    _, _ = p.communicate()
622
623    return self.ReadFile()
624
625  def ReadFile(self):
626    result = {}
627    if self.pwfile is None: return result
628    try:
629      f = open(self.pwfile, "r")
630      for line in f:
631        line = line.strip()
632        if not line or line[0] == '#': continue
633        m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
634        if not m:
635          print "failed to parse password file: ", line
636        else:
637          result[m.group(2)] = m.group(1)
638      f.close()
639    except IOError, e:
640      if e.errno != errno.ENOENT:
641        print "error reading password file: ", str(e)
642    return result
643
644
645def ZipWriteStr(zip, filename, data, perms=0644):
646  # use a fixed timestamp so the output is repeatable.
647  zinfo = zipfile.ZipInfo(filename=filename,
648                          date_time=(2009, 1, 1, 0, 0, 0))
649  zinfo.compress_type = zip.compression
650  zinfo.external_attr = perms << 16
651  zip.writestr(zinfo, data)
652
653
654class DeviceSpecificParams(object):
655  module = None
656  def __init__(self, **kwargs):
657    """Keyword arguments to the constructor become attributes of this
658    object, which is passed to all functions in the device-specific
659    module."""
660    for k, v in kwargs.iteritems():
661      setattr(self, k, v)
662    self.extras = OPTIONS.extras
663
664    if self.module is None:
665      path = OPTIONS.device_specific
666      if not path: return
667      try:
668        if os.path.isdir(path):
669          info = imp.find_module("releasetools", [path])
670        else:
671          d, f = os.path.split(path)
672          b, x = os.path.splitext(f)
673          if x == ".py":
674            f = b
675          info = imp.find_module(f, [d])
676        self.module = imp.load_module("device_specific", *info)
677      except ImportError:
678        print "unable to load device-specific module; assuming none"
679
680  def _DoCall(self, function_name, *args, **kwargs):
681    """Call the named function in the device-specific module, passing
682    the given args and kwargs.  The first argument to the call will be
683    the DeviceSpecific object itself.  If there is no module, or the
684    module does not define the function, return the value of the
685    'default' kwarg (which itself defaults to None)."""
686    if self.module is None or not hasattr(self.module, function_name):
687      return kwargs.get("default", None)
688    return getattr(self.module, function_name)(*((self,) + args), **kwargs)
689
690  def FullOTA_Assertions(self):
691    """Called after emitting the block of assertions at the top of a
692    full OTA package.  Implementations can add whatever additional
693    assertions they like."""
694    return self._DoCall("FullOTA_Assertions")
695
696  def FullOTA_InstallBegin(self):
697    """Called at the start of full OTA installation."""
698    return self._DoCall("FullOTA_InstallBegin")
699
700  def FullOTA_InstallEnd(self):
701    """Called at the end of full OTA installation; typically this is
702    used to install the image for the device's baseband processor."""
703    return self._DoCall("FullOTA_InstallEnd")
704
705  def IncrementalOTA_Assertions(self):
706    """Called after emitting the block of assertions at the top of an
707    incremental OTA package.  Implementations can add whatever
708    additional assertions they like."""
709    return self._DoCall("IncrementalOTA_Assertions")
710
711  def IncrementalOTA_VerifyBegin(self):
712    """Called at the start of the verification phase of incremental
713    OTA installation; additional checks can be placed here to abort
714    the script before any changes are made."""
715    return self._DoCall("IncrementalOTA_VerifyBegin")
716
717  def IncrementalOTA_VerifyEnd(self):
718    """Called at the end of the verification phase of incremental OTA
719    installation; additional checks can be placed here to abort the
720    script before any changes are made."""
721    return self._DoCall("IncrementalOTA_VerifyEnd")
722
723  def IncrementalOTA_InstallBegin(self):
724    """Called at the start of incremental OTA installation (after
725    verification is complete)."""
726    return self._DoCall("IncrementalOTA_InstallBegin")
727
728  def IncrementalOTA_InstallEnd(self):
729    """Called at the end of incremental OTA installation; typically
730    this is used to install the image for the device's baseband
731    processor."""
732    return self._DoCall("IncrementalOTA_InstallEnd")
733
734class File(object):
735  def __init__(self, name, data):
736    self.name = name
737    self.data = data
738    self.size = len(data)
739    self.sha1 = sha1(data).hexdigest()
740
741  @classmethod
742  def FromLocalFile(cls, name, diskname):
743    f = open(diskname, "rb")
744    data = f.read()
745    f.close()
746    return File(name, data)
747
748  def WriteToTemp(self):
749    t = tempfile.NamedTemporaryFile()
750    t.write(self.data)
751    t.flush()
752    return t
753
754  def AddToZip(self, z):
755    ZipWriteStr(z, self.name, self.data)
756
757DIFF_PROGRAM_BY_EXT = {
758    ".gz" : "imgdiff",
759    ".zip" : ["imgdiff", "-z"],
760    ".jar" : ["imgdiff", "-z"],
761    ".apk" : ["imgdiff", "-z"],
762    ".img" : "imgdiff",
763    }
764
765class Difference(object):
766  def __init__(self, tf, sf, diff_program=None):
767    self.tf = tf
768    self.sf = sf
769    self.patch = None
770    self.diff_program = diff_program
771
772  def ComputePatch(self):
773    """Compute the patch (as a string of data) needed to turn sf into
774    tf.  Returns the same tuple as GetPatch()."""
775
776    tf = self.tf
777    sf = self.sf
778
779    if self.diff_program:
780      diff_program = self.diff_program
781    else:
782      ext = os.path.splitext(tf.name)[1]
783      diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
784
785    ttemp = tf.WriteToTemp()
786    stemp = sf.WriteToTemp()
787
788    ext = os.path.splitext(tf.name)[1]
789
790    try:
791      ptemp = tempfile.NamedTemporaryFile()
792      if isinstance(diff_program, list):
793        cmd = copy.copy(diff_program)
794      else:
795        cmd = [diff_program]
796      cmd.append(stemp.name)
797      cmd.append(ttemp.name)
798      cmd.append(ptemp.name)
799      p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
800      _, err = p.communicate()
801      if err or p.returncode != 0:
802        print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
803        return None
804      diff = ptemp.read()
805    finally:
806      ptemp.close()
807      stemp.close()
808      ttemp.close()
809
810    self.patch = diff
811    return self.tf, self.sf, self.patch
812
813
814  def GetPatch(self):
815    """Return a tuple (target_file, source_file, patch_data).
816    patch_data may be None if ComputePatch hasn't been called, or if
817    computing the patch failed."""
818    return self.tf, self.sf, self.patch
819
820
821def ComputeDifferences(diffs):
822  """Call ComputePatch on all the Difference objects in 'diffs'."""
823  print len(diffs), "diffs to compute"
824
825  # Do the largest files first, to try and reduce the long-pole effect.
826  by_size = [(i.tf.size, i) for i in diffs]
827  by_size.sort(reverse=True)
828  by_size = [i[1] for i in by_size]
829
830  lock = threading.Lock()
831  diff_iter = iter(by_size)   # accessed under lock
832
833  def worker():
834    try:
835      lock.acquire()
836      for d in diff_iter:
837        lock.release()
838        start = time.time()
839        d.ComputePatch()
840        dur = time.time() - start
841        lock.acquire()
842
843        tf, sf, patch = d.GetPatch()
844        if sf.name == tf.name:
845          name = tf.name
846        else:
847          name = "%s (%s)" % (tf.name, sf.name)
848        if patch is None:
849          print "patching failed!                                  %s" % (name,)
850        else:
851          print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
852              dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
853      lock.release()
854    except Exception, e:
855      print e
856      raise
857
858  # start worker threads; wait for them all to finish.
859  threads = [threading.Thread(target=worker)
860             for i in range(OPTIONS.worker_threads)]
861  for th in threads:
862    th.start()
863  while threads:
864    threads.pop().join()
865
866
867# map recovery.fstab's fs_types to mount/format "partition types"
868PARTITION_TYPES = { "yaffs2": "MTD", "mtd": "MTD",
869                    "ext4": "EMMC", "emmc": "EMMC" }
870
871def GetTypeAndDevice(mount_point, info):
872  fstab = info["fstab"]
873  if fstab:
874    return PARTITION_TYPES[fstab[mount_point].fs_type], fstab[mount_point].device
875  else:
876    return None
877