common.py revision 4de6b5bfc89de95b18514e75329a2ec7491bebbd
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 errno
16import getopt
17import getpass
18import imp
19import os
20import re
21import shutil
22import subprocess
23import sys
24import tempfile
25import zipfile
26
27# missing in Python 2.4 and before
28if not hasattr(os, "SEEK_SET"):
29  os.SEEK_SET = 0
30
31class Options(object): pass
32OPTIONS = Options()
33OPTIONS.search_path = "out/host/linux-x86"
34OPTIONS.max_image_size = {}
35OPTIONS.verbose = False
36OPTIONS.tempfiles = []
37OPTIONS.device_specific = None
38OPTIONS.extras = {}
39
40
41# Values for "certificate" in apkcerts that mean special things.
42SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL")
43
44
45class ExternalError(RuntimeError): pass
46
47
48def Run(args, **kwargs):
49  """Create and return a subprocess.Popen object, printing the command
50  line on the terminal if -v was specified."""
51  if OPTIONS.verbose:
52    print "  running: ", " ".join(args)
53  return subprocess.Popen(args, **kwargs)
54
55
56def LoadMaxSizes():
57  """Load the maximum allowable images sizes from the input
58  target_files size."""
59  OPTIONS.max_image_size = {}
60  try:
61    for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")):
62      pieces = line.split()
63      if len(pieces) != 2: continue
64      image = pieces[0]
65      size = int(pieces[1])
66      OPTIONS.max_image_size[image + ".img"] = size
67  except IOError, e:
68    if e.errno == errno.ENOENT:
69      pass
70
71
72def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
73  """Take a kernel, cmdline, and ramdisk directory from the input (in
74  'sourcedir'), and turn them into a boot image.  Put the boot image
75  into the output zip file under the name 'targetname'.  Returns
76  targetname on success or None on failure (if sourcedir does not
77  appear to contain files for the requested image)."""
78
79  print "creating %s..." % (targetname,)
80
81  img = BuildBootableImage(sourcedir)
82  if img is None:
83    return None
84
85  CheckSize(img, targetname)
86  ZipWriteStr(output_zip, targetname, img)
87  return targetname
88
89def BuildBootableImage(sourcedir):
90  """Take a kernel, cmdline, and ramdisk directory from the input (in
91  'sourcedir'), and turn them into a boot image.  Return the image
92  data, or None if sourcedir does not appear to contains files for
93  building the requested image."""
94
95  if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
96      not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
97    return None
98
99  ramdisk_img = tempfile.NamedTemporaryFile()
100  img = tempfile.NamedTemporaryFile()
101
102  p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
103           stdout=subprocess.PIPE)
104  p2 = Run(["minigzip"],
105           stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
106
107  p2.wait()
108  p1.wait()
109  assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
110  assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
111
112  cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
113
114  fn = os.path.join(sourcedir, "cmdline")
115  if os.access(fn, os.F_OK):
116    cmd.append("--cmdline")
117    cmd.append(open(fn).read().rstrip("\n"))
118
119  fn = os.path.join(sourcedir, "base")
120  if os.access(fn, os.F_OK):
121    cmd.append("--base")
122    cmd.append(open(fn).read().rstrip("\n"))
123
124  fn = os.path.join(sourcedir, "pagesize")
125  if os.access(fn, os.F_OK):
126    cmd.append("--pagesize")
127    cmd.append(open(fn).read().rstrip("\n"))
128
129  cmd.extend(["--ramdisk", ramdisk_img.name,
130              "--output", img.name])
131
132  p = Run(cmd, stdout=subprocess.PIPE)
133  p.communicate()
134  assert p.returncode == 0, "mkbootimg of %s image failed" % (
135      os.path.basename(sourcedir),)
136
137  img.seek(os.SEEK_SET, 0)
138  data = img.read()
139
140  ramdisk_img.close()
141  img.close()
142
143  return data
144
145
146def AddRecovery(output_zip):
147  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
148                           "recovery.img", output_zip)
149
150def AddBoot(output_zip):
151  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
152                           "boot.img", output_zip)
153
154def UnzipTemp(filename, pattern=None):
155  """Unzip the given archive into a temporary directory and return the name."""
156
157  tmp = tempfile.mkdtemp(prefix="targetfiles-")
158  OPTIONS.tempfiles.append(tmp)
159  cmd = ["unzip", "-o", "-q", filename, "-d", tmp]
160  if pattern is not None:
161    cmd.append(pattern)
162  p = Run(cmd, stdout=subprocess.PIPE)
163  p.communicate()
164  if p.returncode != 0:
165    raise ExternalError("failed to unzip input target-files \"%s\"" %
166                        (filename,))
167  return tmp
168
169
170def GetKeyPasswords(keylist):
171  """Given a list of keys, prompt the user to enter passwords for
172  those which require them.  Return a {key: password} dict.  password
173  will be None if the key has no password."""
174
175  no_passwords = []
176  need_passwords = []
177  devnull = open("/dev/null", "w+b")
178  for k in sorted(keylist):
179    # We don't need a password for things that aren't really keys.
180    if k in SPECIAL_CERT_STRINGS:
181      no_passwords.append(k)
182      continue
183
184    p = Run(["openssl", "pkcs8", "-in", k+".pk8",
185             "-inform", "DER", "-nocrypt"],
186            stdin=devnull.fileno(),
187            stdout=devnull.fileno(),
188            stderr=subprocess.STDOUT)
189    p.communicate()
190    if p.returncode == 0:
191      no_passwords.append(k)
192    else:
193      need_passwords.append(k)
194  devnull.close()
195
196  key_passwords = PasswordManager().GetPasswords(need_passwords)
197  key_passwords.update(dict.fromkeys(no_passwords, None))
198  return key_passwords
199
200
201def SignFile(input_name, output_name, key, password, align=None,
202             whole_file=False):
203  """Sign the input_name zip/jar/apk, producing output_name.  Use the
204  given key and password (the latter may be None if the key does not
205  have a password.
206
207  If align is an integer > 1, zipalign is run to align stored files in
208  the output zip on 'align'-byte boundaries.
209
210  If whole_file is true, use the "-w" option to SignApk to embed a
211  signature that covers the whole file in the archive comment of the
212  zip file.
213  """
214
215  if align == 0 or align == 1:
216    align = None
217
218  if align:
219    temp = tempfile.NamedTemporaryFile()
220    sign_name = temp.name
221  else:
222    sign_name = output_name
223
224  cmd = ["java", "-Xmx512m", "-jar",
225           os.path.join(OPTIONS.search_path, "framework", "signapk.jar")]
226  if whole_file:
227    cmd.append("-w")
228  cmd.extend([key + ".x509.pem", key + ".pk8",
229              input_name, sign_name])
230
231  p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
232  if password is not None:
233    password += "\n"
234  p.communicate(password)
235  if p.returncode != 0:
236    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
237
238  if align:
239    p = Run(["zipalign", "-f", str(align), sign_name, output_name])
240    p.communicate()
241    if p.returncode != 0:
242      raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
243    temp.close()
244
245
246def CheckSize(data, target):
247  """Check the data string passed against the max size limit, if
248  any, for the given target.  Raise exception if the data is too big.
249  Print a warning if the data is nearing the maximum size."""
250  limit = OPTIONS.max_image_size.get(target, None)
251  if limit is None: return
252
253  size = len(data)
254  pct = float(size) * 100.0 / limit
255  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
256  if pct >= 99.0:
257    raise ExternalError(msg)
258  elif pct >= 95.0:
259    print
260    print "  WARNING: ", msg
261    print
262  elif OPTIONS.verbose:
263    print "  ", msg
264
265
266def ReadApkCerts(tf_zip):
267  """Given a target_files ZipFile, parse the META/apkcerts.txt file
268  and return a {package: cert} dict."""
269  certmap = {}
270  for line in tf_zip.read("META/apkcerts.txt").split("\n"):
271    line = line.strip()
272    if not line: continue
273    m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
274                 r'private_key="(.*)"$', line)
275    if m:
276      name, cert, privkey = m.groups()
277      if cert in SPECIAL_CERT_STRINGS and not privkey:
278        certmap[name] = cert
279      elif (cert.endswith(".x509.pem") and
280            privkey.endswith(".pk8") and
281            cert[:-9] == privkey[:-4]):
282        certmap[name] = cert[:-9]
283      else:
284        raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
285  return certmap
286
287
288COMMON_DOCSTRING = """
289  -p  (--path)  <dir>
290      Prepend <dir>/bin to the list of places to search for binaries
291      run by this script, and expect to find jars in <dir>/framework.
292
293  -s  (--device_specific) <file>
294      Path to the python module containing device-specific
295      releasetools code.
296
297  -x  (--extra)  <key=value>
298      Add a key/value pair to the 'extras' dict, which device-specific
299      extension code may look at.
300
301  -v  (--verbose)
302      Show command lines being executed.
303
304  -h  (--help)
305      Display this usage message and exit.
306"""
307
308def Usage(docstring):
309  print docstring.rstrip("\n")
310  print COMMON_DOCSTRING
311
312
313def ParseOptions(argv,
314                 docstring,
315                 extra_opts="", extra_long_opts=(),
316                 extra_option_handler=None):
317  """Parse the options in argv and return any arguments that aren't
318  flags.  docstring is the calling module's docstring, to be displayed
319  for errors and -h.  extra_opts and extra_long_opts are for flags
320  defined by the caller, which are processed by passing them to
321  extra_option_handler."""
322
323  try:
324    opts, args = getopt.getopt(
325        argv, "hvp:s:x:" + extra_opts,
326        ["help", "verbose", "path=", "device_specific=", "extra="] +
327          list(extra_long_opts))
328  except getopt.GetoptError, err:
329    Usage(docstring)
330    print "**", str(err), "**"
331    sys.exit(2)
332
333  path_specified = False
334
335  for o, a in opts:
336    if o in ("-h", "--help"):
337      Usage(docstring)
338      sys.exit()
339    elif o in ("-v", "--verbose"):
340      OPTIONS.verbose = True
341    elif o in ("-p", "--path"):
342      OPTIONS.search_path = a
343    elif o in ("-s", "--device_specific"):
344      OPTIONS.device_specific = a
345    elif o in ("-x", "--extra"):
346      key, value = a.split("=", 1)
347      OPTIONS.extras[key] = value
348    else:
349      if extra_option_handler is None or not extra_option_handler(o, a):
350        assert False, "unknown option \"%s\"" % (o,)
351
352  os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
353                        os.pathsep + os.environ["PATH"])
354
355  return args
356
357
358def Cleanup():
359  for i in OPTIONS.tempfiles:
360    if os.path.isdir(i):
361      shutil.rmtree(i)
362    else:
363      os.remove(i)
364
365
366class PasswordManager(object):
367  def __init__(self):
368    self.editor = os.getenv("EDITOR", None)
369    self.pwfile = os.getenv("ANDROID_PW_FILE", None)
370
371  def GetPasswords(self, items):
372    """Get passwords corresponding to each string in 'items',
373    returning a dict.  (The dict may have keys in addition to the
374    values in 'items'.)
375
376    Uses the passwords in $ANDROID_PW_FILE if available, letting the
377    user edit that file to add more needed passwords.  If no editor is
378    available, or $ANDROID_PW_FILE isn't define, prompts the user
379    interactively in the ordinary way.
380    """
381
382    current = self.ReadFile()
383
384    first = True
385    while True:
386      missing = []
387      for i in items:
388        if i not in current or not current[i]:
389          missing.append(i)
390      # Are all the passwords already in the file?
391      if not missing: return current
392
393      for i in missing:
394        current[i] = ""
395
396      if not first:
397        print "key file %s still missing some passwords." % (self.pwfile,)
398        answer = raw_input("try to edit again? [y]> ").strip()
399        if answer and answer[0] not in 'yY':
400          raise RuntimeError("key passwords unavailable")
401      first = False
402
403      current = self.UpdateAndReadFile(current)
404
405  def PromptResult(self, current):
406    """Prompt the user to enter a value (password) for each key in
407    'current' whose value is fales.  Returns a new dict with all the
408    values.
409    """
410    result = {}
411    for k, v in sorted(current.iteritems()):
412      if v:
413        result[k] = v
414      else:
415        while True:
416          result[k] = getpass.getpass("Enter password for %s key> "
417                                      % (k,)).strip()
418          if result[k]: break
419    return result
420
421  def UpdateAndReadFile(self, current):
422    if not self.editor or not self.pwfile:
423      return self.PromptResult(current)
424
425    f = open(self.pwfile, "w")
426    os.chmod(self.pwfile, 0600)
427    f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
428    f.write("# (Additional spaces are harmless.)\n\n")
429
430    first_line = None
431    sorted = [(not v, k, v) for (k, v) in current.iteritems()]
432    sorted.sort()
433    for i, (_, k, v) in enumerate(sorted):
434      f.write("[[[  %s  ]]] %s\n" % (v, k))
435      if not v and first_line is None:
436        # position cursor on first line with no password.
437        first_line = i + 4
438    f.close()
439
440    p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
441    _, _ = p.communicate()
442
443    return self.ReadFile()
444
445  def ReadFile(self):
446    result = {}
447    if self.pwfile is None: return result
448    try:
449      f = open(self.pwfile, "r")
450      for line in f:
451        line = line.strip()
452        if not line or line[0] == '#': continue
453        m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
454        if not m:
455          print "failed to parse password file: ", line
456        else:
457          result[m.group(2)] = m.group(1)
458      f.close()
459    except IOError, e:
460      if e.errno != errno.ENOENT:
461        print "error reading password file: ", str(e)
462    return result
463
464
465def ZipWriteStr(zip, filename, data, perms=0644):
466  # use a fixed timestamp so the output is repeatable.
467  zinfo = zipfile.ZipInfo(filename=filename,
468                          date_time=(2009, 1, 1, 0, 0, 0))
469  zinfo.compress_type = zip.compression
470  zinfo.external_attr = perms << 16
471  zip.writestr(zinfo, data)
472
473
474class DeviceSpecificParams(object):
475  module = None
476  def __init__(self, **kwargs):
477    """Keyword arguments to the constructor become attributes of this
478    object, which is passed to all functions in the device-specific
479    module."""
480    for k, v in kwargs.iteritems():
481      setattr(self, k, v)
482    self.extras = OPTIONS.extras
483
484    if self.module is None:
485      path = OPTIONS.device_specific
486      if not path: return
487      try:
488        if os.path.isdir(path):
489          info = imp.find_module("releasetools", [path])
490        else:
491          d, f = os.path.split(path)
492          b, x = os.path.splitext(f)
493          if x == ".py":
494            f = b
495          info = imp.find_module(f, [d])
496        self.module = imp.load_module("device_specific", *info)
497      except ImportError:
498        print "unable to load device-specific module; assuming none"
499
500  def _DoCall(self, function_name, *args, **kwargs):
501    """Call the named function in the device-specific module, passing
502    the given args and kwargs.  The first argument to the call will be
503    the DeviceSpecific object itself.  If there is no module, or the
504    module does not define the function, return the value of the
505    'default' kwarg (which itself defaults to None)."""
506    if self.module is None or not hasattr(self.module, function_name):
507      return kwargs.get("default", None)
508    return getattr(self.module, function_name)(*((self,) + args), **kwargs)
509
510  def FullOTA_Assertions(self):
511    """Called after emitting the block of assertions at the top of a
512    full OTA package.  Implementations can add whatever additional
513    assertions they like."""
514    return self._DoCall("FullOTA_Assertions")
515
516  def FullOTA_InstallEnd(self):
517    """Called at the end of full OTA installation; typically this is
518    used to install the image for the device's baseband processor."""
519    return self._DoCall("FullOTA_InstallEnd")
520
521  def IncrementalOTA_Assertions(self):
522    """Called after emitting the block of assertions at the top of an
523    incremental OTA package.  Implementations can add whatever
524    additional assertions they like."""
525    return self._DoCall("IncrementalOTA_Assertions")
526
527  def IncrementalOTA_VerifyEnd(self):
528    """Called at the end of the verification phase of incremental OTA
529    installation; additional checks can be placed here to abort the
530    script before any changes are made."""
531    return self._DoCall("IncrementalOTA_VerifyEnd")
532
533  def IncrementalOTA_InstallEnd(self):
534    """Called at the end of incremental OTA installation; typically
535    this is used to install the image for the device's baseband
536    processor."""
537    return self._DoCall("IncrementalOTA_InstallEnd")
538