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