1# 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 15import copy 16import errno 17import getopt 18import getpass 19import imp 20import os 21import platform 22import re 23import shutil 24import subprocess 25import sys 26import tempfile 27import threading 28import time 29import zipfile 30 31try: 32 from hashlib import sha1 as sha1 33except ImportError: 34 from sha import sha as sha1 35 36# missing in Python 2.4 and before 37if not hasattr(os, "SEEK_SET"): 38 os.SEEK_SET = 0 39 40class Options(object): pass 41OPTIONS = Options() 42OPTIONS.search_path = "out/host/linux-x86" 43OPTIONS.verbose = False 44OPTIONS.tempfiles = [] 45OPTIONS.device_specific = None 46OPTIONS.extras = {} 47OPTIONS.info_dict = None 48 49 50# Values for "certificate" in apkcerts that mean special things. 51SPECIAL_CERT_STRINGS = ("PRESIGNED", "EXTERNAL") 52 53 54class ExternalError(RuntimeError): pass 55 56 57def Run(args, **kwargs): 58 """Create and return a subprocess.Popen object, printing the command 59 line on the terminal if -v was specified.""" 60 if OPTIONS.verbose: 61 print " running: ", " ".join(args) 62 return subprocess.Popen(args, **kwargs) 63 64 65def CloseInheritedPipes(): 66 """ Gmake in MAC OS has file descriptor (PIPE) leak. We close those fds 67 before doing other work.""" 68 if platform.system() != "Darwin": 69 return 70 for d in range(3, 1025): 71 try: 72 stat = os.fstat(d) 73 if stat is not None: 74 pipebit = stat[0] & 0x1000 75 if pipebit != 0: 76 os.close(d) 77 except OSError: 78 pass 79 80 81def LoadInfoDict(zip): 82 """Read and parse the META/misc_info.txt key/value pairs from the 83 input target files and return a dict.""" 84 85 d = {} 86 try: 87 for line in zip.read("META/misc_info.txt").split("\n"): 88 line = line.strip() 89 if not line or line.startswith("#"): continue 90 k, v = line.split("=", 1) 91 d[k] = v 92 except KeyError: 93 # ok if misc_info.txt doesn't exist 94 pass 95 96 # backwards compatibility: These values used to be in their own 97 # files. Look for them, in case we're processing an old 98 # target_files zip. 99 100 if "mkyaffs2_extra_flags" not in d: 101 try: 102 d["mkyaffs2_extra_flags"] = zip.read("META/mkyaffs2-extra-flags.txt").strip() 103 except KeyError: 104 # ok if flags don't exist 105 pass 106 107 if "recovery_api_version" not in d: 108 try: 109 d["recovery_api_version"] = zip.read("META/recovery-api-version.txt").strip() 110 except KeyError: 111 raise ValueError("can't find recovery API version in input target-files") 112 113 if "tool_extensions" not in d: 114 try: 115 d["tool_extensions"] = zip.read("META/tool-extensions.txt").strip() 116 except KeyError: 117 # ok if extensions don't exist 118 pass 119 120 if "fstab_version" not in d: 121 d["fstab_version"] = "1" 122 123 try: 124 data = zip.read("META/imagesizes.txt") 125 for line in data.split("\n"): 126 if not line: continue 127 name, value = line.split(" ", 1) 128 if not value: continue 129 if name == "blocksize": 130 d[name] = value 131 else: 132 d[name + "_size"] = value 133 except KeyError: 134 pass 135 136 def makeint(key): 137 if key in d: 138 d[key] = int(d[key], 0) 139 140 makeint("recovery_api_version") 141 makeint("blocksize") 142 makeint("system_size") 143 makeint("userdata_size") 144 makeint("cache_size") 145 makeint("recovery_size") 146 makeint("boot_size") 147 makeint("fstab_version") 148 149 d["fstab"] = LoadRecoveryFSTab(zip, d["fstab_version"]) 150 d["build.prop"] = LoadBuildProp(zip) 151 return d 152 153def LoadBuildProp(zip): 154 try: 155 data = zip.read("SYSTEM/build.prop") 156 except KeyError: 157 print "Warning: could not find SYSTEM/build.prop in %s" % zip 158 data = "" 159 160 d = {} 161 for line in data.split("\n"): 162 line = line.strip() 163 if not line or line.startswith("#"): continue 164 name, value = line.split("=", 1) 165 d[name] = value 166 return d 167 168def LoadRecoveryFSTab(zip, fstab_version): 169 class Partition(object): 170 pass 171 172 try: 173 data = zip.read("RECOVERY/RAMDISK/etc/recovery.fstab") 174 except KeyError: 175 print "Warning: could not find RECOVERY/RAMDISK/etc/recovery.fstab in %s." % zip 176 data = "" 177 178 if fstab_version == 1: 179 d = {} 180 for line in data.split("\n"): 181 line = line.strip() 182 if not line or line.startswith("#"): continue 183 pieces = line.split() 184 if not (3 <= len(pieces) <= 4): 185 raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,)) 186 187 p = Partition() 188 p.mount_point = pieces[0] 189 p.fs_type = pieces[1] 190 p.device = pieces[2] 191 p.length = 0 192 options = None 193 if len(pieces) >= 4: 194 if pieces[3].startswith("/"): 195 p.device2 = pieces[3] 196 if len(pieces) >= 5: 197 options = pieces[4] 198 else: 199 p.device2 = None 200 options = pieces[3] 201 else: 202 p.device2 = None 203 204 if options: 205 options = options.split(",") 206 for i in options: 207 if i.startswith("length="): 208 p.length = int(i[7:]) 209 else: 210 print "%s: unknown option \"%s\"" % (p.mount_point, i) 211 212 d[p.mount_point] = p 213 214 elif fstab_version == 2: 215 d = {} 216 for line in data.split("\n"): 217 line = line.strip() 218 if not line or line.startswith("#"): continue 219 pieces = line.split() 220 if len(pieces) != 5: 221 raise ValueError("malformed recovery.fstab line: \"%s\"" % (line,)) 222 223 # Ignore entries that are managed by vold 224 options = pieces[4] 225 if "voldmanaged=" in options: continue 226 227 # It's a good line, parse it 228 p = Partition() 229 p.device = pieces[0] 230 p.mount_point = pieces[1] 231 p.fs_type = pieces[2] 232 p.device2 = None 233 p.length = 0 234 235 options = options.split(",") 236 for i in options: 237 if i.startswith("length="): 238 p.length = int(i[7:]) 239 else: 240 # Ignore all unknown options in the unified fstab 241 continue 242 243 d[p.mount_point] = p 244 245 else: 246 raise ValueError("Unknown fstab_version: \"%d\"" % (fstab_version,)) 247 248 return d 249 250 251def DumpInfoDict(d): 252 for k, v in sorted(d.items()): 253 print "%-25s = (%s) %s" % (k, type(v).__name__, v) 254 255def BuildBootableImage(sourcedir, fs_config_file, info_dict=None): 256 """Take a kernel, cmdline, and ramdisk directory from the input (in 257 'sourcedir'), and turn them into a boot image. Return the image 258 data, or None if sourcedir does not appear to contains files for 259 building the requested image.""" 260 261 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or 262 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)): 263 return None 264 265 if info_dict is None: 266 info_dict = OPTIONS.info_dict 267 268 ramdisk_img = tempfile.NamedTemporaryFile() 269 img = tempfile.NamedTemporaryFile() 270 271 if os.access(fs_config_file, os.F_OK): 272 cmd = ["mkbootfs", "-f", fs_config_file, os.path.join(sourcedir, "RAMDISK")] 273 else: 274 cmd = ["mkbootfs", os.path.join(sourcedir, "RAMDISK")] 275 p1 = Run(cmd, stdout=subprocess.PIPE) 276 p2 = Run(["minigzip"], 277 stdin=p1.stdout, stdout=ramdisk_img.file.fileno()) 278 279 p2.wait() 280 p1.wait() 281 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,) 282 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,) 283 284 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")] 285 286 fn = os.path.join(sourcedir, "cmdline") 287 if os.access(fn, os.F_OK): 288 cmd.append("--cmdline") 289 cmd.append(open(fn).read().rstrip("\n")) 290 291 fn = os.path.join(sourcedir, "base") 292 if os.access(fn, os.F_OK): 293 cmd.append("--base") 294 cmd.append(open(fn).read().rstrip("\n")) 295 296 fn = os.path.join(sourcedir, "pagesize") 297 if os.access(fn, os.F_OK): 298 cmd.append("--pagesize") 299 cmd.append(open(fn).read().rstrip("\n")) 300 301 args = info_dict.get("mkbootimg_args", None) 302 if args and args.strip(): 303 cmd.extend(args.split()) 304 305 cmd.extend(["--ramdisk", ramdisk_img.name, 306 "--output", img.name]) 307 308 p = Run(cmd, stdout=subprocess.PIPE) 309 p.communicate() 310 assert p.returncode == 0, "mkbootimg of %s image failed" % ( 311 os.path.basename(sourcedir),) 312 313 img.seek(os.SEEK_SET, 0) 314 data = img.read() 315 316 ramdisk_img.close() 317 img.close() 318 319 return data 320 321 322def GetBootableImage(name, prebuilt_name, unpack_dir, tree_subdir, 323 info_dict=None): 324 """Return a File object (with name 'name') with the desired bootable 325 image. Look for it in 'unpack_dir'/BOOTABLE_IMAGES under the name 326 'prebuilt_name', otherwise construct it from the source files in 327 'unpack_dir'/'tree_subdir'.""" 328 329 prebuilt_path = os.path.join(unpack_dir, "BOOTABLE_IMAGES", prebuilt_name) 330 if os.path.exists(prebuilt_path): 331 print "using prebuilt %s..." % (prebuilt_name,) 332 return File.FromLocalFile(name, prebuilt_path) 333 else: 334 print "building image from target_files %s..." % (tree_subdir,) 335 fs_config = "META/" + tree_subdir.lower() + "_filesystem_config.txt" 336 return File(name, BuildBootableImage(os.path.join(unpack_dir, tree_subdir), 337 os.path.join(unpack_dir, fs_config), 338 info_dict)) 339 340 341def UnzipTemp(filename, pattern=None): 342 """Unzip the given archive into a temporary directory and return the name. 343 344 If filename is of the form "foo.zip+bar.zip", unzip foo.zip into a 345 temp dir, then unzip bar.zip into that_dir/BOOTABLE_IMAGES. 346 347 Returns (tempdir, zipobj) where zipobj is a zipfile.ZipFile (of the 348 main file), open for reading. 349 """ 350 351 tmp = tempfile.mkdtemp(prefix="targetfiles-") 352 OPTIONS.tempfiles.append(tmp) 353 354 def unzip_to_dir(filename, dirname): 355 cmd = ["unzip", "-o", "-q", filename, "-d", dirname] 356 if pattern is not None: 357 cmd.append(pattern) 358 p = Run(cmd, stdout=subprocess.PIPE) 359 p.communicate() 360 if p.returncode != 0: 361 raise ExternalError("failed to unzip input target-files \"%s\"" % 362 (filename,)) 363 364 m = re.match(r"^(.*[.]zip)\+(.*[.]zip)$", filename, re.IGNORECASE) 365 if m: 366 unzip_to_dir(m.group(1), tmp) 367 unzip_to_dir(m.group(2), os.path.join(tmp, "BOOTABLE_IMAGES")) 368 filename = m.group(1) 369 else: 370 unzip_to_dir(filename, tmp) 371 372 return tmp, zipfile.ZipFile(filename, "r") 373 374 375def GetKeyPasswords(keylist): 376 """Given a list of keys, prompt the user to enter passwords for 377 those which require them. Return a {key: password} dict. password 378 will be None if the key has no password.""" 379 380 no_passwords = [] 381 need_passwords = [] 382 devnull = open("/dev/null", "w+b") 383 for k in sorted(keylist): 384 # We don't need a password for things that aren't really keys. 385 if k in SPECIAL_CERT_STRINGS: 386 no_passwords.append(k) 387 continue 388 389 p = Run(["openssl", "pkcs8", "-in", k+".pk8", 390 "-inform", "DER", "-nocrypt"], 391 stdin=devnull.fileno(), 392 stdout=devnull.fileno(), 393 stderr=subprocess.STDOUT) 394 p.communicate() 395 if p.returncode == 0: 396 no_passwords.append(k) 397 else: 398 need_passwords.append(k) 399 devnull.close() 400 401 key_passwords = PasswordManager().GetPasswords(need_passwords) 402 key_passwords.update(dict.fromkeys(no_passwords, None)) 403 return key_passwords 404 405 406def SignFile(input_name, output_name, key, password, align=None, 407 whole_file=False): 408 """Sign the input_name zip/jar/apk, producing output_name. Use the 409 given key and password (the latter may be None if the key does not 410 have a password. 411 412 If align is an integer > 1, zipalign is run to align stored files in 413 the output zip on 'align'-byte boundaries. 414 415 If whole_file is true, use the "-w" option to SignApk to embed a 416 signature that covers the whole file in the archive comment of the 417 zip file. 418 """ 419 420 if align == 0 or align == 1: 421 align = None 422 423 if align: 424 temp = tempfile.NamedTemporaryFile() 425 sign_name = temp.name 426 else: 427 sign_name = output_name 428 429 cmd = ["java", "-Xmx2048m", "-jar", 430 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")] 431 if whole_file: 432 cmd.append("-w") 433 cmd.extend([key + ".x509.pem", key + ".pk8", 434 input_name, sign_name]) 435 436 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 437 if password is not None: 438 password += "\n" 439 p.communicate(password) 440 if p.returncode != 0: 441 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,)) 442 443 if align: 444 p = Run(["zipalign", "-f", str(align), sign_name, output_name]) 445 p.communicate() 446 if p.returncode != 0: 447 raise ExternalError("zipalign failed: return code %s" % (p.returncode,)) 448 temp.close() 449 450 451def CheckSize(data, target, info_dict): 452 """Check the data string passed against the max size limit, if 453 any, for the given target. Raise exception if the data is too big. 454 Print a warning if the data is nearing the maximum size.""" 455 456 if target.endswith(".img"): target = target[:-4] 457 mount_point = "/" + target 458 459 if info_dict["fstab"]: 460 if mount_point == "/userdata": mount_point = "/data" 461 p = info_dict["fstab"][mount_point] 462 fs_type = p.fs_type 463 device = p.device 464 if "/" in device: 465 device = device[device.rfind("/")+1:] 466 limit = info_dict.get(device + "_size", None) 467 if not fs_type or not limit: return 468 469 if fs_type == "yaffs2": 470 # image size should be increased by 1/64th to account for the 471 # spare area (64 bytes per 2k page) 472 limit = limit / 2048 * (2048+64) 473 size = len(data) 474 pct = float(size) * 100.0 / limit 475 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit) 476 if pct >= 99.0: 477 raise ExternalError(msg) 478 elif pct >= 95.0: 479 print 480 print " WARNING: ", msg 481 print 482 elif OPTIONS.verbose: 483 print " ", msg 484 485 486def ReadApkCerts(tf_zip): 487 """Given a target_files ZipFile, parse the META/apkcerts.txt file 488 and return a {package: cert} dict.""" 489 certmap = {} 490 for line in tf_zip.read("META/apkcerts.txt").split("\n"): 491 line = line.strip() 492 if not line: continue 493 m = re.match(r'^name="(.*)"\s+certificate="(.*)"\s+' 494 r'private_key="(.*)"$', line) 495 if m: 496 name, cert, privkey = m.groups() 497 if cert in SPECIAL_CERT_STRINGS and not privkey: 498 certmap[name] = cert 499 elif (cert.endswith(".x509.pem") and 500 privkey.endswith(".pk8") and 501 cert[:-9] == privkey[:-4]): 502 certmap[name] = cert[:-9] 503 else: 504 raise ValueError("failed to parse line from apkcerts.txt:\n" + line) 505 return certmap 506 507 508COMMON_DOCSTRING = """ 509 -p (--path) <dir> 510 Prepend <dir>/bin to the list of places to search for binaries 511 run by this script, and expect to find jars in <dir>/framework. 512 513 -s (--device_specific) <file> 514 Path to the python module containing device-specific 515 releasetools code. 516 517 -x (--extra) <key=value> 518 Add a key/value pair to the 'extras' dict, which device-specific 519 extension code may look at. 520 521 -v (--verbose) 522 Show command lines being executed. 523 524 -h (--help) 525 Display this usage message and exit. 526""" 527 528def Usage(docstring): 529 print docstring.rstrip("\n") 530 print COMMON_DOCSTRING 531 532 533def ParseOptions(argv, 534 docstring, 535 extra_opts="", extra_long_opts=(), 536 extra_option_handler=None): 537 """Parse the options in argv and return any arguments that aren't 538 flags. docstring is the calling module's docstring, to be displayed 539 for errors and -h. extra_opts and extra_long_opts are for flags 540 defined by the caller, which are processed by passing them to 541 extra_option_handler.""" 542 543 try: 544 opts, args = getopt.getopt( 545 argv, "hvp:s:x:" + extra_opts, 546 ["help", "verbose", "path=", "device_specific=", "extra="] + 547 list(extra_long_opts)) 548 except getopt.GetoptError, err: 549 Usage(docstring) 550 print "**", str(err), "**" 551 sys.exit(2) 552 553 path_specified = False 554 555 for o, a in opts: 556 if o in ("-h", "--help"): 557 Usage(docstring) 558 sys.exit() 559 elif o in ("-v", "--verbose"): 560 OPTIONS.verbose = True 561 elif o in ("-p", "--path"): 562 OPTIONS.search_path = a 563 elif o in ("-s", "--device_specific"): 564 OPTIONS.device_specific = a 565 elif o in ("-x", "--extra"): 566 key, value = a.split("=", 1) 567 OPTIONS.extras[key] = value 568 else: 569 if extra_option_handler is None or not extra_option_handler(o, a): 570 assert False, "unknown option \"%s\"" % (o,) 571 572 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") + 573 os.pathsep + os.environ["PATH"]) 574 575 return args 576 577 578def Cleanup(): 579 for i in OPTIONS.tempfiles: 580 if os.path.isdir(i): 581 shutil.rmtree(i) 582 else: 583 os.remove(i) 584 585 586class PasswordManager(object): 587 def __init__(self): 588 self.editor = os.getenv("EDITOR", None) 589 self.pwfile = os.getenv("ANDROID_PW_FILE", None) 590 591 def GetPasswords(self, items): 592 """Get passwords corresponding to each string in 'items', 593 returning a dict. (The dict may have keys in addition to the 594 values in 'items'.) 595 596 Uses the passwords in $ANDROID_PW_FILE if available, letting the 597 user edit that file to add more needed passwords. If no editor is 598 available, or $ANDROID_PW_FILE isn't define, prompts the user 599 interactively in the ordinary way. 600 """ 601 602 current = self.ReadFile() 603 604 first = True 605 while True: 606 missing = [] 607 for i in items: 608 if i not in current or not current[i]: 609 missing.append(i) 610 # Are all the passwords already in the file? 611 if not missing: return current 612 613 for i in missing: 614 current[i] = "" 615 616 if not first: 617 print "key file %s still missing some passwords." % (self.pwfile,) 618 answer = raw_input("try to edit again? [y]> ").strip() 619 if answer and answer[0] not in 'yY': 620 raise RuntimeError("key passwords unavailable") 621 first = False 622 623 current = self.UpdateAndReadFile(current) 624 625 def PromptResult(self, current): 626 """Prompt the user to enter a value (password) for each key in 627 'current' whose value is fales. Returns a new dict with all the 628 values. 629 """ 630 result = {} 631 for k, v in sorted(current.iteritems()): 632 if v: 633 result[k] = v 634 else: 635 while True: 636 result[k] = getpass.getpass("Enter password for %s key> " 637 % (k,)).strip() 638 if result[k]: break 639 return result 640 641 def UpdateAndReadFile(self, current): 642 if not self.editor or not self.pwfile: 643 return self.PromptResult(current) 644 645 f = open(self.pwfile, "w") 646 os.chmod(self.pwfile, 0600) 647 f.write("# Enter key passwords between the [[[ ]]] brackets.\n") 648 f.write("# (Additional spaces are harmless.)\n\n") 649 650 first_line = None 651 sorted = [(not v, k, v) for (k, v) in current.iteritems()] 652 sorted.sort() 653 for i, (_, k, v) in enumerate(sorted): 654 f.write("[[[ %s ]]] %s\n" % (v, k)) 655 if not v and first_line is None: 656 # position cursor on first line with no password. 657 first_line = i + 4 658 f.close() 659 660 p = Run([self.editor, "+%d" % (first_line,), self.pwfile]) 661 _, _ = p.communicate() 662 663 return self.ReadFile() 664 665 def ReadFile(self): 666 result = {} 667 if self.pwfile is None: return result 668 try: 669 f = open(self.pwfile, "r") 670 for line in f: 671 line = line.strip() 672 if not line or line[0] == '#': continue 673 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line) 674 if not m: 675 print "failed to parse password file: ", line 676 else: 677 result[m.group(2)] = m.group(1) 678 f.close() 679 except IOError, e: 680 if e.errno != errno.ENOENT: 681 print "error reading password file: ", str(e) 682 return result 683 684 685def ZipWriteStr(zip, filename, data, perms=0644): 686 # use a fixed timestamp so the output is repeatable. 687 zinfo = zipfile.ZipInfo(filename=filename, 688 date_time=(2009, 1, 1, 0, 0, 0)) 689 zinfo.compress_type = zip.compression 690 zinfo.external_attr = perms << 16 691 zip.writestr(zinfo, data) 692 693 694class DeviceSpecificParams(object): 695 module = None 696 def __init__(self, **kwargs): 697 """Keyword arguments to the constructor become attributes of this 698 object, which is passed to all functions in the device-specific 699 module.""" 700 for k, v in kwargs.iteritems(): 701 setattr(self, k, v) 702 self.extras = OPTIONS.extras 703 704 if self.module is None: 705 path = OPTIONS.device_specific 706 if not path: return 707 try: 708 if os.path.isdir(path): 709 info = imp.find_module("releasetools", [path]) 710 else: 711 d, f = os.path.split(path) 712 b, x = os.path.splitext(f) 713 if x == ".py": 714 f = b 715 info = imp.find_module(f, [d]) 716 self.module = imp.load_module("device_specific", *info) 717 except ImportError: 718 print "unable to load device-specific module; assuming none" 719 720 def _DoCall(self, function_name, *args, **kwargs): 721 """Call the named function in the device-specific module, passing 722 the given args and kwargs. The first argument to the call will be 723 the DeviceSpecific object itself. If there is no module, or the 724 module does not define the function, return the value of the 725 'default' kwarg (which itself defaults to None).""" 726 if self.module is None or not hasattr(self.module, function_name): 727 return kwargs.get("default", None) 728 return getattr(self.module, function_name)(*((self,) + args), **kwargs) 729 730 def FullOTA_Assertions(self): 731 """Called after emitting the block of assertions at the top of a 732 full OTA package. Implementations can add whatever additional 733 assertions they like.""" 734 return self._DoCall("FullOTA_Assertions") 735 736 def FullOTA_InstallBegin(self): 737 """Called at the start of full OTA installation.""" 738 return self._DoCall("FullOTA_InstallBegin") 739 740 def FullOTA_InstallEnd(self): 741 """Called at the end of full OTA installation; typically this is 742 used to install the image for the device's baseband processor.""" 743 return self._DoCall("FullOTA_InstallEnd") 744 745 def IncrementalOTA_Assertions(self): 746 """Called after emitting the block of assertions at the top of an 747 incremental OTA package. Implementations can add whatever 748 additional assertions they like.""" 749 return self._DoCall("IncrementalOTA_Assertions") 750 751 def IncrementalOTA_VerifyBegin(self): 752 """Called at the start of the verification phase of incremental 753 OTA installation; additional checks can be placed here to abort 754 the script before any changes are made.""" 755 return self._DoCall("IncrementalOTA_VerifyBegin") 756 757 def IncrementalOTA_VerifyEnd(self): 758 """Called at the end of the verification phase of incremental OTA 759 installation; additional checks can be placed here to abort the 760 script before any changes are made.""" 761 return self._DoCall("IncrementalOTA_VerifyEnd") 762 763 def IncrementalOTA_InstallBegin(self): 764 """Called at the start of incremental OTA installation (after 765 verification is complete).""" 766 return self._DoCall("IncrementalOTA_InstallBegin") 767 768 def IncrementalOTA_InstallEnd(self): 769 """Called at the end of incremental OTA installation; typically 770 this is used to install the image for the device's baseband 771 processor.""" 772 return self._DoCall("IncrementalOTA_InstallEnd") 773 774class File(object): 775 def __init__(self, name, data): 776 self.name = name 777 self.data = data 778 self.size = len(data) 779 self.sha1 = sha1(data).hexdigest() 780 781 @classmethod 782 def FromLocalFile(cls, name, diskname): 783 f = open(diskname, "rb") 784 data = f.read() 785 f.close() 786 return File(name, data) 787 788 def WriteToTemp(self): 789 t = tempfile.NamedTemporaryFile() 790 t.write(self.data) 791 t.flush() 792 return t 793 794 def AddToZip(self, z): 795 ZipWriteStr(z, self.name, self.data) 796 797DIFF_PROGRAM_BY_EXT = { 798 ".gz" : "imgdiff", 799 ".zip" : ["imgdiff", "-z"], 800 ".jar" : ["imgdiff", "-z"], 801 ".apk" : ["imgdiff", "-z"], 802 ".img" : "imgdiff", 803 } 804 805class Difference(object): 806 def __init__(self, tf, sf, diff_program=None): 807 self.tf = tf 808 self.sf = sf 809 self.patch = None 810 self.diff_program = diff_program 811 812 def ComputePatch(self): 813 """Compute the patch (as a string of data) needed to turn sf into 814 tf. Returns the same tuple as GetPatch().""" 815 816 tf = self.tf 817 sf = self.sf 818 819 if self.diff_program: 820 diff_program = self.diff_program 821 else: 822 ext = os.path.splitext(tf.name)[1] 823 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 824 825 ttemp = tf.WriteToTemp() 826 stemp = sf.WriteToTemp() 827 828 ext = os.path.splitext(tf.name)[1] 829 830 try: 831 ptemp = tempfile.NamedTemporaryFile() 832 if isinstance(diff_program, list): 833 cmd = copy.copy(diff_program) 834 else: 835 cmd = [diff_program] 836 cmd.append(stemp.name) 837 cmd.append(ttemp.name) 838 cmd.append(ptemp.name) 839 p = Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 840 _, err = p.communicate() 841 if err or p.returncode != 0: 842 print "WARNING: failure running %s:\n%s\n" % (diff_program, err) 843 return None 844 diff = ptemp.read() 845 finally: 846 ptemp.close() 847 stemp.close() 848 ttemp.close() 849 850 self.patch = diff 851 return self.tf, self.sf, self.patch 852 853 854 def GetPatch(self): 855 """Return a tuple (target_file, source_file, patch_data). 856 patch_data may be None if ComputePatch hasn't been called, or if 857 computing the patch failed.""" 858 return self.tf, self.sf, self.patch 859 860 861def ComputeDifferences(diffs): 862 """Call ComputePatch on all the Difference objects in 'diffs'.""" 863 print len(diffs), "diffs to compute" 864 865 # Do the largest files first, to try and reduce the long-pole effect. 866 by_size = [(i.tf.size, i) for i in diffs] 867 by_size.sort(reverse=True) 868 by_size = [i[1] for i in by_size] 869 870 lock = threading.Lock() 871 diff_iter = iter(by_size) # accessed under lock 872 873 def worker(): 874 try: 875 lock.acquire() 876 for d in diff_iter: 877 lock.release() 878 start = time.time() 879 d.ComputePatch() 880 dur = time.time() - start 881 lock.acquire() 882 883 tf, sf, patch = d.GetPatch() 884 if sf.name == tf.name: 885 name = tf.name 886 else: 887 name = "%s (%s)" % (tf.name, sf.name) 888 if patch is None: 889 print "patching failed! %s" % (name,) 890 else: 891 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % ( 892 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name) 893 lock.release() 894 except Exception, e: 895 print e 896 raise 897 898 # start worker threads; wait for them all to finish. 899 threads = [threading.Thread(target=worker) 900 for i in range(OPTIONS.worker_threads)] 901 for th in threads: 902 th.start() 903 while threads: 904 threads.pop().join() 905 906 907# map recovery.fstab's fs_types to mount/format "partition types" 908PARTITION_TYPES = { "yaffs2": "MTD", "mtd": "MTD", 909 "ext4": "EMMC", "emmc": "EMMC" } 910 911def GetTypeAndDevice(mount_point, info): 912 fstab = info["fstab"] 913 if fstab: 914 return PARTITION_TYPES[fstab[mount_point].fs_type], fstab[mount_point].device 915 else: 916 return None 917