ota_from_target_files.py revision 73ef8257ce54ce1ddc2d4cfc3b3814ca734c0e11
1#!/usr/bin/env python
2#
3# Copyright (C) 2008 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Given a target-files zipfile, produces an OTA package that installs
19that build.  An incremental OTA is produced if -i is given, otherwise
20a full OTA is produced.
21
22Usage:  ota_from_target_files [flags] input_target_files output_ota_package
23
24  -b  (--board_config)  <file>
25      Specifies a BoardConfig.mk file containing image max sizes
26      against which the generated image files are checked.
27
28  -k  (--package_key)  <key>
29      Key to use to sign the package (default is
30      "build/target/product/security/testkey").
31
32  -i  (--incremental_from)  <file>
33      Generate an incremental OTA using the given target-files zip as
34      the starting build.
35
36  -w  (--wipe_user_data)
37      Generate an OTA package that will wipe the user data partition
38      when installed.
39
40  -n  (--no_prereq)
41      Omit the timestamp prereq check normally included at the top of
42      the build scripts (used for developer OTA packages which
43      legitimately need to go back and forth).
44
45  -e  (--extra_script)  <file>
46      Insert the contents of file at the end of the update script.
47
48  -m  (--script_mode)  <mode>
49      Specify 'amend' or 'edify' scripts, or 'auto' to pick
50      automatically (this is the default).
51
52"""
53
54import sys
55
56if sys.hexversion < 0x02040000:
57  print >> sys.stderr, "Python 2.4 or newer is required."
58  sys.exit(1)
59
60import copy
61import os
62import re
63import sha
64import subprocess
65import tempfile
66import time
67import zipfile
68
69import common
70import amend_generator
71import edify_generator
72import both_generator
73
74OPTIONS = common.OPTIONS
75OPTIONS.package_key = "build/target/product/security/testkey"
76OPTIONS.incremental_source = None
77OPTIONS.require_verbatim = set()
78OPTIONS.prohibit_verbatim = set(("system/build.prop",))
79OPTIONS.patch_threshold = 0.95
80OPTIONS.wipe_user_data = False
81OPTIONS.omit_prereq = False
82OPTIONS.extra_script = None
83OPTIONS.script_mode = 'auto'
84
85def MostPopularKey(d, default):
86  """Given a dict, return the key corresponding to the largest
87  value.  Returns 'default' if the dict is empty."""
88  x = [(v, k) for (k, v) in d.iteritems()]
89  if not x: return default
90  x.sort()
91  return x[-1][1]
92
93
94def IsSymlink(info):
95  """Return true if the zipfile.ZipInfo object passed in represents a
96  symlink."""
97  return (info.external_attr >> 16) == 0120777
98
99
100
101class Item:
102  """Items represent the metadata (user, group, mode) of files and
103  directories in the system image."""
104  ITEMS = {}
105  def __init__(self, name, dir=False):
106    self.name = name
107    self.uid = None
108    self.gid = None
109    self.mode = None
110    self.dir = dir
111
112    if name:
113      self.parent = Item.Get(os.path.dirname(name), dir=True)
114      self.parent.children.append(self)
115    else:
116      self.parent = None
117    if dir:
118      self.children = []
119
120  def Dump(self, indent=0):
121    if self.uid is not None:
122      print "%s%s %d %d %o" % ("  "*indent, self.name, self.uid, self.gid, self.mode)
123    else:
124      print "%s%s %s %s %s" % ("  "*indent, self.name, self.uid, self.gid, self.mode)
125    if self.dir:
126      print "%s%s" % ("  "*indent, self.descendants)
127      print "%s%s" % ("  "*indent, self.best_subtree)
128      for i in self.children:
129        i.Dump(indent=indent+1)
130
131  @classmethod
132  def Get(cls, name, dir=False):
133    if name not in cls.ITEMS:
134      cls.ITEMS[name] = Item(name, dir=dir)
135    return cls.ITEMS[name]
136
137  @classmethod
138  def GetMetadata(cls):
139    """Run the external 'fs_config' program to determine the desired
140    uid, gid, and mode for every Item object."""
141    p = common.Run(["fs_config"], stdin=subprocess.PIPE,
142                  stdout=subprocess.PIPE, stderr=subprocess.PIPE)
143    suffix = { False: "", True: "/" }
144    input = "".join(["%s%s\n" % (i.name, suffix[i.dir])
145                     for i in cls.ITEMS.itervalues() if i.name])
146    output, error = p.communicate(input)
147    assert not error
148
149    for line in output.split("\n"):
150      if not line: continue
151      name, uid, gid, mode = line.split()
152      i = cls.ITEMS[name]
153      i.uid = int(uid)
154      i.gid = int(gid)
155      i.mode = int(mode, 8)
156      if i.dir:
157        i.children.sort(key=lambda i: i.name)
158
159  def CountChildMetadata(self):
160    """Count up the (uid, gid, mode) tuples for all children and
161    determine the best strategy for using set_perm_recursive and
162    set_perm to correctly chown/chmod all the files to their desired
163    values.  Recursively calls itself for all descendants.
164
165    Returns a dict of {(uid, gid, dmode, fmode): count} counting up
166    all descendants of this node.  (dmode or fmode may be None.)  Also
167    sets the best_subtree of each directory Item to the (uid, gid,
168    dmode, fmode) tuple that will match the most descendants of that
169    Item.
170    """
171
172    assert self.dir
173    d = self.descendants = {(self.uid, self.gid, self.mode, None): 1}
174    for i in self.children:
175      if i.dir:
176        for k, v in i.CountChildMetadata().iteritems():
177          d[k] = d.get(k, 0) + v
178      else:
179        k = (i.uid, i.gid, None, i.mode)
180        d[k] = d.get(k, 0) + 1
181
182    # Find the (uid, gid, dmode, fmode) tuple that matches the most
183    # descendants.
184
185    # First, find the (uid, gid) pair that matches the most
186    # descendants.
187    ug = {}
188    for (uid, gid, _, _), count in d.iteritems():
189      ug[(uid, gid)] = ug.get((uid, gid), 0) + count
190    ug = MostPopularKey(ug, (0, 0))
191
192    # Now find the dmode and fmode that match the most descendants
193    # with that (uid, gid), and choose those.
194    best_dmode = (0, 0755)
195    best_fmode = (0, 0644)
196    for k, count in d.iteritems():
197      if k[:2] != ug: continue
198      if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2])
199      if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3])
200    self.best_subtree = ug + (best_dmode[1], best_fmode[1])
201
202    return d
203
204  def SetPermissions(self, script):
205    """Append set_perm/set_perm_recursive commands to 'script' to
206    set all permissions, users, and groups for the tree of files
207    rooted at 'self'."""
208
209    self.CountChildMetadata()
210
211    def recurse(item, current):
212      # current is the (uid, gid, dmode, fmode) tuple that the current
213      # item (and all its children) have already been set to.  We only
214      # need to issue set_perm/set_perm_recursive commands if we're
215      # supposed to be something different.
216      if item.dir:
217        if current != item.best_subtree:
218          script.SetPermissionsRecursive("/"+item.name, *item.best_subtree)
219          current = item.best_subtree
220
221        if item.uid != current[0] or item.gid != current[1] or \
222           item.mode != current[2]:
223          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
224
225        for i in item.children:
226          recurse(i, current)
227      else:
228        if item.uid != current[0] or item.gid != current[1] or \
229               item.mode != current[3]:
230          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
231
232    recurse(self, (-1, -1, -1, -1))
233
234
235def CopySystemFiles(input_zip, output_zip=None,
236                    substitute=None):
237  """Copies files underneath system/ in the input zip to the output
238  zip.  Populates the Item class with their metadata, and returns a
239  list of symlinks.  output_zip may be None, in which case the copy is
240  skipped (but the other side effects still happen).  substitute is an
241  optional dict of {output filename: contents} to be output instead of
242  certain input files.
243  """
244
245  symlinks = []
246
247  for info in input_zip.infolist():
248    if info.filename.startswith("SYSTEM/"):
249      basefilename = info.filename[7:]
250      if IsSymlink(info):
251        symlinks.append((input_zip.read(info.filename),
252                         "/system/" + basefilename))
253      else:
254        info2 = copy.copy(info)
255        fn = info2.filename = "system/" + basefilename
256        if substitute and fn in substitute and substitute[fn] is None:
257          continue
258        if output_zip is not None:
259          if substitute and fn in substitute:
260            data = substitute[fn]
261          else:
262            data = input_zip.read(info.filename)
263          output_zip.writestr(info2, data)
264        if fn.endswith("/"):
265          Item.Get(fn[:-1], dir=True)
266        else:
267          Item.Get(fn, dir=False)
268
269  symlinks.sort()
270  return symlinks
271
272
273def SignOutput(temp_zip_name, output_zip_name):
274  key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
275  pw = key_passwords[OPTIONS.package_key]
276
277  common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw)
278
279
280def AppendAssertions(script, input_zip):
281  device = GetBuildProp("ro.product.device", input_zip)
282  script.AssertDevice(device)
283
284  info = input_zip.read("OTA/android-info.txt")
285  m = re.search(r"require\s+version-bootloader\s*=\s*(\S+)", info)
286  if m:
287    bootloaders = m.group(1).split("|")
288    script.AssertSomeBootloader(*bootloaders)
289
290
291def MakeRecoveryPatch(output_zip, recovery_img, boot_img):
292  """Generate a binary patch that creates the recovery image starting
293  with the boot image.  (Most of the space in these images is just the
294  kernel, which is identical for the two, so the resulting patch
295  should be efficient.)  Add it to the output zip, along with a shell
296  script that is run from init.rc on first boot to actually do the
297  patching and install the new recovery image.
298
299  recovery_img and boot_img should be File objects for the
300  corresponding images.
301
302  Returns an Item for the shell script, which must be made
303  executable.
304  """
305
306  patch = Difference(recovery_img, boot_img, "imgdiff")
307  common.ZipWriteStr(output_zip, "system/recovery-from-boot.p", patch)
308  Item.Get("system/recovery-from-boot.p", dir=False)
309
310  # Images with different content will have a different first page, so
311  # we check to see if this recovery has already been installed by
312  # testing just the first 2k.
313  HEADER_SIZE = 2048
314  header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()
315  sh = """#!/system/bin/sh
316if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then
317  log -t recovery "Installing new recovery image"
318  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
319else
320  log -t recovery "Recovery image already installed"
321fi
322""" % { 'boot_size': boot_img.size,
323        'boot_sha1': boot_img.sha1,
324        'header_size': HEADER_SIZE,
325        'header_sha1': header_sha1,
326        'recovery_size': recovery_img.size,
327        'recovery_sha1': recovery_img.sha1 }
328  common.ZipWriteStr(output_zip, "system/etc/install-recovery.sh", sh)
329  return Item.Get("system/etc/install-recovery.sh", dir=False)
330
331
332def WriteFullOTAPackage(input_zip, output_zip):
333  if OPTIONS.script_mode == "auto":
334    script = both_generator.BothGenerator(2)
335  elif OPTIONS.script_mode == "amend":
336    script = amend_generator.AmendGenerator()
337  else:
338    # TODO: how to determine this?  We don't know what version it will
339    # be installed on top of.  For now, we expect the API just won't
340    # change very often.
341    script = edify_generator.EdifyGenerator(2)
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
349  script.ShowProgress(0.1, 0)
350
351  try:
352    common.ZipWriteStr(output_zip, "radio.img", input_zip.read("RADIO/image"))
353    script.WriteFirmwareImage("radio", "radio.img")
354  except KeyError:
355    print "warning: no radio image in input target_files; not flashing radio"
356
357  script.ShowProgress(0.5, 0)
358
359  if OPTIONS.wipe_user_data:
360    script.FormatPartition("userdata")
361
362  script.FormatPartition("system")
363  script.Mount("MTD", "system", "/system")
364  script.UnpackPackageDir("system", "/system")
365
366  symlinks = CopySystemFiles(input_zip, output_zip)
367  script.MakeSymlinks(symlinks)
368
369  boot_img = File("boot.img", common.BuildBootableImage(
370      os.path.join(OPTIONS.input_tmp, "BOOT")))
371  recovery_img = File("recovery.img", common.BuildBootableImage(
372      os.path.join(OPTIONS.input_tmp, "RECOVERY")))
373  i = MakeRecoveryPatch(output_zip, recovery_img, boot_img)
374
375  Item.GetMetadata()
376
377  # GetMetadata uses the data in android_filesystem_config.h to assign
378  # the uid/gid/mode of all files.  We want to override that for the
379  # recovery patching shell script to make it executable.
380  i.uid = 0
381  i.gid = 0
382  i.mode = 0544
383  Item.Get("system").SetPermissions(script)
384
385  common.CheckSize(boot_img.data, "boot.img")
386  common.ZipWriteStr(output_zip, "boot.img", boot_img.data)
387  script.ShowProgress(0.2, 0)
388
389  script.WriteRawImage("boot", "boot.img")
390  script.ShowProgress(0.2, 10)
391
392  if OPTIONS.extra_script is not None:
393    script.AppendExtra(OPTIONS.extra_script)
394
395  script.AddToZip(input_zip, output_zip)
396
397
398class File(object):
399  def __init__(self, name, data):
400    self.name = name
401    self.data = data
402    self.size = len(data)
403    self.sha1 = sha.sha(data).hexdigest()
404
405  def WriteToTemp(self):
406    t = tempfile.NamedTemporaryFile()
407    t.write(self.data)
408    t.flush()
409    return t
410
411  def AddToZip(self, z):
412    common.ZipWriteStr(z, self.name, self.data)
413
414
415def LoadSystemFiles(z):
416  """Load all the files from SYSTEM/... in a given target-files
417  ZipFile, and return a dict of {filename: File object}."""
418  out = {}
419  for info in z.infolist():
420    if info.filename.startswith("SYSTEM/") and not IsSymlink(info):
421      fn = "system/" + info.filename[7:]
422      data = z.read(info.filename)
423      out[fn] = File(fn, data)
424  return out
425
426
427def Difference(tf, sf, diff_program):
428  """Return the patch (as a string of data) needed to turn sf into tf.
429  diff_program is the name of an external program (or list, if
430  additional arguments are desired) to run to generate the diff.
431  """
432
433  ttemp = tf.WriteToTemp()
434  stemp = sf.WriteToTemp()
435
436  ext = os.path.splitext(tf.name)[1]
437
438  try:
439    ptemp = tempfile.NamedTemporaryFile()
440    if isinstance(diff_program, list):
441      cmd = copy.copy(diff_program)
442    else:
443      cmd = [diff_program]
444    cmd.append(stemp.name)
445    cmd.append(ttemp.name)
446    cmd.append(ptemp.name)
447    p = common.Run(cmd)
448    _, err = p.communicate()
449    if err or p.returncode != 0:
450      print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
451      return None
452    diff = ptemp.read()
453  finally:
454    ptemp.close()
455    stemp.close()
456    ttemp.close()
457
458  return diff
459
460
461def GetBuildProp(property, z):
462  """Return the fingerprint of the build of a given target-files
463  ZipFile object."""
464  bp = z.read("SYSTEM/build.prop")
465  if not property:
466    return bp
467  m = re.search(re.escape(property) + r"=(.*)\n", bp)
468  if not m:
469    raise common.ExternalError("couldn't find %s in build.prop" % (property,))
470  return m.group(1).strip()
471
472
473def GetRecoveryAPIVersion(zip):
474  """Returns the version of the recovery API.  Version 0 is the older
475  amend code (no separate binary)."""
476  try:
477    version = zip.read("META/recovery-api-version.txt")
478    return int(version)
479  except KeyError:
480    try:
481      # version one didn't have the recovery-api-version.txt file, but
482      # it did include an updater binary.
483      zip.getinfo("OTA/bin/updater")
484      return 1
485    except KeyError:
486      return 0
487
488def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
489  source_version = GetRecoveryAPIVersion(source_zip)
490
491  if OPTIONS.script_mode == 'amend':
492    script = amend_generator.AmendGenerator()
493  elif OPTIONS.script_mode == 'edify':
494    if source_version == 0:
495      print ("WARNING: generating edify script for a source that "
496             "can't install it.")
497    script = edify_generator.EdifyGenerator(source_version)
498  elif OPTIONS.script_mode == 'auto':
499    if source_version > 0:
500      script = edify_generator.EdifyGenerator(source_version)
501    else:
502      script = amend_generator.AmendGenerator()
503  else:
504    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
505
506  print "Loading target..."
507  target_data = LoadSystemFiles(target_zip)
508  print "Loading source..."
509  source_data = LoadSystemFiles(source_zip)
510
511  verbatim_targets = []
512  patch_list = []
513  largest_source_size = 0
514  for fn in sorted(target_data.keys()):
515    tf = target_data[fn]
516    sf = source_data.get(fn, None)
517
518    if sf is None or fn in OPTIONS.require_verbatim:
519      # This file should be included verbatim
520      if fn in OPTIONS.prohibit_verbatim:
521        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
522      print "send", fn, "verbatim"
523      tf.AddToZip(output_zip)
524      verbatim_targets.append((fn, tf.size))
525    elif tf.sha1 != sf.sha1:
526      # File is different; consider sending as a patch
527      diff_method = "bsdiff"
528      if tf.name.endswith(".gz"):
529        diff_method = "imgdiff"
530      d = Difference(tf, sf, diff_method)
531      if d is not None:
532        print fn, tf.size, len(d), (float(len(d)) / tf.size)
533      if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
534        # patch is almost as big as the file; don't bother patching
535        tf.AddToZip(output_zip)
536        verbatim_targets.append((fn, tf.size))
537      else:
538        common.ZipWriteStr(output_zip, "patch/" + fn + ".p", d)
539        patch_list.append((fn, tf, sf, tf.size))
540        largest_source_size = max(largest_source_size, sf.size)
541    else:
542      # Target file identical to source.
543      pass
544
545  total_verbatim_size = sum([i[1] for i in verbatim_targets])
546  total_patched_size = sum([i[3] for i in patch_list])
547
548  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
549  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
550
551  script.Mount("MTD", "system", "/system")
552  script.AssertSomeFingerprint(source_fp, target_fp)
553
554  source_boot = File("/tmp/boot.img",
555                     common.BuildBootableImage(
556      os.path.join(OPTIONS.source_tmp, "BOOT")))
557  target_boot = File("/tmp/boot.img",
558                     common.BuildBootableImage(
559      os.path.join(OPTIONS.target_tmp, "BOOT")))
560  updating_boot = (source_boot.data != target_boot.data)
561
562  source_recovery = File("system/recovery.img",
563                         common.BuildBootableImage(
564      os.path.join(OPTIONS.source_tmp, "RECOVERY")))
565  target_recovery = File("system/recovery.img",
566                         common.BuildBootableImage(
567      os.path.join(OPTIONS.target_tmp, "RECOVERY")))
568  updating_recovery = (source_recovery.data != target_recovery.data)
569
570  source_radio = source_zip.read("RADIO/image")
571  target_radio = target_zip.read("RADIO/image")
572  updating_radio = (source_radio != target_radio)
573
574  # The last 0.1 is reserved for creating symlinks, fixing
575  # permissions, and writing the boot image (if necessary).
576  progress_bar_total = 1.0
577  if updating_boot:
578    progress_bar_total -= 0.1
579  if updating_radio:
580    progress_bar_total -= 0.3
581
582  AppendAssertions(script, target_zip)
583
584  script.Print("Verifying current system...")
585
586  pb_verify = progress_bar_total * 0.3 * \
587              (total_patched_size /
588               float(total_patched_size+total_verbatim_size+1))
589
590  for i, (fn, tf, sf, size) in enumerate(patch_list):
591    if i % 5 == 0:
592      next_sizes = sum([i[3] for i in patch_list[i:i+5]])
593      script.ShowProgress(next_sizes * pb_verify / (total_patched_size+1), 1)
594
595    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
596
597  if updating_boot:
598    d = Difference(target_boot, source_boot, "imgdiff")
599    print "boot      target: %d  source: %d  diff: %d" % (
600        target_boot.size, source_boot.size, len(d))
601
602    common.ZipWriteStr(output_zip, "patch/boot.img.p", d)
603
604    script.PatchCheck("MTD:boot:%d:%s:%d:%s" %
605                      (source_boot.size, source_boot.sha1,
606                       target_boot.size, target_boot.sha1))
607
608  if patch_list or updating_recovery or updating_boot:
609    script.CacheFreeSpaceCheck(largest_source_size)
610    script.Print("Unpacking patches...")
611    script.UnpackPackageDir("patch", "/tmp/patchtmp")
612
613  script.Comment("---- start making changes here ----")
614
615  if OPTIONS.wipe_user_data:
616    script.Print("Erasing user data...")
617    script.FormatPartition("userdata")
618
619  script.Print("Removing unneeded files...")
620  script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
621                     ["/"+i for i in sorted(source_data)
622                            if i not in target_data])
623
624  if updating_boot:
625    # Produce the boot image by applying a patch to the current
626    # contents of the boot partition, and write it back to the
627    # partition.
628    script.Print("Patching boot image...")
629    script.ApplyPatch("MTD:boot:%d:%s:%d:%s"
630                      % (source_boot.size, source_boot.sha1,
631                         target_boot.size, target_boot.sha1),
632                      "-",
633                      target_boot.size, target_boot.sha1,
634                      source_boot.sha1, "/tmp/patchtmp/boot.img.p")
635    print "boot image changed; including."
636  else:
637    print "boot image unchanged; skipping."
638
639  if updating_recovery:
640    # Is it better to generate recovery as a patch from the current
641    # boot image, or from the previous recovery image?  For large
642    # updates with significant kernel changes, probably the former.
643    # For small updates where the kernel hasn't changed, almost
644    # certainly the latter.  We pick the first option.  Future
645    # complicated schemes may let us effectively use both.
646    #
647    # A wacky possibility: as long as there is room in the boot
648    # partition, include the binaries and image files from recovery in
649    # the boot image (though not in the ramdisk) so they can be used
650    # as fodder for constructing the recovery image.
651    recovery_sh_item = MakeRecoveryPatch(output_zip,
652                                         target_recovery, target_boot)
653    print "recovery image changed; including as patch from boot."
654  else:
655    print "recovery image unchanged; skipping."
656
657  if updating_radio:
658    script.ShowProgress(0.3, 10)
659    script.Print("Writing radio image...")
660    script.WriteFirmwareImage("radio", "radio.img")
661    common.ZipWriteStr(output_zip, "radio.img", target_radio)
662    print "radio image changed; including."
663  else:
664    print "radio 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("Finishing up...")
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  if OPTIONS.extra_script is not None:
730    scirpt.AppendExtra(OPTIONS.extra_script)
731
732  script.AddToZip(target_zip, output_zip)
733
734
735def main(argv):
736
737  def option_handler(o, a):
738    if o in ("-b", "--board_config"):
739      common.LoadBoardConfig(a)
740    elif o in ("-k", "--package_key"):
741      OPTIONS.package_key = a
742    elif o in ("-i", "--incremental_from"):
743      OPTIONS.incremental_source = a
744    elif o in ("-w", "--wipe_user_data"):
745      OPTIONS.wipe_user_data = True
746    elif o in ("-n", "--no_prereq"):
747      OPTIONS.omit_prereq = True
748    elif o in ("-e", "--extra_script"):
749      OPTIONS.extra_script = a
750    elif o in ("-m", "--script_mode"):
751      OPTIONS.script_mode = a
752    else:
753      return False
754    return True
755
756  args = common.ParseOptions(argv, __doc__,
757                             extra_opts="b:k:i:d:wne:m:",
758                             extra_long_opts=["board_config=",
759                                              "package_key=",
760                                              "incremental_from=",
761                                              "wipe_user_data",
762                                              "no_prereq",
763                                              "extra_script=",
764                                              "script_mode="],
765                             extra_option_handler=option_handler)
766
767  if len(args) != 2:
768    common.Usage(__doc__)
769    sys.exit(1)
770
771  if not OPTIONS.max_image_size:
772    print
773    print "  WARNING:  No board config specified; will not check image"
774    print "  sizes against limits.  Use -b to make sure the generated"
775    print "  images don't exceed partition sizes."
776    print
777
778  if OPTIONS.script_mode not in ("amend", "edify", "auto"):
779    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
780
781  if OPTIONS.extra_script is not None:
782    OPTIONS.extra_script = open(OPTIONS.extra_script).read()
783
784  print "unzipping target target-files..."
785  OPTIONS.input_tmp = common.UnzipTemp(args[0])
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