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