OSDN Git Service

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