blob: 8ca778157ffa9f01ef7abc9af97d65ec3bbc4b89 [file] [log] [blame]
Doug Zongkereef39442009-04-02 12:14:19 -07001# 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
Doug Zongker8ce7c252009-05-22 13:34:54 -070015import errno
Doug Zongkereef39442009-04-02 12:14:19 -070016import getopt
17import getpass
Doug Zongker05d3dea2009-06-22 11:32:31 -070018import imp
Doug Zongkereef39442009-04-02 12:14:19 -070019import os
20import re
21import shutil
22import subprocess
23import sys
24import tempfile
Doug Zongker048e7ca2009-06-15 14:31:53 -070025import zipfile
Doug Zongkereef39442009-04-02 12:14:19 -070026
27# missing in Python 2.4 and before
28if not hasattr(os, "SEEK_SET"):
29 os.SEEK_SET = 0
30
31class Options(object): pass
32OPTIONS = Options()
Doug Zongker602a84e2009-06-18 08:35:12 -070033OPTIONS.search_path = "out/host/linux-x86"
Doug Zongkereef39442009-04-02 12:14:19 -070034OPTIONS.max_image_size = {}
35OPTIONS.verbose = False
36OPTIONS.tempfiles = []
Doug Zongker05d3dea2009-06-22 11:32:31 -070037OPTIONS.device_specific = None
Doug Zongkereef39442009-04-02 12:14:19 -070038
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
Doug Zongkerfdd8e692009-08-03 17:27:48 -070050def LoadMaxSizes():
51 """Load the maximum allowable images sizes from the input
52 target_files size."""
Doug Zongkereef39442009-04-02 12:14:19 -070053 OPTIONS.max_image_size = {}
Doug Zongkerfdd8e692009-08-03 17:27:48 -070054 try:
55 for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")):
Doug Zongker1aca9622009-08-04 15:09:27 -070056 pieces = line.split()
57 if len(pieces) != 2: continue
58 image = pieces[0]
59 size = int(pieces[1])
Doug Zongkerfdd8e692009-08-03 17:27:48 -070060 OPTIONS.max_image_size[image + ".img"] = size
61 except IOError, e:
62 if e.errno == errno.ENOENT:
63 pass
Doug Zongkereef39442009-04-02 12:14:19 -070064
65
66def 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
Doug Zongkere1c31ba2009-06-23 17:40:35 -070069 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)."""
Doug Zongkereef39442009-04-02 12:14:19 -070072
73 print "creating %s..." % (targetname,)
74
75 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070076 if img is None:
77 return None
Doug Zongkereef39442009-04-02 12:14:19 -070078
79 CheckSize(img, targetname)
Doug Zongker048e7ca2009-06-15 14:31:53 -070080 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070081 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -070082
83def BuildBootableImage(sourcedir):
84 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -070085 '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
Doug Zongkereef39442009-04-02 12:14:19 -070092
93 ramdisk_img = tempfile.NamedTemporaryFile()
94 img = tempfile.NamedTemporaryFile()
95
96 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
97 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -070098 p2 = Run(["minigzip"],
99 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -0700100
101 p2.wait()
102 p1.wait()
103 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700104 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700105
Doug Zongker38a649f2009-06-17 09:07:09 -0700106 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
107
Doug Zongker171f1cd2009-06-15 22:36:37 -0700108 fn = os.path.join(sourcedir, "cmdline")
109 if os.access(fn, os.F_OK):
Doug Zongker38a649f2009-06-17 09:07:09 -0700110 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)
Doug Zongkereef39442009-04-02 12:14:19 -0700122 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700123 assert p.returncode == 0, "mkbootimg of %s image failed" % (
124 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700125
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
135def AddRecovery(output_zip):
136 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
137 "recovery.img", output_zip)
138
139def AddBoot(output_zip):
140 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
141 "boot.img", output_zip)
142
143def 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", "-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
156def 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
Doug Zongker8ce7c252009-05-22 13:34:54 -0700161 no_passwords = []
162 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700163 devnull = open("/dev/null", "w+b")
164 for k in sorted(keylist):
Doug Zongker43874f82009-04-14 14:05:15 -0700165 # 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:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700168 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700169 continue
170
Doug Zongker602a84e2009-06-18 08:35:12 -0700171 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
172 "-inform", "DER", "-nocrypt"],
173 stdin=devnull.fileno(),
174 stdout=devnull.fileno(),
175 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700176 p.communicate()
177 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700178 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700179 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700180 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700181 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700182
183 key_passwords = PasswordManager().GetPasswords(need_passwords)
184 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700185 return key_passwords
186
187
188def SignFile(input_name, output_name, key, password, align=None):
189 """Sign the input_name zip/jar/apk, producing output_name. Use the
190 given key and password (the latter may be None if the key does not
191 have a password.
192
193 If align is an integer > 1, zipalign is run to align stored files in
194 the output zip on 'align'-byte boundaries.
195 """
196 if align == 0 or align == 1:
197 align = None
198
199 if align:
200 temp = tempfile.NamedTemporaryFile()
201 sign_name = temp.name
202 else:
203 sign_name = output_name
204
Doug Zongker602a84e2009-06-18 08:35:12 -0700205 p = Run(["java", "-jar",
206 os.path.join(OPTIONS.search_path, "framework", "signapk.jar"),
207 key + ".x509.pem",
208 key + ".pk8",
209 input_name, sign_name],
210 stdin=subprocess.PIPE,
211 stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700212 if password is not None:
213 password += "\n"
214 p.communicate(password)
215 if p.returncode != 0:
216 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
217
218 if align:
Doug Zongker602a84e2009-06-18 08:35:12 -0700219 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700220 p.communicate()
221 if p.returncode != 0:
222 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
223 temp.close()
224
225
226def CheckSize(data, target):
227 """Check the data string passed against the max size limit, if
228 any, for the given target. Raise exception if the data is too big.
229 Print a warning if the data is nearing the maximum size."""
230 limit = OPTIONS.max_image_size.get(target, None)
231 if limit is None: return
232
233 size = len(data)
234 pct = float(size) * 100.0 / limit
235 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
236 if pct >= 99.0:
237 raise ExternalError(msg)
238 elif pct >= 95.0:
239 print
240 print " WARNING: ", msg
241 print
242 elif OPTIONS.verbose:
243 print " ", msg
244
245
246COMMON_DOCSTRING = """
247 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700248 Prepend <dir>/bin to the list of places to search for binaries
249 run by this script, and expect to find jars in <dir>/framework.
Doug Zongkereef39442009-04-02 12:14:19 -0700250
Doug Zongker05d3dea2009-06-22 11:32:31 -0700251 -s (--device_specific) <file>
252 Path to the python module containing device-specific
253 releasetools code.
254
Doug Zongkereef39442009-04-02 12:14:19 -0700255 -v (--verbose)
256 Show command lines being executed.
257
258 -h (--help)
259 Display this usage message and exit.
260"""
261
262def Usage(docstring):
263 print docstring.rstrip("\n")
264 print COMMON_DOCSTRING
265
266
267def ParseOptions(argv,
268 docstring,
269 extra_opts="", extra_long_opts=(),
270 extra_option_handler=None):
271 """Parse the options in argv and return any arguments that aren't
272 flags. docstring is the calling module's docstring, to be displayed
273 for errors and -h. extra_opts and extra_long_opts are for flags
274 defined by the caller, which are processed by passing them to
275 extra_option_handler."""
276
277 try:
278 opts, args = getopt.getopt(
Doug Zongker05d3dea2009-06-22 11:32:31 -0700279 argv, "hvp:s:" + extra_opts,
280 ["help", "verbose", "path=", "device_specific="] +
281 list(extra_long_opts))
Doug Zongkereef39442009-04-02 12:14:19 -0700282 except getopt.GetoptError, err:
283 Usage(docstring)
284 print "**", str(err), "**"
285 sys.exit(2)
286
287 path_specified = False
288
289 for o, a in opts:
290 if o in ("-h", "--help"):
291 Usage(docstring)
292 sys.exit()
293 elif o in ("-v", "--verbose"):
294 OPTIONS.verbose = True
295 elif o in ("-p", "--path"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700296 OPTIONS.search_path = a
Doug Zongker05d3dea2009-06-22 11:32:31 -0700297 elif o in ("-s", "--device_specific"):
298 OPTIONS.device_specific = a
Doug Zongkereef39442009-04-02 12:14:19 -0700299 else:
300 if extra_option_handler is None or not extra_option_handler(o, a):
301 assert False, "unknown option \"%s\"" % (o,)
302
Doug Zongker602a84e2009-06-18 08:35:12 -0700303 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
304 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700305
306 return args
307
308
309def Cleanup():
310 for i in OPTIONS.tempfiles:
311 if os.path.isdir(i):
312 shutil.rmtree(i)
313 else:
314 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700315
316
317class PasswordManager(object):
318 def __init__(self):
319 self.editor = os.getenv("EDITOR", None)
320 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
321
322 def GetPasswords(self, items):
323 """Get passwords corresponding to each string in 'items',
324 returning a dict. (The dict may have keys in addition to the
325 values in 'items'.)
326
327 Uses the passwords in $ANDROID_PW_FILE if available, letting the
328 user edit that file to add more needed passwords. If no editor is
329 available, or $ANDROID_PW_FILE isn't define, prompts the user
330 interactively in the ordinary way.
331 """
332
333 current = self.ReadFile()
334
335 first = True
336 while True:
337 missing = []
338 for i in items:
339 if i not in current or not current[i]:
340 missing.append(i)
341 # Are all the passwords already in the file?
342 if not missing: return current
343
344 for i in missing:
345 current[i] = ""
346
347 if not first:
348 print "key file %s still missing some passwords." % (self.pwfile,)
349 answer = raw_input("try to edit again? [y]> ").strip()
350 if answer and answer[0] not in 'yY':
351 raise RuntimeError("key passwords unavailable")
352 first = False
353
354 current = self.UpdateAndReadFile(current)
355
356 def PromptResult(self, current):
357 """Prompt the user to enter a value (password) for each key in
358 'current' whose value is fales. Returns a new dict with all the
359 values.
360 """
361 result = {}
362 for k, v in sorted(current.iteritems()):
363 if v:
364 result[k] = v
365 else:
366 while True:
367 result[k] = getpass.getpass("Enter password for %s key> "
368 % (k,)).strip()
369 if result[k]: break
370 return result
371
372 def UpdateAndReadFile(self, current):
373 if not self.editor or not self.pwfile:
374 return self.PromptResult(current)
375
376 f = open(self.pwfile, "w")
377 os.chmod(self.pwfile, 0600)
378 f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
379 f.write("# (Additional spaces are harmless.)\n\n")
380
381 first_line = None
382 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
383 sorted.sort()
384 for i, (_, k, v) in enumerate(sorted):
385 f.write("[[[ %s ]]] %s\n" % (v, k))
386 if not v and first_line is None:
387 # position cursor on first line with no password.
388 first_line = i + 4
389 f.close()
390
391 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
392 _, _ = p.communicate()
393
394 return self.ReadFile()
395
396 def ReadFile(self):
397 result = {}
398 if self.pwfile is None: return result
399 try:
400 f = open(self.pwfile, "r")
401 for line in f:
402 line = line.strip()
403 if not line or line[0] == '#': continue
404 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
405 if not m:
406 print "failed to parse password file: ", line
407 else:
408 result[m.group(2)] = m.group(1)
409 f.close()
410 except IOError, e:
411 if e.errno != errno.ENOENT:
412 print "error reading password file: ", str(e)
413 return result
Doug Zongker048e7ca2009-06-15 14:31:53 -0700414
415
416def ZipWriteStr(zip, filename, data, perms=0644):
417 # use a fixed timestamp so the output is repeatable.
418 zinfo = zipfile.ZipInfo(filename=filename,
419 date_time=(2009, 1, 1, 0, 0, 0))
420 zinfo.compress_type = zip.compression
421 zinfo.external_attr = perms << 16
422 zip.writestr(zinfo, data)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700423
424
425class DeviceSpecificParams(object):
426 module = None
427 def __init__(self, **kwargs):
428 """Keyword arguments to the constructor become attributes of this
429 object, which is passed to all functions in the device-specific
430 module."""
431 for k, v in kwargs.iteritems():
432 setattr(self, k, v)
433
434 if self.module is None:
435 path = OPTIONS.device_specific
436 if path is None: return
Doug Zongker8e2f2b92009-06-24 14:34:57 -0700437 try:
438 if os.path.isdir(path):
439 info = imp.find_module("releasetools", [path])
440 else:
441 d, f = os.path.split(path)
442 b, x = os.path.splitext(f)
443 if x == ".py":
444 f = b
445 info = imp.find_module(f, [d])
446 self.module = imp.load_module("device_specific", *info)
447 except ImportError:
448 print "unable to load device-specific module; assuming none"
Doug Zongker05d3dea2009-06-22 11:32:31 -0700449
450 def _DoCall(self, function_name, *args, **kwargs):
451 """Call the named function in the device-specific module, passing
452 the given args and kwargs. The first argument to the call will be
453 the DeviceSpecific object itself. If there is no module, or the
454 module does not define the function, return the value of the
455 'default' kwarg (which itself defaults to None)."""
456 if self.module is None or not hasattr(self.module, function_name):
457 return kwargs.get("default", None)
458 return getattr(self.module, function_name)(*((self,) + args), **kwargs)
459
460 def FullOTA_Assertions(self):
461 """Called after emitting the block of assertions at the top of a
462 full OTA package. Implementations can add whatever additional
463 assertions they like."""
464 return self._DoCall("FullOTA_Assertions")
465
466 def FullOTA_InstallEnd(self):
467 """Called at the end of full OTA installation; typically this is
468 used to install the image for the device's baseband processor."""
469 return self._DoCall("FullOTA_InstallEnd")
470
471 def IncrementalOTA_Assertions(self):
472 """Called after emitting the block of assertions at the top of an
473 incremental OTA package. Implementations can add whatever
474 additional assertions they like."""
475 return self._DoCall("IncrementalOTA_Assertions")
476
477 def IncrementalOTA_VerifyEnd(self):
478 """Called at the end of the verification phase of incremental OTA
479 installation; additional checks can be placed here to abort the
480 script before any changes are made."""
481 return self._DoCall("IncrementalOTA_VerifyEnd")
482
483 def IncrementalOTA_InstallEnd(self):
484 """Called at the end of incremental OTA installation; typically
485 this is used to install the image for the device's baseband
486 processor."""
487 return self._DoCall("IncrementalOTA_InstallEnd")