common.py revision 38a649f873da8d4df7df14619cbd967570d2de9b
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 os
19import re
20import shutil
21import subprocess
22import sys
23import tempfile
24import zipfile
25
26# missing in Python 2.4 and before
27if not hasattr(os, "SEEK_SET"):
28  os.SEEK_SET = 0
29
30class Options(object): pass
31OPTIONS = Options()
32OPTIONS.signapk_jar = "out/host/linux-x86/framework/signapk.jar"
33OPTIONS.dumpkey_jar = "out/host/linux-x86/framework/dumpkey.jar"
34OPTIONS.max_image_size = {}
35OPTIONS.verbose = False
36OPTIONS.tempfiles = []
37
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 LoadBoardConfig(fn):
51  """Parse a board_config.mk file looking for lines that specify the
52  maximum size of various images, and parse them into the
53  OPTIONS.max_image_size dict."""
54  OPTIONS.max_image_size = {}
55  for line in open(fn):
56    line = line.strip()
57    m = re.match(r"BOARD_(BOOT|RECOVERY|SYSTEM|USERDATA)IMAGE_MAX_SIZE"
58                 r"\s*:=\s*(\d+)", line)
59    if not m: continue
60
61    OPTIONS.max_image_size[m.group(1).lower() + ".img"] = int(m.group(2))
62
63
64def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
65  """Take a kernel, cmdline, and ramdisk directory from the input (in
66  'sourcedir'), and turn them into a boot image.  Put the boot image
67  into the output zip file under the name 'targetname'."""
68
69  print "creating %s..." % (targetname,)
70
71  img = BuildBootableImage(sourcedir)
72
73  CheckSize(img, targetname)
74  ZipWriteStr(output_zip, targetname, img)
75
76def BuildBootableImage(sourcedir):
77  """Take a kernel, cmdline, and ramdisk directory from the input (in
78  'sourcedir'), and turn them into a boot image.  Return the image data."""
79
80  ramdisk_img = tempfile.NamedTemporaryFile()
81  img = tempfile.NamedTemporaryFile()
82
83  p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
84           stdout=subprocess.PIPE)
85  p2 = Run(["minigzip"],
86           stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
87
88  p2.wait()
89  p1.wait()
90  assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
91  assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
92
93  cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
94
95  fn = os.path.join(sourcedir, "cmdline")
96  if os.access(fn, os.F_OK):
97    cmd.append("--cmdline")
98    cmd.append(open(fn).read().rstrip("\n"))
99
100  fn = os.path.join(sourcedir, "base")
101  if os.access(fn, os.F_OK):
102    cmd.append("--base")
103    cmd.append(open(fn).read().rstrip("\n"))
104
105  cmd.extend(["--ramdisk", ramdisk_img.name,
106              "--output", img.name])
107
108  p = Run(cmd, stdout=subprocess.PIPE)
109  p.communicate()
110  assert p.returncode == 0, "mkbootimg of %s image failed" % (targetname,)
111
112  img.seek(os.SEEK_SET, 0)
113  data = img.read()
114
115  ramdisk_img.close()
116  img.close()
117
118  return data
119
120
121def AddRecovery(output_zip):
122  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
123                           "recovery.img", output_zip)
124
125def AddBoot(output_zip):
126  BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
127                           "boot.img", output_zip)
128
129def UnzipTemp(filename):
130  """Unzip the given archive into a temporary directory and return the name."""
131
132  tmp = tempfile.mkdtemp(prefix="targetfiles-")
133  OPTIONS.tempfiles.append(tmp)
134  p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
135  p.communicate()
136  if p.returncode != 0:
137    raise ExternalError("failed to unzip input target-files \"%s\"" %
138                        (filename,))
139  return tmp
140
141
142def GetKeyPasswords(keylist):
143  """Given a list of keys, prompt the user to enter passwords for
144  those which require them.  Return a {key: password} dict.  password
145  will be None if the key has no password."""
146
147  no_passwords = []
148  need_passwords = []
149  devnull = open("/dev/null", "w+b")
150  for k in sorted(keylist):
151    # An empty-string key is used to mean don't re-sign this package.
152    # Obviously we don't need a password for this non-key.
153    if not k:
154      no_passwords.append(k)
155      continue
156
157    p = subprocess.Popen(["openssl", "pkcs8", "-in", k+".pk8",
158                          "-inform", "DER", "-nocrypt"],
159                         stdin=devnull.fileno(),
160                         stdout=devnull.fileno(),
161                         stderr=subprocess.STDOUT)
162    p.communicate()
163    if p.returncode == 0:
164      no_passwords.append(k)
165    else:
166      need_passwords.append(k)
167  devnull.close()
168
169  key_passwords = PasswordManager().GetPasswords(need_passwords)
170  key_passwords.update(dict.fromkeys(no_passwords, None))
171  return key_passwords
172
173
174def SignFile(input_name, output_name, key, password, align=None):
175  """Sign the input_name zip/jar/apk, producing output_name.  Use the
176  given key and password (the latter may be None if the key does not
177  have a password.
178
179  If align is an integer > 1, zipalign is run to align stored files in
180  the output zip on 'align'-byte boundaries.
181  """
182  if align == 0 or align == 1:
183    align = None
184
185  if align:
186    temp = tempfile.NamedTemporaryFile()
187    sign_name = temp.name
188  else:
189    sign_name = output_name
190
191  p = subprocess.Popen(["java", "-jar", OPTIONS.signapk_jar,
192                        key + ".x509.pem",
193                        key + ".pk8",
194                        input_name, sign_name],
195                       stdin=subprocess.PIPE,
196                       stdout=subprocess.PIPE)
197  if password is not None:
198    password += "\n"
199  p.communicate(password)
200  if p.returncode != 0:
201    raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
202
203  if align:
204    p = subprocess.Popen(["zipalign", "-f", str(align), sign_name, output_name])
205    p.communicate()
206    if p.returncode != 0:
207      raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
208    temp.close()
209
210
211def CheckSize(data, target):
212  """Check the data string passed against the max size limit, if
213  any, for the given target.  Raise exception if the data is too big.
214  Print a warning if the data is nearing the maximum size."""
215  limit = OPTIONS.max_image_size.get(target, None)
216  if limit is None: return
217
218  size = len(data)
219  pct = float(size) * 100.0 / limit
220  msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
221  if pct >= 99.0:
222    raise ExternalError(msg)
223  elif pct >= 95.0:
224    print
225    print "  WARNING: ", msg
226    print
227  elif OPTIONS.verbose:
228    print "  ", msg
229
230
231COMMON_DOCSTRING = """
232  -p  (--path)  <dir>
233      Prepend <dir> to the list of places to search for binaries run
234      by this script.
235
236  -v  (--verbose)
237      Show command lines being executed.
238
239  -h  (--help)
240      Display this usage message and exit.
241"""
242
243def Usage(docstring):
244  print docstring.rstrip("\n")
245  print COMMON_DOCSTRING
246
247
248def ParseOptions(argv,
249                 docstring,
250                 extra_opts="", extra_long_opts=(),
251                 extra_option_handler=None):
252  """Parse the options in argv and return any arguments that aren't
253  flags.  docstring is the calling module's docstring, to be displayed
254  for errors and -h.  extra_opts and extra_long_opts are for flags
255  defined by the caller, which are processed by passing them to
256  extra_option_handler."""
257
258  try:
259    opts, args = getopt.getopt(
260        argv, "hvp:" + extra_opts,
261        ["help", "verbose", "path="] + list(extra_long_opts))
262  except getopt.GetoptError, err:
263    Usage(docstring)
264    print "**", str(err), "**"
265    sys.exit(2)
266
267  path_specified = False
268
269  for o, a in opts:
270    if o in ("-h", "--help"):
271      Usage(docstring)
272      sys.exit()
273    elif o in ("-v", "--verbose"):
274      OPTIONS.verbose = True
275    elif o in ("-p", "--path"):
276      os.environ["PATH"] = a + os.pathsep + os.environ["PATH"]
277      path_specified = True
278    else:
279      if extra_option_handler is None or not extra_option_handler(o, a):
280        assert False, "unknown option \"%s\"" % (o,)
281
282  if not path_specified:
283    os.environ["PATH"] = ("out/host/linux-x86/bin" + os.pathsep +
284                          os.environ["PATH"])
285
286  return args
287
288
289def Cleanup():
290  for i in OPTIONS.tempfiles:
291    if os.path.isdir(i):
292      shutil.rmtree(i)
293    else:
294      os.remove(i)
295
296
297class PasswordManager(object):
298  def __init__(self):
299    self.editor = os.getenv("EDITOR", None)
300    self.pwfile = os.getenv("ANDROID_PW_FILE", None)
301
302  def GetPasswords(self, items):
303    """Get passwords corresponding to each string in 'items',
304    returning a dict.  (The dict may have keys in addition to the
305    values in 'items'.)
306
307    Uses the passwords in $ANDROID_PW_FILE if available, letting the
308    user edit that file to add more needed passwords.  If no editor is
309    available, or $ANDROID_PW_FILE isn't define, prompts the user
310    interactively in the ordinary way.
311    """
312
313    current = self.ReadFile()
314
315    first = True
316    while True:
317      missing = []
318      for i in items:
319        if i not in current or not current[i]:
320          missing.append(i)
321      # Are all the passwords already in the file?
322      if not missing: return current
323
324      for i in missing:
325        current[i] = ""
326
327      if not first:
328        print "key file %s still missing some passwords." % (self.pwfile,)
329        answer = raw_input("try to edit again? [y]> ").strip()
330        if answer and answer[0] not in 'yY':
331          raise RuntimeError("key passwords unavailable")
332      first = False
333
334      current = self.UpdateAndReadFile(current)
335
336  def PromptResult(self, current):
337    """Prompt the user to enter a value (password) for each key in
338    'current' whose value is fales.  Returns a new dict with all the
339    values.
340    """
341    result = {}
342    for k, v in sorted(current.iteritems()):
343      if v:
344        result[k] = v
345      else:
346        while True:
347          result[k] = getpass.getpass("Enter password for %s key> "
348                                      % (k,)).strip()
349          if result[k]: break
350    return result
351
352  def UpdateAndReadFile(self, current):
353    if not self.editor or not self.pwfile:
354      return self.PromptResult(current)
355
356    f = open(self.pwfile, "w")
357    os.chmod(self.pwfile, 0600)
358    f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
359    f.write("# (Additional spaces are harmless.)\n\n")
360
361    first_line = None
362    sorted = [(not v, k, v) for (k, v) in current.iteritems()]
363    sorted.sort()
364    for i, (_, k, v) in enumerate(sorted):
365      f.write("[[[  %s  ]]] %s\n" % (v, k))
366      if not v and first_line is None:
367        # position cursor on first line with no password.
368        first_line = i + 4
369    f.close()
370
371    p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
372    _, _ = p.communicate()
373
374    return self.ReadFile()
375
376  def ReadFile(self):
377    result = {}
378    if self.pwfile is None: return result
379    try:
380      f = open(self.pwfile, "r")
381      for line in f:
382        line = line.strip()
383        if not line or line[0] == '#': continue
384        m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
385        if not m:
386          print "failed to parse password file: ", line
387        else:
388          result[m.group(2)] = m.group(1)
389      f.close()
390    except IOError, e:
391      if e.errno != errno.ENOENT:
392        print "error reading password file: ", str(e)
393    return result
394
395
396def ZipWriteStr(zip, filename, data, perms=0644):
397  # use a fixed timestamp so the output is repeatable.
398  zinfo = zipfile.ZipInfo(filename=filename,
399                          date_time=(2009, 1, 1, 0, 0, 0))
400  zinfo.compress_type = zip.compression
401  zinfo.external_attr = perms << 16
402  zip.writestr(zinfo, data)
403