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