OSDN Git Service

Merge change 20232 into donut
[android-x86/build.git] / tools / releasetools / common.py
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
15 import errno
16 import getopt
17 import getpass
18 import os
19 import re
20 import shutil
21 import subprocess
22 import sys
23 import tempfile
24 import zipfile
25
26 # missing in Python 2.4 and before
27 if not hasattr(os, "SEEK_SET"):
28   os.SEEK_SET = 0
29
30 class Options(object): pass
31 OPTIONS = Options()
32 OPTIONS.search_path = "out/host/linux-x86"
33 OPTIONS.max_image_size = {}
34 OPTIONS.verbose = False
35 OPTIONS.tempfiles = []
36
37
38 class ExternalError(RuntimeError): pass
39
40
41 def 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
49 def 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
65 def 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
82 def 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
134 def AddRecovery(output_zip):
135   BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
136                            "recovery.img", output_zip)
137
138 def AddBoot(output_zip):
139   BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
140                            "boot.img", output_zip)
141
142 def 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", "-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
155 def 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
187 def 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
225 def 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
245 COMMON_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
257 def Usage(docstring):
258   print docstring.rstrip("\n")
259   print COMMON_DOCSTRING
260
261
262 def 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
301 def 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
309 class 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
408 def 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)