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