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