ota_from_target_files.py revision fdd8e69c42e66fb70384bcaca1747f504f2c021c
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
278
279def AppendAssertions(script, input_zip):
280  device = GetBuildProp("ro.product.device", input_zip)
281  script.AssertDevice(device)
282
283  info = input_zip.read("OTA/android-info.txt")
284  m = re.search(r"require\s+version-bootloader\s*=\s*(\S+)", info)
285  if m:
286    bootloaders = m.group(1).split("|")
287    script.AssertSomeBootloader(*bootloaders)
288
289
290def MakeRecoveryPatch(output_zip, recovery_img, boot_img):
291  """Generate a binary patch that creates the recovery image starting
292  with the boot image.  (Most of the space in these images is just the
293  kernel, which is identical for the two, so the resulting patch
294  should be efficient.)  Add it to the output zip, along with a shell
295  script that is run from init.rc on first boot to actually do the
296  patching and install the new recovery image.
297
298  recovery_img and boot_img should be File objects for the
299  corresponding images.
300
301  Returns an Item for the shell script, which must be made
302  executable.
303  """
304
305  patch = Difference(recovery_img, boot_img, "imgdiff")
306  common.ZipWriteStr(output_zip, "system/recovery-from-boot.p", patch)
307  Item.Get("system/recovery-from-boot.p", dir=False)
308
309  # Images with different content will have a different first page, so
310  # we check to see if this recovery has already been installed by
311  # testing just the first 2k.
312  HEADER_SIZE = 2048
313  header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()
314  sh = """#!/system/bin/sh
315if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then
316  log -t recovery "Installing new recovery image"
317  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
318else
319  log -t recovery "Recovery image already installed"
320fi
321""" % { 'boot_size': boot_img.size,
322        'boot_sha1': boot_img.sha1,
323        'header_size': HEADER_SIZE,
324        'header_sha1': header_sha1,
325        'recovery_size': recovery_img.size,
326        'recovery_sha1': recovery_img.sha1 }
327  common.ZipWriteStr(output_zip, "system/etc/install-recovery.sh", sh)
328  return Item.Get("system/etc/install-recovery.sh", dir=False)
329
330
331def WriteFullOTAPackage(input_zip, output_zip):
332  if OPTIONS.script_mode == "auto":
333    script = both_generator.BothGenerator(2)
334  elif OPTIONS.script_mode == "amend":
335    script = amend_generator.AmendGenerator()
336  else:
337    # TODO: how to determine this?  We don't know what version it will
338    # be installed on top of.  For now, we expect the API just won't
339    # change very often.
340    script = edify_generator.EdifyGenerator(2)
341
342  if not OPTIONS.omit_prereq:
343    ts = GetBuildProp("ro.build.date.utc", input_zip)
344    script.AssertOlderBuild(ts)
345
346  AppendAssertions(script, input_zip)
347
348  script.ShowProgress(0.1, 0)
349
350  try:
351    common.ZipWriteStr(output_zip, "radio.img", input_zip.read("RADIO/image"))
352    script.WriteFirmwareImage("radio", "radio.img")
353  except KeyError:
354    print "warning: no radio image in input target_files; not flashing radio"
355
356  script.ShowProgress(0.5, 0)
357
358  if OPTIONS.wipe_user_data:
359    script.FormatPartition("userdata")
360
361  script.FormatPartition("system")
362  script.Mount("MTD", "system", "/system")
363  script.UnpackPackageDir("system", "/system")
364
365  symlinks = CopySystemFiles(input_zip, output_zip)
366  script.MakeSymlinks(symlinks)
367
368  boot_img = File("boot.img", common.BuildBootableImage(
369      os.path.join(OPTIONS.input_tmp, "BOOT")))
370  recovery_img = File("recovery.img", common.BuildBootableImage(
371      os.path.join(OPTIONS.input_tmp, "RECOVERY")))
372  i = MakeRecoveryPatch(output_zip, recovery_img, boot_img)
373
374  Item.GetMetadata()
375
376  # GetMetadata uses the data in android_filesystem_config.h to assign
377  # the uid/gid/mode of all files.  We want to override that for the
378  # recovery patching shell script to make it executable.
379  i.uid = 0
380  i.gid = 0
381  i.mode = 0544
382  Item.Get("system").SetPermissions(script)
383
384  common.CheckSize(boot_img.data, "boot.img")
385  common.ZipWriteStr(output_zip, "boot.img", boot_img.data)
386  script.ShowProgress(0.2, 0)
387
388  script.WriteRawImage("boot", "boot.img")
389  script.ShowProgress(0.2, 10)
390
391  if OPTIONS.extra_script is not None:
392    script.AppendExtra(OPTIONS.extra_script)
393
394  script.AddToZip(input_zip, output_zip)
395
396
397class File(object):
398  def __init__(self, name, data):
399    self.name = name
400    self.data = data
401    self.size = len(data)
402    self.sha1 = sha.sha(data).hexdigest()
403
404  def WriteToTemp(self):
405    t = tempfile.NamedTemporaryFile()
406    t.write(self.data)
407    t.flush()
408    return t
409
410  def AddToZip(self, z):
411    common.ZipWriteStr(z, self.name, self.data)
412
413
414def LoadSystemFiles(z):
415  """Load all the files from SYSTEM/... in a given target-files
416  ZipFile, and return a dict of {filename: File object}."""
417  out = {}
418  for info in z.infolist():
419    if info.filename.startswith("SYSTEM/") and not IsSymlink(info):
420      fn = "system/" + info.filename[7:]
421      data = z.read(info.filename)
422      out[fn] = File(fn, data)
423  return out
424
425
426def Difference(tf, sf, diff_program):
427  """Return the patch (as a string of data) needed to turn sf into tf.
428  diff_program is the name of an external program (or list, if
429  additional arguments are desired) to run to generate the diff.
430  """
431
432  ttemp = tf.WriteToTemp()
433  stemp = sf.WriteToTemp()
434
435  ext = os.path.splitext(tf.name)[1]
436
437  try:
438    ptemp = tempfile.NamedTemporaryFile()
439    if isinstance(diff_program, list):
440      cmd = copy.copy(diff_program)
441    else:
442      cmd = [diff_program]
443    cmd.append(stemp.name)
444    cmd.append(ttemp.name)
445    cmd.append(ptemp.name)
446    p = common.Run(cmd)
447    _, err = p.communicate()
448    if err or p.returncode != 0:
449      print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
450      return None
451    diff = ptemp.read()
452  finally:
453    ptemp.close()
454    stemp.close()
455    ttemp.close()
456
457  return diff
458
459
460def GetBuildProp(property, z):
461  """Return the fingerprint of the build of a given target-files
462  ZipFile object."""
463  bp = z.read("SYSTEM/build.prop")
464  if not property:
465    return bp
466  m = re.search(re.escape(property) + r"=(.*)\n", bp)
467  if not m:
468    raise common.ExternalError("couldn't find %s in build.prop" % (property,))
469  return m.group(1).strip()
470
471
472def GetRecoveryAPIVersion(zip):
473  """Returns the version of the recovery API.  Version 0 is the older
474  amend code (no separate binary)."""
475  try:
476    version = zip.read("META/recovery-api-version.txt")
477    return int(version)
478  except KeyError:
479    try:
480      # version one didn't have the recovery-api-version.txt file, but
481      # it did include an updater binary.
482      zip.getinfo("OTA/bin/updater")
483      return 1
484    except KeyError:
485      return 0
486
487def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
488  source_version = GetRecoveryAPIVersion(source_zip)
489
490  if OPTIONS.script_mode == 'amend':
491    script = amend_generator.AmendGenerator()
492  elif OPTIONS.script_mode == 'edify':
493    if source_version == 0:
494      print ("WARNING: generating edify script for a source that "
495             "can't install it.")
496    script = edify_generator.EdifyGenerator(source_version)
497  elif OPTIONS.script_mode == 'auto':
498    if source_version > 0:
499      script = edify_generator.EdifyGenerator(source_version)
500    else:
501      script = amend_generator.AmendGenerator()
502  else:
503    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
504
505  print "Loading target..."
506  target_data = LoadSystemFiles(target_zip)
507  print "Loading source..."
508  source_data = LoadSystemFiles(source_zip)
509
510  verbatim_targets = []
511  patch_list = []
512  largest_source_size = 0
513  for fn in sorted(target_data.keys()):
514    tf = target_data[fn]
515    sf = source_data.get(fn, None)
516
517    if sf is None or fn in OPTIONS.require_verbatim:
518      # This file should be included verbatim
519      if fn in OPTIONS.prohibit_verbatim:
520        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
521      print "send", fn, "verbatim"
522      tf.AddToZip(output_zip)
523      verbatim_targets.append((fn, tf.size))
524    elif tf.sha1 != sf.sha1:
525      # File is different; consider sending as a patch
526      diff_method = "bsdiff"
527      if tf.name.endswith(".gz"):
528        diff_method = "imgdiff"
529      d = Difference(tf, sf, diff_method)
530      if d is not None:
531        print fn, tf.size, len(d), (float(len(d)) / tf.size)
532      if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
533        # patch is almost as big as the file; don't bother patching
534        tf.AddToZip(output_zip)
535        verbatim_targets.append((fn, tf.size))
536      else:
537        common.ZipWriteStr(output_zip, "patch/" + fn + ".p", d)
538        patch_list.append((fn, tf, sf, tf.size))
539        largest_source_size = max(largest_source_size, sf.size)
540    else:
541      # Target file identical to source.
542      pass
543
544  total_verbatim_size = sum([i[1] for i in verbatim_targets])
545  total_patched_size = sum([i[3] for i in patch_list])
546
547  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
548  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
549
550  script.Mount("MTD", "system", "/system")
551  script.AssertSomeFingerprint(source_fp, target_fp)
552
553  source_boot = File("/tmp/boot.img",
554                     common.BuildBootableImage(
555      os.path.join(OPTIONS.source_tmp, "BOOT")))
556  target_boot = File("/tmp/boot.img",
557                     common.BuildBootableImage(
558      os.path.join(OPTIONS.target_tmp, "BOOT")))
559  updating_boot = (source_boot.data != target_boot.data)
560
561  source_recovery = File("system/recovery.img",
562                         common.BuildBootableImage(
563      os.path.join(OPTIONS.source_tmp, "RECOVERY")))
564  target_recovery = File("system/recovery.img",
565                         common.BuildBootableImage(
566      os.path.join(OPTIONS.target_tmp, "RECOVERY")))
567  updating_recovery = (source_recovery.data != target_recovery.data)
568
569  source_radio = source_zip.read("RADIO/image")
570  target_radio = target_zip.read("RADIO/image")
571  updating_radio = (source_radio != target_radio)
572
573  # The last 0.1 is reserved for creating symlinks, fixing
574  # permissions, and writing the boot image (if necessary).
575  progress_bar_total = 1.0
576  if updating_boot:
577    progress_bar_total -= 0.1
578  if updating_radio:
579    progress_bar_total -= 0.3
580
581  AppendAssertions(script, target_zip)
582
583  script.Print("Verifying current system...")
584
585  pb_verify = progress_bar_total * 0.3 * \
586              (total_patched_size /
587               float(total_patched_size+total_verbatim_size+1))
588
589  for i, (fn, tf, sf, size) in enumerate(patch_list):
590    if i % 5 == 0:
591      next_sizes = sum([i[3] for i in patch_list[i:i+5]])
592      script.ShowProgress(next_sizes * pb_verify / (total_patched_size+1), 1)
593
594    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
595
596  if updating_boot:
597    d = Difference(target_boot, source_boot, "imgdiff")
598    print "boot      target: %d  source: %d  diff: %d" % (
599        target_boot.size, source_boot.size, len(d))
600
601    common.ZipWriteStr(output_zip, "patch/boot.img.p", d)
602
603    script.PatchCheck("MTD:boot:%d:%s:%d:%s" %
604                      (source_boot.size, source_boot.sha1,
605                       target_boot.size, target_boot.sha1))
606
607  if patch_list or updating_recovery or updating_boot:
608    script.CacheFreeSpaceCheck(largest_source_size)
609    script.Print("Unpacking patches...")
610    script.UnpackPackageDir("patch", "/tmp/patchtmp")
611
612  script.Comment("---- start making changes here ----")
613
614  if OPTIONS.wipe_user_data:
615    script.Print("Erasing user data...")
616    script.FormatPartition("userdata")
617
618  script.Print("Removing unneeded files...")
619  script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
620                     ["/"+i for i in sorted(source_data)
621                            if i not in target_data])
622
623  if updating_boot:
624    # Produce the boot image by applying a patch to the current
625    # contents of the boot partition, and write it back to the
626    # partition.
627    script.Print("Patching boot image...")
628    script.ApplyPatch("MTD:boot:%d:%s:%d:%s"
629                      % (source_boot.size, source_boot.sha1,
630                         target_boot.size, target_boot.sha1),
631                      "-",
632                      target_boot.size, target_boot.sha1,
633                      source_boot.sha1, "/tmp/patchtmp/boot.img.p")
634    print "boot image changed; including."
635  else:
636    print "boot image unchanged; skipping."
637
638  if updating_recovery:
639    # Is it better to generate recovery as a patch from the current
640    # boot image, or from the previous recovery image?  For large
641    # updates with significant kernel changes, probably the former.
642    # For small updates where the kernel hasn't changed, almost
643    # certainly the latter.  We pick the first option.  Future
644    # complicated schemes may let us effectively use both.
645    #
646    # A wacky possibility: as long as there is room in the boot
647    # partition, include the binaries and image files from recovery in
648    # the boot image (though not in the ramdisk) so they can be used
649    # as fodder for constructing the recovery image.
650    recovery_sh_item = MakeRecoveryPatch(output_zip,
651                                         target_recovery, target_boot)
652    print "recovery image changed; including as patch from boot."
653  else:
654    print "recovery image unchanged; skipping."
655
656  if updating_radio:
657    script.ShowProgress(0.3, 10)
658    script.Print("Writing radio image...")
659    script.WriteFirmwareImage("radio", "radio.img")
660    common.ZipWriteStr(output_zip, "radio.img", target_radio)
661    print "radio image changed; including."
662  else:
663    print "radio image unchanged; skipping."
664
665  script.Print("Patching system files...")
666  pb_apply = progress_bar_total * 0.7 * \
667             (total_patched_size /
668              float(total_patched_size+total_verbatim_size+1))
669  for i, (fn, tf, sf, size) in enumerate(patch_list):
670    if i % 5 == 0:
671      next_sizes = sum([i[3] for i in patch_list[i:i+5]])
672      script.ShowProgress(next_sizes * pb_apply / (total_patched_size+1), 1)
673    script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1,
674                      sf.sha1, "/tmp/patchtmp/"+fn+".p")
675
676  target_symlinks = CopySystemFiles(target_zip, None)
677
678  target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
679  temp_script = script.MakeTemporary()
680  Item.GetMetadata()
681  if updating_recovery:
682    recovery_sh_item.uid = 0
683    recovery_sh_item.gid = 0
684    recovery_sh_item.mode = 0544
685  Item.Get("system").SetPermissions(temp_script)
686
687  # Note that this call will mess up the tree of Items, so make sure
688  # we're done with it.
689  source_symlinks = CopySystemFiles(source_zip, None)
690  source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
691
692  # Delete all the symlinks in source that aren't in target.  This
693  # needs to happen before verbatim files are unpacked, in case a
694  # symlink in the source is replaced by a real file in the target.
695  to_delete = []
696  for dest, link in source_symlinks:
697    if link not in target_symlinks_d:
698      to_delete.append(link)
699  script.DeleteFiles(to_delete)
700
701  if verbatim_targets:
702    pb_verbatim = progress_bar_total * \
703                  (total_verbatim_size /
704                   float(total_patched_size+total_verbatim_size+1))
705    script.ShowProgress(pb_verbatim, 5)
706    script.Print("Unpacking new files...")
707    script.UnpackPackageDir("system", "/system")
708
709  script.Print("Finishing up...")
710
711  # Create all the symlinks that don't already exist, or point to
712  # somewhere different than what we want.  Delete each symlink before
713  # creating it, since the 'symlink' command won't overwrite.
714  to_create = []
715  for dest, link in target_symlinks:
716    if link in source_symlinks_d:
717      if dest != source_symlinks_d[link]:
718        to_create.append((dest, link))
719    else:
720      to_create.append((dest, link))
721  script.DeleteFiles([i[1] for i in to_create])
722  script.MakeSymlinks(to_create)
723
724  # Now that the symlinks are created, we can set all the
725  # permissions.
726  script.AppendScript(temp_script)
727
728  if OPTIONS.extra_script is not None:
729    scirpt.AppendExtra(OPTIONS.extra_script)
730
731  script.AddToZip(target_zip, output_zip)
732
733
734def main(argv):
735
736  def option_handler(o, a):
737    if o in ("-b", "--board_config"):
738      pass   # deprecated
739    elif o in ("-k", "--package_key"):
740      OPTIONS.package_key = a
741    elif o in ("-i", "--incremental_from"):
742      OPTIONS.incremental_source = a
743    elif o in ("-w", "--wipe_user_data"):
744      OPTIONS.wipe_user_data = True
745    elif o in ("-n", "--no_prereq"):
746      OPTIONS.omit_prereq = True
747    elif o in ("-e", "--extra_script"):
748      OPTIONS.extra_script = a
749    elif o in ("-m", "--script_mode"):
750      OPTIONS.script_mode = a
751    else:
752      return False
753    return True
754
755  args = common.ParseOptions(argv, __doc__,
756                             extra_opts="b:k:i:d:wne:m:",
757                             extra_long_opts=["board_config=",
758                                              "package_key=",
759                                              "incremental_from=",
760                                              "wipe_user_data",
761                                              "no_prereq",
762                                              "extra_script=",
763                                              "script_mode="],
764                             extra_option_handler=option_handler)
765
766  if len(args) != 2:
767    common.Usage(__doc__)
768    sys.exit(1)
769
770  if OPTIONS.script_mode not in ("amend", "edify", "auto"):
771    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
772
773  if OPTIONS.extra_script is not None:
774    OPTIONS.extra_script = open(OPTIONS.extra_script).read()
775
776  print "unzipping target target-files..."
777  OPTIONS.input_tmp = common.UnzipTemp(args[0])
778
779  common.LoadMaxSizes()
780  if not OPTIONS.max_image_size:
781    print
782    print "  WARNING:  Failed to load max image sizes; will not enforce"
783    print "  image size limits."
784    print
785
786  OPTIONS.target_tmp = OPTIONS.input_tmp
787  input_zip = zipfile.ZipFile(args[0], "r")
788  if OPTIONS.package_key:
789    temp_zip_file = tempfile.NamedTemporaryFile()
790    output_zip = zipfile.ZipFile(temp_zip_file, "w",
791                                 compression=zipfile.ZIP_DEFLATED)
792  else:
793    output_zip = zipfile.ZipFile(args[1], "w",
794                                 compression=zipfile.ZIP_DEFLATED)
795
796  if OPTIONS.incremental_source is None:
797    WriteFullOTAPackage(input_zip, output_zip)
798  else:
799    print "unzipping source target-files..."
800    OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
801    source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
802    WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
803
804  output_zip.close()
805  if OPTIONS.package_key:
806    SignOutput(temp_zip_file.name, args[1])
807    temp_zip_file.close()
808
809  common.Cleanup()
810
811  print "done."
812
813
814if __name__ == '__main__':
815  try:
816    main(sys.argv[1:])
817  except common.ExternalError, e:
818    print
819    print "   ERROR: %s" % (e,)
820    print
821    sys.exit(1)
822