blob: 8b163c0ad1e66ed9f63bfaec60410f499ecdfb7d [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.max_image_size = {}
39OPTIONS.verbose = False
40OPTIONS.tempfiles = []
Doug Zongker05d3dea2009-06-22 11:32:31 -070041OPTIONS.device_specific = None
Doug Zongker8bec09e2009-11-30 15:37:14 -080042OPTIONS.extras = {}
Ying Wangd421f572010-08-25 20:39:41 -070043OPTIONS.mkyaffs2_extra_flags = None
Doug Zongkerc77a9ad2010-09-16 11:28:43 -070044OPTIONS.info_dict = None
Doug Zongkereef39442009-04-02 12:14:19 -070045
Doug Zongkerf6a53aa2009-12-15 15:06:55 -080046
47# Values for "certificate" in apkcerts that mean special things.
48SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL")
49
50
Doug Zongkereef39442009-04-02 12:14:19 -070051class ExternalError(RuntimeError): pass
52
53
54def Run(args, **kwargs):
55 """Create and return a subprocess.Popen object, printing the command
56 line on the terminal if -v was specified."""
57 if OPTIONS.verbose:
58 print " running: ", " ".join(args)
59 return subprocess.Popen(args, **kwargs)
60
61
Doug Zongkerc19a8d52010-07-01 15:30:11 -070062def LoadInfoDict():
63 """Read and parse the META/misc_info.txt key/value pairs from the
64 input target files and return a dict."""
65
66 d = {}
67 try:
68 for line in open(os.path.join(OPTIONS.input_tmp, "META", "misc_info.txt")):
69 line = line.strip()
70 if not line or line.startswith("#"): continue
71 k, v = line.split("=", 1)
72 d[k] = v
73 except IOError, e:
74 if e.errno == errno.ENOENT:
75 # ok if misc_info.txt file doesn't exist
76 pass
77 else:
78 raise
79
80 if "fs_type" not in d: info["fs_type"] = "yaffs2"
81 if "partition_type" not in d: info["partition_type"] = "MTD"
82
83 return d
84
85
86def LoadMaxSizes(info):
Doug Zongkerfdd8e692009-08-03 17:27:48 -070087 """Load the maximum allowable images sizes from the input
Doug Zongkerc19a8d52010-07-01 15:30:11 -070088 target_files. Uses the imagesizes.txt file if it's available
Doug Zongkerc77a9ad2010-09-16 11:28:43 -070089 (pre-gingerbread target_files), or the more general info dict (which
Doug Zongkerc19a8d52010-07-01 15:30:11 -070090 must be passed in) if not."""
Doug Zongkereef39442009-04-02 12:14:19 -070091 OPTIONS.max_image_size = {}
Doug Zongkerfdd8e692009-08-03 17:27:48 -070092 try:
93 for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")):
Doug Zongker1aca9622009-08-04 15:09:27 -070094 pieces = line.split()
95 if len(pieces) != 2: continue
96 image = pieces[0]
97 size = int(pieces[1])
Doug Zongkerfdd8e692009-08-03 17:27:48 -070098 OPTIONS.max_image_size[image + ".img"] = size
99 except IOError, e:
100 if e.errno == errno.ENOENT:
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700101 def copy(x, y):
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700102 if x+y in info: OPTIONS.max_image_size[x+".img"] = int(info[x+y])
Doug Zongkerc19a8d52010-07-01 15:30:11 -0700103 copy("blocksize", "")
104 copy("boot", "_size")
105 copy("recovery", "_size")
106 copy("system", "_size")
107 copy("userdata", "_size")
108 else:
109 raise
Doug Zongkereef39442009-04-02 12:14:19 -0700110
111
Ying Wangd421f572010-08-25 20:39:41 -0700112def LoadMkyaffs2ExtraFlags():
113 """Load mkyaffs2 extra flags."""
114 try:
115 fn = os.path.join(OPTIONS.input_tmp, "META", "mkyaffs2-extra-flags.txt");
116 if os.access(fn, os.F_OK):
117 OPTIONS.mkyaffs2_extra_flags = open(fn).read().rstrip("\n")
118 except IOError, e:
119 if e.errno == errno.ENOENT:
120 pass
121
122
Doug Zongkereef39442009-04-02 12:14:19 -0700123def BuildAndAddBootableImage(sourcedir, targetname, output_zip):
124 """Take a kernel, cmdline, and ramdisk directory from the input (in
125 'sourcedir'), and turn them into a boot image. Put the boot image
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700126 into the output zip file under the name 'targetname'. Returns
127 targetname on success or None on failure (if sourcedir does not
128 appear to contain files for the requested image)."""
Doug Zongkereef39442009-04-02 12:14:19 -0700129
130 print "creating %s..." % (targetname,)
131
132 img = BuildBootableImage(sourcedir)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700133 if img is None:
134 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700135
136 CheckSize(img, targetname)
Doug Zongker048e7ca2009-06-15 14:31:53 -0700137 ZipWriteStr(output_zip, targetname, img)
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700138 return targetname
Doug Zongkereef39442009-04-02 12:14:19 -0700139
140def BuildBootableImage(sourcedir):
141 """Take a kernel, cmdline, and ramdisk directory from the input (in
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700142 'sourcedir'), and turn them into a boot image. Return the image
143 data, or None if sourcedir does not appear to contains files for
144 building the requested image."""
145
146 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or
147 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)):
148 return None
Doug Zongkereef39442009-04-02 12:14:19 -0700149
150 ramdisk_img = tempfile.NamedTemporaryFile()
151 img = tempfile.NamedTemporaryFile()
152
153 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")],
154 stdout=subprocess.PIPE)
Doug Zongker32da27a2009-05-29 09:35:56 -0700155 p2 = Run(["minigzip"],
156 stdin=p1.stdout, stdout=ramdisk_img.file.fileno())
Doug Zongkereef39442009-04-02 12:14:19 -0700157
158 p2.wait()
159 p1.wait()
160 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
Doug Zongker32da27a2009-05-29 09:35:56 -0700161 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
Doug Zongkereef39442009-04-02 12:14:19 -0700162
Doug Zongker38a649f2009-06-17 09:07:09 -0700163 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
164
Doug Zongker171f1cd2009-06-15 22:36:37 -0700165 fn = os.path.join(sourcedir, "cmdline")
166 if os.access(fn, os.F_OK):
Doug Zongker38a649f2009-06-17 09:07:09 -0700167 cmd.append("--cmdline")
168 cmd.append(open(fn).read().rstrip("\n"))
169
170 fn = os.path.join(sourcedir, "base")
171 if os.access(fn, os.F_OK):
172 cmd.append("--base")
173 cmd.append(open(fn).read().rstrip("\n"))
174
Ying Wang4de6b5b2010-08-25 14:29:34 -0700175 fn = os.path.join(sourcedir, "pagesize")
176 if os.access(fn, os.F_OK):
177 cmd.append("--pagesize")
178 cmd.append(open(fn).read().rstrip("\n"))
179
Doug Zongker38a649f2009-06-17 09:07:09 -0700180 cmd.extend(["--ramdisk", ramdisk_img.name,
181 "--output", img.name])
182
183 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700184 p.communicate()
Doug Zongkere1c31ba2009-06-23 17:40:35 -0700185 assert p.returncode == 0, "mkbootimg of %s image failed" % (
186 os.path.basename(sourcedir),)
Doug Zongkereef39442009-04-02 12:14:19 -0700187
188 img.seek(os.SEEK_SET, 0)
189 data = img.read()
190
191 ramdisk_img.close()
192 img.close()
193
194 return data
195
196
197def AddRecovery(output_zip):
198 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
199 "recovery.img", output_zip)
200
201def AddBoot(output_zip):
202 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
203 "boot.img", output_zip)
204
Doug Zongker75f17362009-12-08 13:46:44 -0800205def UnzipTemp(filename, pattern=None):
Doug Zongkereef39442009-04-02 12:14:19 -0700206 """Unzip the given archive into a temporary directory and return the name."""
207
208 tmp = tempfile.mkdtemp(prefix="targetfiles-")
209 OPTIONS.tempfiles.append(tmp)
Doug Zongker75f17362009-12-08 13:46:44 -0800210 cmd = ["unzip", "-o", "-q", filename, "-d", tmp]
211 if pattern is not None:
212 cmd.append(pattern)
213 p = Run(cmd, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700214 p.communicate()
215 if p.returncode != 0:
216 raise ExternalError("failed to unzip input target-files \"%s\"" %
217 (filename,))
218 return tmp
219
220
221def GetKeyPasswords(keylist):
222 """Given a list of keys, prompt the user to enter passwords for
223 those which require them. Return a {key: password} dict. password
224 will be None if the key has no password."""
225
Doug Zongker8ce7c252009-05-22 13:34:54 -0700226 no_passwords = []
227 need_passwords = []
Doug Zongkereef39442009-04-02 12:14:19 -0700228 devnull = open("/dev/null", "w+b")
229 for k in sorted(keylist):
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800230 # We don't need a password for things that aren't really keys.
231 if k in SPECIAL_CERT_STRINGS:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700232 no_passwords.append(k)
Doug Zongker43874f82009-04-14 14:05:15 -0700233 continue
234
Doug Zongker602a84e2009-06-18 08:35:12 -0700235 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
236 "-inform", "DER", "-nocrypt"],
237 stdin=devnull.fileno(),
238 stdout=devnull.fileno(),
239 stderr=subprocess.STDOUT)
Doug Zongkereef39442009-04-02 12:14:19 -0700240 p.communicate()
241 if p.returncode == 0:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700242 no_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700243 else:
Doug Zongker8ce7c252009-05-22 13:34:54 -0700244 need_passwords.append(k)
Doug Zongkereef39442009-04-02 12:14:19 -0700245 devnull.close()
Doug Zongker8ce7c252009-05-22 13:34:54 -0700246
247 key_passwords = PasswordManager().GetPasswords(need_passwords)
248 key_passwords.update(dict.fromkeys(no_passwords, None))
Doug Zongkereef39442009-04-02 12:14:19 -0700249 return key_passwords
250
251
Doug Zongker951495f2009-08-14 12:44:19 -0700252def SignFile(input_name, output_name, key, password, align=None,
253 whole_file=False):
Doug Zongkereef39442009-04-02 12:14:19 -0700254 """Sign the input_name zip/jar/apk, producing output_name. Use the
255 given key and password (the latter may be None if the key does not
256 have a password.
257
258 If align is an integer > 1, zipalign is run to align stored files in
259 the output zip on 'align'-byte boundaries.
Doug Zongker951495f2009-08-14 12:44:19 -0700260
261 If whole_file is true, use the "-w" option to SignApk to embed a
262 signature that covers the whole file in the archive comment of the
263 zip file.
Doug Zongkereef39442009-04-02 12:14:19 -0700264 """
Doug Zongker951495f2009-08-14 12:44:19 -0700265
Doug Zongkereef39442009-04-02 12:14:19 -0700266 if align == 0 or align == 1:
267 align = None
268
269 if align:
270 temp = tempfile.NamedTemporaryFile()
271 sign_name = temp.name
272 else:
273 sign_name = output_name
274
Doug Zongker09cf5602009-08-14 15:25:06 -0700275 cmd = ["java", "-Xmx512m", "-jar",
Doug Zongker951495f2009-08-14 12:44:19 -0700276 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")]
277 if whole_file:
278 cmd.append("-w")
279 cmd.extend([key + ".x509.pem", key + ".pk8",
280 input_name, sign_name])
281
282 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Doug Zongkereef39442009-04-02 12:14:19 -0700283 if password is not None:
284 password += "\n"
285 p.communicate(password)
286 if p.returncode != 0:
287 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
288
289 if align:
Doug Zongker602a84e2009-06-18 08:35:12 -0700290 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
Doug Zongkereef39442009-04-02 12:14:19 -0700291 p.communicate()
292 if p.returncode != 0:
293 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
294 temp.close()
295
296
297def CheckSize(data, target):
298 """Check the data string passed against the max size limit, if
299 any, for the given target. Raise exception if the data is too big.
300 Print a warning if the data is nearing the maximum size."""
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700301
302 fs_type = OPTIONS.info_dict.get("fs_type", None)
303 if not fs_type: return
304
Doug Zongkereef39442009-04-02 12:14:19 -0700305 limit = OPTIONS.max_image_size.get(target, None)
306 if limit is None: return
307
Doug Zongkerc77a9ad2010-09-16 11:28:43 -0700308 if fs_type == "yaffs2":
309 # image size should be increased by 1/64th to account for the
310 # spare area (64 bytes per 2k page)
311 limit = limit / 2048 * (2048+64)
312
Doug Zongkereef39442009-04-02 12:14:19 -0700313 size = len(data)
314 pct = float(size) * 100.0 / limit
315 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
316 if pct >= 99.0:
317 raise ExternalError(msg)
318 elif pct >= 95.0:
319 print
320 print " WARNING: ", msg
321 print
322 elif OPTIONS.verbose:
323 print " ", msg
324
325
Doug Zongkerf6a53aa2009-12-15 15:06:55 -0800326def ReadApkCerts(tf_zip):
327 """Given a target_files ZipFile, parse the META/apkcerts.txt file
328 and return a {package: cert} dict."""
329 certmap = {}
330 for line in tf_zip.read("META/apkcerts.txt").split("\n"):
331 line = line.strip()
332 if not line: continue
333 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+'
334 r'private_key="(.*)"$', line)
335 if m:
336 name, cert, privkey = m.groups()
337 if cert in SPECIAL_CERT_STRINGS and not privkey:
338 certmap[name] = cert
339 elif (cert.endswith(".x509.pem") and
340 privkey.endswith(".pk8") and
341 cert[:-9] == privkey[:-4]):
342 certmap[name] = cert[:-9]
343 else:
344 raise ValueError("failed to parse line from apkcerts.txt:\n" + line)
345 return certmap
346
347
Doug Zongkereef39442009-04-02 12:14:19 -0700348COMMON_DOCSTRING = """
349 -p (--path) <dir>
Doug Zongker602a84e2009-06-18 08:35:12 -0700350 Prepend <dir>/bin to the list of places to search for binaries
351 run by this script, and expect to find jars in <dir>/framework.
Doug Zongkereef39442009-04-02 12:14:19 -0700352
Doug Zongker05d3dea2009-06-22 11:32:31 -0700353 -s (--device_specific) <file>
354 Path to the python module containing device-specific
355 releasetools code.
356
Doug Zongker8bec09e2009-11-30 15:37:14 -0800357 -x (--extra) <key=value>
358 Add a key/value pair to the 'extras' dict, which device-specific
359 extension code may look at.
360
Doug Zongkereef39442009-04-02 12:14:19 -0700361 -v (--verbose)
362 Show command lines being executed.
363
364 -h (--help)
365 Display this usage message and exit.
366"""
367
368def Usage(docstring):
369 print docstring.rstrip("\n")
370 print COMMON_DOCSTRING
371
372
373def ParseOptions(argv,
374 docstring,
375 extra_opts="", extra_long_opts=(),
376 extra_option_handler=None):
377 """Parse the options in argv and return any arguments that aren't
378 flags. docstring is the calling module's docstring, to be displayed
379 for errors and -h. extra_opts and extra_long_opts are for flags
380 defined by the caller, which are processed by passing them to
381 extra_option_handler."""
382
383 try:
384 opts, args = getopt.getopt(
Doug Zongker8bec09e2009-11-30 15:37:14 -0800385 argv, "hvp:s:x:" + extra_opts,
386 ["help", "verbose", "path=", "device_specific=", "extra="] +
Doug Zongker05d3dea2009-06-22 11:32:31 -0700387 list(extra_long_opts))
Doug Zongkereef39442009-04-02 12:14:19 -0700388 except getopt.GetoptError, err:
389 Usage(docstring)
390 print "**", str(err), "**"
391 sys.exit(2)
392
393 path_specified = False
394
395 for o, a in opts:
396 if o in ("-h", "--help"):
397 Usage(docstring)
398 sys.exit()
399 elif o in ("-v", "--verbose"):
400 OPTIONS.verbose = True
401 elif o in ("-p", "--path"):
Doug Zongker602a84e2009-06-18 08:35:12 -0700402 OPTIONS.search_path = a
Doug Zongker05d3dea2009-06-22 11:32:31 -0700403 elif o in ("-s", "--device_specific"):
404 OPTIONS.device_specific = a
Doug Zongker5ecba702009-12-03 16:36:20 -0800405 elif o in ("-x", "--extra"):
Doug Zongker8bec09e2009-11-30 15:37:14 -0800406 key, value = a.split("=", 1)
407 OPTIONS.extras[key] = value
Doug Zongkereef39442009-04-02 12:14:19 -0700408 else:
409 if extra_option_handler is None or not extra_option_handler(o, a):
410 assert False, "unknown option \"%s\"" % (o,)
411
Doug Zongker602a84e2009-06-18 08:35:12 -0700412 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
413 os.pathsep + os.environ["PATH"])
Doug Zongkereef39442009-04-02 12:14:19 -0700414
415 return args
416
417
418def Cleanup():
419 for i in OPTIONS.tempfiles:
420 if os.path.isdir(i):
421 shutil.rmtree(i)
422 else:
423 os.remove(i)
Doug Zongker8ce7c252009-05-22 13:34:54 -0700424
425
426class PasswordManager(object):
427 def __init__(self):
428 self.editor = os.getenv("EDITOR", None)
429 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
430
431 def GetPasswords(self, items):
432 """Get passwords corresponding to each string in 'items',
433 returning a dict. (The dict may have keys in addition to the
434 values in 'items'.)
435
436 Uses the passwords in $ANDROID_PW_FILE if available, letting the
437 user edit that file to add more needed passwords. If no editor is
438 available, or $ANDROID_PW_FILE isn't define, prompts the user
439 interactively in the ordinary way.
440 """
441
442 current = self.ReadFile()
443
444 first = True
445 while True:
446 missing = []
447 for i in items:
448 if i not in current or not current[i]:
449 missing.append(i)
450 # Are all the passwords already in the file?
451 if not missing: return current
452
453 for i in missing:
454 current[i] = ""
455
456 if not first:
457 print "key file %s still missing some passwords." % (self.pwfile,)
458 answer = raw_input("try to edit again? [y]> ").strip()
459 if answer and answer[0] not in 'yY':
460 raise RuntimeError("key passwords unavailable")
461 first = False
462
463 current = self.UpdateAndReadFile(current)
464
465 def PromptResult(self, current):
466 """Prompt the user to enter a value (password) for each key in
467 'current' whose value is fales. Returns a new dict with all the
468 values.
469 """
470 result = {}
471 for k, v in sorted(current.iteritems()):
472 if v:
473 result[k] = v
474 else:
475 while True:
476 result[k] = getpass.getpass("Enter password for %s key> "
477 % (k,)).strip()
478 if result[k]: break
479 return result
480
481 def UpdateAndReadFile(self, current):
482 if not self.editor or not self.pwfile:
483 return self.PromptResult(current)
484
485 f = open(self.pwfile, "w")
486 os.chmod(self.pwfile, 0600)
487 f.write("# Enter key passwords between the [[[ ]]] brackets.\n")
488 f.write("# (Additional spaces are harmless.)\n\n")
489
490 first_line = None
491 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
492 sorted.sort()
493 for i, (_, k, v) in enumerate(sorted):
494 f.write("[[[ %s ]]] %s\n" % (v, k))
495 if not v and first_line is None:
496 # position cursor on first line with no password.
497 first_line = i + 4
498 f.close()
499
500 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
501 _, _ = p.communicate()
502
503 return self.ReadFile()
504
505 def ReadFile(self):
506 result = {}
507 if self.pwfile is None: return result
508 try:
509 f = open(self.pwfile, "r")
510 for line in f:
511 line = line.strip()
512 if not line or line[0] == '#': continue
513 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
514 if not m:
515 print "failed to parse password file: ", line
516 else:
517 result[m.group(2)] = m.group(1)
518 f.close()
519 except IOError, e:
520 if e.errno != errno.ENOENT:
521 print "error reading password file: ", str(e)
522 return result
Doug Zongker048e7ca2009-06-15 14:31:53 -0700523
524
525def ZipWriteStr(zip, filename, data, perms=0644):
526 # use a fixed timestamp so the output is repeatable.
527 zinfo = zipfile.ZipInfo(filename=filename,
528 date_time=(2009, 1, 1, 0, 0, 0))
529 zinfo.compress_type = zip.compression
530 zinfo.external_attr = perms << 16
531 zip.writestr(zinfo, data)
Doug Zongker05d3dea2009-06-22 11:32:31 -0700532
533
534class DeviceSpecificParams(object):
535 module = None
536 def __init__(self, **kwargs):
537 """Keyword arguments to the constructor become attributes of this
538 object, which is passed to all functions in the device-specific
539 module."""
540 for k, v in kwargs.iteritems():
541 setattr(self, k, v)
Doug Zongker8bec09e2009-11-30 15:37:14 -0800542 self.extras = OPTIONS.extras
Doug Zongker05d3dea2009-06-22 11:32:31 -0700543
544 if self.module is None:
545 path = OPTIONS.device_specific
Doug Zongkerc18736b2009-09-30 09:20:32 -0700546 if not path: return
Doug Zongker8e2f2b92009-06-24 14:34:57 -0700547 try:
548 if os.path.isdir(path):
549 info = imp.find_module("releasetools", [path])
550 else:
551 d, f = os.path.split(path)
552 b, x = os.path.splitext(f)
553 if x == ".py":
554 f = b
555 info = imp.find_module(f, [d])
556 self.module = imp.load_module("device_specific", *info)
557 except ImportError:
558 print "unable to load device-specific module; assuming none"
Doug Zongker05d3dea2009-06-22 11:32:31 -0700559
560 def _DoCall(self, function_name, *args, **kwargs):
561 """Call the named function in the device-specific module, passing
562 the given args and kwargs. The first argument to the call will be
563 the DeviceSpecific object itself. If there is no module, or the
564 module does not define the function, return the value of the
565 'default' kwarg (which itself defaults to None)."""
566 if self.module is None or not hasattr(self.module, function_name):
567 return kwargs.get("default", None)
568 return getattr(self.module, function_name)(*((self,) + args), **kwargs)
569
570 def FullOTA_Assertions(self):
571 """Called after emitting the block of assertions at the top of a
572 full OTA package. Implementations can add whatever additional
573 assertions they like."""
574 return self._DoCall("FullOTA_Assertions")
575
576 def FullOTA_InstallEnd(self):
577 """Called at the end of full OTA installation; typically this is
578 used to install the image for the device's baseband processor."""
579 return self._DoCall("FullOTA_InstallEnd")
580
581 def IncrementalOTA_Assertions(self):
582 """Called after emitting the block of assertions at the top of an
583 incremental OTA package. Implementations can add whatever
584 additional assertions they like."""
585 return self._DoCall("IncrementalOTA_Assertions")
586
587 def IncrementalOTA_VerifyEnd(self):
588 """Called at the end of the verification phase of incremental OTA
589 installation; additional checks can be placed here to abort the
590 script before any changes are made."""
591 return self._DoCall("IncrementalOTA_VerifyEnd")
592
593 def IncrementalOTA_InstallEnd(self):
594 """Called at the end of incremental OTA installation; typically
595 this is used to install the image for the device's baseband
596 processor."""
597 return self._DoCall("IncrementalOTA_InstallEnd")
Doug Zongkerea5d7a92010-09-12 15:26:16 -0700598
599class File(object):
600 def __init__(self, name, data):
601 self.name = name
602 self.data = data
603 self.size = len(data)
604 self.sha1 = sha.sha(data).hexdigest()
605
606 def WriteToTemp(self):
607 t = tempfile.NamedTemporaryFile()
608 t.write(self.data)
609 t.flush()
610 return t
611
612 def AddToZip(self, z):
613 ZipWriteStr(z, self.name, self.data)
614
615DIFF_PROGRAM_BY_EXT = {
616 ".gz" : "imgdiff",
617 ".zip" : ["imgdiff", "-z"],
618 ".jar" : ["imgdiff", "-z"],
619 ".apk" : ["imgdiff", "-z"],
620 ".img" : "imgdiff",
621 }
622
623class Difference(object):
624 def __init__(self, tf, sf):
625 self.tf = tf
626 self.sf = sf
627 self.patch = None
628
629 def ComputePatch(self):
630 """Compute the patch (as a string of data) needed to turn sf into
631 tf. Returns the same tuple as GetPatch()."""
632
633 tf = self.tf
634 sf = self.sf
635
636 ext = os.path.splitext(tf.name)[1]
637 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff")
638
639 ttemp = tf.WriteToTemp()
640 stemp = sf.WriteToTemp()
641
642 ext = os.path.splitext(tf.name)[1]
643
644 try:
645 ptemp = tempfile.NamedTemporaryFile()
646 if isinstance(diff_program, list):
647 cmd = copy.copy(diff_program)
648 else:
649 cmd = [diff_program]
650 cmd.append(stemp.name)
651 cmd.append(ttemp.name)
652 cmd.append(ptemp.name)
653 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
654 _, err = p.communicate()
655 if err or p.returncode != 0:
656 print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
657 return None
658 diff = ptemp.read()
659 finally:
660 ptemp.close()
661 stemp.close()
662 ttemp.close()
663
664 self.patch = diff
665 return self.tf, self.sf, self.patch
666
667
668 def GetPatch(self):
669 """Return a tuple (target_file, source_file, patch_data).
670 patch_data may be None if ComputePatch hasn't been called, or if
671 computing the patch failed."""
672 return self.tf, self.sf, self.patch
673
674
675def ComputeDifferences(diffs):
676 """Call ComputePatch on all the Difference objects in 'diffs'."""
677 print len(diffs), "diffs to compute"
678
679 # Do the largest files first, to try and reduce the long-pole effect.
680 by_size = [(i.tf.size, i) for i in diffs]
681 by_size.sort(reverse=True)
682 by_size = [i[1] for i in by_size]
683
684 lock = threading.Lock()
685 diff_iter = iter(by_size) # accessed under lock
686
687 def worker():
688 try:
689 lock.acquire()
690 for d in diff_iter:
691 lock.release()
692 start = time.time()
693 d.ComputePatch()
694 dur = time.time() - start
695 lock.acquire()
696
697 tf, sf, patch = d.GetPatch()
698 if sf.name == tf.name:
699 name = tf.name
700 else:
701 name = "%s (%s)" % (tf.name, sf.name)
702 if patch is None:
703 print "patching failed! %s" % (name,)
704 else:
705 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % (
706 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name)
707 lock.release()
708 except Exception, e:
709 print e
710 raise
711
712 # start worker threads; wait for them all to finish.
713 threads = [threading.Thread(target=worker)
714 for i in range(OPTIONS.worker_threads)]
715 for th in threads:
716 th.start()
717 while threads:
718 threads.pop().join()