blob: a07ff7c36e6f4de861e0ced447a117adfd322a17 [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
49def LoadBoardConfig(fn):
50 """Parse a board_config.mk file looking for lines that specify the
51 maximum size of various images, and parse them into the
52 OPTIONS.max_image_size dict."""
53 OPTIONS.max_image_size = {}
54 for line in open(fn):
55 line = line.strip()
56 m = re.match(r"BOARD_(BOOT|RECOVERY|SYSTEM|USERDATA)IMAGE_MAX_SIZE"
57 r"\s*:=\s*(\d+)", line)
58 if not m: continue
59
60 OPTIONS.max_image_size[m.group(1).lower() + ".img"] = int(m.group(2))
61
62
63def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
64 """Take a kernel, cmdline, and ramdisk directory from the input (in
65 'sourcedir'), and turn them into a boot image. Put the boot image
Doug Zongkere1c31ba2009-06-23 17:40:35 -070066 into the output zip file under the name 'targetname'. Returns
67 targetname on success or None on failure (if sourcedir does not
68 appear to contain files for the requested image)."""
Doug Zongkereef39442009-04-02 12:14:19 -070069
70 print "creating %s..." % (targetname,)
71
72 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070073 if img is None:
74 return None
Doug Zongkereef39442009-04-02 12:14:19 -070075
76 CheckSize(img, targetname)
Doug Zongker048e7ca2009-06-15 14:31:53 -070077 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -070078 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -070079
80def BuildBootableImage(sourcedir):
81 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -070082 'sourcedir'), and turn them into a boot image. Return the image
83 data, or None if sourcedir does not appear to contains files for
84 building the requested image."""
85
86 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
87 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
88 return None
Doug Zongkereef39442009-04-02 12:14:19 -070089
90 ramdisk_img = tempfile.NamedTemporaryFile()
91 img = tempfile.NamedTemporaryFile()
92
93 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
94 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -070095 p2 = Run(["minigzip"],
96 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -070097
98 p2.wait()
99 p1.wait()
100 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700101 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700102
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700103 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
104
105 fn = os.path.join(sourcedir, "cmdline")
106 if os.access(fn, os.F_OK):
107 cmd.append("--cmdline")
108 cmd.append(open(fn).read().rstrip("\n"))
109
110 fn = os.path.join(sourcedir, "base")
111 if os.access(fn, os.F_OK):
112 cmd.append("--base")
113 cmd.append(open(fn).read().rstrip("\n"))
114
115 cmd.extend(["--ramdisk", ramdisk_img.name,
116 "--output", img.name])
117
118 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700119 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700120 assert p.returncode == 0, "mkbootimg of %s image failed" % (
121 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700122
123 img.seek(os.SEEK_SET, 0)
124 data = img.read()
125
126 ramdisk_img.close()
127 img.close()
128
129 return data
130
131
132def AddRecovery(output_zip):
133 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
134 "recovery.img", output_zip)
135
136def AddBoot(output_zip):
137 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
138 "boot.img", output_zip)
139
140def UnzipTemp(filename):
141 """Unzip the given archive into a temporary directory and return the name."""
142
143 tmp = tempfile.mkdtemp(prefix="targetfiles-")
144 OPTIONS.tempfiles.append(tmp)
145 p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
146 p.communicate()
147 if p.returncode != 0:
148 raise ExternalError("failed to unzip input target-files \"%s\"" %
149 (filename,))
150 return tmp
151
152
153def GetKeyPasswords(keylist):
154 """Given a list of keys, prompt the user to enter passwords for
155 those which require them. Return a {key: password} dict. password
156 will be None if the key has no password."""
157
Doug Zongker8ce7c252009-05-22 13:34:54 -0700158 no_passwords = []
159 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700160 devnull = open("/dev/null", "w+b")
161 for k in sorted(keylist):
Doug Zongker43874f82009-04-14 14:05:15 -0700162 # An empty-string key is used to mean don't re-sign this package.
163 # Obviously we don't need a password for this non-key.
164 if not k:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700165 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700166 continue
167
Doug Zongker602a84e2009-06-18 08:35:12 -0700168 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
169 "-inform", "DER", "-nocrypt"],
170 stdin=devnull.fileno(),
171 stdout=devnull.fileno(),
172 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700173 p.communicate()
174 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700175 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700176 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700177 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700178 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700179
180 key_passwords = PasswordManager().GetPasswords(need_passwords)
181 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700182 return key_passwords
183
184
185def SignFile(input_name, output_name, key, password, align=None):
186 """Sign the input_name zip/jar/apk, producing output_name. Use the
187 given key and password (the latter may be None if the key does not
188 have a password.
189
190 If align is an integer > 1, zipalign is run to align stored files in
191 the output zip on 'align'-byte boundaries.
192 """
193 if align == 0 or align == 1:
194 align = None
195
196 if align:
197 temp = tempfile.NamedTemporaryFile()
198 sign_name = temp.name
199 else:
200 sign_name = output_name
201
Doug Zongker602a84e2009-06-18 08:35:12 -0700202 p = Run(["java", "-jar",
203 os.path.join(OPTIONS.search_path, "framework", "signapk.jar"),
204 key + ".x509.pem",
205 key + ".pk8",
206 input_name, sign_name],
207 stdin=subprocess.PIPE,
208 stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700209 if password is not None:
210 password += "\n"
211 p.communicate(password)
212 if p.returncode != 0:
213 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
214
215 if align:
Doug Zongker602a84e2009-06-18 08:35:12 -0700216 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700217 p.communicate()
218 if p.returncode != 0:
219 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
220 temp.close()
221
222
223def CheckSize(data, target):
224 """Check the data string passed against the max size limit, if
225 any, for the given target. Raise exception if the data is too big.
226 Print a warning if the data is nearing the maximum size."""
227 limit = OPTIONS.max_image_size.get(target, None)
228 if limit is None: return
229
230 size = len(data)
231 pct = float(size) * 100.0 / limit
232 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
233 if pct >= 99.0:
234 raise ExternalError(msg)
235 elif pct >= 95.0:
236 print
237 print " WARNING: ", msg
238 print
239 elif OPTIONS.verbose:
240 print " ", msg
241
242
243COMMON_DOCSTRING = """
244 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700245 Prepend <dir>/bin to the list of places to search for binaries
246 run by this script, and expect to find jars in <dir>/framework.
Doug Zongkereef39442009-04-02 12:14:19 -0700247
248 -v (--verbose)
249 Show command lines being executed.
250
251 -h (--help)
252 Display this usage message and exit.
253"""
254
255def Usage(docstring):
256 print docstring.rstrip("\n")
257 print COMMON_DOCSTRING
258
259
260def ParseOptions(argv,
261 docstring,
262 extra_opts="", extra_long_opts=(),
263 extra_option_handler=None):
264 """Parse the options in argv and return any arguments that aren't
265 flags. docstring is the calling module's docstring, to be displayed
266 for errors and -h. extra_opts and extra_long_opts are for flags
267 defined by the caller, which are processed by passing them to
268 extra_option_handler."""
269
270 try:
271 opts, args = getopt.getopt(
272 argv, "hvp:" + extra_opts,
273 ["help", "verbose", "path="] + list(extra_long_opts))
274 except getopt.GetoptError, err:
275 Usage(docstring)
276 print "**", str(err), "**"
277 sys.exit(2)
278
279 path_specified = False
280
281 for o, a in opts:
282 if o in ("-h", "--help"):
283 Usage(docstring)
284 sys.exit()
285 elif o in ("-v", "--verbose"):
286 OPTIONS.verbose = True
287 elif o in ("-p", "--path"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700288 OPTIONS.search_path = a
Doug Zongkereef39442009-04-02 12:14:19 -0700289 else:
290 if extra_option_handler is None or not extra_option_handler(o, a):
291 assert False, "unknown option \"%s\"" % (o,)
292
Doug Zongker602a84e2009-06-18 08:35:12 -0700293 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
294 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700295
296 return args
297
298
299def Cleanup():
300 for i in OPTIONS.tempfiles:
301 if os.path.isdir(i):
302 shutil.rmtree(i)
303 else:
304 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700305
306
307class PasswordManager(object):
308 def __init__(self):
309 self.editor = os.getenv("EDITOR", None)
310 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
311
312 def GetPasswords(self, items):
313 """Get passwords corresponding to each string in 'items',
314 returning a dict. (The dict may have keys in addition to the
315 values in 'items'.)
316
317 Uses the passwords in $ANDROID_PW_FILE if available, letting the
318 user edit that file to add more needed passwords. If no editor is
319 available, or $ANDROID_PW_FILE isn't define, prompts the user
320 interactively in the ordinary way.
321 """
322
323 current = self.ReadFile()
324
325 first = True
326 while True:
327 missing = []
328 for i in items:
329 if i not in current or not current[i]:
330 missing.append(i)
331 # Are all the passwords already in the file?
332 if not missing: return current
333
334 for i in missing:
335 current[i] = ""
336
337 if not first:
338 print "key file %s still missing some passwords." % (self.pwfile,)
339 answer = raw_input("try to edit again? [y]> ").strip()
340 if answer and answer[0] not in 'yY':
341 raise RuntimeError("key passwords unavailable")
342 first = False
343
344 current = self.UpdateAndReadFile(current)
345
346 def PromptResult(self, current):
347 """Prompt the user to enter a value (password) for each key in
348 'current' whose value is fales. Returns a new dict with all the
349 values.
350 """
351 result = {}
352 for k, v in sorted(current.iteritems()):
353 if v:
354 result[k] = v
355 else:
356 while True:
357 result[k] = getpass.getpass("Enter password for %s key> "
358 % (k,)).strip()
359 if result[k]: break
360 return result
361
362 def UpdateAndReadFile(self, current):
363 if not self.editor or not self.pwfile:
364 return self.PromptResult(current)
365
366 f = open(self.pwfile, "w")
367 os.chmod(self.pwfile, 0600)
368 f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
369 f.write("# (Additional spaces are harmless.)\n\n")
370
371 first_line = None
372 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
373 sorted.sort()
374 for i, (_, k, v) in enumerate(sorted):
375 f.write("[[[ %s ]]] %s\n" % (v, k))
376 if not v and first_line is None:
377 # position cursor on first line with no password.
378 first_line = i + 4
379 f.close()
380
381 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
382 _, _ = p.communicate()
383
384 return self.ReadFile()
385
386 def ReadFile(self):
387 result = {}
388 if self.pwfile is None: return result
389 try:
390 f = open(self.pwfile, "r")
391 for line in f:
392 line = line.strip()
393 if not line or line[0] == '#': continue
394 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
395 if not m:
396 print "failed to parse password file: ", line
397 else:
398 result[m.group(2)] = m.group(1)
399 f.close()
400 except IOError, e:
401 if e.errno != errno.ENOENT:
402 print "error reading password file: ", str(e)
403 return result
Doug Zongker048e7ca2009-06-15 14:31:53 -0700404
405
406def ZipWriteStr(zip, filename, data, perms=0644):
407 # use a fixed timestamp so the output is repeatable.
408 zinfo = zipfile.ZipInfo(filename=filename,
409 date_time=(2009, 1, 1, 0, 0, 0))
410 zinfo.compress_type = zip.compression
411 zinfo.external_attr = perms << 16
412 zip.writestr(zinfo, data)