OSDN Git Service

eclair snapshot
[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 imp
19 import os
20 import re
21 import shutil
22 import subprocess
23 import sys
24 import tempfile
25 import zipfile
26
27 # missing in Python 2.4 and before
28 if not hasattr(os, "SEEK_SET"):
29   os.SEEK_SET = 0
30
31 class Options(object): pass
32 OPTIONS = Options()
33 OPTIONS.search_path = "out/host/linux-x86"
34 OPTIONS.max_image_size = {}
35 OPTIONS.verbose = False
36 OPTIONS.tempfiles = []
37 OPTIONS.device_specific = None
38
39 class ExternalError(RuntimeError): pass
40
41
42 def 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
50 def 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
66 def 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
83 def 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
135 def AddRecovery(output_zip):
136   BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
137                            "recovery.img", output_zip)
138
139 def AddBoot(output_zip):
140   BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
141                            "boot.img", output_zip)
142
143 def 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", "-o", "-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
156 def 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
188 def 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
233 def 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
253 COMMON_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
269 def Usage(docstring):
270   print docstring.rstrip("\n")
271   print COMMON_DOCSTRING
272
273
274 def 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
316 def 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
324 class 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
423 def 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
432 class 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 not path: 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")