blob: 5e5b856155475a414ca3fbe546323c2ec49a45e8 [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
18import os
19import re
20import shutil
21import subprocess
22import sys
23import tempfile
Doug Zongker048e7ca2009-06-15 14:31:53 -070024import zipfile
Doug Zongkereef39442009-04-02 12:14:19 -070025
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()
Doug Zongker602a84e2009-06-18 08:35:12 -070032OPTIONS.search_path = "out/host/linux-x86"
Doug Zongkereef39442009-04-02 12:14:19 -070033OPTIONS.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
Doug Zongkerfdd8e692009-08-03 17:27:48 -070049def LoadMaxSizes():
50 """Load the maximum allowable images sizes from the input
51 target_files size."""
Doug Zongkereef39442009-04-02 12:14:19 -070052 OPTIONS.max_image_size = {}
Doug Zongkerfdd8e692009-08-03 17:27:48 -070053 try:
54 for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")):
Doug Zongker1aca9622009-08-04 15:09:27 -070055 pieces = line.split()
56 if len(pieces) != 2: continue
57 image = pieces[0]
58 size = int(pieces[1])
Doug Zongkerfdd8e692009-08-03 17:27:48 -070059 OPTIONS.max_image_size[image + ".img"] = size
60 except IOError, e:
61 if e.errno == errno.ENOENT:
62 pass
Doug Zongkereef39442009-04-02 12:14:19 -070063
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
Doug Zongkere1c31ba2009-06-23 17:40:35 -070068 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)."""
Doug Zongkereef39442009-04-02 12:14:19 -070071
72 print "creating %s..." % (targetname,)
73
74 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070075 if img is None:
76 return None
Doug Zongkereef39442009-04-02 12:14:19 -070077
78 CheckSize(img, targetname)
Doug Zongker048e7ca2009-06-15 14:31:53 -070079 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070080 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -070081
82def BuildBootableImage(sourcedir):
83 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -070084 '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
Doug Zongkereef39442009-04-02 12:14:19 -070091
92 ramdisk_img = tempfile.NamedTemporaryFile()
93 img = tempfile.NamedTemporaryFile()
94
95 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
96 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -070097 p2 = Run(["minigzip"],
98 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -070099
100 p2.wait()
101 p1.wait()
102 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700103 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700104
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700105 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)
Doug Zongkereef39442009-04-02 12:14:19 -0700121 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700122 assert p.returncode == 0, "mkbootimg of %s image failed" % (
123 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700124
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", "-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
Doug Zongker8ce7c252009-05-22 13:34:54 -0700160 no_passwords = []
161 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700162 devnull = open("/dev/null", "w+b")
163 for k in sorted(keylist):
Doug Zongker43874f82009-04-14 14:05:15 -0700164 # 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:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700167 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700168 continue
169
Doug Zongker602a84e2009-06-18 08:35:12 -0700170 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
171 "-inform", "DER", "-nocrypt"],
172 stdin=devnull.fileno(),
173 stdout=devnull.fileno(),
174 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700175 p.communicate()
176 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700177 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700178 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700179 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700180 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700181
182 key_passwords = PasswordManager().GetPasswords(need_passwords)
183 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700184 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
Doug Zongker602a84e2009-06-18 08:35:12 -0700204 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)
Doug Zongkereef39442009-04-02 12:14:19 -0700211 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:
Doug Zongker602a84e2009-06-18 08:35:12 -0700218 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700219 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>
Doug Zongker602a84e2009-06-18 08:35:12 -0700247 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.
Doug Zongkereef39442009-04-02 12:14:19 -0700249
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"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700290 OPTIONS.search_path = a
Doug Zongkereef39442009-04-02 12:14:19 -0700291 else:
292 if extra_option_handler is None or not extra_option_handler(o, a):
293 assert False, "unknown option \"%s\"" % (o,)
294
Doug Zongker602a84e2009-06-18 08:35:12 -0700295 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
296 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700297
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)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700307
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
Doug Zongker048e7ca2009-06-15 14:31:53 -0700406
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)