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