blob: 31e86a1537ef54e5c4c7611384a03b1655c20e06 [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 if "fs_type" not in d: d["fs_type"] = "yaffs2"
76 if "partition_type" not in d: d["partition_type"] = "MTD"
77
78 # backwards compatibility: These values used to be in their own
79 # files. Look for them, in case we're processing an old
80 # target_files zip.
81
82 if "mkyaffs2_extra_flags" not in d:
83 try:
84 d["mkyaffs2_extra_flags"] = zip.read("META/mkyaffs2-extra-flags.txt").strip()
85 except KeyError:
86 # ok if flags don't exist
87 pass
88
89 if "recovery_api_version" not in d:
90 try:
91 d["recovery_api_version"] = zip.read("META/recovery-api-version.txt").strip()
92 except KeyError:
93 raise ValueError("can't find recovery API version in input target-files")
94
95 if "tool_extensions" not in d:
96 try:
97 d["tool_extensions"] = zip.read("META/tool-extensions.txt").strip()
98 except KeyError:
99 # ok if extensions don't exist
100 pass
101
102 try:
103 data = zip.read("META/imagesizes.txt")
104 for line in data.split("\n"):
105 if not line: continue
Doug Zongker1684d9c2010-09-17 07:44:38 -0700106 name, value = line.split(" ", 1)
107 if not value: continue
Doug Zongker37974732010-09-16 17:44:38 -0700108 if name == "blocksize":
109 d[name] = value
110 else:
111 d[name + "_size"] = value
112 except KeyError:
113 pass
114
115 def makeint(key):
116 if key in d:
117 d[key] = int(d[key], 0)
118
119 makeint("recovery_api_version")
120 makeint("blocksize")
121 makeint("system_size")
122 makeint("userdata_size")
123 makeint("recovery_size")
124 makeint("boot_size")
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700125
126 return d
127
Doug Zongker37974732010-09-16 17:44:38 -0700128def DumpInfoDict(d):
129 for k, v in sorted(d.items()):
130 print "%-25s = (%s) %s" % (k, type(v).__name__, v)
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700131
Doug Zongker37974732010-09-16 17:44:38 -0700132def BuildAndAddBootableImage(sourcedir, targetname, output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700133 """Take a kernel, cmdline, and ramdisk directory from the input (in
134 'sourcedir'), and turn them into a boot image. Put the boot image
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700135 into the output zip file under the name 'targetname'. Returns
136 targetname on success or None on failure (if sourcedir does not
137 appear to contain files for the requested image)."""
Doug Zongkereef39442009-04-02 12:14:19 -0700138
139 print "creating %s..." % (targetname,)
140
141 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700142 if img is None:
143 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700144
Doug Zongker37974732010-09-16 17:44:38 -0700145 CheckSize(img, targetname, info_dict)
Doug Zongker048e7ca2009-06-15 14:31:53 -0700146 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700147 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -0700148
149def BuildBootableImage(sourcedir):
150 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700151 'sourcedir'), and turn them into a boot image. Return the image
152 data, or None if sourcedir does not appear to contains files for
153 building the requested image."""
154
155 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
156 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
157 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700158
159 ramdisk_img = tempfile.NamedTemporaryFile()
160 img = tempfile.NamedTemporaryFile()
161
162 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
163 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -0700164 p2 = Run(["minigzip"],
165 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -0700166
167 p2.wait()
168 p1.wait()
169 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700170 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700171
Doug Zongker38a649f2009-06-17 09:07:09 -0700172 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
173
Doug Zongker171f1cd2009-06-15 22:36:37 -0700174 fn = os.path.join(sourcedir, "cmdline")
175 if os.access(fn, os.F_OK):
Doug Zongker38a649f2009-06-17 09:07:09 -0700176 cmd.append("--cmdline")
177 cmd.append(open(fn).read().rstrip("\n"))
178
179 fn = os.path.join(sourcedir, "base")
180 if os.access(fn, os.F_OK):
181 cmd.append("--base")
182 cmd.append(open(fn).read().rstrip("\n"))
183
Ying Wang4de6b5b2010-08-25 14:29:34 -0700184 fn = os.path.join(sourcedir, "pagesize")
185 if os.access(fn, os.F_OK):
186 cmd.append("--pagesize")
187 cmd.append(open(fn).read().rstrip("\n"))
188
Doug Zongker38a649f2009-06-17 09:07:09 -0700189 cmd.extend(["--ramdisk", ramdisk_img.name,
190 "--output", img.name])
191
192 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700193 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700194 assert p.returncode == 0, "mkbootimg of %s image failed" % (
195 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700196
197 img.seek(os.SEEK_SET, 0)
198 data = img.read()
199
200 ramdisk_img.close()
201 img.close()
202
203 return data
204
205
Doug Zongker37974732010-09-16 17:44:38 -0700206def AddRecovery(output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700207 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
Doug Zongker37974732010-09-16 17:44:38 -0700208 "recovery.img", output_zip, info_dict)
Doug Zongkereef39442009-04-02 12:14:19 -0700209
Doug Zongker37974732010-09-16 17:44:38 -0700210def AddBoot(output_zip, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700211 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
Doug Zongker37974732010-09-16 17:44:38 -0700212 "boot.img", output_zip, info_dict)
Doug Zongkereef39442009-04-02 12:14:19 -0700213
Doug Zongker75f17362009-12-08 13:46:44 -0800214def UnzipTemp(filename, pattern=None):
Doug Zongkereef39442009-04-02 12:14:19 -0700215 """Unzip the given archive into a temporary directory and return the name."""
216
217 tmp = tempfile.mkdtemp(prefix="targetfiles-")
218 OPTIONS.tempfiles.append(tmp)
Doug Zongker75f17362009-12-08 13:46:44 -0800219 cmd = ["unzip", "-o", "-q", filename, "-d", tmp]
220 if pattern is not None:
221 cmd.append(pattern)
222 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700223 p.communicate()
224 if p.returncode != 0:
225 raise ExternalError("failed to unzip input target-files \"%s\"" %
226 (filename,))
227 return tmp
228
229
230def GetKeyPasswords(keylist):
231 """Given a list of keys, prompt the user to enter passwords for
232 those which require them. Return a {key: password} dict. password
233 will be None if the key has no password."""
234
Doug Zongker8ce7c252009-05-22 13:34:54 -0700235 no_passwords = []
236 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700237 devnull = open("/dev/null", "w+b")
238 for k in sorted(keylist):
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800239 # We don't need a password for things that aren't really keys.
240 if k in SPECIAL_CERT_STRINGS:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700241 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700242 continue
243
Doug Zongker602a84e2009-06-18 08:35:12 -0700244 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
245 "-inform", "DER", "-nocrypt"],
246 stdin=devnull.fileno(),
247 stdout=devnull.fileno(),
248 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700249 p.communicate()
250 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700251 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700252 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700253 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700254 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700255
256 key_passwords = PasswordManager().GetPasswords(need_passwords)
257 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700258 return key_passwords
259
260
Doug Zongker951495f2009-08-14 12:44:19 -0700261def SignFile(input_name, output_name, key, password, align=None,
262 whole_file=False):
Doug Zongkereef39442009-04-02 12:14:19 -0700263 """Sign the input_name zip/jar/apk, producing output_name. Use the
264 given key and password (the latter may be None if the key does not
265 have a password.
266
267 If align is an integer > 1, zipalign is run to align stored files in
268 the output zip on 'align'-byte boundaries.
Doug Zongker951495f2009-08-14 12:44:19 -0700269
270 If whole_file is true, use the "-w" option to SignApk to embed a
271 signature that covers the whole file in the archive comment of the
272 zip file.
Doug Zongkereef39442009-04-02 12:14:19 -0700273 """
Doug Zongker951495f2009-08-14 12:44:19 -0700274
Doug Zongkereef39442009-04-02 12:14:19 -0700275 if align == 0 or align == 1:
276 align = None
277
278 if align:
279 temp = tempfile.NamedTemporaryFile()
280 sign_name = temp.name
281 else:
282 sign_name = output_name
283
Doug Zongker09cf5602009-08-14 15:25:06 -0700284 cmd = ["java", "-Xmx512m", "-jar",
Doug Zongker951495f2009-08-14 12:44:19 -0700285 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")]
286 if whole_file:
287 cmd.append("-w")
288 cmd.extend([key + ".x509.pem", key + ".pk8",
289 input_name, sign_name])
290
291 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700292 if password is not None:
293 password += "\n"
294 p.communicate(password)
295 if p.returncode != 0:
296 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
297
298 if align:
Doug Zongker602a84e2009-06-18 08:35:12 -0700299 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700300 p.communicate()
301 if p.returncode != 0:
302 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
303 temp.close()
304
305
Doug Zongker37974732010-09-16 17:44:38 -0700306def CheckSize(data, target, info_dict):
Doug Zongkereef39442009-04-02 12:14:19 -0700307 """Check the data string passed against the max size limit, if
308 any, for the given target. Raise exception if the data is too big.
309 Print a warning if the data is nearing the maximum size."""
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700310
Doug Zongker37974732010-09-16 17:44:38 -0700311 fs_type = info_dict.get("fs_type", None)
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700312 if not fs_type: return
313
Doug Zongker1684d9c2010-09-17 07:44:38 -0700314 if target.endswith(".img"): target = target[:-4]
315 limit = info_dict.get(target + "_size", None)
Doug Zongkereef39442009-04-02 12:14:19 -0700316 if limit is None: return
317
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700318 if fs_type == "yaffs2":
319 # image size should be increased by 1/64th to account for the
320 # spare area (64 bytes per 2k page)
321 limit = limit / 2048 * (2048+64)
322
Doug Zongkereef39442009-04-02 12:14:19 -0700323 size = len(data)
324 pct = float(size) * 100.0 / limit
325 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
326 if pct >= 99.0:
327 raise ExternalError(msg)
328 elif pct >= 95.0:
329 print
330 print " WARNING: ", msg
331 print
332 elif OPTIONS.verbose:
333 print " ", msg
334
335
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800336def ReadApkCerts(tf_zip):
337 """Given a target_files ZipFile, parse the META/apkcerts.txt file
338 and return a {package: cert} dict."""
339 certmap = {}
340 for line in tf_zip.read("META/apkcerts.txt").split("\n"):
341 line = line.strip()
342 if not line: continue
343 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
344 r'private_key="(.*)"$', line)
345 if m:
346 name, cert, privkey = m.groups()
347 if cert in SPECIAL_CERT_STRINGS and not privkey:
348 certmap[name] = cert
349 elif (cert.endswith(".x509.pem") and
350 privkey.endswith(".pk8") and
351 cert[:-9] == privkey[:-4]):
352 certmap[name] = cert[:-9]
353 else:
354 raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
355 return certmap
356
357
Doug Zongkereef39442009-04-02 12:14:19 -0700358COMMON_DOCSTRING = """
359 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700360 Prepend <dir>/bin to the list of places to search for binaries
361 run by this script, and expect to find jars in <dir>/framework.
Doug Zongkereef39442009-04-02 12:14:19 -0700362
Doug Zongker05d3dea2009-06-22 11:32:31 -0700363 -s (--device_specific) <file>
364 Path to the python module containing device-specific
365 releasetools code.
366
Doug Zongker8bec09e2009-11-30 15:37:14 -0800367 -x (--extra) <key=value>
368 Add a key/value pair to the 'extras' dict, which device-specific
369 extension code may look at.
370
Doug Zongkereef39442009-04-02 12:14:19 -0700371 -v (--verbose)
372 Show command lines being executed.
373
374 -h (--help)
375 Display this usage message and exit.
376"""
377
378def Usage(docstring):
379 print docstring.rstrip("\n")
380 print COMMON_DOCSTRING
381
382
383def ParseOptions(argv,
384 docstring,
385 extra_opts="", extra_long_opts=(),
386 extra_option_handler=None):
387 """Parse the options in argv and return any arguments that aren't
388 flags. docstring is the calling module's docstring, to be displayed
389 for errors and -h. extra_opts and extra_long_opts are for flags
390 defined by the caller, which are processed by passing them to
391 extra_option_handler."""
392
393 try:
394 opts, args = getopt.getopt(
Doug Zongker8bec09e2009-11-30 15:37:14 -0800395 argv, "hvp:s:x:" + extra_opts,
396 ["help", "verbose", "path=", "device_specific=", "extra="] +
Doug Zongker05d3dea2009-06-22 11:32:31 -0700397 list(extra_long_opts))
Doug Zongkereef39442009-04-02 12:14:19 -0700398 except getopt.GetoptError, err:
399 Usage(docstring)
400 print "**", str(err), "**"
401 sys.exit(2)
402
403 path_specified = False
404
405 for o, a in opts:
406 if o in ("-h", "--help"):
407 Usage(docstring)
408 sys.exit()
409 elif o in ("-v", "--verbose"):
410 OPTIONS.verbose = True
411 elif o in ("-p", "--path"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700412 OPTIONS.search_path = a
Doug Zongker05d3dea2009-06-22 11:32:31 -0700413 elif o in ("-s", "--device_specific"):
414 OPTIONS.device_specific = a
Doug Zongker5ecba702009-12-03 16:36:20 -0800415 elif o in ("-x", "--extra"):
Doug Zongker8bec09e2009-11-30 15:37:14 -0800416 key, value = a.split("=", 1)
417 OPTIONS.extras[key] = value
Doug Zongkereef39442009-04-02 12:14:19 -0700418 else:
419 if extra_option_handler is None or not extra_option_handler(o, a):
420 assert False, "unknown option \"%s\"" % (o,)
421
Doug Zongker602a84e2009-06-18 08:35:12 -0700422 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
423 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700424
425 return args
426
427
428def Cleanup():
429 for i in OPTIONS.tempfiles:
430 if os.path.isdir(i):
431 shutil.rmtree(i)
432 else:
433 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700434
435
436class PasswordManager(object):
437 def __init__(self):
438 self.editor = os.getenv("EDITOR", None)
439 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
440
441 def GetPasswords(self, items):
442 """Get passwords corresponding to each string in 'items',
443 returning a dict. (The dict may have keys in addition to the
444 values in 'items'.)
445
446 Uses the passwords in $ANDROID_PW_FILE if available, letting the
447 user edit that file to add more needed passwords. If no editor is
448 available, or $ANDROID_PW_FILE isn't define, prompts the user
449 interactively in the ordinary way.
450 """
451
452 current = self.ReadFile()
453
454 first = True
455 while True:
456 missing = []
457 for i in items:
458 if i not in current or not current[i]:
459 missing.append(i)
460 # Are all the passwords already in the file?
461 if not missing: return current
462
463 for i in missing:
464 current[i] = ""
465
466 if not first:
467 print "key file %s still missing some passwords." % (self.pwfile,)
468 answer = raw_input("try to edit again? [y]> ").strip()
469 if answer and answer[0] not in 'yY':
470 raise RuntimeError("key passwords unavailable")
471 first = False
472
473 current = self.UpdateAndReadFile(current)
474
475 def PromptResult(self, current):
476 """Prompt the user to enter a value (password) for each key in
477 'current' whose value is fales. Returns a new dict with all the
478 values.
479 """
480 result = {}
481 for k, v in sorted(current.iteritems()):
482 if v:
483 result[k] = v
484 else:
485 while True:
486 result[k] = getpass.getpass("Enter password for %s key> "
487 % (k,)).strip()
488 if result[k]: break
489 return result
490
491 def UpdateAndReadFile(self, current):
492 if not self.editor or not self.pwfile:
493 return self.PromptResult(current)
494
495 f = open(self.pwfile, "w")
496 os.chmod(self.pwfile, 0600)
497 f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
498 f.write("# (Additional spaces are harmless.)\n\n")
499
500 first_line = None
501 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
502 sorted.sort()
503 for i, (_, k, v) in enumerate(sorted):
504 f.write("[[[ %s ]]] %s\n" % (v, k))
505 if not v and first_line is None:
506 # position cursor on first line with no password.
507 first_line = i + 4
508 f.close()
509
510 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
511 _, _ = p.communicate()
512
513 return self.ReadFile()
514
515 def ReadFile(self):
516 result = {}
517 if self.pwfile is None: return result
518 try:
519 f = open(self.pwfile, "r")
520 for line in f:
521 line = line.strip()
522 if not line or line[0] == '#': continue
523 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
524 if not m:
525 print "failed to parse password file: ", line
526 else:
527 result[m.group(2)] = m.group(1)
528 f.close()
529 except IOError, e:
530 if e.errno != errno.ENOENT:
531 print "error reading password file: ", str(e)
532 return result
Doug Zongker048e7ca2009-06-15 14:31:53 -0700533
534
535def ZipWriteStr(zip, filename, data, perms=0644):
536 # use a fixed timestamp so the output is repeatable.
537 zinfo = zipfile.ZipInfo(filename=filename,
538 date_time=(2009, 1, 1, 0, 0, 0))
539 zinfo.compress_type = zip.compression
540 zinfo.external_attr = perms << 16
541 zip.writestr(zinfo, data)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700542
543
544class DeviceSpecificParams(object):
545 module = None
546 def __init__(self, **kwargs):
547 """Keyword arguments to the constructor become attributes of this
548 object, which is passed to all functions in the device-specific
549 module."""
550 for k, v in kwargs.iteritems():
551 setattr(self, k, v)
Doug Zongker8bec09e2009-11-30 15:37:14 -0800552 self.extras = OPTIONS.extras
Doug Zongker05d3dea2009-06-22 11:32:31 -0700553
554 if self.module is None:
555 path = OPTIONS.device_specific
Doug Zongkerc18736b2009-09-30 09:20:32 -0700556 if not path: return
Doug Zongker8e2f2b92009-06-24 14:34:57 -0700557 try:
558 if os.path.isdir(path):
559 info = imp.find_module("releasetools", [path])
560 else:
561 d, f = os.path.split(path)
562 b, x = os.path.splitext(f)
563 if x == ".py":
564 f = b
565 info = imp.find_module(f, [d])
566 self.module = imp.load_module("device_specific", *info)
567 except ImportError:
568 print "unable to load device-specific module; assuming none"
Doug Zongker05d3dea2009-06-22 11:32:31 -0700569
570 def _DoCall(self, function_name, *args, **kwargs):
571 """Call the named function in the device-specific module, passing
572 the given args and kwargs. The first argument to the call will be
573 the DeviceSpecific object itself. If there is no module, or the
574 module does not define the function, return the value of the
575 'default' kwarg (which itself defaults to None)."""
576 if self.module is None or not hasattr(self.module, function_name):
577 return kwargs.get("default", None)
578 return getattr(self.module, function_name)(*((self,) + args), **kwargs)
579
580 def FullOTA_Assertions(self):
581 """Called after emitting the block of assertions at the top of a
582 full OTA package. Implementations can add whatever additional
583 assertions they like."""
584 return self._DoCall("FullOTA_Assertions")
585
586 def FullOTA_InstallEnd(self):
587 """Called at the end of full OTA installation; typically this is
588 used to install the image for the device's baseband processor."""
589 return self._DoCall("FullOTA_InstallEnd")
590
591 def IncrementalOTA_Assertions(self):
592 """Called after emitting the block of assertions at the top of an
593 incremental OTA package. Implementations can add whatever
594 additional assertions they like."""
595 return self._DoCall("IncrementalOTA_Assertions")
596
597 def IncrementalOTA_VerifyEnd(self):
598 """Called at the end of the verification phase of incremental OTA
599 installation; additional checks can be placed here to abort the
600 script before any changes are made."""
601 return self._DoCall("IncrementalOTA_VerifyEnd")
602
603 def IncrementalOTA_InstallEnd(self):
604 """Called at the end of incremental OTA installation; typically
605 this is used to install the image for the device's baseband
606 processor."""
607 return self._DoCall("IncrementalOTA_InstallEnd")
Doug Zongkerea5d7a92010-09-12 15:26:16 -0700608
609class File(object):
610 def __init__(self, name, data):
611 self.name = name
612 self.data = data
613 self.size = len(data)
614 self.sha1 = sha.sha(data).hexdigest()
615
616 def WriteToTemp(self):
617 t = tempfile.NamedTemporaryFile()
618 t.write(self.data)
619 t.flush()
620 return t
621
622 def AddToZip(self, z):
623 ZipWriteStr(z, self.name, self.data)
624
625DIFF_PROGRAM_BY_EXT = {
626 ".gz" : "imgdiff",
627 ".zip" : ["imgdiff", "-z"],
628 ".jar" : ["imgdiff", "-z"],
629 ".apk" : ["imgdiff", "-z"],
630 ".img" : "imgdiff",
631 }
632
633class Difference(object):
634 def __init__(self, tf, sf):
635 self.tf = tf
636 self.sf = sf
637 self.patch = None
638
639 def ComputePatch(self):
640 """Compute the patch (as a string of data) needed to turn sf into
641 tf. Returns the same tuple as GetPatch()."""
642
643 tf = self.tf
644 sf = self.sf
645
646 ext = os.path.splitext(tf.name)[1]
647 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
648
649 ttemp = tf.WriteToTemp()
650 stemp = sf.WriteToTemp()
651
652 ext = os.path.splitext(tf.name)[1]
653
654 try:
655 ptemp = tempfile.NamedTemporaryFile()
656 if isinstance(diff_program, list):
657 cmd = copy.copy(diff_program)
658 else:
659 cmd = [diff_program]
660 cmd.append(stemp.name)
661 cmd.append(ttemp.name)
662 cmd.append(ptemp.name)
663 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
664 _, err = p.communicate()
665 if err or p.returncode != 0:
666 print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
667 return None
668 diff = ptemp.read()
669 finally:
670 ptemp.close()
671 stemp.close()
672 ttemp.close()
673
674 self.patch = diff
675 return self.tf, self.sf, self.patch
676
677
678 def GetPatch(self):
679 """Return a tuple (target_file, source_file, patch_data).
680 patch_data may be None if ComputePatch hasn't been called, or if
681 computing the patch failed."""
682 return self.tf, self.sf, self.patch
683
684
685def ComputeDifferences(diffs):
686 """Call ComputePatch on all the Difference objects in 'diffs'."""
687 print len(diffs), "diffs to compute"
688
689 # Do the largest files first, to try and reduce the long-pole effect.
690 by_size = [(i.tf.size, i) for i in diffs]
691 by_size.sort(reverse=True)
692 by_size = [i[1] for i in by_size]
693
694 lock = threading.Lock()
695 diff_iter = iter(by_size) # accessed under lock
696
697 def worker():
698 try:
699 lock.acquire()
700 for d in diff_iter:
701 lock.release()
702 start = time.time()
703 d.ComputePatch()
704 dur = time.time() - start
705 lock.acquire()
706
707 tf, sf, patch = d.GetPatch()
708 if sf.name == tf.name:
709 name = tf.name
710 else:
711 name = "%s (%s)" % (tf.name, sf.name)
712 if patch is None:
713 print "patching failed! %s" % (name,)
714 else:
715 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
716 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
717 lock.release()
718 except Exception, e:
719 print e
720 raise
721
722 # start worker threads; wait for them all to finish.
723 threads = [threading.Thread(target=worker)
724 for i in range(OPTIONS.worker_threads)]
725 for th in threads:
726 th.start()
727 while threads:
728 threads.pop().join()