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