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