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