common.py revision 43874f8c864972b9dae7e4927aa347455b774c94
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 getopt
16import getpass
17import os
18import re
19import shutil
20import subprocess
21import sys
22import tempfile
23
24# missing in Python 2.4 and before
25if not hasattr(os, "SEEK_SET"):
26  os.SEEK_SET = 0
27
28class Options(object): pass
29OPTIONS = Options()
30OPTIONS.signapk_jar = "out/host/linux-x86/framework/signapk.jar"
31OPTIONS.dumpkey_jar = "out/host/linux-x86/framework/dumpkey.jar"
32OPTIONS.max_image_size = {}
33OPTIONS.verbose = False
34OPTIONS.tempfiles = []
35
36
37class ExternalError(RuntimeError): pass
38
39
40def Run(args, **kwargs):
41  """Create and return a subprocess.Popen object, printing the command
42  line on the terminal if -v was specified."""
43  if OPTIONS.verbose:
44    print "  running: ", " ".join(args)
45  return subprocess.Popen(args, **kwargs)
46
47
48def LoadBoardConfig(fn):
49  """Parse a board_config.mk file looking for lines that specify the
50  maximum size of various images, and parse them into the
51  OPTIONS.max_image_size dict."""
52  OPTIONS.max_image_size = {}
53  for line in open(fn):
54    line = line.strip()
55    m = re.match(r"BOARD_(BOOT|RECOVERY|SYSTEM|USERDATA)IMAGE_MAX_SIZE"
56                 r"\s*:=\s*(\d+)", line)
57    if not m: continue
58
59    OPTIONS.max_image_size[m.group(1).lower() + ".img"] = int(m.group(2))
60
61
62def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
63  """Take a kernel, cmdline, and ramdisk directory from the input (in
64  'sourcedir'), and turn them into a boot image.  Put the boot image
65  into the output zip file under the name 'targetname'."""
66
67  print "creating %s..." % (targetname,)
68
69  img = BuildBootableImage(sourcedir)
70
71  CheckSize(img, targetname)
72  output_zip.writestr(targetname, img)
73
74def BuildBootableImage(sourcedir):
75  """Take a kernel, cmdline, and ramdisk directory from the input (in
76  'sourcedir'), and turn them into a boot image.  Return the image data."""
77
78  ramdisk_img = tempfile.NamedTemporaryFile()
79  img = tempfile.NamedTemporaryFile()
80
81  p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
82           stdout=subprocess.PIPE)
83  p2 = Run(["gzip", "-n"], stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
84
85  p2.wait()
86  p1.wait()
87  assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
88  assert p2.returncode == 0, "gzip of %s ramdisk failed" % (targetname,)
89
90  cmdline = open(os.path.join(sourcedir, "cmdline")).read().rstrip("\n")
91  p = Run(["mkbootimg",
92           "--kernel", os.path.join(sourcedir, "kernel"),
93           "--cmdline", cmdline,
94           "--ramdisk", ramdisk_img.name,
95           "--output", img.name],
96          stdout=subprocess.PIPE)
97  p.communicate()
98  assert p.returncode == 0, "mkbootimg of %s image failed" % (targetname,)
99
100  img.seek(os.SEEK_SET, 0)
101  data = img.read()
102
103  ramdisk_img.close()
104  img.close()
105
106  return data
107
108
109def AddRecovery(output_zip):
110  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
111                           "recovery.img", output_zip)
112
113def AddBoot(output_zip):
114  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
115                           "boot.img", output_zip)
116
117def UnzipTemp(filename):
118  """Unzip the given archive into a temporary directory and return the name."""
119
120  tmp = tempfile.mkdtemp(prefix="targetfiles-")
121  OPTIONS.tempfiles.append(tmp)
122  p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
123  p.communicate()
124  if p.returncode != 0:
125    raise ExternalError("failed to unzip input target-files \"%s\"" %
126                        (filename,))
127  return tmp
128
129
130def GetKeyPasswords(keylist):
131  """Given a list of keys, prompt the user to enter passwords for
132  those which require them.  Return a {key: password} dict.  password
133  will be None if the key has no password."""
134
135  key_passwords = {}
136  devnull = open("/dev/null", "w+b")
137  for k in sorted(keylist):
138    # An empty-string key is used to mean don't re-sign this package.
139    # Obviously we don't need a password for this non-key.
140    if not k:
141      key_passwords[k] = None
142      continue
143
144    p = subprocess.Popen(["openssl", "pkcs8", "-in", k+".pk8",
145                          "-inform", "DER", "-nocrypt"],
146                         stdin=devnull.fileno(),
147                         stdout=devnull.fileno(),
148                         stderr=subprocess.STDOUT)
149    p.communicate()
150    if p.returncode == 0:
151      print "%s.pk8 does not require a password" % (k,)
152      key_passwords[k] = None
153    else:
154      key_passwords[k] = getpass.getpass("Enter password for %s.pk8> " % (k,))
155  devnull.close()
156  print
157  return key_passwords
158
159
160def SignFile(input_name, output_name, key, password, align=None):
161  """Sign the input_name zip/jar/apk, producing output_name.  Use the
162  given key and password (the latter may be None if the key does not
163  have a password.
164
165  If align is an integer > 1, zipalign is run to align stored files in
166  the output zip on 'align'-byte boundaries.
167  """
168  if align == 0 or align == 1:
169    align = None
170
171  if align:
172    temp = tempfile.NamedTemporaryFile()
173    sign_name = temp.name
174  else:
175    sign_name = output_name
176
177  p = subprocess.Popen(["java", "-jar", OPTIONS.signapk_jar,
178                        key + ".x509.pem",
179                        key + ".pk8",
180                        input_name, sign_name],
181                       stdin=subprocess.PIPE,
182                       stdout=subprocess.PIPE)
183  if password is not None:
184    password += "\n"
185  p.communicate(password)
186  if p.returncode != 0:
187    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
188
189  if align:
190    p = subprocess.Popen(["zipalign", "-f", str(align), sign_name, output_name])
191    p.communicate()
192    if p.returncode != 0:
193      raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
194    temp.close()
195
196
197def CheckSize(data, target):
198  """Check the data string passed against the max size limit, if
199  any, for the given target.  Raise exception if the data is too big.
200  Print a warning if the data is nearing the maximum size."""
201  limit = OPTIONS.max_image_size.get(target, None)
202  if limit is None: return
203
204  size = len(data)
205  pct = float(size) * 100.0 / limit
206  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
207  if pct >= 99.0:
208    raise ExternalError(msg)
209  elif pct >= 95.0:
210    print
211    print "  WARNING: ", msg
212    print
213  elif OPTIONS.verbose:
214    print "  ", msg
215
216
217COMMON_DOCSTRING = """
218  -p  (--path)  <dir>
219      Prepend <dir> to the list of places to search for binaries run
220      by this script.
221
222  -v  (--verbose)
223      Show command lines being executed.
224
225  -h  (--help)
226      Display this usage message and exit.
227"""
228
229def Usage(docstring):
230  print docstring.rstrip("\n")
231  print COMMON_DOCSTRING
232
233
234def ParseOptions(argv,
235                 docstring,
236                 extra_opts="", extra_long_opts=(),
237                 extra_option_handler=None):
238  """Parse the options in argv and return any arguments that aren't
239  flags.  docstring is the calling module's docstring, to be displayed
240  for errors and -h.  extra_opts and extra_long_opts are for flags
241  defined by the caller, which are processed by passing them to
242  extra_option_handler."""
243
244  try:
245    opts, args = getopt.getopt(
246        argv, "hvp:" + extra_opts,
247        ["help", "verbose", "path="] + list(extra_long_opts))
248  except getopt.GetoptError, err:
249    Usage(docstring)
250    print "**", str(err), "**"
251    sys.exit(2)
252
253  path_specified = False
254
255  for o, a in opts:
256    if o in ("-h", "--help"):
257      Usage(docstring)
258      sys.exit()
259    elif o in ("-v", "--verbose"):
260      OPTIONS.verbose = True
261    elif o in ("-p", "--path"):
262      os.environ["PATH"] = a + os.pathsep + os.environ["PATH"]
263      path_specified = True
264    else:
265      if extra_option_handler is None or not extra_option_handler(o, a):
266        assert False, "unknown option \"%s\"" % (o,)
267
268  if not path_specified:
269    os.environ["PATH"] = ("out/host/linux-x86/bin" + os.pathsep +
270                          os.environ["PATH"])
271
272  return args
273
274
275def Cleanup():
276  for i in OPTIONS.tempfiles:
277    if os.path.isdir(i):
278      shutil.rmtree(i)
279    else:
280      os.remove(i)
281