blob: feb8ce36aca49b166e09e550a05e9cf8645cfd90 [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
Doug Zongker951495f2009-08-14 12:44:19 -0700188def SignFile(input_name, output_name, key, password, align=None,
189 whole_file=False):
Doug Zongkereef39442009-04-02 12:14:19 -0700190 """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.
Doug Zongker951495f2009-08-14 12:44:19 -0700196
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.
Doug Zongkereef39442009-04-02 12:14:19 -0700200 """
Doug Zongker951495f2009-08-14 12:44:19 -0700201
Doug Zongkereef39442009-04-02 12:14:19 -0700202 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
Doug Zongker951495f2009-08-14 12:44:19 -0700211 cmd = ["java", "-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)
Doug Zongkereef39442009-04-02 12:14:19 -0700219 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:
Doug Zongker602a84e2009-06-18 08:35:12 -0700226 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700227 p.communicate()
228 if p.returncode != 0:
229 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
230 temp.close()
231
232
233def 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
253COMMON_DOCSTRING = """
254 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700255 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.
Doug Zongkereef39442009-04-02 12:14:19 -0700257
Doug Zongker05d3dea2009-06-22 11:32:31 -0700258 -s (--device_specific) <file>
259 Path to the python module containing device-specific
260 releasetools code.
261
Doug Zongkereef39442009-04-02 12:14:19 -0700262 -v (--verbose)
263 Show command lines being executed.
264
265 -h (--help)
266 Display this usage message and exit.
267"""
268
269def Usage(docstring):
270 print docstring.rstrip("\n")
271 print COMMON_DOCSTRING
272
273
274def 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(
Doug Zongker05d3dea2009-06-22 11:32:31 -0700286 argv, "hvp:s:" + extra_opts,
287 ["help", "verbose", "path=", "device_specific="] +
288 list(extra_long_opts))
Doug Zongkereef39442009-04-02 12:14:19 -0700289 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"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700303 OPTIONS.search_path = a
Doug Zongker05d3dea2009-06-22 11:32:31 -0700304 elif o in ("-s", "--device_specific"):
305 OPTIONS.device_specific = a
Doug Zongkereef39442009-04-02 12:14:19 -0700306 else:
307 if extra_option_handler is None or not extra_option_handler(o, a):
308 assert False, "unknown option \"%s\"" % (o,)
309
Doug Zongker602a84e2009-06-18 08:35:12 -0700310 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
311 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700312
313 return args
314
315
316def Cleanup():
317 for i in OPTIONS.tempfiles:
318 if os.path.isdir(i):
319 shutil.rmtree(i)
320 else:
321 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700322
323
324class 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
Doug Zongker048e7ca2009-06-15 14:31:53 -0700421
422
423def 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)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700430
431
432class 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 path is None: return
Doug Zongker8e2f2b92009-06-24 14:34:57 -0700444 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"
Doug Zongker05d3dea2009-06-22 11:32:31 -0700456
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")