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