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