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