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