ota_from_target_files revision b4c7d32cbac0ef36062c4e2f348b47e027610eef
1#!/usr/bin/env python 2# 3# Copyright (C) 2008 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17""" 18Given a target-files zipfile, produces an OTA package that installs 19that build. An incremental OTA is produced if -i is given, otherwise 20a full OTA is produced. 21 22Usage: ota_from_target_files [flags] input_target_files output_ota_package 23 24 -b (--board_config) <file> 25 Deprecated. 26 27 -k (--package_key) <key> 28 Key to use to sign the package (default is 29 "build/target/product/security/testkey"). 30 31 -i (--incremental_from) <file> 32 Generate an incremental OTA using the given target-files zip as 33 the starting build. 34 35 -w (--wipe_user_data) 36 Generate an OTA package that will wipe the user data partition 37 when installed. 38 39 -n (--no_prereq) 40 Omit the timestamp prereq check normally included at the top of 41 the build scripts (used for developer OTA packages which 42 legitimately need to go back and forth). 43 44 -e (--extra_script) <file> 45 Insert the contents of file at the end of the update script. 46 47""" 48 49import sys 50 51if sys.hexversion < 0x02040000: 52 print >> sys.stderr, "Python 2.4 or newer is required." 53 sys.exit(1) 54 55import copy 56import errno 57import os 58import re 59import sha 60import subprocess 61import tempfile 62import threading 63import time 64import zipfile 65 66import common 67import edify_generator 68 69OPTIONS = common.OPTIONS 70OPTIONS.package_key = "build/target/product/security/testkey" 71OPTIONS.incremental_source = None 72OPTIONS.require_verbatim = set() 73OPTIONS.prohibit_verbatim = set(("system/build.prop",)) 74OPTIONS.patch_threshold = 0.95 75OPTIONS.wipe_user_data = False 76OPTIONS.omit_prereq = False 77OPTIONS.extra_script = None 78OPTIONS.worker_threads = 3 79 80def MostPopularKey(d, default): 81 """Given a dict, return the key corresponding to the largest 82 value. Returns 'default' if the dict is empty.""" 83 x = [(v, k) for (k, v) in d.iteritems()] 84 if not x: return default 85 x.sort() 86 return x[-1][1] 87 88 89def IsSymlink(info): 90 """Return true if the zipfile.ZipInfo object passed in represents a 91 symlink.""" 92 return (info.external_attr >> 16) == 0120777 93 94 95 96class Item: 97 """Items represent the metadata (user, group, mode) of files and 98 directories in the system image.""" 99 ITEMS = {} 100 def __init__(self, name, dir=False): 101 self.name = name 102 self.uid = None 103 self.gid = None 104 self.mode = None 105 self.dir = dir 106 107 if name: 108 self.parent = Item.Get(os.path.dirname(name), dir=True) 109 self.parent.children.append(self) 110 else: 111 self.parent = None 112 if dir: 113 self.children = [] 114 115 def Dump(self, indent=0): 116 if self.uid is not None: 117 print "%s%s %d %d %o" % (" "*indent, self.name, self.uid, self.gid, self.mode) 118 else: 119 print "%s%s %s %s %s" % (" "*indent, self.name, self.uid, self.gid, self.mode) 120 if self.dir: 121 print "%s%s" % (" "*indent, self.descendants) 122 print "%s%s" % (" "*indent, self.best_subtree) 123 for i in self.children: 124 i.Dump(indent=indent+1) 125 126 @classmethod 127 def Get(cls, name, dir=False): 128 if name not in cls.ITEMS: 129 cls.ITEMS[name] = Item(name, dir=dir) 130 return cls.ITEMS[name] 131 132 @classmethod 133 def GetMetadata(cls, input_zip): 134 135 try: 136 # See if the target_files contains a record of what the uid, 137 # gid, and mode is supposed to be. 138 output = input_zip.read("META/filesystem_config.txt") 139 except KeyError: 140 # Run the external 'fs_config' program to determine the desired 141 # uid, gid, and mode for every Item object. Note this uses the 142 # one in the client now, which might not be the same as the one 143 # used when this target_files was built. 144 p = common.Run(["fs_config"], stdin=subprocess.PIPE, 145 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 146 suffix = { False: "", True: "/" } 147 input = "".join(["%s%s\n" % (i.name, suffix[i.dir]) 148 for i in cls.ITEMS.itervalues() if i.name]) 149 output, error = p.communicate(input) 150 assert not error 151 152 for line in output.split("\n"): 153 if not line: continue 154 name, uid, gid, mode = line.split() 155 i = cls.ITEMS.get(name, None) 156 if i is not None: 157 i.uid = int(uid) 158 i.gid = int(gid) 159 i.mode = int(mode, 8) 160 if i.dir: 161 i.children.sort(key=lambda i: i.name) 162 163 # set metadata for the files generated by this script. 164 i = cls.ITEMS.get("system/recovery-from-boot.p", None) 165 if i: i.uid, i.gid, i.mode = 0, 0, 0644 166 i = cls.ITEMS.get("system/etc/install-recovery.sh", None) 167 if i: i.uid, i.gid, i.mode = 0, 0, 0544 168 169 def CountChildMetadata(self): 170 """Count up the (uid, gid, mode) tuples for all children and 171 determine the best strategy for using set_perm_recursive and 172 set_perm to correctly chown/chmod all the files to their desired 173 values. Recursively calls itself for all descendants. 174 175 Returns a dict of {(uid, gid, dmode, fmode): count} counting up 176 all descendants of this node. (dmode or fmode may be None.) Also 177 sets the best_subtree of each directory Item to the (uid, gid, 178 dmode, fmode) tuple that will match the most descendants of that 179 Item. 180 """ 181 182 assert self.dir 183 d = self.descendants = {(self.uid, self.gid, self.mode, None): 1} 184 for i in self.children: 185 if i.dir: 186 for k, v in i.CountChildMetadata().iteritems(): 187 d[k] = d.get(k, 0) + v 188 else: 189 k = (i.uid, i.gid, None, i.mode) 190 d[k] = d.get(k, 0) + 1 191 192 # Find the (uid, gid, dmode, fmode) tuple that matches the most 193 # descendants. 194 195 # First, find the (uid, gid) pair that matches the most 196 # descendants. 197 ug = {} 198 for (uid, gid, _, _), count in d.iteritems(): 199 ug[(uid, gid)] = ug.get((uid, gid), 0) + count 200 ug = MostPopularKey(ug, (0, 0)) 201 202 # Now find the dmode and fmode that match the most descendants 203 # with that (uid, gid), and choose those. 204 best_dmode = (0, 0755) 205 best_fmode = (0, 0644) 206 for k, count in d.iteritems(): 207 if k[:2] != ug: continue 208 if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2]) 209 if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3]) 210 self.best_subtree = ug + (best_dmode[1], best_fmode[1]) 211 212 return d 213 214 def SetPermissions(self, script): 215 """Append set_perm/set_perm_recursive commands to 'script' to 216 set all permissions, users, and groups for the tree of files 217 rooted at 'self'.""" 218 219 self.CountChildMetadata() 220 221 def recurse(item, current): 222 # current is the (uid, gid, dmode, fmode) tuple that the current 223 # item (and all its children) have already been set to. We only 224 # need to issue set_perm/set_perm_recursive commands if we're 225 # supposed to be something different. 226 if item.dir: 227 if current != item.best_subtree: 228 script.SetPermissionsRecursive("/"+item.name, *item.best_subtree) 229 current = item.best_subtree 230 231 if item.uid != current[0] or item.gid != current[1] or \ 232 item.mode != current[2]: 233 script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode) 234 235 for i in item.children: 236 recurse(i, current) 237 else: 238 if item.uid != current[0] or item.gid != current[1] or \ 239 item.mode != current[3]: 240 script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode) 241 242 recurse(self, (-1, -1, -1, -1)) 243 244 245def CopySystemFiles(input_zip, output_zip=None, 246 substitute=None): 247 """Copies files underneath system/ in the input zip to the output 248 zip. Populates the Item class with their metadata, and returns a 249 list of symlinks. output_zip may be None, in which case the copy is 250 skipped (but the other side effects still happen). substitute is an 251 optional dict of {output filename: contents} to be output instead of 252 certain input files. 253 """ 254 255 symlinks = [] 256 257 for info in input_zip.infolist(): 258 if info.filename.startswith("SYSTEM/"): 259 basefilename = info.filename[7:] 260 if IsSymlink(info): 261 symlinks.append((input_zip.read(info.filename), 262 "/system/" + basefilename)) 263 else: 264 info2 = copy.copy(info) 265 fn = info2.filename = "system/" + basefilename 266 if substitute and fn in substitute and substitute[fn] is None: 267 continue 268 if output_zip is not None: 269 if substitute and fn in substitute: 270 data = substitute[fn] 271 else: 272 data = input_zip.read(info.filename) 273 output_zip.writestr(info2, data) 274 if fn.endswith("/"): 275 Item.Get(fn[:-1], dir=True) 276 else: 277 Item.Get(fn, dir=False) 278 279 symlinks.sort() 280 return symlinks 281 282 283def SignOutput(temp_zip_name, output_zip_name): 284 key_passwords = common.GetKeyPasswords([OPTIONS.package_key]) 285 pw = key_passwords[OPTIONS.package_key] 286 287 common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw, 288 whole_file=True) 289 290 291def AppendAssertions(script, input_zip): 292 device = GetBuildProp("ro.product.device", input_zip) 293 script.AssertDevice(device) 294 295 296def MakeRecoveryPatch(output_zip, recovery_img, boot_img): 297 """Generate a binary patch that creates the recovery image starting 298 with the boot image. (Most of the space in these images is just the 299 kernel, which is identical for the two, so the resulting patch 300 should be efficient.) Add it to the output zip, along with a shell 301 script that is run from init.rc on first boot to actually do the 302 patching and install the new recovery image. 303 304 recovery_img and boot_img should be File objects for the 305 corresponding images. 306 307 Returns an Item for the shell script, which must be made 308 executable. 309 """ 310 311 d = Difference(recovery_img, boot_img) 312 _, _, patch = d.ComputePatch() 313 common.ZipWriteStr(output_zip, "recovery/recovery-from-boot.p", patch) 314 Item.Get("system/recovery-from-boot.p", dir=False) 315 316 # Images with different content will have a different first page, so 317 # we check to see if this recovery has already been installed by 318 # testing just the first 2k. 319 HEADER_SIZE = 2048 320 header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest() 321 sh = """#!/system/bin/sh 322if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then 323 log -t recovery "Installing new recovery image" 324 applypatch MTD:boot:%(boot_size)d:%(boot_sha1)s MTD:recovery %(recovery_sha1)s %(recovery_size)d %(boot_sha1)s:/system/recovery-from-boot.p 325else 326 log -t recovery "Recovery image already installed" 327fi 328""" % { 'boot_size': boot_img.size, 329 'boot_sha1': boot_img.sha1, 330 'header_size': HEADER_SIZE, 331 'header_sha1': header_sha1, 332 'recovery_size': recovery_img.size, 333 'recovery_sha1': recovery_img.sha1 } 334 common.ZipWriteStr(output_zip, "recovery/etc/install-recovery.sh", sh) 335 return Item.Get("system/etc/install-recovery.sh", dir=False) 336 337 338def WriteFullOTAPackage(input_zip, output_zip, info): 339 # TODO: how to determine this? We don't know what version it will 340 # be installed on top of. For now, we expect the API just won't 341 # change very often. 342 script = edify_generator.EdifyGenerator(3, info) 343 344 metadata = {"post-build": GetBuildProp("ro.build.fingerprint", input_zip), 345 "pre-device": GetBuildProp("ro.product.device", input_zip), 346 "post-timestamp": GetBuildProp("ro.build.date.utc", input_zip), 347 } 348 349 device_specific = common.DeviceSpecificParams( 350 input_zip=input_zip, 351 input_version=GetRecoveryAPIVersion(input_zip), 352 output_zip=output_zip, 353 script=script, 354 input_tmp=OPTIONS.input_tmp, 355 metadata=metadata) 356 357 if not OPTIONS.omit_prereq: 358 ts = GetBuildProp("ro.build.date.utc", input_zip) 359 script.AssertOlderBuild(ts) 360 361 AppendAssertions(script, input_zip) 362 device_specific.FullOTA_Assertions() 363 364 script.ShowProgress(0.5, 0) 365 366 if OPTIONS.wipe_user_data: 367 script.FormatPartition("userdata") 368 369 script.FormatPartition("system") 370 script.Mount("system", "/system") 371 script.UnpackPackageDir("recovery", "/system") 372 script.UnpackPackageDir("system", "/system") 373 374 symlinks = CopySystemFiles(input_zip, output_zip) 375 script.MakeSymlinks(symlinks) 376 377 boot_img = File("boot.img", common.BuildBootableImage( 378 os.path.join(OPTIONS.input_tmp, "BOOT"))) 379 recovery_img = File("recovery.img", common.BuildBootableImage( 380 os.path.join(OPTIONS.input_tmp, "RECOVERY"))) 381 MakeRecoveryPatch(output_zip, recovery_img, boot_img) 382 383 Item.GetMetadata(input_zip) 384 Item.Get("system").SetPermissions(script) 385 386 common.CheckSize(boot_img.data, "boot.img") 387 common.ZipWriteStr(output_zip, "boot.img", boot_img.data) 388 script.ShowProgress(0.2, 0) 389 390 script.ShowProgress(0.2, 10) 391 script.WriteRawImage("boot", "boot.img") 392 393 script.ShowProgress(0.1, 0) 394 device_specific.FullOTA_InstallEnd() 395 396 if OPTIONS.extra_script is not None: 397 script.AppendExtra(OPTIONS.extra_script) 398 399 script.UnmountAll() 400 script.AddToZip(input_zip, output_zip) 401 WriteMetadata(metadata, output_zip) 402 403 404def WriteMetadata(metadata, output_zip): 405 common.ZipWriteStr(output_zip, "META-INF/com/android/metadata", 406 "".join(["%s=%s\n" % kv 407 for kv in sorted(metadata.iteritems())])) 408 409 410class File(object): 411 def __init__(self, name, data): 412 self.name = name 413 self.data = data 414 self.size = len(data) 415 self.sha1 = sha.sha(data).hexdigest() 416 417 def WriteToTemp(self): 418 t = tempfile.NamedTemporaryFile() 419 t.write(self.data) 420 t.flush() 421 return t 422 423 def AddToZip(self, z): 424 common.ZipWriteStr(z, self.name, self.data) 425 426 427def LoadSystemFiles(z): 428 """Load all the files from SYSTEM/... in a given target-files 429 ZipFile, and return a dict of {filename: File object}.""" 430 out = {} 431 for info in z.infolist(): 432 if info.filename.startswith("SYSTEM/") and not IsSymlink(info): 433 fn = "system/" + info.filename[7:] 434 data = z.read(info.filename) 435 out[fn] = File(fn, data) 436 return out 437 438 439DIFF_PROGRAM_BY_EXT = { 440 ".gz" : "imgdiff", 441 ".zip" : ["imgdiff", "-z"], 442 ".jar" : ["imgdiff", "-z"], 443 ".apk" : ["imgdiff", "-z"], 444 ".img" : "imgdiff", 445 } 446 447 448class Difference(object): 449 def __init__(self, tf, sf): 450 self.tf = tf 451 self.sf = sf 452 self.patch = None 453 454 def ComputePatch(self): 455 """Compute the patch (as a string of data) needed to turn sf into 456 tf. Returns the same tuple as GetPatch().""" 457 458 tf = self.tf 459 sf = self.sf 460 461 ext = os.path.splitext(tf.name)[1] 462 diff_program = DIFF_PROGRAM_BY_EXT.get(ext, "bsdiff") 463 464 ttemp = tf.WriteToTemp() 465 stemp = sf.WriteToTemp() 466 467 ext = os.path.splitext(tf.name)[1] 468 469 try: 470 ptemp = tempfile.NamedTemporaryFile() 471 if isinstance(diff_program, list): 472 cmd = copy.copy(diff_program) 473 else: 474 cmd = [diff_program] 475 cmd.append(stemp.name) 476 cmd.append(ttemp.name) 477 cmd.append(ptemp.name) 478 p = common.Run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 479 _, err = p.communicate() 480 if err or p.returncode != 0: 481 print "WARNING: failure running %s:\n%s\n" % (diff_program, err) 482 return None 483 diff = ptemp.read() 484 finally: 485 ptemp.close() 486 stemp.close() 487 ttemp.close() 488 489 self.patch = diff 490 return self.tf, self.sf, self.patch 491 492 493 def GetPatch(self): 494 """Return a tuple (target_file, source_file, patch_data). 495 patch_data may be None if ComputePatch hasn't been called, or if 496 computing the patch failed.""" 497 return self.tf, self.sf, self.patch 498 499 500def ComputeDifferences(diffs): 501 """Call ComputePatch on all the Difference objects in 'diffs'.""" 502 print len(diffs), "diffs to compute" 503 504 # Do the largest files first, to try and reduce the long-pole effect. 505 by_size = [(i.tf.size, i) for i in diffs] 506 by_size.sort(reverse=True) 507 by_size = [i[1] for i in by_size] 508 509 lock = threading.Lock() 510 diff_iter = iter(by_size) # accessed under lock 511 512 def worker(): 513 try: 514 lock.acquire() 515 for d in diff_iter: 516 lock.release() 517 start = time.time() 518 d.ComputePatch() 519 dur = time.time() - start 520 lock.acquire() 521 522 tf, sf, patch = d.GetPatch() 523 if sf.name == tf.name: 524 name = tf.name 525 else: 526 name = "%s (%s)" % (tf.name, sf.name) 527 if patch is None: 528 print "patching failed! %s" % (name,) 529 else: 530 print "%8.2f sec %8d / %8d bytes (%6.2f%%) %s" % ( 531 dur, len(patch), tf.size, 100.0 * len(patch) / tf.size, name) 532 lock.release() 533 except Exception, e: 534 print e 535 raise 536 537 # start worker threads; wait for them all to finish. 538 threads = [threading.Thread(target=worker) 539 for i in range(OPTIONS.worker_threads)] 540 for th in threads: 541 th.start() 542 while threads: 543 threads.pop().join() 544 545 546def GetBuildProp(property, z): 547 """Return the fingerprint of the build of a given target-files 548 ZipFile object.""" 549 bp = z.read("SYSTEM/build.prop") 550 if not property: 551 return bp 552 m = re.search(re.escape(property) + r"=(.*)\n", bp) 553 if not m: 554 raise common.ExternalError("couldn't find %s in build.prop" % (property,)) 555 return m.group(1).strip() 556 557 558def GetRecoveryAPIVersion(zip): 559 """Returns the version of the recovery API. Version 0 is the older 560 amend code (no separate binary).""" 561 try: 562 version = zip.read("META/recovery-api-version.txt") 563 return int(version) 564 except KeyError: 565 try: 566 # version one didn't have the recovery-api-version.txt file, but 567 # it did include an updater binary. 568 zip.getinfo("OTA/bin/updater") 569 return 1 570 except KeyError: 571 return 0 572 573 574def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip, info): 575 source_version = GetRecoveryAPIVersion(source_zip) 576 target_version = GetRecoveryAPIVersion(target_zip) 577 578 if source_version == 0: 579 print ("WARNING: generating edify script for a source that " 580 "can't install it.") 581 script = edify_generator.EdifyGenerator(source_version, info) 582 583 metadata = {"pre-device": GetBuildProp("ro.product.device", source_zip), 584 "post-timestamp": GetBuildProp("ro.build.date.utc", target_zip), 585 } 586 587 device_specific = common.DeviceSpecificParams( 588 source_zip=source_zip, 589 source_version=source_version, 590 target_zip=target_zip, 591 target_version=target_version, 592 output_zip=output_zip, 593 script=script, 594 metadata=metadata) 595 596 print "Loading target..." 597 target_data = LoadSystemFiles(target_zip) 598 print "Loading source..." 599 source_data = LoadSystemFiles(source_zip) 600 601 verbatim_targets = [] 602 patch_list = [] 603 diffs = [] 604 largest_source_size = 0 605 for fn in sorted(target_data.keys()): 606 tf = target_data[fn] 607 assert fn == tf.name 608 sf = source_data.get(fn, None) 609 610 if sf is None or fn in OPTIONS.require_verbatim: 611 # This file should be included verbatim 612 if fn in OPTIONS.prohibit_verbatim: 613 raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,)) 614 print "send", fn, "verbatim" 615 tf.AddToZip(output_zip) 616 verbatim_targets.append((fn, tf.size)) 617 elif tf.sha1 != sf.sha1: 618 # File is different; consider sending as a patch 619 diffs.append(Difference(tf, sf)) 620 else: 621 # Target file identical to source. 622 pass 623 624 ComputeDifferences(diffs) 625 626 for diff in diffs: 627 tf, sf, d = diff.GetPatch() 628 if d is None or len(d) > tf.size * OPTIONS.patch_threshold: 629 # patch is almost as big as the file; don't bother patching 630 tf.AddToZip(output_zip) 631 verbatim_targets.append((tf.name, tf.size)) 632 else: 633 common.ZipWriteStr(output_zip, "patch/" + tf.name + ".p", d) 634 patch_list.append((tf.name, tf, sf, tf.size, sha.sha(d).hexdigest())) 635 largest_source_size = max(largest_source_size, sf.size) 636 637 source_fp = GetBuildProp("ro.build.fingerprint", source_zip) 638 target_fp = GetBuildProp("ro.build.fingerprint", target_zip) 639 metadata["pre-build"] = source_fp 640 metadata["post-build"] = target_fp 641 642 script.Mount("system", "/system") 643 script.AssertSomeFingerprint(source_fp, target_fp) 644 645 source_boot = File("/tmp/boot.img", 646 common.BuildBootableImage( 647 os.path.join(OPTIONS.source_tmp, "BOOT"))) 648 target_boot = File("/tmp/boot.img", 649 common.BuildBootableImage( 650 os.path.join(OPTIONS.target_tmp, "BOOT"))) 651 updating_boot = (source_boot.data != target_boot.data) 652 653 source_recovery = File("system/recovery.img", 654 common.BuildBootableImage( 655 os.path.join(OPTIONS.source_tmp, "RECOVERY"))) 656 target_recovery = File("system/recovery.img", 657 common.BuildBootableImage( 658 os.path.join(OPTIONS.target_tmp, "RECOVERY"))) 659 updating_recovery = (source_recovery.data != target_recovery.data) 660 661 # Here's how we divide up the progress bar: 662 # 0.1 for verifying the start state (PatchCheck calls) 663 # 0.8 for applying patches (ApplyPatch calls) 664 # 0.1 for unpacking verbatim files, symlinking, and doing the 665 # device-specific commands. 666 667 AppendAssertions(script, target_zip) 668 device_specific.IncrementalOTA_Assertions() 669 670 script.Print("Verifying current system...") 671 672 script.ShowProgress(0.1, 0) 673 total_verify_size = float(sum([i[2].size for i in patch_list]) + 1) 674 if updating_boot: 675 total_verify_size += source_boot.size 676 so_far = 0 677 678 for fn, tf, sf, size, patch_sha in patch_list: 679 script.PatchCheck("/"+fn, tf.sha1, sf.sha1) 680 so_far += sf.size 681 script.SetProgress(so_far / total_verify_size) 682 683 if updating_boot: 684 d = Difference(target_boot, source_boot) 685 _, _, d = d.ComputePatch() 686 print "boot target: %d source: %d diff: %d" % ( 687 target_boot.size, source_boot.size, len(d)) 688 689 common.ZipWriteStr(output_zip, "patch/boot.img.p", d) 690 691 script.PatchCheck("MTD:boot:%d:%s:%d:%s" % 692 (source_boot.size, source_boot.sha1, 693 target_boot.size, target_boot.sha1)) 694 so_far += source_boot.size 695 script.SetProgress(so_far / total_verify_size) 696 697 if patch_list or updating_recovery or updating_boot: 698 script.CacheFreeSpaceCheck(largest_source_size) 699 700 device_specific.IncrementalOTA_VerifyEnd() 701 702 script.Comment("---- start making changes here ----") 703 704 if OPTIONS.wipe_user_data: 705 script.Print("Erasing user data...") 706 script.FormatPartition("userdata") 707 708 script.Print("Removing unneeded files...") 709 script.DeleteFiles(["/"+i[0] for i in verbatim_targets] + 710 ["/"+i for i in sorted(source_data) 711 if i not in target_data] + 712 ["/system/recovery.img"]) 713 714 script.ShowProgress(0.8, 0) 715 total_patch_size = float(sum([i[1].size for i in patch_list]) + 1) 716 if updating_boot: 717 total_patch_size += target_boot.size 718 so_far = 0 719 720 script.Print("Patching system files...") 721 for fn, tf, sf, size, _ in patch_list: 722 script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1, sf.sha1, "patch/"+fn+".p") 723 so_far += tf.size 724 script.SetProgress(so_far / total_patch_size) 725 726 if updating_boot: 727 # Produce the boot image by applying a patch to the current 728 # contents of the boot partition, and write it back to the 729 # partition. 730 script.Print("Patching boot image...") 731 script.ApplyPatch("MTD:boot:%d:%s:%d:%s" 732 % (source_boot.size, source_boot.sha1, 733 target_boot.size, target_boot.sha1), 734 "-", 735 target_boot.size, target_boot.sha1, 736 source_boot.sha1, "patch/boot.img.p") 737 so_far += target_boot.size 738 script.SetProgress(so_far / total_patch_size) 739 print "boot image changed; including." 740 else: 741 print "boot image unchanged; skipping." 742 743 if updating_recovery: 744 # Is it better to generate recovery as a patch from the current 745 # boot image, or from the previous recovery image? For large 746 # updates with significant kernel changes, probably the former. 747 # For small updates where the kernel hasn't changed, almost 748 # certainly the latter. We pick the first option. Future 749 # complicated schemes may let us effectively use both. 750 # 751 # A wacky possibility: as long as there is room in the boot 752 # partition, include the binaries and image files from recovery in 753 # the boot image (though not in the ramdisk) so they can be used 754 # as fodder for constructing the recovery image. 755 MakeRecoveryPatch(output_zip, target_recovery, target_boot) 756 script.DeleteFiles(["/system/recovery-from-boot.p", 757 "/system/etc/install-recovery.sh"]) 758 print "recovery image changed; including as patch from boot." 759 else: 760 print "recovery image unchanged; skipping." 761 762 script.ShowProgress(0.1, 10) 763 764 target_symlinks = CopySystemFiles(target_zip, None) 765 766 target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks]) 767 temp_script = script.MakeTemporary() 768 Item.GetMetadata(target_zip) 769 Item.Get("system").SetPermissions(temp_script) 770 771 # Note that this call will mess up the tree of Items, so make sure 772 # we're done with it. 773 source_symlinks = CopySystemFiles(source_zip, None) 774 source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks]) 775 776 # Delete all the symlinks in source that aren't in target. This 777 # needs to happen before verbatim files are unpacked, in case a 778 # symlink in the source is replaced by a real file in the target. 779 to_delete = [] 780 for dest, link in source_symlinks: 781 if link not in target_symlinks_d: 782 to_delete.append(link) 783 script.DeleteFiles(to_delete) 784 785 if verbatim_targets: 786 script.Print("Unpacking new files...") 787 script.UnpackPackageDir("system", "/system") 788 789 if updating_recovery: 790 script.Print("Unpacking new recovery...") 791 script.UnpackPackageDir("recovery", "/system") 792 793 script.Print("Symlinks and permissions...") 794 795 # Create all the symlinks that don't already exist, or point to 796 # somewhere different than what we want. Delete each symlink before 797 # creating it, since the 'symlink' command won't overwrite. 798 to_create = [] 799 for dest, link in target_symlinks: 800 if link in source_symlinks_d: 801 if dest != source_symlinks_d[link]: 802 to_create.append((dest, link)) 803 else: 804 to_create.append((dest, link)) 805 script.DeleteFiles([i[1] for i in to_create]) 806 script.MakeSymlinks(to_create) 807 808 # Now that the symlinks are created, we can set all the 809 # permissions. 810 script.AppendScript(temp_script) 811 812 # Do device-specific installation (eg, write radio image). 813 device_specific.IncrementalOTA_InstallEnd() 814 815 if OPTIONS.extra_script is not None: 816 scirpt.AppendExtra(OPTIONS.extra_script) 817 818 script.AddToZip(target_zip, output_zip) 819 WriteMetadata(metadata, output_zip) 820 821 822def main(argv): 823 824 def option_handler(o, a): 825 if o in ("-b", "--board_config"): 826 pass # deprecated 827 elif o in ("-k", "--package_key"): 828 OPTIONS.package_key = a 829 elif o in ("-i", "--incremental_from"): 830 OPTIONS.incremental_source = a 831 elif o in ("-w", "--wipe_user_data"): 832 OPTIONS.wipe_user_data = True 833 elif o in ("-n", "--no_prereq"): 834 OPTIONS.omit_prereq = True 835 elif o in ("-e", "--extra_script"): 836 OPTIONS.extra_script = a 837 elif o in ("--worker_threads"): 838 OPTIONS.worker_threads = int(a) 839 else: 840 return False 841 return True 842 843 args = common.ParseOptions(argv, __doc__, 844 extra_opts="b:k:i:d:wne:", 845 extra_long_opts=["board_config=", 846 "package_key=", 847 "incremental_from=", 848 "wipe_user_data", 849 "no_prereq", 850 "extra_script=", 851 "worker_threads="], 852 extra_option_handler=option_handler) 853 854 if len(args) != 2: 855 common.Usage(__doc__) 856 sys.exit(1) 857 858 if OPTIONS.extra_script is not None: 859 OPTIONS.extra_script = open(OPTIONS.extra_script).read() 860 861 print "unzipping target target-files..." 862 OPTIONS.input_tmp = common.UnzipTemp(args[0]) 863 864 if OPTIONS.device_specific is None: 865 # look for the device-specific tools extension location in the input 866 try: 867 f = open(os.path.join(OPTIONS.input_tmp, "META", "tool-extensions.txt")) 868 ds = f.read().strip() 869 f.close() 870 if ds: 871 ds = os.path.normpath(ds) 872 print "using device-specific extensions in", ds 873 OPTIONS.device_specific = ds 874 except IOError, e: 875 if e.errno == errno.ENOENT: 876 # nothing specified in the file 877 pass 878 else: 879 raise 880 881 info = common.LoadInfoDict() 882 common.LoadMaxSizes(info) 883 if not OPTIONS.max_image_size: 884 print 885 print " WARNING: Failed to load max image sizes; will not enforce" 886 print " image size limits." 887 print 888 889 OPTIONS.target_tmp = OPTIONS.input_tmp 890 input_zip = zipfile.ZipFile(args[0], "r") 891 if OPTIONS.package_key: 892 temp_zip_file = tempfile.NamedTemporaryFile() 893 output_zip = zipfile.ZipFile(temp_zip_file, "w", 894 compression=zipfile.ZIP_DEFLATED) 895 else: 896 output_zip = zipfile.ZipFile(args[1], "w", 897 compression=zipfile.ZIP_DEFLATED) 898 899 if OPTIONS.incremental_source is None: 900 WriteFullOTAPackage(input_zip, output_zip, info) 901 else: 902 print "unzipping source target-files..." 903 OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source) 904 source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r") 905 WriteIncrementalOTAPackage(input_zip, source_zip, output_zip, info) 906 907 output_zip.close() 908 if OPTIONS.package_key: 909 SignOutput(temp_zip_file.name, args[1]) 910 temp_zip_file.close() 911 912 common.Cleanup() 913 914 print "done." 915 916 917if __name__ == '__main__': 918 try: 919 main(sys.argv[1:]) 920 except common.ExternalError, e: 921 print 922 print " ERROR: %s" % (e,) 923 print 924 sys.exit(1) 925