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