blob: 8884fe8fcacf5022d149ce65135f74010f813d75 [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 Zongkerea5d7a92010-09-12 15:26:16 -070015import copy
Doug Zongker8ce7c252009-05-22 13:34:54 -070016import errno
Doug Zongkereef39442009-04-02 12:14:19 -070017import getopt
18import getpass
Doug Zongker05d3dea2009-06-22 11:32:31 -070019import imp
Doug Zongkereef39442009-04-02 12:14:19 -070020import os
21import re
Doug Zongkerea5d7a92010-09-12 15:26:16 -070022import sha
Doug Zongkereef39442009-04-02 12:14:19 -070023import shutil
24import subprocess
25import sys
26import tempfile
Doug Zongkerea5d7a92010-09-12 15:26:16 -070027import threading
28import time
Doug Zongker048e7ca2009-06-15 14:31:53 -070029import zipfile
Doug Zongkereef39442009-04-02 12:14:19 -070030
31# missing in Python 2.4 and before
32if not hasattr(os, "SEEK_SET"):
33 os.SEEK_SET = 0
34
35class Options(object): pass
36OPTIONS = Options()
Doug Zongker602a84e2009-06-18 08:35:12 -070037OPTIONS.search_path = "out/host/linux-x86"
Doug Zongkereef39442009-04-02 12:14:19 -070038OPTIONS.verbose = False
39OPTIONS.tempfiles = []
Doug Zongker05d3dea2009-06-22 11:32:31 -070040OPTIONS.device_specific = None
Doug Zongker8bec09e2009-11-30 15:37:14 -080041OPTIONS.extras = {}
Doug Zongkerc77a9ad2010-09-16 11:28:43 -070042OPTIONS.info_dict = None
Doug Zongkereef39442009-04-02 12:14:19 -070043
Doug Zongkerf6a53aa2009-12-15 15:06:55 -080044
45# Values for "certificate" in apkcerts that mean special things.
46SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL")
47
48
Doug Zongkereef39442009-04-02 12:14:19 -070049class ExternalError(RuntimeError): pass
50
51
52def Run(args, **kwargs):
53 """Create and return a subprocess.Popen object, printing the command
54 line on the terminal if -v was specified."""
55 if OPTIONS.verbose:
56 print " running: ", " ".join(args)
57 return subprocess.Popen(args, **kwargs)
58
59
Doug Zongker37974732010-09-16 17:44:38 -070060def LoadInfoDict(zip):
Doug Zongkerc19a8d52010-07-01 15:30:11 -070061 """Read and parse the META/misc_info.txt key/value pairs from the
62 input target files and return a dict."""
63
64 d = {}
65 try:
Doug Zongker37974732010-09-16 17:44:38 -070066 for line in zip.read("META/misc_info.txt").split("\n"):
Doug Zongkerc19a8d52010-07-01 15:30:11 -070067 line = line.strip()
68 if not line or line.startswith("#"): continue
69 k, v = line.split("=", 1)
70 d[k] = v
Doug Zongker37974732010-09-16 17:44:38 -070071 except KeyError:
72 # ok if misc_info.txt doesn't exist
73 pass
Doug Zongkerc19a8d52010-07-01 15:30:11 -070074
Doug Zongker37974732010-09-16 17:44:38 -070075 # backwards compatibility: These values used to be in their own
76 # files. Look for them, in case we're processing an old
77 # target_files zip.
78
79 if "mkyaffs2_extra_flags" not in d:
80 try:
81 d["mkyaffs2_extra_flags"] = zip.read("META/mkyaffs2-extra-flags.txt").strip()
82 except KeyError:
83 # ok if flags don't exist
84 pass
85
86 if "recovery_api_version" not in d:
87 try:
88 d["recovery_api_version"] = zip.read("META/recovery-api-version.txt").strip()
89 except KeyError:
90 raise ValueError("can't find recovery API version in input target-files")
91
92 if "tool_extensions" not in d:
93 try:
94 d["tool_extensions"] = zip.read("META/tool-extensions.txt").strip()
95 except KeyError:
96 # ok if extensions don't exist
97 pass
98
99 try:
100 data = zip.read("META/imagesizes.txt")
101 for line in data.split("\n"):
102 if not line: continue
Doug Zongker1684d9c2010-09-17 07:44:38 -0700103 name, value = line.split(" ", 1)
104 if not value: continue
Doug Zongker37974732010-09-16 17:44:38 -0700105 if name == "blocksize":
106 d[name] = value
107 else:
108 d[name + "_size"] = value
109 except KeyError:
110 pass
111
112 def makeint(key):
113 if key in d:
114 d[key] = int(d[key], 0)
115
116 makeint("recovery_api_version")
117 makeint("blocksize")
118 makeint("system_size")
119 makeint("userdata_size")
120 makeint("recovery_size")
121 makeint("boot_size")
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700122
Doug Zongker9ce0fb62010-09-20 18:04:41 -0700123 d["fstab"] = LoadRecoveryFSTab(zip)
124 if not d["fstab"]:
125 if "fs_type" not in d: d["fs_type"] = "yaffs2"
126 if "partition_type" not in d: d["partition_type"] = "MTD"
127
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700128 return d
129
Doug Zongker9ce0fb62010-09-20 18:04:41 -0700130def LoadRecoveryFSTab(zip):
131 class Partition(object):
132 pass
133
134 try:
135 data = zip.read("RECOVERY/RAMDISK/etc/recovery.fstab")
136 except KeyError:
137 # older target-files that doesn't have a recovery.fstab; fall back
138 # to the fs_type and partition_type keys.
139 return
140
141 d = {}
142 for line in data.split("\n"):
143 line = line.strip()
144 if not line or line.startswith("#"): continue
145 pieces = line.split()
146 if not (3 <= len(pieces) <= 4):
147 raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,))
148
149 p = Partition()
150 p.mount_point = pieces[0]
151 p.fs_type = pieces[1]
152 p.device = pieces[2]
153 if len(pieces) == 4:
154 p.device2 = pieces[3]
155 else:
156 p.device2 = None
157
158 d[p.mount_point] = p
159 return d
160
161
Doug Zongker37974732010-09-16 17:44:38 -0700162def DumpInfoDict(d):
163 for k, v in sorted(d.items()):
164 print "%-25s = (%s) %s" % (k, type(v).__name__, v)
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700165
Doug Zongker37974732010-09-16 17:44:38 -0700166def BuildAndAddBootableImage(sourcedir, targetname, output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700167 """Take a kernel, cmdline, and ramdisk directory from the input (in
168 'sourcedir'), and turn them into a boot image. Put the boot image
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700169 into the output zip file under the name 'targetname'. Returns
170 targetname on success or None on failure (if sourcedir does not
171 appear to contain files for the requested image)."""
Doug Zongkereef39442009-04-02 12:14:19 -0700172
173 print "creating %s..." % (targetname,)
174
175 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700176 if img is None:
177 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700178
Doug Zongker37974732010-09-16 17:44:38 -0700179 CheckSize(img, targetname, info_dict)
Doug Zongker048e7ca2009-06-15 14:31:53 -0700180 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700181 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -0700182
183def BuildBootableImage(sourcedir):
184 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700185 'sourcedir'), and turn them into a boot image. Return the image
186 data, or None if sourcedir does not appear to contains files for
187 building the requested image."""
188
189 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
190 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
191 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700192
193 ramdisk_img = tempfile.NamedTemporaryFile()
194 img = tempfile.NamedTemporaryFile()
195
196 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
197 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -0700198 p2 = Run(["minigzip"],
199 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -0700200
201 p2.wait()
202 p1.wait()
203 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700204 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700205
Doug Zongker38a649f2009-06-17 09:07:09 -0700206 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
207
Doug Zongker171f1cd2009-06-15 22:36:37 -0700208 fn = os.path.join(sourcedir, "cmdline")
209 if os.access(fn, os.F_OK):
Doug Zongker38a649f2009-06-17 09:07:09 -0700210 cmd.append("--cmdline")
211 cmd.append(open(fn).read().rstrip("\n"))
212
213 fn = os.path.join(sourcedir, "base")
214 if os.access(fn, os.F_OK):
215 cmd.append("--base")
216 cmd.append(open(fn).read().rstrip("\n"))
217
Ying Wang4de6b5b2010-08-25 14:29:34 -0700218 fn = os.path.join(sourcedir, "pagesize")
219 if os.access(fn, os.F_OK):
220 cmd.append("--pagesize")
221 cmd.append(open(fn).read().rstrip("\n"))
222
Doug Zongker38a649f2009-06-17 09:07:09 -0700223 cmd.extend(["--ramdisk", ramdisk_img.name,
224 "--output", img.name])
225
226 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700227 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700228 assert p.returncode == 0, "mkbootimg of %s image failed" % (
229 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700230
231 img.seek(os.SEEK_SET, 0)
232 data = img.read()
233
234 ramdisk_img.close()
235 img.close()
236
237 return data
238
239
Doug Zongker37974732010-09-16 17:44:38 -0700240def AddRecovery(output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700241 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
Doug Zongker37974732010-09-16 17:44:38 -0700242 "recovery.img", output_zip, info_dict)
Doug Zongkereef39442009-04-02 12:14:19 -0700243
Doug Zongker37974732010-09-16 17:44:38 -0700244def AddBoot(output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700245 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
Doug Zongker37974732010-09-16 17:44:38 -0700246 "boot.img", output_zip, info_dict)
Doug Zongkereef39442009-04-02 12:14:19 -0700247
Doug Zongker75f17362009-12-08 13:46:44 -0800248def UnzipTemp(filename, pattern=None):
Doug Zongkereef39442009-04-02 12:14:19 -0700249 """Unzip the given archive into a temporary directory and return the name."""
250
251 tmp = tempfile.mkdtemp(prefix="targetfiles-")
252 OPTIONS.tempfiles.append(tmp)
Doug Zongker75f17362009-12-08 13:46:44 -0800253 cmd = ["unzip", "-o", "-q", filename, "-d", tmp]
254 if pattern is not None:
255 cmd.append(pattern)
256 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700257 p.communicate()
258 if p.returncode != 0:
259 raise ExternalError("failed to unzip input target-files \"%s\"" %
260 (filename,))
261 return tmp
262
263
264def GetKeyPasswords(keylist):
265 """Given a list of keys, prompt the user to enter passwords for
266 those which require them. Return a {key: password} dict. password
267 will be None if the key has no password."""
268
Doug Zongker8ce7c252009-05-22 13:34:54 -0700269 no_passwords = []
270 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700271 devnull = open("/dev/null", "w+b")
272 for k in sorted(keylist):
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800273 # We don't need a password for things that aren't really keys.
274 if k in SPECIAL_CERT_STRINGS:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700275 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700276 continue
277
Doug Zongker602a84e2009-06-18 08:35:12 -0700278 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
279 "-inform", "DER", "-nocrypt"],
280 stdin=devnull.fileno(),
281 stdout=devnull.fileno(),
282 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700283 p.communicate()
284 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700285 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700286 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700287 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700288 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700289
290 key_passwords = PasswordManager().GetPasswords(need_passwords)
291 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700292 return key_passwords
293
294
Doug Zongker951495f2009-08-14 12:44:19 -0700295def SignFile(input_name, output_name, key, password, align=None,
296 whole_file=False):
Doug Zongkereef39442009-04-02 12:14:19 -0700297 """Sign the input_name zip/jar/apk, producing output_name. Use the
298 given key and password (the latter may be None if the key does not
299 have a password.
300
301 If align is an integer > 1, zipalign is run to align stored files in
302 the output zip on 'align'-byte boundaries.
Doug Zongker951495f2009-08-14 12:44:19 -0700303
304 If whole_file is true, use the "-w" option to SignApk to embed a
305 signature that covers the whole file in the archive comment of the
306 zip file.
Doug Zongkereef39442009-04-02 12:14:19 -0700307 """
Doug Zongker951495f2009-08-14 12:44:19 -0700308
Doug Zongkereef39442009-04-02 12:14:19 -0700309 if align == 0 or align == 1:
310 align = None
311
312 if align:
313 temp = tempfile.NamedTemporaryFile()
314 sign_name = temp.name
315 else:
316 sign_name = output_name
317
Doug Zongker09cf5602009-08-14 15:25:06 -0700318 cmd = ["java", "-Xmx512m", "-jar",
Doug Zongker951495f2009-08-14 12:44:19 -0700319 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")]
320 if whole_file:
321 cmd.append("-w")
322 cmd.extend([key + ".x509.pem", key + ".pk8",
323 input_name, sign_name])
324
325 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700326 if password is not None:
327 password += "\n"
328 p.communicate(password)
329 if p.returncode != 0:
330 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
331
332 if align:
Doug Zongker602a84e2009-06-18 08:35:12 -0700333 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700334 p.communicate()
335 if p.returncode != 0:
336 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
337 temp.close()
338
339
Doug Zongker37974732010-09-16 17:44:38 -0700340def CheckSize(data, target, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700341 """Check the data string passed against the max size limit, if
342 any, for the given target. Raise exception if the data is too big.
343 Print a warning if the data is nearing the maximum size."""
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700344
Doug Zongker1684d9c2010-09-17 07:44:38 -0700345 if target.endswith(".img"): target = target[:-4]
Doug Zongker9ce0fb62010-09-20 18:04:41 -0700346 mount_point = "/" + target
347
348 if info_dict["fstab"]:
349 if mount_point == "/userdata": mount_point = "/data"
350 p = info_dict["fstab"][mount_point]
351 fs_type = p.fs_type
352 limit = info_dict.get(p.device + "_size", None)
353 else:
354 fs_type = info_dict.get("fs_type", None)
355 limit = info_dict.get(target + "_size", None)
356 if not fs_type or not limit: return
Doug Zongkereef39442009-04-02 12:14:19 -0700357
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700358 if fs_type == "yaffs2":
359 # image size should be increased by 1/64th to account for the
360 # spare area (64 bytes per 2k page)
361 limit = limit / 2048 * (2048+64)
Doug Zongker486de122010-09-16 14:01:56 -0700362 size = len(data)
363 pct = float(size) * 100.0 / limit
364 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
365 if pct >= 99.0:
366 raise ExternalError(msg)
367 elif pct >= 95.0:
368 print
369 print " WARNING: ", msg
370 print
371 elif OPTIONS.verbose:
372 print " ", msg
Doug Zongkereef39442009-04-02 12:14:19 -0700373
374
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800375def ReadApkCerts(tf_zip):
376 """Given a target_files ZipFile, parse the META/apkcerts.txt file
377 and return a {package: cert} dict."""
378 certmap = {}
379 for line in tf_zip.read("META/apkcerts.txt").split("\n"):
380 line = line.strip()
381 if not line: continue
382 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
383 r'private_key="(.*)"$', line)
384 if m:
385 name, cert, privkey = m.groups()
386 if cert in SPECIAL_CERT_STRINGS and not privkey:
387 certmap[name] = cert
388 elif (cert.endswith(".x509.pem") and
389 privkey.endswith(".pk8") and
390 cert[:-9] == privkey[:-4]):
391 certmap[name] = cert[:-9]
392 else:
393 raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
394 return certmap
395
396
Doug Zongkereef39442009-04-02 12:14:19 -0700397COMMON_DOCSTRING = """
398 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700399 Prepend <dir>/bin to the list of places to search for binaries
400 run by this script, and expect to find jars in <dir>/framework.
Doug Zongkereef39442009-04-02 12:14:19 -0700401
Doug Zongker05d3dea2009-06-22 11:32:31 -0700402 -s (--device_specific) <file>
403 Path to the python module containing device-specific
404 releasetools code.
405
Doug Zongker8bec09e2009-11-30 15:37:14 -0800406 -x (--extra) <key=value>
407 Add a key/value pair to the 'extras' dict, which device-specific
408 extension code may look at.
409
Doug Zongkereef39442009-04-02 12:14:19 -0700410 -v (--verbose)
411 Show command lines being executed.
412
413 -h (--help)
414 Display this usage message and exit.
415"""
416
417def Usage(docstring):
418 print docstring.rstrip("\n")
419 print COMMON_DOCSTRING
420
421
422def ParseOptions(argv,
423 docstring,
424 extra_opts="", extra_long_opts=(),
425 extra_option_handler=None):
426 """Parse the options in argv and return any arguments that aren't
427 flags. docstring is the calling module's docstring, to be displayed
428 for errors and -h. extra_opts and extra_long_opts are for flags
429 defined by the caller, which are processed by passing them to
430 extra_option_handler."""
431
432 try:
433 opts, args = getopt.getopt(
Doug Zongker8bec09e2009-11-30 15:37:14 -0800434 argv, "hvp:s:x:" + extra_opts,
435 ["help", "verbose", "path=", "device_specific=", "extra="] +
Doug Zongker05d3dea2009-06-22 11:32:31 -0700436 list(extra_long_opts))
Doug Zongkereef39442009-04-02 12:14:19 -0700437 except getopt.GetoptError, err:
438 Usage(docstring)
439 print "**", str(err), "**"
440 sys.exit(2)
441
442 path_specified = False
443
444 for o, a in opts:
445 if o in ("-h", "--help"):
446 Usage(docstring)
447 sys.exit()
448 elif o in ("-v", "--verbose"):
449 OPTIONS.verbose = True
450 elif o in ("-p", "--path"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700451 OPTIONS.search_path = a
Doug Zongker05d3dea2009-06-22 11:32:31 -0700452 elif o in ("-s", "--device_specific"):
453 OPTIONS.device_specific = a
Doug Zongker5ecba702009-12-03 16:36:20 -0800454 elif o in ("-x", "--extra"):
Doug Zongker8bec09e2009-11-30 15:37:14 -0800455 key, value = a.split("=", 1)
456 OPTIONS.extras[key] = value
Doug Zongkereef39442009-04-02 12:14:19 -0700457 else:
458 if extra_option_handler is None or not extra_option_handler(o, a):
459 assert False, "unknown option \"%s\"" % (o,)
460
Doug Zongker602a84e2009-06-18 08:35:12 -0700461 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
462 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700463
464 return args
465
466
467def Cleanup():
468 for i in OPTIONS.tempfiles:
469 if os.path.isdir(i):
470 shutil.rmtree(i)
471 else:
472 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700473
474
475class PasswordManager(object):
476 def __init__(self):
477 self.editor = os.getenv("EDITOR", None)
478 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
479
480 def GetPasswords(self, items):
481 """Get passwords corresponding to each string in 'items',
482 returning a dict. (The dict may have keys in addition to the
483 values in 'items'.)
484
485 Uses the passwords in $ANDROID_PW_FILE if available, letting the
486 user edit that file to add more needed passwords. If no editor is
487 available, or $ANDROID_PW_FILE isn't define, prompts the user
488 interactively in the ordinary way.
489 """
490
491 current = self.ReadFile()
492
493 first = True
494 while True:
495 missing = []
496 for i in items:
497 if i not in current or not current[i]:
498 missing.append(i)
499 # Are all the passwords already in the file?
500 if not missing: return current
501
502 for i in missing:
503 current[i] = ""
504
505 if not first:
506 print "key file %s still missing some passwords." % (self.pwfile,)
507 answer = raw_input("try to edit again? [y]> ").strip()
508 if answer and answer[0] not in 'yY':
509 raise RuntimeError("key passwords unavailable")
510 first = False
511
512 current = self.UpdateAndReadFile(current)
513
514 def PromptResult(self, current):
515 """Prompt the user to enter a value (password) for each key in
516 'current' whose value is fales. Returns a new dict with all the
517 values.
518 """
519 result = {}
520 for k, v in sorted(current.iteritems()):
521 if v:
522 result[k] = v
523 else:
524 while True:
525 result[k] = getpass.getpass("Enter password for %s key> "
526 % (k,)).strip()
527 if result[k]: break
528 return result
529
530 def UpdateAndReadFile(self, current):
531 if not self.editor or not self.pwfile:
532 return self.PromptResult(current)
533
534 f = open(self.pwfile, "w")
535 os.chmod(self.pwfile, 0600)
536 f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
537 f.write("# (Additional spaces are harmless.)\n\n")
538
539 first_line = None
540 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
541 sorted.sort()
542 for i, (_, k, v) in enumerate(sorted):
543 f.write("[[[ %s ]]] %s\n" % (v, k))
544 if not v and first_line is None:
545 # position cursor on first line with no password.
546 first_line = i + 4
547 f.close()
548
549 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
550 _, _ = p.communicate()
551
552 return self.ReadFile()
553
554 def ReadFile(self):
555 result = {}
556 if self.pwfile is None: return result
557 try:
558 f = open(self.pwfile, "r")
559 for line in f:
560 line = line.strip()
561 if not line or line[0] == '#': continue
562 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
563 if not m:
564 print "failed to parse password file: ", line
565 else:
566 result[m.group(2)] = m.group(1)
567 f.close()
568 except IOError, e:
569 if e.errno != errno.ENOENT:
570 print "error reading password file: ", str(e)
571 return result
Doug Zongker048e7ca2009-06-15 14:31:53 -0700572
573
574def ZipWriteStr(zip, filename, data, perms=0644):
575 # use a fixed timestamp so the output is repeatable.
576 zinfo = zipfile.ZipInfo(filename=filename,
577 date_time=(2009, 1, 1, 0, 0, 0))
578 zinfo.compress_type = zip.compression
579 zinfo.external_attr = perms << 16
580 zip.writestr(zinfo, data)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700581
582
583class DeviceSpecificParams(object):
584 module = None
585 def __init__(self, **kwargs):
586 """Keyword arguments to the constructor become attributes of this
587 object, which is passed to all functions in the device-specific
588 module."""
589 for k, v in kwargs.iteritems():
590 setattr(self, k, v)
Doug Zongker8bec09e2009-11-30 15:37:14 -0800591 self.extras = OPTIONS.extras
Doug Zongker05d3dea2009-06-22 11:32:31 -0700592
593 if self.module is None:
594 path = OPTIONS.device_specific
Doug Zongkerc18736b2009-09-30 09:20:32 -0700595 if not path: return
Doug Zongker8e2f2b92009-06-24 14:34:57 -0700596 try:
597 if os.path.isdir(path):
598 info = imp.find_module("releasetools", [path])
599 else:
600 d, f = os.path.split(path)
601 b, x = os.path.splitext(f)
602 if x == ".py":
603 f = b
604 info = imp.find_module(f, [d])
605 self.module = imp.load_module("device_specific", *info)
606 except ImportError:
607 print "unable to load device-specific module; assuming none"
Doug Zongker05d3dea2009-06-22 11:32:31 -0700608
609 def _DoCall(self, function_name, *args, **kwargs):
610 """Call the named function in the device-specific module, passing
611 the given args and kwargs. The first argument to the call will be
612 the DeviceSpecific object itself. If there is no module, or the
613 module does not define the function, return the value of the
614 'default' kwarg (which itself defaults to None)."""
615 if self.module is None or not hasattr(self.module, function_name):
616 return kwargs.get("default", None)
617 return getattr(self.module, function_name)(*((self,) + args), **kwargs)
618
619 def FullOTA_Assertions(self):
620 """Called after emitting the block of assertions at the top of a
621 full OTA package. Implementations can add whatever additional
622 assertions they like."""
623 return self._DoCall("FullOTA_Assertions")
624
625 def FullOTA_InstallEnd(self):
626 """Called at the end of full OTA installation; typically this is
627 used to install the image for the device's baseband processor."""
628 return self._DoCall("FullOTA_InstallEnd")
629
630 def IncrementalOTA_Assertions(self):
631 """Called after emitting the block of assertions at the top of an
632 incremental OTA package. Implementations can add whatever
633 additional assertions they like."""
634 return self._DoCall("IncrementalOTA_Assertions")
635
636 def IncrementalOTA_VerifyEnd(self):
637 """Called at the end of the verification phase of incremental OTA
638 installation; additional checks can be placed here to abort the
639 script before any changes are made."""
640 return self._DoCall("IncrementalOTA_VerifyEnd")
641
642 def IncrementalOTA_InstallEnd(self):
643 """Called at the end of incremental OTA installation; typically
644 this is used to install the image for the device's baseband
645 processor."""
646 return self._DoCall("IncrementalOTA_InstallEnd")
Doug Zongkerea5d7a92010-09-12 15:26:16 -0700647
648class File(object):
649 def __init__(self, name, data):
650 self.name = name
651 self.data = data
652 self.size = len(data)
653 self.sha1 = sha.sha(data).hexdigest()
654
655 def WriteToTemp(self):
656 t = tempfile.NamedTemporaryFile()
657 t.write(self.data)
658 t.flush()
659 return t
660
661 def AddToZip(self, z):
662 ZipWriteStr(z, self.name, self.data)
663
664DIFF_PROGRAM_BY_EXT = {
665 ".gz" : "imgdiff",
666 ".zip" : ["imgdiff", "-z"],
667 ".jar" : ["imgdiff", "-z"],
668 ".apk" : ["imgdiff", "-z"],
669 ".img" : "imgdiff",
670 }
671
672class Difference(object):
673 def __init__(self, tf, sf):
674 self.tf = tf
675 self.sf = sf
676 self.patch = None
677
678 def ComputePatch(self):
679 """Compute the patch (as a string of data) needed to turn sf into
680 tf. Returns the same tuple as GetPatch()."""
681
682 tf = self.tf
683 sf = self.sf
684
685 ext = os.path.splitext(tf.name)[1]
686 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
687
688 ttemp = tf.WriteToTemp()
689 stemp = sf.WriteToTemp()
690
691 ext = os.path.splitext(tf.name)[1]
692
693 try:
694 ptemp = tempfile.NamedTemporaryFile()
695 if isinstance(diff_program, list):
696 cmd = copy.copy(diff_program)
697 else:
698 cmd = [diff_program]
699 cmd.append(stemp.name)
700 cmd.append(ttemp.name)
701 cmd.append(ptemp.name)
702 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
703 _, err = p.communicate()
704 if err or p.returncode != 0:
705 print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
706 return None
707 diff = ptemp.read()
708 finally:
709 ptemp.close()
710 stemp.close()
711 ttemp.close()
712
713 self.patch = diff
714 return self.tf, self.sf, self.patch
715
716
717 def GetPatch(self):
718 """Return a tuple (target_file, source_file, patch_data).
719 patch_data may be None if ComputePatch hasn't been called, or if
720 computing the patch failed."""
721 return self.tf, self.sf, self.patch
722
723
724def ComputeDifferences(diffs):
725 """Call ComputePatch on all the Difference objects in 'diffs'."""
726 print len(diffs), "diffs to compute"
727
728 # Do the largest files first, to try and reduce the long-pole effect.
729 by_size = [(i.tf.size, i) for i in diffs]
730 by_size.sort(reverse=True)
731 by_size = [i[1] for i in by_size]
732
733 lock = threading.Lock()
734 diff_iter = iter(by_size) # accessed under lock
735
736 def worker():
737 try:
738 lock.acquire()
739 for d in diff_iter:
740 lock.release()
741 start = time.time()
742 d.ComputePatch()
743 dur = time.time() - start
744 lock.acquire()
745
746 tf, sf, patch = d.GetPatch()
747 if sf.name == tf.name:
748 name = tf.name
749 else:
750 name = "%s (%s)" % (tf.name, sf.name)
751 if patch is None:
752 print "patching failed! %s" % (name,)
753 else:
754 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
755 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
756 lock.release()
757 except Exception, e:
758 print e
759 raise
760
761 # start worker threads; wait for them all to finish.
762 threads = [threading.Thread(target=worker)
763 for i in range(OPTIONS.worker_threads)]
764 for th in threads:
765 th.start()
766 while threads:
767 threads.pop().join()