common.py revision 293fd135c7bc0c21b41f1782d21c26de64e8854a
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 shlex
24import shutil
25import subprocess
26import sys
27import tempfile
28import threading
29import time
30import zipfile
31
32import blockimgdiff
33
34from hashlib import sha1 as sha1
35
36
37class Options(object):
38  def __init__(self):
39    platform_search_path = {
40        "linux2": "out/host/linux-x86",
41        "darwin": "out/host/darwin-x86",
42    }
43
44    self.search_path = platform_search_path.get(sys.platform, None)
45    self.signapk_path = "framework/signapk.jar"  # Relative to search_path
46    self.signapk_shared_library_path = "lib64"   # Relative to search_path
47    self.extra_signapk_args = []
48    self.java_path = "java"  # Use the one on the path by default.
49    self.java_args = "-Xmx2048m" # JVM Args
50    self.public_key_suffix = ".x509.pem"
51    self.private_key_suffix = ".pk8"
52    # use otatools built boot_signer by default
53    self.boot_signer_path = "boot_signer"
54    self.boot_signer_args = []
55    self.verity_signer_path = None
56    self.verity_signer_args = []
57    self.verbose = False
58    self.tempfiles = []
59    self.device_specific = None
60    self.extras = {}
61    self.info_dict = None
62    self.source_info_dict = None
63    self.target_info_dict = None
64    self.worker_threads = None
65    # Stash size cannot exceed cache_size * threshold.
66    self.cache_size = None
67    self.stash_threshold = 0.8
68
69
70OPTIONS = Options()
71
72
73# Values for "certificate" in apkcerts that mean special things.
74SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL")
75
76class ErrorCode(object):
77  """Define error_codes for failures that happen during the actual
78  update package installation.
79
80  Error codes 0-999 are reserved for failures before the package
81  installation (i.e. low battery, package verification failure).
82  Detailed code in 'bootable/recovery/error_code.h' """
83
84  SYSTEM_VERIFICATION_FAILURE = 1000
85  SYSTEM_UPDATE_FAILURE = 1001
86  SYSTEM_UNEXPECTED_CONTENTS = 1002
87  SYSTEM_NONZERO_CONTENTS = 1003
88  SYSTEM_RECOVER_FAILURE = 1004
89  VENDOR_VERIFICATION_FAILURE = 2000
90  VENDOR_UPDATE_FAILURE = 2001
91  VENDOR_UNEXPECTED_CONTENTS = 2002
92  VENDOR_NONZERO_CONTENTS = 2003
93  VENDOR_RECOVER_FAILURE = 2004
94  OEM_PROP_MISMATCH = 3000
95  FINGERPRINT_MISMATCH = 3001
96  THUMBPRINT_MISMATCH = 3002
97  OLDER_BUILD = 3003
98  DEVICE_MISMATCH = 3004
99  BAD_PATCH_FILE = 3005
100  INSUFFICIENT_CACHE_SPACE = 3006
101  TUNE_PARTITION_FAILURE = 3007
102  APPLY_PATCH_FAILURE = 3008
103
104class ExternalError(RuntimeError):
105  pass
106
107
108def Run(args, **kwargs):
109  """Create and return a subprocess.Popen object, printing the command
110  line on the terminal if -v was specified."""
111  if OPTIONS.verbose:
112    print "  running: ", " ".join(args)
113  return subprocess.Popen(args, **kwargs)
114
115
116def CloseInheritedPipes():
117  """ Gmake in MAC OS has file descriptor (PIPE) leak. We close those fds
118  before doing other work."""
119  if platform.system() != "Darwin":
120    return
121  for d in range(3, 1025):
122    try:
123      stat = os.fstat(d)
124      if stat is not None:
125        pipebit = stat[0] & 0x1000
126        if pipebit != 0:
127          os.close(d)
128    except OSError:
129      pass
130
131
132def LoadInfoDict(input_file, input_dir=None):
133  """Read and parse the META/misc_info.txt key/value pairs from the
134  input target files and return a dict."""
135
136  def read_helper(fn):
137    if isinstance(input_file, zipfile.ZipFile):
138      return input_file.read(fn)
139    else:
140      path = os.path.join(input_file, *fn.split("/"))
141      try:
142        with open(path) as f:
143          return f.read()
144      except IOError as e:
145        if e.errno == errno.ENOENT:
146          raise KeyError(fn)
147  d = {}
148  try:
149    d = LoadDictionaryFromLines(read_helper("META/misc_info.txt").split("\n"))
150  except KeyError:
151    # ok if misc_info.txt doesn't exist
152    pass
153
154  # backwards compatibility: These values used to be in their own
155  # files.  Look for them, in case we're processing an old
156  # target_files zip.
157
158  if "mkyaffs2_extra_flags" not in d:
159    try:
160      d["mkyaffs2_extra_flags"] = read_helper(
161          "META/mkyaffs2-extra-flags.txt").strip()
162    except KeyError:
163      # ok if flags don't exist
164      pass
165
166  if "recovery_api_version" not in d:
167    try:
168      d["recovery_api_version"] = read_helper(
169          "META/recovery-api-version.txt").strip()
170    except KeyError:
171      raise ValueError("can't find recovery API version in input target-files")
172
173  if "tool_extensions" not in d:
174    try:
175      d["tool_extensions"] = read_helper("META/tool-extensions.txt").strip()
176    except KeyError:
177      # ok if extensions don't exist
178      pass
179
180  if "fstab_version" not in d:
181    d["fstab_version"] = "1"
182
183  # A few properties are stored as links to the files in the out/ directory.
184  # It works fine with the build system. However, they are no longer available
185  # when (re)generating from target_files zip. If input_dir is not None, we
186  # are doing repacking. Redirect those properties to the actual files in the
187  # unzipped directory.
188  if input_dir is not None:
189    # We carry a copy of file_contexts.bin under META/. If not available,
190    # search BOOT/RAMDISK/. Note that sometimes we may need a different file
191    # to build images than the one running on device, such as when enabling
192    # system_root_image. In that case, we must have the one for image
193    # generation copied to META/.
194    fc_basename = os.path.basename(d.get("selinux_fc", "file_contexts"))
195    fc_config = os.path.join(input_dir, "META", fc_basename)
196    if d.get("system_root_image") == "true":
197      assert os.path.exists(fc_config)
198    if not os.path.exists(fc_config):
199      fc_config = os.path.join(input_dir, "BOOT", "RAMDISK", fc_basename)
200      if not os.path.exists(fc_config):
201        fc_config = None
202
203    if fc_config:
204      d["selinux_fc"] = fc_config
205
206    # Similarly we need to redirect "ramdisk_dir" and "ramdisk_fs_config".
207    if d.get("system_root_image") == "true":
208      d["ramdisk_dir"] = os.path.join(input_dir, "ROOT")
209      d["ramdisk_fs_config"] = os.path.join(
210          input_dir, "META", "root_filesystem_config.txt")
211
212    # Redirect {system,vendor}_base_fs_file.
213    if "system_base_fs_file" in d:
214      basename = os.path.basename(d["system_base_fs_file"])
215      system_base_fs_file = os.path.join(input_dir, "META", basename)
216      if os.path.exists(system_base_fs_file):
217        d["system_base_fs_file"] = system_base_fs_file
218      else:
219        print "Warning: failed to find system base fs file: %s" % (
220            system_base_fs_file,)
221        del d["system_base_fs_file"]
222
223    if "vendor_base_fs_file" in d:
224      basename = os.path.basename(d["vendor_base_fs_file"])
225      vendor_base_fs_file = os.path.join(input_dir, "META", basename)
226      if os.path.exists(vendor_base_fs_file):
227        d["vendor_base_fs_file"] = vendor_base_fs_file
228      else:
229        print "Warning: failed to find vendor base fs file: %s" % (
230            vendor_base_fs_file,)
231        del d["vendor_base_fs_file"]
232
233  try:
234    data = read_helper("META/imagesizes.txt")
235    for line in data.split("\n"):
236      if not line:
237        continue
238      name, value = line.split(" ", 1)
239      if not value:
240        continue
241      if name == "blocksize":
242        d[name] = value
243      else:
244        d[name + "_size"] = value
245  except KeyError:
246    pass
247
248  def makeint(key):
249    if key in d:
250      d[key] = int(d[key], 0)
251
252  makeint("recovery_api_version")
253  makeint("blocksize")
254  makeint("system_size")
255  makeint("vendor_size")
256  makeint("userdata_size")
257  makeint("cache_size")
258  makeint("recovery_size")
259  makeint("boot_size")
260  makeint("fstab_version")
261
262  if d.get("no_recovery", False) == "true":
263    d["fstab"] = None
264  else:
265    d["fstab"] = LoadRecoveryFSTab(read_helper, d["fstab_version"],
266                                   d.get("system_root_image", False))
267  d["build.prop"] = LoadBuildProp(read_helper)
268  return d
269
270def LoadBuildProp(read_helper):
271  try:
272    data = read_helper("SYSTEM/build.prop")
273  except KeyError:
274    print "Warning: could not find SYSTEM/build.prop in %s" % zip
275    data = ""
276  return LoadDictionaryFromLines(data.split("\n"))
277
278def LoadDictionaryFromLines(lines):
279  d = {}
280  for line in lines:
281    line = line.strip()
282    if not line or line.startswith("#"):
283      continue
284    if "=" in line:
285      name, value = line.split("=", 1)
286      d[name] = value
287  return d
288
289def LoadRecoveryFSTab(read_helper, fstab_version, system_root_image=False):
290  class Partition(object):
291    def __init__(self, mount_point, fs_type, device, length, device2, context):
292      self.mount_point = mount_point
293      self.fs_type = fs_type
294      self.device = device
295      self.length = length
296      self.device2 = device2
297      self.context = context
298
299  try:
300    data = read_helper("RECOVERY/RAMDISK/etc/recovery.fstab")
301  except KeyError:
302    print "Warning: could not find RECOVERY/RAMDISK/etc/recovery.fstab"
303    data = ""
304
305  if fstab_version == 1:
306    d = {}
307    for line in data.split("\n"):
308      line = line.strip()
309      if not line or line.startswith("#"):
310        continue
311      pieces = line.split()
312      if not 3 <= len(pieces) <= 4:
313        raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,))
314      options = None
315      if len(pieces) >= 4:
316        if pieces[3].startswith("/"):
317          device2 = pieces[3]
318          if len(pieces) >= 5:
319            options = pieces[4]
320        else:
321          device2 = None
322          options = pieces[3]
323      else:
324        device2 = None
325
326      mount_point = pieces[0]
327      length = 0
328      if options:
329        options = options.split(",")
330        for i in options:
331          if i.startswith("length="):
332            length = int(i[7:])
333          else:
334            print "%s: unknown option \"%s\"" % (mount_point, i)
335
336      d[mount_point] = Partition(mount_point=mount_point, fs_type=pieces[1],
337                                 device=pieces[2], length=length,
338                                 device2=device2)
339
340  elif fstab_version == 2:
341    d = {}
342    for line in data.split("\n"):
343      line = line.strip()
344      if not line or line.startswith("#"):
345        continue
346      # <src> <mnt_point> <type> <mnt_flags and options> <fs_mgr_flags>
347      pieces = line.split()
348      if len(pieces) != 5:
349        raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,))
350
351      # Ignore entries that are managed by vold
352      options = pieces[4]
353      if "voldmanaged=" in options:
354        continue
355
356      # It's a good line, parse it
357      length = 0
358      options = options.split(",")
359      for i in options:
360        if i.startswith("length="):
361          length = int(i[7:])
362        else:
363          # Ignore all unknown options in the unified fstab
364          continue
365
366      mount_flags = pieces[3]
367      # Honor the SELinux context if present.
368      context = None
369      for i in mount_flags.split(","):
370        if i.startswith("context="):
371          context = i
372
373      mount_point = pieces[1]
374      d[mount_point] = Partition(mount_point=mount_point, fs_type=pieces[2],
375                                 device=pieces[0], length=length,
376                                 device2=None, context=context)
377
378  else:
379    raise ValueError("Unknown fstab_version: \"%d\"" % (fstab_version,))
380
381  # / is used for the system mount point when the root directory is included in
382  # system. Other areas assume system is always at "/system" so point /system
383  # at /.
384  if system_root_image:
385    assert not d.has_key("/system") and d.has_key("/")
386    d["/system"] = d["/"]
387  return d
388
389
390def DumpInfoDict(d):
391  for k, v in sorted(d.items()):
392    print "%-25s = (%s) %s" % (k, type(v).__name__, v)
393
394
395def _BuildBootableImage(sourcedir, fs_config_file, info_dict=None,
396                        has_ramdisk=False):
397  """Build a bootable image from the specified sourcedir.
398
399  Take a kernel, cmdline, and optionally a ramdisk directory from the input (in
400  'sourcedir'), and turn them into a boot image.  Return the image data, or
401  None if sourcedir does not appear to contains files for building the
402  requested image."""
403
404  def make_ramdisk():
405    ramdisk_img = tempfile.NamedTemporaryFile()
406
407    if os.access(fs_config_file, os.F_OK):
408      cmd = ["mkbootfs", "-f", fs_config_file,
409             os.path.join(sourcedir, "RAMDISK")]
410    else:
411      cmd = ["mkbootfs", os.path.join(sourcedir, "RAMDISK")]
412    p1 = Run(cmd, stdout=subprocess.PIPE)
413    p2 = Run(["minigzip"], stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
414
415    p2.wait()
416    p1.wait()
417    assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (sourcedir,)
418    assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (sourcedir,)
419
420    return ramdisk_img
421
422  if not os.access(os.path.join(sourcedir, "kernel"), os.F_OK):
423    return None
424
425  if has_ramdisk and not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK):
426    return None
427
428  if info_dict is None:
429    info_dict = OPTIONS.info_dict
430
431  img = tempfile.NamedTemporaryFile()
432
433  if has_ramdisk:
434    ramdisk_img = make_ramdisk()
435
436  # use MKBOOTIMG from environ, or "mkbootimg" if empty or not set
437  mkbootimg = os.getenv('MKBOOTIMG') or "mkbootimg"
438
439  cmd = [mkbootimg, "--kernel", os.path.join(sourcedir, "kernel")]
440
441  fn = os.path.join(sourcedir, "second")
442  if os.access(fn, os.F_OK):
443    cmd.append("--second")
444    cmd.append(fn)
445
446  fn = os.path.join(sourcedir, "cmdline")
447  if os.access(fn, os.F_OK):
448    cmd.append("--cmdline")
449    cmd.append(open(fn).read().rstrip("\n"))
450
451  fn = os.path.join(sourcedir, "base")
452  if os.access(fn, os.F_OK):
453    cmd.append("--base")
454    cmd.append(open(fn).read().rstrip("\n"))
455
456  fn = os.path.join(sourcedir, "pagesize")
457  if os.access(fn, os.F_OK):
458    cmd.append("--pagesize")
459    cmd.append(open(fn).read().rstrip("\n"))
460
461  args = info_dict.get("mkbootimg_args", None)
462  if args and args.strip():
463    cmd.extend(shlex.split(args))
464
465  args = info_dict.get("mkbootimg_version_args", None)
466  if args and args.strip():
467    cmd.extend(shlex.split(args))
468
469  if has_ramdisk:
470    cmd.extend(["--ramdisk", ramdisk_img.name])
471
472  img_unsigned = None
473  if info_dict.get("vboot", None):
474    img_unsigned = tempfile.NamedTemporaryFile()
475    cmd.extend(["--output", img_unsigned.name])
476  else:
477    cmd.extend(["--output", img.name])
478
479  p = Run(cmd, stdout=subprocess.PIPE)
480  p.communicate()
481  assert p.returncode == 0, "mkbootimg of %s image failed" % (
482      os.path.basename(sourcedir),)
483
484  if (info_dict.get("boot_signer", None) == "true" and
485      info_dict.get("verity_key", None)):
486    path = "/" + os.path.basename(sourcedir).lower()
487    cmd = [OPTIONS.boot_signer_path]
488    cmd.extend(OPTIONS.boot_signer_args)
489    cmd.extend([path, img.name,
490                info_dict["verity_key"] + ".pk8",
491                info_dict["verity_key"] + ".x509.pem", img.name])
492    p = Run(cmd, stdout=subprocess.PIPE)
493    p.communicate()
494    assert p.returncode == 0, "boot_signer of %s image failed" % path
495
496  # Sign the image if vboot is non-empty.
497  elif info_dict.get("vboot", None):
498    path = "/" + os.path.basename(sourcedir).lower()
499    img_keyblock = tempfile.NamedTemporaryFile()
500    cmd = [info_dict["vboot_signer_cmd"], info_dict["futility"],
501           img_unsigned.name, info_dict["vboot_key"] + ".vbpubk",
502           info_dict["vboot_key"] + ".vbprivk",
503           info_dict["vboot_subkey"] + ".vbprivk",
504           img_keyblock.name,
505           img.name]
506    p = Run(cmd, stdout=subprocess.PIPE)
507    p.communicate()
508    assert p.returncode == 0, "vboot_signer of %s image failed" % path
509
510    # Clean up the temp files.
511    img_unsigned.close()
512    img_keyblock.close()
513
514  img.seek(os.SEEK_SET, 0)
515  data = img.read()
516
517  if has_ramdisk:
518    ramdisk_img.close()
519  img.close()
520
521  return data
522
523
524def GetBootableImage(name, prebuilt_name, unpack_dir, tree_subdir,
525                     info_dict=None):
526  """Return a File object with the desired bootable image.
527
528  Look for it in 'unpack_dir'/BOOTABLE_IMAGES under the name 'prebuilt_name',
529  otherwise look for it under 'unpack_dir'/IMAGES, otherwise construct it from
530  the source files in 'unpack_dir'/'tree_subdir'."""
531
532  prebuilt_path = os.path.join(unpack_dir, "BOOTABLE_IMAGES", prebuilt_name)
533  if os.path.exists(prebuilt_path):
534    print "using prebuilt %s from BOOTABLE_IMAGES..." % (prebuilt_name,)
535    return File.FromLocalFile(name, prebuilt_path)
536
537  prebuilt_path = os.path.join(unpack_dir, "IMAGES", prebuilt_name)
538  if os.path.exists(prebuilt_path):
539    print "using prebuilt %s from IMAGES..." % (prebuilt_name,)
540    return File.FromLocalFile(name, prebuilt_path)
541
542  print "building image from target_files %s..." % (tree_subdir,)
543
544  if info_dict is None:
545    info_dict = OPTIONS.info_dict
546
547  # With system_root_image == "true", we don't pack ramdisk into the boot image.
548  # Unless "recovery_as_boot" is specified, in which case we carry the ramdisk
549  # for recovery.
550  has_ramdisk = (info_dict.get("system_root_image") != "true" or
551                 prebuilt_name != "boot.img" or
552                 info_dict.get("recovery_as_boot") == "true")
553
554  fs_config = "META/" + tree_subdir.lower() + "_filesystem_config.txt"
555  data = _BuildBootableImage(os.path.join(unpack_dir, tree_subdir),
556                             os.path.join(unpack_dir, fs_config),
557                             info_dict, has_ramdisk)
558  if data:
559    return File(name, data)
560  return None
561
562
563def UnzipTemp(filename, pattern=None):
564  """Unzip the given archive into a temporary directory and return the name.
565
566  If filename is of the form "foo.zip+bar.zip", unzip foo.zip into a
567  temp dir, then unzip bar.zip into that_dir/BOOTABLE_IMAGES.
568
569  Returns (tempdir, zipobj) where zipobj is a zipfile.ZipFile (of the
570  main file), open for reading.
571  """
572
573  tmp = tempfile.mkdtemp(prefix="targetfiles-")
574  OPTIONS.tempfiles.append(tmp)
575
576  def unzip_to_dir(filename, dirname):
577    cmd = ["unzip", "-o", "-q", filename, "-d", dirname]
578    if pattern is not None:
579      cmd.append(pattern)
580    p = Run(cmd, stdout=subprocess.PIPE)
581    p.communicate()
582    if p.returncode != 0:
583      raise ExternalError("failed to unzip input target-files \"%s\"" %
584                          (filename,))
585
586  m = re.match(r"^(.*[.]zip)\+(.*[.]zip)$", filename, re.IGNORECASE)
587  if m:
588    unzip_to_dir(m.group(1), tmp)
589    unzip_to_dir(m.group(2), os.path.join(tmp, "BOOTABLE_IMAGES"))
590    filename = m.group(1)
591  else:
592    unzip_to_dir(filename, tmp)
593
594  return tmp, zipfile.ZipFile(filename, "r")
595
596
597def GetKeyPasswords(keylist):
598  """Given a list of keys, prompt the user to enter passwords for
599  those which require them.  Return a {key: password} dict.  password
600  will be None if the key has no password."""
601
602  no_passwords = []
603  need_passwords = []
604  key_passwords = {}
605  devnull = open("/dev/null", "w+b")
606  for k in sorted(keylist):
607    # We don't need a password for things that aren't really keys.
608    if k in SPECIAL_CERT_STRINGS:
609      no_passwords.append(k)
610      continue
611
612    p = Run(["openssl", "pkcs8", "-in", k+OPTIONS.private_key_suffix,
613             "-inform", "DER", "-nocrypt"],
614            stdin=devnull.fileno(),
615            stdout=devnull.fileno(),
616            stderr=subprocess.STDOUT)
617    p.communicate()
618    if p.returncode == 0:
619      # Definitely an unencrypted key.
620      no_passwords.append(k)
621    else:
622      p = Run(["openssl", "pkcs8", "-in", k+OPTIONS.private_key_suffix,
623               "-inform", "DER", "-passin", "pass:"],
624              stdin=devnull.fileno(),
625              stdout=devnull.fileno(),
626              stderr=subprocess.PIPE)
627      _, stderr = p.communicate()
628      if p.returncode == 0:
629        # Encrypted key with empty string as password.
630        key_passwords[k] = ''
631      elif stderr.startswith('Error decrypting key'):
632        # Definitely encrypted key.
633        # It would have said "Error reading key" if it didn't parse correctly.
634        need_passwords.append(k)
635      else:
636        # Potentially, a type of key that openssl doesn't understand.
637        # We'll let the routines in signapk.jar handle it.
638        no_passwords.append(k)
639  devnull.close()
640
641  key_passwords.update(PasswordManager().GetPasswords(need_passwords))
642  key_passwords.update(dict.fromkeys(no_passwords, None))
643  return key_passwords
644
645
646def GetMinSdkVersion(apk_name):
647  """Get the minSdkVersion delared in the APK. This can be both a decimal number
648  (API Level) or a codename.
649  """
650
651  p = Run(["aapt", "dump", "badging", apk_name], stdout=subprocess.PIPE)
652  output, err = p.communicate()
653  if err:
654    raise ExternalError("Failed to obtain minSdkVersion: aapt return code %s"
655        % (p.returncode,))
656
657  for line in output.split("\n"):
658    # Looking for lines such as sdkVersion:'23' or sdkVersion:'M'
659    m = re.match(r'sdkVersion:\'([^\']*)\'', line)
660    if m:
661      return m.group(1)
662  raise ExternalError("No minSdkVersion returned by aapt")
663
664
665def GetMinSdkVersionInt(apk_name, codename_to_api_level_map):
666  """Get the minSdkVersion declared in the APK as a number (API Level). If
667  minSdkVersion is set to a codename, it is translated to a number using the
668  provided map.
669  """
670
671  version = GetMinSdkVersion(apk_name)
672  try:
673    return int(version)
674  except ValueError:
675    # Not a decimal number. Codename?
676    if version in codename_to_api_level_map:
677      return codename_to_api_level_map[version]
678    else:
679      raise ExternalError("Unknown minSdkVersion: '%s'. Known codenames: %s"
680                          % (version, codename_to_api_level_map))
681
682
683def SignFile(input_name, output_name, key, password, min_api_level=None,
684    codename_to_api_level_map=dict(),
685    whole_file=False):
686  """Sign the input_name zip/jar/apk, producing output_name.  Use the
687  given key and password (the latter may be None if the key does not
688  have a password.
689
690  If whole_file is true, use the "-w" option to SignApk to embed a
691  signature that covers the whole file in the archive comment of the
692  zip file.
693
694  min_api_level is the API Level (int) of the oldest platform this file may end
695  up on. If not specified for an APK, the API Level is obtained by interpreting
696  the minSdkVersion attribute of the APK's AndroidManifest.xml.
697
698  codename_to_api_level_map is needed to translate the codename which may be
699  encountered as the APK's minSdkVersion.
700  """
701
702  java_library_path = os.path.join(
703      OPTIONS.search_path, OPTIONS.signapk_shared_library_path)
704
705  cmd = [OPTIONS.java_path, OPTIONS.java_args,
706         "-Djava.library.path=" + java_library_path,
707         "-jar",
708         os.path.join(OPTIONS.search_path, OPTIONS.signapk_path)]
709  cmd.extend(OPTIONS.extra_signapk_args)
710  if whole_file:
711    cmd.append("-w")
712
713  min_sdk_version = min_api_level
714  if min_sdk_version is None:
715    if not whole_file:
716      min_sdk_version = GetMinSdkVersionInt(
717          input_name, codename_to_api_level_map)
718  if min_sdk_version is not None:
719    cmd.extend(["--min-sdk-version", str(min_sdk_version)])
720
721  cmd.extend([key + OPTIONS.public_key_suffix,
722              key + OPTIONS.private_key_suffix,
723              input_name, output_name])
724
725  p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
726  if password is not None:
727    password += "\n"
728  p.communicate(password)
729  if p.returncode != 0:
730    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
731
732
733def CheckSize(data, target, info_dict):
734  """Check the data string passed against the max size limit, if
735  any, for the given target.  Raise exception if the data is too big.
736  Print a warning if the data is nearing the maximum size."""
737
738  if target.endswith(".img"):
739    target = target[:-4]
740  mount_point = "/" + target
741
742  fs_type = None
743  limit = None
744  if info_dict["fstab"]:
745    if mount_point == "/userdata":
746      mount_point = "/data"
747    p = info_dict["fstab"][mount_point]
748    fs_type = p.fs_type
749    device = p.device
750    if "/" in device:
751      device = device[device.rfind("/")+1:]
752    limit = info_dict.get(device + "_size", None)
753  if not fs_type or not limit:
754    return
755
756  if fs_type == "yaffs2":
757    # image size should be increased by 1/64th to account for the
758    # spare area (64 bytes per 2k page)
759    limit = limit / 2048 * (2048+64)
760  size = len(data)
761  pct = float(size) * 100.0 / limit
762  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
763  if pct >= 99.0:
764    raise ExternalError(msg)
765  elif pct >= 95.0:
766    print
767    print "  WARNING: ", msg
768    print
769  elif OPTIONS.verbose:
770    print "  ", msg
771
772
773def ReadApkCerts(tf_zip):
774  """Given a target_files ZipFile, parse the META/apkcerts.txt file
775  and return a {package: cert} dict."""
776  certmap = {}
777  for line in tf_zip.read("META/apkcerts.txt").split("\n"):
778    line = line.strip()
779    if not line:
780      continue
781    m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
782                 r'private_key="(.*)"$', line)
783    if m:
784      name, cert, privkey = m.groups()
785      public_key_suffix_len = len(OPTIONS.public_key_suffix)
786      private_key_suffix_len = len(OPTIONS.private_key_suffix)
787      if cert in SPECIAL_CERT_STRINGS and not privkey:
788        certmap[name] = cert
789      elif (cert.endswith(OPTIONS.public_key_suffix) and
790            privkey.endswith(OPTIONS.private_key_suffix) and
791            cert[:-public_key_suffix_len] == privkey[:-private_key_suffix_len]):
792        certmap[name] = cert[:-public_key_suffix_len]
793      else:
794        raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
795  return certmap
796
797
798COMMON_DOCSTRING = """
799  -p  (--path)  <dir>
800      Prepend <dir>/bin to the list of places to search for binaries
801      run by this script, and expect to find jars in <dir>/framework.
802
803  -s  (--device_specific) <file>
804      Path to the python module containing device-specific
805      releasetools code.
806
807  -x  (--extra)  <key=value>
808      Add a key/value pair to the 'extras' dict, which device-specific
809      extension code may look at.
810
811  -v  (--verbose)
812      Show command lines being executed.
813
814  -h  (--help)
815      Display this usage message and exit.
816"""
817
818def Usage(docstring):
819  print docstring.rstrip("\n")
820  print COMMON_DOCSTRING
821
822
823def ParseOptions(argv,
824                 docstring,
825                 extra_opts="", extra_long_opts=(),
826                 extra_option_handler=None):
827  """Parse the options in argv and return any arguments that aren't
828  flags.  docstring is the calling module's docstring, to be displayed
829  for errors and -h.  extra_opts and extra_long_opts are for flags
830  defined by the caller, which are processed by passing them to
831  extra_option_handler."""
832
833  try:
834    opts, args = getopt.getopt(
835        argv, "hvp:s:x:" + extra_opts,
836        ["help", "verbose", "path=", "signapk_path=",
837         "signapk_shared_library_path=", "extra_signapk_args=",
838         "java_path=", "java_args=", "public_key_suffix=",
839         "private_key_suffix=", "boot_signer_path=", "boot_signer_args=",
840         "verity_signer_path=", "verity_signer_args=", "device_specific=",
841         "extra="] +
842        list(extra_long_opts))
843  except getopt.GetoptError as err:
844    Usage(docstring)
845    print "**", str(err), "**"
846    sys.exit(2)
847
848  for o, a in opts:
849    if o in ("-h", "--help"):
850      Usage(docstring)
851      sys.exit()
852    elif o in ("-v", "--verbose"):
853      OPTIONS.verbose = True
854    elif o in ("-p", "--path"):
855      OPTIONS.search_path = a
856    elif o in ("--signapk_path",):
857      OPTIONS.signapk_path = a
858    elif o in ("--signapk_shared_library_path",):
859      OPTIONS.signapk_shared_library_path = a
860    elif o in ("--extra_signapk_args",):
861      OPTIONS.extra_signapk_args = shlex.split(a)
862    elif o in ("--java_path",):
863      OPTIONS.java_path = a
864    elif o in ("--java_args",):
865      OPTIONS.java_args = a
866    elif o in ("--public_key_suffix",):
867      OPTIONS.public_key_suffix = a
868    elif o in ("--private_key_suffix",):
869      OPTIONS.private_key_suffix = a
870    elif o in ("--boot_signer_path",):
871      OPTIONS.boot_signer_path = a
872    elif o in ("--boot_signer_args",):
873      OPTIONS.boot_signer_args = shlex.split(a)
874    elif o in ("--verity_signer_path",):
875      OPTIONS.verity_signer_path = a
876    elif o in ("--verity_signer_args",):
877      OPTIONS.verity_signer_args = shlex.split(a)
878    elif o in ("-s", "--device_specific"):
879      OPTIONS.device_specific = a
880    elif o in ("-x", "--extra"):
881      key, value = a.split("=", 1)
882      OPTIONS.extras[key] = value
883    else:
884      if extra_option_handler is None or not extra_option_handler(o, a):
885        assert False, "unknown option \"%s\"" % (o,)
886
887  if OPTIONS.search_path:
888    os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
889                          os.pathsep + os.environ["PATH"])
890
891  return args
892
893
894def MakeTempFile(prefix=None, suffix=None):
895  """Make a temp file and add it to the list of things to be deleted
896  when Cleanup() is called.  Return the filename."""
897  fd, fn = tempfile.mkstemp(prefix=prefix, suffix=suffix)
898  os.close(fd)
899  OPTIONS.tempfiles.append(fn)
900  return fn
901
902
903def Cleanup():
904  for i in OPTIONS.tempfiles:
905    if os.path.isdir(i):
906      shutil.rmtree(i)
907    else:
908      os.remove(i)
909
910
911class PasswordManager(object):
912  def __init__(self):
913    self.editor = os.getenv("EDITOR", None)
914    self.pwfile = os.getenv("ANDROID_PW_FILE", None)
915
916  def GetPasswords(self, items):
917    """Get passwords corresponding to each string in 'items',
918    returning a dict.  (The dict may have keys in addition to the
919    values in 'items'.)
920
921    Uses the passwords in $ANDROID_PW_FILE if available, letting the
922    user edit that file to add more needed passwords.  If no editor is
923    available, or $ANDROID_PW_FILE isn't define, prompts the user
924    interactively in the ordinary way.
925    """
926
927    current = self.ReadFile()
928
929    first = True
930    while True:
931      missing = []
932      for i in items:
933        if i not in current or not current[i]:
934          missing.append(i)
935      # Are all the passwords already in the file?
936      if not missing:
937        return current
938
939      for i in missing:
940        current[i] = ""
941
942      if not first:
943        print "key file %s still missing some passwords." % (self.pwfile,)
944        answer = raw_input("try to edit again? [y]> ").strip()
945        if answer and answer[0] not in 'yY':
946          raise RuntimeError("key passwords unavailable")
947      first = False
948
949      current = self.UpdateAndReadFile(current)
950
951  def PromptResult(self, current): # pylint: disable=no-self-use
952    """Prompt the user to enter a value (password) for each key in
953    'current' whose value is fales.  Returns a new dict with all the
954    values.
955    """
956    result = {}
957    for k, v in sorted(current.iteritems()):
958      if v:
959        result[k] = v
960      else:
961        while True:
962          result[k] = getpass.getpass(
963              "Enter password for %s key> " % k).strip()
964          if result[k]:
965            break
966    return result
967
968  def UpdateAndReadFile(self, current):
969    if not self.editor or not self.pwfile:
970      return self.PromptResult(current)
971
972    f = open(self.pwfile, "w")
973    os.chmod(self.pwfile, 0o600)
974    f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
975    f.write("# (Additional spaces are harmless.)\n\n")
976
977    first_line = None
978    sorted_list = sorted([(not v, k, v) for (k, v) in current.iteritems()])
979    for i, (_, k, v) in enumerate(sorted_list):
980      f.write("[[[  %s  ]]] %s\n" % (v, k))
981      if not v and first_line is None:
982        # position cursor on first line with no password.
983        first_line = i + 4
984    f.close()
985
986    p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
987    _, _ = p.communicate()
988
989    return self.ReadFile()
990
991  def ReadFile(self):
992    result = {}
993    if self.pwfile is None:
994      return result
995    try:
996      f = open(self.pwfile, "r")
997      for line in f:
998        line = line.strip()
999        if not line or line[0] == '#':
1000          continue
1001        m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
1002        if not m:
1003          print "failed to parse password file: ", line
1004        else:
1005          result[m.group(2)] = m.group(1)
1006      f.close()
1007    except IOError as e:
1008      if e.errno != errno.ENOENT:
1009        print "error reading password file: ", str(e)
1010    return result
1011
1012
1013def ZipWrite(zip_file, filename, arcname=None, perms=0o644,
1014             compress_type=None):
1015  import datetime
1016
1017  # http://b/18015246
1018  # Python 2.7's zipfile implementation wrongly thinks that zip64 is required
1019  # for files larger than 2GiB. We can work around this by adjusting their
1020  # limit. Note that `zipfile.writestr()` will not work for strings larger than
1021  # 2GiB. The Python interpreter sometimes rejects strings that large (though
1022  # it isn't clear to me exactly what circumstances cause this).
1023  # `zipfile.write()` must be used directly to work around this.
1024  #
1025  # This mess can be avoided if we port to python3.
1026  saved_zip64_limit = zipfile.ZIP64_LIMIT
1027  zipfile.ZIP64_LIMIT = (1 << 32) - 1
1028
1029  if compress_type is None:
1030    compress_type = zip_file.compression
1031  if arcname is None:
1032    arcname = filename
1033
1034  saved_stat = os.stat(filename)
1035
1036  try:
1037    # `zipfile.write()` doesn't allow us to pass ZipInfo, so just modify the
1038    # file to be zipped and reset it when we're done.
1039    os.chmod(filename, perms)
1040
1041    # Use a fixed timestamp so the output is repeatable.
1042    epoch = datetime.datetime.fromtimestamp(0)
1043    timestamp = (datetime.datetime(2009, 1, 1) - epoch).total_seconds()
1044    os.utime(filename, (timestamp, timestamp))
1045
1046    zip_file.write(filename, arcname=arcname, compress_type=compress_type)
1047  finally:
1048    os.chmod(filename, saved_stat.st_mode)
1049    os.utime(filename, (saved_stat.st_atime, saved_stat.st_mtime))
1050    zipfile.ZIP64_LIMIT = saved_zip64_limit
1051
1052
1053def ZipWriteStr(zip_file, zinfo_or_arcname, data, perms=None,
1054                compress_type=None):
1055  """Wrap zipfile.writestr() function to work around the zip64 limit.
1056
1057  Even with the ZIP64_LIMIT workaround, it won't allow writing a string
1058  longer than 2GiB. It gives 'OverflowError: size does not fit in an int'
1059  when calling crc32(bytes).
1060
1061  But it still works fine to write a shorter string into a large zip file.
1062  We should use ZipWrite() whenever possible, and only use ZipWriteStr()
1063  when we know the string won't be too long.
1064  """
1065
1066  saved_zip64_limit = zipfile.ZIP64_LIMIT
1067  zipfile.ZIP64_LIMIT = (1 << 32) - 1
1068
1069  if not isinstance(zinfo_or_arcname, zipfile.ZipInfo):
1070    zinfo = zipfile.ZipInfo(filename=zinfo_or_arcname)
1071    zinfo.compress_type = zip_file.compression
1072    if perms is None:
1073      perms = 0o100644
1074  else:
1075    zinfo = zinfo_or_arcname
1076
1077  # If compress_type is given, it overrides the value in zinfo.
1078  if compress_type is not None:
1079    zinfo.compress_type = compress_type
1080
1081  # If perms is given, it has a priority.
1082  if perms is not None:
1083    # If perms doesn't set the file type, mark it as a regular file.
1084    if perms & 0o770000 == 0:
1085      perms |= 0o100000
1086    zinfo.external_attr = perms << 16
1087
1088  # Use a fixed timestamp so the output is repeatable.
1089  zinfo.date_time = (2009, 1, 1, 0, 0, 0)
1090
1091  zip_file.writestr(zinfo, data)
1092  zipfile.ZIP64_LIMIT = saved_zip64_limit
1093
1094
1095def ZipClose(zip_file):
1096  # http://b/18015246
1097  # zipfile also refers to ZIP64_LIMIT during close() when it writes out the
1098  # central directory.
1099  saved_zip64_limit = zipfile.ZIP64_LIMIT
1100  zipfile.ZIP64_LIMIT = (1 << 32) - 1
1101
1102  zip_file.close()
1103
1104  zipfile.ZIP64_LIMIT = saved_zip64_limit
1105
1106
1107class DeviceSpecificParams(object):
1108  module = None
1109  def __init__(self, **kwargs):
1110    """Keyword arguments to the constructor become attributes of this
1111    object, which is passed to all functions in the device-specific
1112    module."""
1113    for k, v in kwargs.iteritems():
1114      setattr(self, k, v)
1115    self.extras = OPTIONS.extras
1116
1117    if self.module is None:
1118      path = OPTIONS.device_specific
1119      if not path:
1120        return
1121      try:
1122        if os.path.isdir(path):
1123          info = imp.find_module("releasetools", [path])
1124        else:
1125          d, f = os.path.split(path)
1126          b, x = os.path.splitext(f)
1127          if x == ".py":
1128            f = b
1129          info = imp.find_module(f, [d])
1130        print "loaded device-specific extensions from", path
1131        self.module = imp.load_module("device_specific", *info)
1132      except ImportError:
1133        print "unable to load device-specific module; assuming none"
1134
1135  def _DoCall(self, function_name, *args, **kwargs):
1136    """Call the named function in the device-specific module, passing
1137    the given args and kwargs.  The first argument to the call will be
1138    the DeviceSpecific object itself.  If there is no module, or the
1139    module does not define the function, return the value of the
1140    'default' kwarg (which itself defaults to None)."""
1141    if self.module is None or not hasattr(self.module, function_name):
1142      return kwargs.get("default", None)
1143    return getattr(self.module, function_name)(*((self,) + args), **kwargs)
1144
1145  def FullOTA_Assertions(self):
1146    """Called after emitting the block of assertions at the top of a
1147    full OTA package.  Implementations can add whatever additional
1148    assertions they like."""
1149    return self._DoCall("FullOTA_Assertions")
1150
1151  def FullOTA_InstallBegin(self):
1152    """Called at the start of full OTA installation."""
1153    return self._DoCall("FullOTA_InstallBegin")
1154
1155  def FullOTA_InstallEnd(self):
1156    """Called at the end of full OTA installation; typically this is
1157    used to install the image for the device's baseband processor."""
1158    return self._DoCall("FullOTA_InstallEnd")
1159
1160  def IncrementalOTA_Assertions(self):
1161    """Called after emitting the block of assertions at the top of an
1162    incremental OTA package.  Implementations can add whatever
1163    additional assertions they like."""
1164    return self._DoCall("IncrementalOTA_Assertions")
1165
1166  def IncrementalOTA_VerifyBegin(self):
1167    """Called at the start of the verification phase of incremental
1168    OTA installation; additional checks can be placed here to abort
1169    the script before any changes are made."""
1170    return self._DoCall("IncrementalOTA_VerifyBegin")
1171
1172  def IncrementalOTA_VerifyEnd(self):
1173    """Called at the end of the verification phase of incremental OTA
1174    installation; additional checks can be placed here to abort the
1175    script before any changes are made."""
1176    return self._DoCall("IncrementalOTA_VerifyEnd")
1177
1178  def IncrementalOTA_InstallBegin(self):
1179    """Called at the start of incremental OTA installation (after
1180    verification is complete)."""
1181    return self._DoCall("IncrementalOTA_InstallBegin")
1182
1183  def IncrementalOTA_InstallEnd(self):
1184    """Called at the end of incremental OTA installation; typically
1185    this is used to install the image for the device's baseband
1186    processor."""
1187    return self._DoCall("IncrementalOTA_InstallEnd")
1188
1189  def VerifyOTA_Assertions(self):
1190    return self._DoCall("VerifyOTA_Assertions")
1191
1192class File(object):
1193  def __init__(self, name, data):
1194    self.name = name
1195    self.data = data
1196    self.size = len(data)
1197    self.sha1 = sha1(data).hexdigest()
1198
1199  @classmethod
1200  def FromLocalFile(cls, name, diskname):
1201    f = open(diskname, "rb")
1202    data = f.read()
1203    f.close()
1204    return File(name, data)
1205
1206  def WriteToTemp(self):
1207    t = tempfile.NamedTemporaryFile()
1208    t.write(self.data)
1209    t.flush()
1210    return t
1211
1212  def AddToZip(self, z, compression=None):
1213    ZipWriteStr(z, self.name, self.data, compress_type=compression)
1214
1215DIFF_PROGRAM_BY_EXT = {
1216    ".gz" : "imgdiff",
1217    ".zip" : ["imgdiff", "-z"],
1218    ".jar" : ["imgdiff", "-z"],
1219    ".apk" : ["imgdiff", "-z"],
1220    ".img" : "imgdiff",
1221    }
1222
1223class Difference(object):
1224  def __init__(self, tf, sf, diff_program=None):
1225    self.tf = tf
1226    self.sf = sf
1227    self.patch = None
1228    self.diff_program = diff_program
1229
1230  def ComputePatch(self):
1231    """Compute the patch (as a string of data) needed to turn sf into
1232    tf.  Returns the same tuple as GetPatch()."""
1233
1234    tf = self.tf
1235    sf = self.sf
1236
1237    if self.diff_program:
1238      diff_program = self.diff_program
1239    else:
1240      ext = os.path.splitext(tf.name)[1]
1241      diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
1242
1243    ttemp = tf.WriteToTemp()
1244    stemp = sf.WriteToTemp()
1245
1246    ext = os.path.splitext(tf.name)[1]
1247
1248    try:
1249      ptemp = tempfile.NamedTemporaryFile()
1250      if isinstance(diff_program, list):
1251        cmd = copy.copy(diff_program)
1252      else:
1253        cmd = [diff_program]
1254      cmd.append(stemp.name)
1255      cmd.append(ttemp.name)
1256      cmd.append(ptemp.name)
1257      p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1258      err = []
1259      def run():
1260        _, e = p.communicate()
1261        if e:
1262          err.append(e)
1263      th = threading.Thread(target=run)
1264      th.start()
1265      th.join(timeout=300)   # 5 mins
1266      if th.is_alive():
1267        print "WARNING: diff command timed out"
1268        p.terminate()
1269        th.join(5)
1270        if th.is_alive():
1271          p.kill()
1272          th.join()
1273
1274      if err or p.returncode != 0:
1275        print "WARNING: failure running %s:\n%s\n" % (
1276            diff_program, "".join(err))
1277        self.patch = None
1278        return None, None, None
1279      diff = ptemp.read()
1280    finally:
1281      ptemp.close()
1282      stemp.close()
1283      ttemp.close()
1284
1285    self.patch = diff
1286    return self.tf, self.sf, self.patch
1287
1288
1289  def GetPatch(self):
1290    """Return a tuple (target_file, source_file, patch_data).
1291    patch_data may be None if ComputePatch hasn't been called, or if
1292    computing the patch failed."""
1293    return self.tf, self.sf, self.patch
1294
1295
1296def ComputeDifferences(diffs):
1297  """Call ComputePatch on all the Difference objects in 'diffs'."""
1298  print len(diffs), "diffs to compute"
1299
1300  # Do the largest files first, to try and reduce the long-pole effect.
1301  by_size = [(i.tf.size, i) for i in diffs]
1302  by_size.sort(reverse=True)
1303  by_size = [i[1] for i in by_size]
1304
1305  lock = threading.Lock()
1306  diff_iter = iter(by_size)   # accessed under lock
1307
1308  def worker():
1309    try:
1310      lock.acquire()
1311      for d in diff_iter:
1312        lock.release()
1313        start = time.time()
1314        d.ComputePatch()
1315        dur = time.time() - start
1316        lock.acquire()
1317
1318        tf, sf, patch = d.GetPatch()
1319        if sf.name == tf.name:
1320          name = tf.name
1321        else:
1322          name = "%s (%s)" % (tf.name, sf.name)
1323        if patch is None:
1324          print "patching failed!                                  %s" % (name,)
1325        else:
1326          print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
1327              dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
1328      lock.release()
1329    except Exception as e:
1330      print e
1331      raise
1332
1333  # start worker threads; wait for them all to finish.
1334  threads = [threading.Thread(target=worker)
1335             for i in range(OPTIONS.worker_threads)]
1336  for th in threads:
1337    th.start()
1338  while threads:
1339    threads.pop().join()
1340
1341
1342class BlockDifference(object):
1343  def __init__(self, partition, tgt, src=None, check_first_block=False,
1344               version=None, disable_imgdiff=False):
1345    self.tgt = tgt
1346    self.src = src
1347    self.partition = partition
1348    self.check_first_block = check_first_block
1349    self.disable_imgdiff = disable_imgdiff
1350
1351    if version is None:
1352      version = 1
1353      if OPTIONS.info_dict:
1354        version = max(
1355            int(i) for i in
1356            OPTIONS.info_dict.get("blockimgdiff_versions", "1").split(","))
1357    self.version = version
1358
1359    b = blockimgdiff.BlockImageDiff(tgt, src, threads=OPTIONS.worker_threads,
1360                                    version=self.version,
1361                                    disable_imgdiff=self.disable_imgdiff)
1362    tmpdir = tempfile.mkdtemp()
1363    OPTIONS.tempfiles.append(tmpdir)
1364    self.path = os.path.join(tmpdir, partition)
1365    b.Compute(self.path)
1366    self._required_cache = b.max_stashed_size
1367    self.touched_src_ranges = b.touched_src_ranges
1368    self.touched_src_sha1 = b.touched_src_sha1
1369
1370    if src is None:
1371      _, self.device = GetTypeAndDevice("/" + partition, OPTIONS.info_dict)
1372    else:
1373      _, self.device = GetTypeAndDevice("/" + partition,
1374                                        OPTIONS.source_info_dict)
1375
1376  @property
1377  def required_cache(self):
1378    return self._required_cache
1379
1380  def WriteScript(self, script, output_zip, progress=None):
1381    if not self.src:
1382      # write the output unconditionally
1383      script.Print("Patching %s image unconditionally..." % (self.partition,))
1384    else:
1385      script.Print("Patching %s image after verification." % (self.partition,))
1386
1387    if progress:
1388      script.ShowProgress(progress, 0)
1389    self._WriteUpdate(script, output_zip)
1390    if OPTIONS.verify:
1391      self._WritePostInstallVerifyScript(script)
1392
1393  def WriteStrictVerifyScript(self, script):
1394    """Verify all the blocks in the care_map, including clobbered blocks.
1395
1396    This differs from the WriteVerifyScript() function: a) it prints different
1397    error messages; b) it doesn't allow half-way updated images to pass the
1398    verification."""
1399
1400    partition = self.partition
1401    script.Print("Verifying %s..." % (partition,))
1402    ranges = self.tgt.care_map
1403    ranges_str = ranges.to_string_raw()
1404    script.AppendExtra('range_sha1("%s", "%s") == "%s" && '
1405                       'ui_print("    Verified.") || '
1406                       'ui_print("\\"%s\\" has unexpected contents.");' % (
1407                       self.device, ranges_str,
1408                       self.tgt.TotalSha1(include_clobbered_blocks=True),
1409                       self.device))
1410    script.AppendExtra("")
1411
1412  def WriteVerifyScript(self, script, touched_blocks_only=False):
1413    partition = self.partition
1414
1415    # full OTA
1416    if not self.src:
1417      script.Print("Image %s will be patched unconditionally." % (partition,))
1418
1419    # incremental OTA
1420    else:
1421      if touched_blocks_only and self.version >= 3:
1422        ranges = self.touched_src_ranges
1423        expected_sha1 = self.touched_src_sha1
1424      else:
1425        ranges = self.src.care_map.subtract(self.src.clobbered_blocks)
1426        expected_sha1 = self.src.TotalSha1()
1427
1428      # No blocks to be checked, skipping.
1429      if not ranges:
1430        return
1431
1432      ranges_str = ranges.to_string_raw()
1433      if self.version >= 4:
1434        script.AppendExtra(('if (range_sha1("%s", "%s") == "%s" || '
1435                            'block_image_verify("%s", '
1436                            'package_extract_file("%s.transfer.list"), '
1437                            '"%s.new.dat", "%s.patch.dat")) then') % (
1438                            self.device, ranges_str, expected_sha1,
1439                            self.device, partition, partition, partition))
1440      elif self.version == 3:
1441        script.AppendExtra(('if (range_sha1("%s", "%s") == "%s" || '
1442                            'block_image_verify("%s", '
1443                            'package_extract_file("%s.transfer.list"), '
1444                            '"%s.new.dat", "%s.patch.dat")) then') % (
1445                            self.device, ranges_str, expected_sha1,
1446                            self.device, partition, partition, partition))
1447      else:
1448        script.AppendExtra('if range_sha1("%s", "%s") == "%s" then' % (
1449                           self.device, ranges_str, self.src.TotalSha1()))
1450      script.Print('Verified %s image...' % (partition,))
1451      script.AppendExtra('else')
1452
1453      if self.version >= 4:
1454
1455        # Bug: 21124327
1456        # When generating incrementals for the system and vendor partitions in
1457        # version 4 or newer, explicitly check the first block (which contains
1458        # the superblock) of the partition to see if it's what we expect. If
1459        # this check fails, give an explicit log message about the partition
1460        # having been remounted R/W (the most likely explanation).
1461        if self.check_first_block:
1462          script.AppendExtra('check_first_block("%s");' % (self.device,))
1463
1464        # If version >= 4, try block recovery before abort update
1465        if partition == "system":
1466          code = ErrorCode.SYSTEM_RECOVER_FAILURE
1467        else:
1468          code = ErrorCode.VENDOR_RECOVER_FAILURE
1469        script.AppendExtra((
1470            'ifelse (block_image_recover("{device}", "{ranges}") && '
1471            'block_image_verify("{device}", '
1472            'package_extract_file("{partition}.transfer.list"), '
1473            '"{partition}.new.dat", "{partition}.patch.dat"), '
1474            'ui_print("{partition} recovered successfully."), '
1475            'abort("E{code}: {partition} partition fails to recover"));\n'
1476            'endif;').format(device=self.device, ranges=ranges_str,
1477                             partition=partition, code=code))
1478
1479      # Abort the OTA update. Note that the incremental OTA cannot be applied
1480      # even if it may match the checksum of the target partition.
1481      # a) If version < 3, operations like move and erase will make changes
1482      #    unconditionally and damage the partition.
1483      # b) If version >= 3, it won't even reach here.
1484      else:
1485        if partition == "system":
1486          code = ErrorCode.SYSTEM_VERIFICATION_FAILURE
1487        else:
1488          code = ErrorCode.VENDOR_VERIFICATION_FAILURE
1489        script.AppendExtra((
1490            'abort("E%d: %s partition has unexpected contents");\n'
1491            'endif;') % (code, partition))
1492
1493  def _WritePostInstallVerifyScript(self, script):
1494    partition = self.partition
1495    script.Print('Verifying the updated %s image...' % (partition,))
1496    # Unlike pre-install verification, clobbered_blocks should not be ignored.
1497    ranges = self.tgt.care_map
1498    ranges_str = ranges.to_string_raw()
1499    script.AppendExtra('if range_sha1("%s", "%s") == "%s" then' % (
1500                       self.device, ranges_str,
1501                       self.tgt.TotalSha1(include_clobbered_blocks=True)))
1502
1503    # Bug: 20881595
1504    # Verify that extended blocks are really zeroed out.
1505    if self.tgt.extended:
1506      ranges_str = self.tgt.extended.to_string_raw()
1507      script.AppendExtra('if range_sha1("%s", "%s") == "%s" then' % (
1508                         self.device, ranges_str,
1509                         self._HashZeroBlocks(self.tgt.extended.size())))
1510      script.Print('Verified the updated %s image.' % (partition,))
1511      if partition == "system":
1512        code = ErrorCode.SYSTEM_NONZERO_CONTENTS
1513      else:
1514        code = ErrorCode.VENDOR_NONZERO_CONTENTS
1515      script.AppendExtra(
1516          'else\n'
1517          '  abort("E%d: %s partition has unexpected non-zero contents after '
1518          'OTA update");\n'
1519          'endif;' % (code, partition))
1520    else:
1521      script.Print('Verified the updated %s image.' % (partition,))
1522
1523    if partition == "system":
1524      code = ErrorCode.SYSTEM_UNEXPECTED_CONTENTS
1525    else:
1526      code = ErrorCode.VENDOR_UNEXPECTED_CONTENTS
1527
1528    script.AppendExtra(
1529        'else\n'
1530        '  abort("E%d: %s partition has unexpected contents after OTA '
1531        'update");\n'
1532        'endif;' % (code, partition))
1533
1534  def _WriteUpdate(self, script, output_zip):
1535    ZipWrite(output_zip,
1536             '{}.transfer.list'.format(self.path),
1537             '{}.transfer.list'.format(self.partition))
1538    ZipWrite(output_zip,
1539             '{}.new.dat'.format(self.path),
1540             '{}.new.dat'.format(self.partition))
1541    ZipWrite(output_zip,
1542             '{}.patch.dat'.format(self.path),
1543             '{}.patch.dat'.format(self.partition),
1544             compress_type=zipfile.ZIP_STORED)
1545
1546    if self.partition == "system":
1547      code = ErrorCode.SYSTEM_UPDATE_FAILURE
1548    else:
1549      code = ErrorCode.VENDOR_UPDATE_FAILURE
1550
1551    call = ('block_image_update("{device}", '
1552            'package_extract_file("{partition}.transfer.list"), '
1553            '"{partition}.new.dat", "{partition}.patch.dat") ||\n'
1554            '  abort("E{code}: Failed to update {partition} image.");'.format(
1555                device=self.device, partition=self.partition, code=code))
1556    script.AppendExtra(script.WordWrap(call))
1557
1558  def _HashBlocks(self, source, ranges): # pylint: disable=no-self-use
1559    data = source.ReadRangeSet(ranges)
1560    ctx = sha1()
1561
1562    for p in data:
1563      ctx.update(p)
1564
1565    return ctx.hexdigest()
1566
1567  def _HashZeroBlocks(self, num_blocks): # pylint: disable=no-self-use
1568    """Return the hash value for all zero blocks."""
1569    zero_block = '\x00' * 4096
1570    ctx = sha1()
1571    for _ in range(num_blocks):
1572      ctx.update(zero_block)
1573
1574    return ctx.hexdigest()
1575
1576
1577DataImage = blockimgdiff.DataImage
1578
1579# map recovery.fstab's fs_types to mount/format "partition types"
1580PARTITION_TYPES = {
1581    "yaffs2": "MTD",
1582    "mtd": "MTD",
1583    "ext4": "EMMC",
1584    "emmc": "EMMC",
1585    "f2fs": "EMMC",
1586    "squashfs": "EMMC"
1587}
1588
1589def GetTypeAndDevice(mount_point, info):
1590  fstab = info["fstab"]
1591  if fstab:
1592    return (PARTITION_TYPES[fstab[mount_point].fs_type],
1593            fstab[mount_point].device)
1594  else:
1595    raise KeyError
1596
1597
1598def ParseCertificate(data):
1599  """Parse a PEM-format certificate."""
1600  cert = []
1601  save = False
1602  for line in data.split("\n"):
1603    if "--END CERTIFICATE--" in line:
1604      break
1605    if save:
1606      cert.append(line)
1607    if "--BEGIN CERTIFICATE--" in line:
1608      save = True
1609  cert = "".join(cert).decode('base64')
1610  return cert
1611
1612def MakeRecoveryPatch(input_dir, output_sink, recovery_img, boot_img,
1613                      info_dict=None):
1614  """Generate a binary patch that creates the recovery image starting
1615  with the boot image.  (Most of the space in these images is just the
1616  kernel, which is identical for the two, so the resulting patch
1617  should be efficient.)  Add it to the output zip, along with a shell
1618  script that is run from init.rc on first boot to actually do the
1619  patching and install the new recovery image.
1620
1621  recovery_img and boot_img should be File objects for the
1622  corresponding images.  info should be the dictionary returned by
1623  common.LoadInfoDict() on the input target_files.
1624  """
1625
1626  if info_dict is None:
1627    info_dict = OPTIONS.info_dict
1628
1629  full_recovery_image = info_dict.get("full_recovery_image", None) == "true"
1630  system_root_image = info_dict.get("system_root_image", None) == "true"
1631
1632  if full_recovery_image:
1633    output_sink("etc/recovery.img", recovery_img.data)
1634
1635  else:
1636    diff_program = ["imgdiff"]
1637    path = os.path.join(input_dir, "SYSTEM", "etc", "recovery-resource.dat")
1638    if os.path.exists(path):
1639      diff_program.append("-b")
1640      diff_program.append(path)
1641      bonus_args = "-b /system/etc/recovery-resource.dat"
1642    else:
1643      bonus_args = ""
1644
1645    d = Difference(recovery_img, boot_img, diff_program=diff_program)
1646    _, _, patch = d.ComputePatch()
1647    output_sink("recovery-from-boot.p", patch)
1648
1649  try:
1650    # The following GetTypeAndDevice()s need to use the path in the target
1651    # info_dict instead of source_info_dict.
1652    boot_type, boot_device = GetTypeAndDevice("/boot", info_dict)
1653    recovery_type, recovery_device = GetTypeAndDevice("/recovery", info_dict)
1654  except KeyError:
1655    return
1656
1657  if full_recovery_image:
1658    sh = """#!/system/bin/sh
1659if ! applypatch -c %(type)s:%(device)s:%(size)d:%(sha1)s; then
1660  applypatch /system/etc/recovery.img %(type)s:%(device)s %(sha1)s %(size)d && log -t recovery "Installing new recovery image: succeeded" || log -t recovery "Installing new recovery image: failed"
1661else
1662  log -t recovery "Recovery image already installed"
1663fi
1664""" % {'type': recovery_type,
1665       'device': recovery_device,
1666       'sha1': recovery_img.sha1,
1667       'size': recovery_img.size}
1668  else:
1669    sh = """#!/system/bin/sh
1670if ! applypatch -c %(recovery_type)s:%(recovery_device)s:%(recovery_size)d:%(recovery_sha1)s; then
1671  applypatch %(bonus_args)s %(boot_type)s:%(boot_device)s:%(boot_size)d:%(boot_sha1)s %(recovery_type)s:%(recovery_device)s %(recovery_sha1)s %(recovery_size)d %(boot_sha1)s:/system/recovery-from-boot.p && log -t recovery "Installing new recovery image: succeeded" || log -t recovery "Installing new recovery image: failed"
1672else
1673  log -t recovery "Recovery image already installed"
1674fi
1675""" % {'boot_size': boot_img.size,
1676       'boot_sha1': boot_img.sha1,
1677       'recovery_size': recovery_img.size,
1678       'recovery_sha1': recovery_img.sha1,
1679       'boot_type': boot_type,
1680       'boot_device': boot_device,
1681       'recovery_type': recovery_type,
1682       'recovery_device': recovery_device,
1683       'bonus_args': bonus_args}
1684
1685  # The install script location moved from /system/etc to /system/bin
1686  # in the L release.  Parse init.*.rc files to find out where the
1687  # target-files expects it to be, and put it there.
1688  sh_location = "etc/install-recovery.sh"
1689  found = False
1690  if system_root_image:
1691    init_rc_dir = os.path.join(input_dir, "ROOT")
1692  else:
1693    init_rc_dir = os.path.join(input_dir, "BOOT", "RAMDISK")
1694  init_rc_files = os.listdir(init_rc_dir)
1695  for init_rc_file in init_rc_files:
1696    if (not init_rc_file.startswith('init.') or
1697        not init_rc_file.endswith('.rc')):
1698      continue
1699
1700    with open(os.path.join(init_rc_dir, init_rc_file)) as f:
1701      for line in f:
1702        m = re.match(r"^service flash_recovery /system/(\S+)\s*$", line)
1703        if m:
1704          sh_location = m.group(1)
1705          found = True
1706          break
1707
1708    if found:
1709      break
1710
1711  print "putting script in", sh_location
1712
1713  output_sink(sh_location, sh)
1714