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