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