ota_from_target_files.py revision eef3944eb3673329b5e89cf188ac592805a0b08d
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 Specifies a BoardConfig.mk file containing image max sizes 26 against which the generated image files are checked. 27 28 -k (--package_key) <key> 29 Key to use to sign the package (default is 30 "build/target/product/security/testkey"). 31 32 -i (--incremental_from) <file> 33 Generate an incremental OTA using the given target-files zip as 34 the starting build. 35 36""" 37 38import sys 39 40if sys.hexversion < 0x02040000: 41 print >> sys.stderr, "Python 2.4 or newer is required." 42 sys.exit(1) 43 44import copy 45import os 46import re 47import sha 48import subprocess 49import tempfile 50import time 51import zipfile 52 53import common 54 55OPTIONS = common.OPTIONS 56OPTIONS.package_key = "build/target/product/security/testkey" 57OPTIONS.incremental_source = None 58OPTIONS.require_verbatim = set() 59OPTIONS.prohibit_verbatim = set(("system/build.prop",)) 60OPTIONS.patch_threshold = 0.95 61 62def MostPopularKey(d, default): 63 """Given a dict, return the key corresponding to the largest 64 value. Returns 'default' if the dict is empty.""" 65 x = [(v, k) for (k, v) in d.iteritems()] 66 if not x: return default 67 x.sort() 68 return x[-1][1] 69 70 71def IsSymlink(info): 72 """Return true if the zipfile.ZipInfo object passed in represents a 73 symlink.""" 74 return (info.external_attr >> 16) == 0120777 75 76 77 78class Item: 79 """Items represent the metadata (user, group, mode) of files and 80 directories in the system image.""" 81 ITEMS = {} 82 def __init__(self, name, dir=False): 83 self.name = name 84 self.uid = None 85 self.gid = None 86 self.mode = None 87 self.dir = dir 88 89 if name: 90 self.parent = Item.Get(os.path.dirname(name), dir=True) 91 self.parent.children.append(self) 92 else: 93 self.parent = None 94 if dir: 95 self.children = [] 96 97 def Dump(self, indent=0): 98 if self.uid is not None: 99 print "%s%s %d %d %o" % (" "*indent, self.name, self.uid, self.gid, self.mode) 100 else: 101 print "%s%s %s %s %s" % (" "*indent, self.name, self.uid, self.gid, self.mode) 102 if self.dir: 103 print "%s%s" % (" "*indent, self.descendants) 104 print "%s%s" % (" "*indent, self.best_subtree) 105 for i in self.children: 106 i.Dump(indent=indent+1) 107 108 @classmethod 109 def Get(cls, name, dir=False): 110 if name not in cls.ITEMS: 111 cls.ITEMS[name] = Item(name, dir=dir) 112 return cls.ITEMS[name] 113 114 @classmethod 115 def GetMetadata(cls): 116 """Run the external 'fs_config' program to determine the desired 117 uid, gid, and mode for every Item object.""" 118 p = common.Run(["fs_config"], stdin=subprocess.PIPE, 119 stdout=subprocess.PIPE, stderr=subprocess.PIPE) 120 suffix = { False: "", True: "/" } 121 input = "".join(["%s%s\n" % (i.name, suffix[i.dir]) 122 for i in cls.ITEMS.itervalues() if i.name]) 123 output, error = p.communicate(input) 124 assert not error 125 126 for line in output.split("\n"): 127 if not line: continue 128 name, uid, gid, mode = line.split() 129 i = cls.ITEMS[name] 130 i.uid = int(uid) 131 i.gid = int(gid) 132 i.mode = int(mode, 8) 133 if i.dir: 134 i.children.sort(key=lambda i: i.name) 135 136 def CountChildMetadata(self): 137 """Count up the (uid, gid, mode) tuples for all children and 138 determine the best strategy for using set_perm_recursive and 139 set_perm to correctly chown/chmod all the files to their desired 140 values. Recursively calls itself for all descendants. 141 142 Returns a dict of {(uid, gid, dmode, fmode): count} counting up 143 all descendants of this node. (dmode or fmode may be None.) Also 144 sets the best_subtree of each directory Item to the (uid, gid, 145 dmode, fmode) tuple that will match the most descendants of that 146 Item. 147 """ 148 149 assert self.dir 150 d = self.descendants = {(self.uid, self.gid, self.mode, None): 1} 151 for i in self.children: 152 if i.dir: 153 for k, v in i.CountChildMetadata().iteritems(): 154 d[k] = d.get(k, 0) + v 155 else: 156 k = (i.uid, i.gid, None, i.mode) 157 d[k] = d.get(k, 0) + 1 158 159 # Find the (uid, gid, dmode, fmode) tuple that matches the most 160 # descendants. 161 162 # First, find the (uid, gid) pair that matches the most 163 # descendants. 164 ug = {} 165 for (uid, gid, _, _), count in d.iteritems(): 166 ug[(uid, gid)] = ug.get((uid, gid), 0) + count 167 ug = MostPopularKey(ug, (0, 0)) 168 169 # Now find the dmode and fmode that match the most descendants 170 # with that (uid, gid), and choose those. 171 best_dmode = (0, 0755) 172 best_fmode = (0, 0644) 173 for k, count in d.iteritems(): 174 if k[:2] != ug: continue 175 if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2]) 176 if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3]) 177 self.best_subtree = ug + (best_dmode[1], best_fmode[1]) 178 179 return d 180 181 def SetPermissions(self, script, renamer=lambda x: x): 182 """Append set_perm/set_perm_recursive commands to 'script' to 183 set all permissions, users, and groups for the tree of files 184 rooted at 'self'. 'renamer' turns the filenames stored in the 185 tree of Items into the strings used in the script.""" 186 187 self.CountChildMetadata() 188 189 def recurse(item, current): 190 # current is the (uid, gid, dmode, fmode) tuple that the current 191 # item (and all its children) have already been set to. We only 192 # need to issue set_perm/set_perm_recursive commands if we're 193 # supposed to be something different. 194 if item.dir: 195 if current != item.best_subtree: 196 script.append("set_perm_recursive %d %d 0%o 0%o %s" % 197 (item.best_subtree + (renamer(item.name),))) 198 current = item.best_subtree 199 200 if item.uid != current[0] or item.gid != current[1] or \ 201 item.mode != current[2]: 202 script.append("set_perm %d %d 0%o %s" % 203 (item.uid, item.gid, item.mode, renamer(item.name))) 204 205 for i in item.children: 206 recurse(i, current) 207 else: 208 if item.uid != current[0] or item.gid != current[1] or \ 209 item.mode != current[3]: 210 script.append("set_perm %d %d 0%o %s" % 211 (item.uid, item.gid, item.mode, renamer(item.name))) 212 213 recurse(self, (-1, -1, -1, -1)) 214 215 216def CopySystemFiles(input_zip, output_zip=None, 217 substitute=None): 218 """Copies files underneath system/ in the input zip to the output 219 zip. Populates the Item class with their metadata, and returns a 220 list of symlinks. output_zip may be None, in which case the copy is 221 skipped (but the other side effects still happen). substitute is an 222 optional dict of {output filename: contents} to be output instead of 223 certain input files. 224 """ 225 226 symlinks = [] 227 228 for info in input_zip.infolist(): 229 if info.filename.startswith("SYSTEM/"): 230 basefilename = info.filename[7:] 231 if IsSymlink(info): 232 symlinks.append((input_zip.read(info.filename), 233 "SYSTEM:" + basefilename)) 234 else: 235 info2 = copy.copy(info) 236 fn = info2.filename = "system/" + basefilename 237 if substitute and fn in substitute and substitute[fn] is None: 238 continue 239 if output_zip is not None: 240 if substitute and fn in substitute: 241 data = substitute[fn] 242 else: 243 data = input_zip.read(info.filename) 244 output_zip.writestr(info2, data) 245 if fn.endswith("/"): 246 Item.Get(fn[:-1], dir=True) 247 else: 248 Item.Get(fn, dir=False) 249 250 symlinks.sort() 251 return symlinks 252 253 254def AddScript(script, output_zip): 255 now = time.localtime() 256 i = zipfile.ZipInfo("META-INF/com/google/android/update-script", 257 (now.tm_year, now.tm_mon, now.tm_mday, 258 now.tm_hour, now.tm_min, now.tm_sec)) 259 output_zip.writestr(i, "\n".join(script) + "\n") 260 261 262def SignOutput(temp_zip_name, output_zip_name): 263 key_passwords = common.GetKeyPasswords([OPTIONS.package_key]) 264 pw = key_passwords[OPTIONS.package_key] 265 266 common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw) 267 268 269def SubstituteRoot(s): 270 if s == "system": return "SYSTEM:" 271 assert s.startswith("system/") 272 return "SYSTEM:" + s[7:] 273 274def FixPermissions(script): 275 Item.GetMetadata() 276 root = Item.Get("system") 277 root.SetPermissions(script, renamer=SubstituteRoot) 278 279def DeleteFiles(script, to_delete): 280 line = [] 281 t = 0 282 for i in to_delete: 283 line.append(i) 284 t += len(i) + 1 285 if t > 80: 286 script.append("delete " + " ".join(line)) 287 line = [] 288 t = 0 289 if line: 290 script.append("delete " + " ".join(line)) 291 292def AppendAssertions(script, input_zip): 293 script.append('assert compatible_with("0.2") == "true"') 294 295 device = GetBuildProp("ro.product.device", input_zip) 296 script.append('assert getprop("ro.product.device") == "%s" || ' 297 'getprop("ro.build.product") == "%s"' % (device, device)) 298 299 info = input_zip.read("OTA/android-info.txt") 300 m = re.search(r"require\s+version-bootloader\s*=\s*(\S+)", info) 301 if not m: 302 raise ExternalError("failed to find required bootloaders in " 303 "android-info.txt") 304 bootloaders = m.group(1).split("|") 305 script.append("assert " + 306 " || ".join(['getprop("ro.bootloader") == "%s"' % (b,) 307 for b in bootloaders])) 308 309 310def IncludeBinary(name, input_zip, output_zip): 311 try: 312 data = input_zip.read(os.path.join("OTA/bin", name)) 313 output_zip.writestr(name, data) 314 except IOError: 315 raise ExternalError('unable to include device binary "%s"' % (name,)) 316 317 318def WriteFullOTAPackage(input_zip, output_zip): 319 script = [] 320 321 ts = GetBuildProp("ro.build.date.utc", input_zip) 322 script.append("run_program PACKAGE:check_prereq %s" % (ts,)) 323 IncludeBinary("check_prereq", input_zip, output_zip) 324 325 AppendAssertions(script, input_zip) 326 327 script.append("format BOOT:") 328 script.append("show_progress 0.1 0") 329 330 output_zip.writestr("radio.img", input_zip.read("RADIO/image")) 331 script.append("write_radio_image PACKAGE:radio.img") 332 script.append("show_progress 0.5 0") 333 334 script.append("format SYSTEM:") 335 script.append("copy_dir PACKAGE:system SYSTEM:") 336 337 symlinks = CopySystemFiles(input_zip, output_zip) 338 script.extend(["symlink %s %s" % s for s in symlinks]) 339 340 common.BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"), 341 "system/recovery.img", output_zip) 342 Item.Get("system/recovery.img", dir=False) 343 344 FixPermissions(script) 345 346 common.AddBoot(output_zip) 347 script.append("show_progress 0.2 0") 348 script.append("write_raw_image PACKAGE:boot.img BOOT:") 349 script.append("show_progress 0.2 10") 350 351 AddScript(script, output_zip) 352 353 354class File(object): 355 def __init__(self, name, data): 356 self.name = name 357 self.data = data 358 self.size = len(data) 359 self.sha1 = sha.sha(data).hexdigest() 360 361 def WriteToTemp(self): 362 t = tempfile.NamedTemporaryFile() 363 t.write(self.data) 364 t.flush() 365 return t 366 367 def AddToZip(self, z): 368 z.writestr(self.name, self.data) 369 370 371def LoadSystemFiles(z): 372 """Load all the files from SYSTEM/... in a given target-files 373 ZipFile, and return a dict of {filename: File object}.""" 374 out = {} 375 for info in z.infolist(): 376 if info.filename.startswith("SYSTEM/") and not IsSymlink(info): 377 fn = "system/" + info.filename[7:] 378 data = z.read(info.filename) 379 out[fn] = File(fn, data) 380 return out 381 382 383def Difference(tf, sf): 384 """Return the patch (as a string of data) needed to turn sf into tf.""" 385 386 ttemp = tf.WriteToTemp() 387 stemp = sf.WriteToTemp() 388 389 ext = os.path.splitext(tf.name)[1] 390 391 try: 392 ptemp = tempfile.NamedTemporaryFile() 393 p = common.Run(["bsdiff", stemp.name, ttemp.name, ptemp.name]) 394 _, err = p.communicate() 395 if err: 396 raise ExternalError("failure running bsdiff:\n%s\n" % (err,)) 397 diff = ptemp.read() 398 ptemp.close() 399 finally: 400 stemp.close() 401 ttemp.close() 402 403 return diff 404 405 406def GetBuildProp(property, z): 407 """Return the fingerprint of the build of a given target-files 408 ZipFile object.""" 409 bp = z.read("SYSTEM/build.prop") 410 if not property: 411 return bp 412 m = re.search(re.escape(property) + r"=(.*)\n", bp) 413 if not m: 414 raise ExternalException("couldn't find %s in build.prop" % (property,)) 415 return m.group(1).strip() 416 417 418def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip): 419 script = [] 420 421 print "Loading target..." 422 target_data = LoadSystemFiles(target_zip) 423 print "Loading source..." 424 source_data = LoadSystemFiles(source_zip) 425 426 verbatim_targets = [] 427 patch_list = [] 428 largest_source_size = 0 429 for fn in sorted(target_data.keys()): 430 tf = target_data[fn] 431 sf = source_data.get(fn, None) 432 433 if sf is None or fn in OPTIONS.require_verbatim: 434 # This file should be included verbatim 435 if fn in OPTIONS.prohibit_verbatim: 436 raise ExternalError("\"%s\" must be sent verbatim" % (fn,)) 437 print "send", fn, "verbatim" 438 tf.AddToZip(output_zip) 439 verbatim_targets.append((fn, tf.size)) 440 elif tf.sha1 != sf.sha1: 441 # File is different; consider sending as a patch 442 d = Difference(tf, sf) 443 print fn, tf.size, len(d), (float(len(d)) / tf.size) 444 if len(d) > tf.size * OPTIONS.patch_threshold: 445 # patch is almost as big as the file; don't bother patching 446 tf.AddToZip(output_zip) 447 verbatim_targets.append((fn, tf.size)) 448 else: 449 output_zip.writestr("patch/" + fn + ".p", d) 450 patch_list.append((fn, tf, sf, tf.size)) 451 largest_source_size = max(largest_source_size, sf.size) 452 else: 453 # Target file identical to source. 454 pass 455 456 total_verbatim_size = sum([i[1] for i in verbatim_targets]) 457 total_patched_size = sum([i[3] for i in patch_list]) 458 459 source_fp = GetBuildProp("ro.build.fingerprint", source_zip) 460 target_fp = GetBuildProp("ro.build.fingerprint", target_zip) 461 462 script.append(('assert file_contains("SYSTEM:build.prop", ' 463 '"ro.build.fingerprint=%s") == "true" || ' 464 'file_contains("SYSTEM:build.prop", ' 465 '"ro.build.fingerprint=%s") == "true"') % 466 (source_fp, target_fp)) 467 468 source_boot = common.BuildBootableImage( 469 os.path.join(OPTIONS.source_tmp, "BOOT")) 470 target_boot = common.BuildBootableImage( 471 os.path.join(OPTIONS.target_tmp, "BOOT")) 472 updating_boot = (source_boot != target_boot) 473 474 source_recovery = common.BuildBootableImage( 475 os.path.join(OPTIONS.source_tmp, "RECOVERY")) 476 target_recovery = common.BuildBootableImage( 477 os.path.join(OPTIONS.target_tmp, "RECOVERY")) 478 updating_recovery = (source_recovery != target_recovery) 479 480 source_radio = source_zip.read("RADIO/image") 481 target_radio = target_zip.read("RADIO/image") 482 updating_radio = (source_radio != target_radio) 483 484 # The last 0.1 is reserved for creating symlinks, fixing 485 # permissions, and writing the boot image (if necessary). 486 progress_bar_total = 1.0 487 if updating_boot: 488 progress_bar_total -= 0.1 489 if updating_radio: 490 progress_bar_total -= 0.3 491 492 AppendAssertions(script, target_zip) 493 494 pb_verify = progress_bar_total * 0.3 * \ 495 (total_patched_size / 496 float(total_patched_size+total_verbatim_size)) 497 498 for i, (fn, tf, sf, size) in enumerate(patch_list): 499 if i % 5 == 0: 500 next_sizes = sum([i[3] for i in patch_list[i:i+5]]) 501 script.append("show_progress %f 1" % 502 (next_sizes * pb_verify / total_patched_size,)) 503 script.append("run_program PACKAGE:applypatch -c /%s %s %s" % 504 (fn, tf.sha1, sf.sha1)) 505 506 if patch_list: 507 script.append("run_program PACKAGE:applypatch -s %d" % 508 (largest_source_size,)) 509 script.append("copy_dir PACKAGE:patch CACHE:../tmp/patchtmp") 510 IncludeBinary("applypatch", target_zip, output_zip) 511 512 script.append("\n# ---- start making changes here\n") 513 514 DeleteFiles(script, [SubstituteRoot(i[0]) for i in verbatim_targets]) 515 516 if updating_boot: 517 script.append("format BOOT:") 518 output_zip.writestr("boot.img", target_boot) 519 print "boot image changed; including." 520 else: 521 print "boot image unchanged; skipping." 522 523 if updating_recovery: 524 output_zip.writestr("system/recovery.img", target_recovery) 525 print "recovery image changed; including." 526 else: 527 print "recovery image unchanged; skipping." 528 529 if updating_radio: 530 script.append("show_progress 0.3 10") 531 script.append("write_radio_image PACKAGE:radio.img") 532 output_zip.writestr("radio.img", target_radio) 533 print "radio image changed; including." 534 else: 535 print "radio image unchanged; skipping." 536 537 pb_apply = progress_bar_total * 0.7 * \ 538 (total_patched_size / 539 float(total_patched_size+total_verbatim_size)) 540 for i, (fn, tf, sf, size) in enumerate(patch_list): 541 if i % 5 == 0: 542 next_sizes = sum([i[3] for i in patch_list[i:i+5]]) 543 script.append("show_progress %f 1" % 544 (next_sizes * pb_apply / total_patched_size,)) 545 script.append(("run_program PACKAGE:applypatch " 546 "/%s %s %d %s:/tmp/patchtmp/%s.p") % 547 (fn, tf.sha1, tf.size, sf.sha1, fn)) 548 549 target_symlinks = CopySystemFiles(target_zip, None) 550 551 target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks]) 552 temp_script = [] 553 FixPermissions(temp_script) 554 555 # Note that this call will mess up the tree of Items, so make sure 556 # we're done with it. 557 source_symlinks = CopySystemFiles(source_zip, None) 558 source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks]) 559 560 # Delete all the symlinks in source that aren't in target. This 561 # needs to happen before verbatim files are unpacked, in case a 562 # symlink in the source is replaced by a real file in the target. 563 to_delete = [] 564 for dest, link in source_symlinks: 565 if link not in target_symlinks_d: 566 to_delete.append(link) 567 DeleteFiles(script, to_delete) 568 569 if verbatim_targets: 570 pb_verbatim = progress_bar_total * \ 571 (total_verbatim_size / 572 float(total_patched_size+total_verbatim_size)) 573 script.append("show_progress %f 5" % (pb_verbatim,)) 574 script.append("copy_dir PACKAGE:system SYSTEM:") 575 576 # Create all the symlinks that don't already exist, or point to 577 # somewhere different than what we want. Delete each symlink before 578 # creating it, since the 'symlink' command won't overwrite. 579 to_create = [] 580 for dest, link in target_symlinks: 581 if link in source_symlinks_d: 582 if dest != source_symlinks_d[link]: 583 to_create.append((dest, link)) 584 else: 585 to_create.append((dest, link)) 586 DeleteFiles(script, [i[1] for i in to_create]) 587 script.extend(["symlink %s %s" % s for s in to_create]) 588 589 # Now that the symlinks are created, we can set all the 590 # permissions. 591 script.extend(temp_script) 592 593 if updating_boot: 594 script.append("show_progress 0.1 5") 595 script.append("write_raw_image PACKAGE:boot.img BOOT:") 596 597 AddScript(script, output_zip) 598 599 600def main(argv): 601 602 def option_handler(o, a): 603 if o in ("-b", "--board_config"): 604 common.LoadBoardConfig(a) 605 return True 606 elif o in ("-k", "--package_key"): 607 OPTIONS.package_key = a 608 return True 609 elif o in ("-i", "--incremental_from"): 610 OPTIONS.incremental_source = a 611 return True 612 else: 613 return False 614 615 args = common.ParseOptions(argv, __doc__, 616 extra_opts="b:k:i:d:", 617 extra_long_opts=["board_config=", 618 "package_key=", 619 "incremental_from="], 620 extra_option_handler=option_handler) 621 622 if len(args) != 2: 623 common.Usage(__doc__) 624 sys.exit(1) 625 626 if not OPTIONS.max_image_size: 627 print 628 print " WARNING: No board config specified; will not check image" 629 print " sizes against limits. Use -b to make sure the generated" 630 print " images don't exceed partition sizes." 631 print 632 633 print "unzipping target target-files..." 634 OPTIONS.input_tmp = common.UnzipTemp(args[0]) 635 OPTIONS.target_tmp = OPTIONS.input_tmp 636 input_zip = zipfile.ZipFile(args[0], "r") 637 if OPTIONS.package_key: 638 temp_zip_file = tempfile.NamedTemporaryFile() 639 output_zip = zipfile.ZipFile(temp_zip_file, "w", 640 compression=zipfile.ZIP_DEFLATED) 641 else: 642 output_zip = zipfile.ZipFile(args[1], "w", 643 compression=zipfile.ZIP_DEFLATED) 644 645 if OPTIONS.incremental_source is None: 646 WriteFullOTAPackage(input_zip, output_zip) 647 else: 648 print "unzipping source target-files..." 649 OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source) 650 source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r") 651 WriteIncrementalOTAPackage(input_zip, source_zip, output_zip) 652 653 output_zip.close() 654 if OPTIONS.package_key: 655 SignOutput(temp_zip_file.name, args[1]) 656 temp_zip_file.close() 657 658 common.Cleanup() 659 660 print "done." 661 662 663if __name__ == '__main__': 664 try: 665 main(sys.argv[1:]) 666 except common.ExternalError, e: 667 print 668 print " ERROR: %s" % (e,) 669 print 670 sys.exit(1) 671