ota_from_target_files.py revision 048e7ca15f6391681490ce564bc71194adf146aa
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 not m:
314    raise ExternalError("failed to find required bootloaders in "
315                        "android-info.txt")
316  bootloaders = m.group(1).split("|")
317  script.append("assert " +
318                " || ".join(['getprop("ro.bootloader") == "%s"' % (b,)
319                             for b in bootloaders]))
320
321
322def IncludeBinary(name, input_zip, output_zip, input_path=None):
323  try:
324    if input_path is not None:
325      data = open(input_path).read()
326    else:
327      data = input_zip.read(os.path.join("OTA/bin", name))
328    common.ZipWriteStr(output_zip, name, data, perms=0755)
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  common.ZipWriteStr(output_zip, "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    common.ZipWriteStr(z, 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, diff_program):
406  """Return the patch (as a string of data) needed to turn sf into tf.
407  diff_program is the name of an external program (or list, if
408  additional arguments are desired) to run to generate the diff.
409  """
410
411  ttemp = tf.WriteToTemp()
412  stemp = sf.WriteToTemp()
413
414  ext = os.path.splitext(tf.name)[1]
415
416  try:
417    ptemp = tempfile.NamedTemporaryFile()
418    if isinstance(diff_program, list):
419      cmd = copy.copy(diff_program)
420    else:
421      cmd = [diff_program]
422    cmd.append(stemp.name)
423    cmd.append(ttemp.name)
424    cmd.append(ptemp.name)
425    p = common.Run(cmd)
426    _, err = p.communicate()
427    if err or p.returncode != 0:
428      print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
429      return None
430    diff = ptemp.read()
431  finally:
432    ptemp.close()
433    stemp.close()
434    ttemp.close()
435
436  return diff
437
438
439def GetBuildProp(property, z):
440  """Return the fingerprint of the build of a given target-files
441  ZipFile object."""
442  bp = z.read("SYSTEM/build.prop")
443  if not property:
444    return bp
445  m = re.search(re.escape(property) + r"=(.*)\n", bp)
446  if not m:
447    raise ExternalException("couldn't find %s in build.prop" % (property,))
448  return m.group(1).strip()
449
450
451def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
452  script = []
453
454  print "Loading target..."
455  target_data = LoadSystemFiles(target_zip)
456  print "Loading source..."
457  source_data = LoadSystemFiles(source_zip)
458
459  verbatim_targets = []
460  patch_list = []
461  largest_source_size = 0
462  for fn in sorted(target_data.keys()):
463    tf = target_data[fn]
464    sf = source_data.get(fn, None)
465
466    if sf is None or fn in OPTIONS.require_verbatim:
467      # This file should be included verbatim
468      if fn in OPTIONS.prohibit_verbatim:
469        raise ExternalError("\"%s\" must be sent verbatim" % (fn,))
470      print "send", fn, "verbatim"
471      tf.AddToZip(output_zip)
472      verbatim_targets.append((fn, tf.size))
473    elif tf.sha1 != sf.sha1:
474      # File is different; consider sending as a patch
475      diff_method = "bsdiff"
476      if tf.name.endswith(".gz"):
477        diff_method = "imgdiff"
478      d = Difference(tf, sf, diff_method)
479      if d is not None:
480        print fn, tf.size, len(d), (float(len(d)) / tf.size)
481      if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
482        # patch is almost as big as the file; don't bother patching
483        tf.AddToZip(output_zip)
484        verbatim_targets.append((fn, tf.size))
485      else:
486        common.ZipWriteStr(output_zip, "patch/" + fn + ".p", d)
487        patch_list.append((fn, tf, sf, tf.size))
488        largest_source_size = max(largest_source_size, sf.size)
489    else:
490      # Target file identical to source.
491      pass
492
493  total_verbatim_size = sum([i[1] for i in verbatim_targets])
494  total_patched_size = sum([i[3] for i in patch_list])
495
496  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
497  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
498
499  script.append(('assert file_contains("SYSTEM:build.prop", '
500                 '"ro.build.fingerprint=%s") == "true" || '
501                 'file_contains("SYSTEM:build.prop", '
502                 '"ro.build.fingerprint=%s") == "true"') %
503                (source_fp, target_fp))
504
505  source_boot = File("/tmp/boot.img",
506                     common.BuildBootableImage(
507      os.path.join(OPTIONS.source_tmp, "BOOT")))
508  target_boot = File("/tmp/boot.img",
509                     common.BuildBootableImage(
510      os.path.join(OPTIONS.target_tmp, "BOOT")))
511  updating_boot = (source_boot.data != target_boot.data)
512
513  source_recovery = File("system/recovery.img",
514                         common.BuildBootableImage(
515      os.path.join(OPTIONS.source_tmp, "RECOVERY")))
516  target_recovery = File("system/recovery.img",
517                         common.BuildBootableImage(
518      os.path.join(OPTIONS.target_tmp, "RECOVERY")))
519  updating_recovery = (source_recovery.data != target_recovery.data)
520
521  source_radio = source_zip.read("RADIO/image")
522  target_radio = target_zip.read("RADIO/image")
523  updating_radio = (source_radio != target_radio)
524
525  # The last 0.1 is reserved for creating symlinks, fixing
526  # permissions, and writing the boot image (if necessary).
527  progress_bar_total = 1.0
528  if updating_boot:
529    progress_bar_total -= 0.1
530  if updating_radio:
531    progress_bar_total -= 0.3
532
533  AppendAssertions(script, target_zip)
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.append("show_progress %f 1" %
543                    (next_sizes * pb_verify / (total_patched_size+1),))
544    script.append("run_program PACKAGE:applypatch -c /%s %s %s" %
545                  (fn, tf.sha1, sf.sha1))
546
547  if updating_recovery:
548    d = Difference(target_recovery, source_recovery, "imgdiff")
549    print "recovery  target: %d  source: %d  diff: %d" % (
550        target_recovery.size, source_recovery.size, len(d))
551
552    common.ZipWriteStr(output_zip, "patch/recovery.img.p", d)
553
554    script.append(("run_program PACKAGE:applypatch -c "
555                   "MTD:recovery:%d:%s:%d:%s") %
556                  (source_recovery.size, source_recovery.sha1,
557                   target_recovery.size, target_recovery.sha1))
558
559  if updating_boot:
560    d = Difference(target_boot, source_boot, "imgdiff")
561    print "boot      target: %d  source: %d  diff: %d" % (
562        target_boot.size, source_boot.size, len(d))
563
564    common.ZipWriteStr(output_zip, "patch/boot.img.p", d)
565
566    script.append(("run_program PACKAGE:applypatch -c "
567                   "MTD:boot:%d:%s:%d:%s") %
568                  (source_boot.size, source_boot.sha1,
569                   target_boot.size, target_boot.sha1))
570
571  if patch_list or updating_recovery or updating_boot:
572    script.append("run_program PACKAGE:applypatch -s %d" %
573                  (largest_source_size,))
574    script.append("copy_dir PACKAGE:patch CACHE:../tmp/patchtmp")
575    IncludeBinary("applypatch", target_zip, output_zip)
576
577  script.append("\n# ---- start making changes here\n")
578
579  if OPTIONS.wipe_user_data:
580    script.append("format DATA:")
581
582  DeleteFiles(script, [SubstituteRoot(i[0]) for i in verbatim_targets])
583
584  if updating_boot:
585    # Produce the boot image by applying a patch to the current
586    # contents of the boot partition, and write it back to the
587    # partition.
588    script.append(("run_program PACKAGE:applypatch "
589                   "MTD:boot:%d:%s:%d:%s - "
590                   "%s %d %s:/tmp/patchtmp/boot.img.p")
591                  % (source_boot.size, source_boot.sha1,
592                     target_boot.size, target_boot.sha1,
593                     target_boot.sha1,
594                     target_boot.size,
595                     source_boot.sha1))
596    print "boot image changed; including."
597  else:
598    print "boot image unchanged; skipping."
599
600  if updating_recovery:
601    # Produce /system/recovery.img by applying a patch to the current
602    # contents of the recovery partition.
603    script.append(("run_program PACKAGE:applypatch MTD:recovery:%d:%s:%d:%s "
604                   "/system/recovery.img %s %d %s:/tmp/patchtmp/recovery.img.p")
605                  % (source_recovery.size, source_recovery.sha1,
606                     target_recovery.size, target_recovery.sha1,
607                     target_recovery.sha1,
608                     target_recovery.size,
609                     source_recovery.sha1))
610    print "recovery image changed; including."
611  else:
612    print "recovery image unchanged; skipping."
613
614  if updating_radio:
615    script.append("show_progress 0.3 10")
616    script.append("write_radio_image PACKAGE:radio.img")
617    common.ZipWriteStr(output_zip, "radio.img", target_radio)
618    print "radio image changed; including."
619  else:
620    print "radio image unchanged; skipping."
621
622  pb_apply = progress_bar_total * 0.7 * \
623             (total_patched_size /
624              float(total_patched_size+total_verbatim_size+1))
625  for i, (fn, tf, sf, size) in enumerate(patch_list):
626    if i % 5 == 0:
627      next_sizes = sum([i[3] for i in patch_list[i:i+5]])
628      script.append("show_progress %f 1" %
629                    (next_sizes * pb_apply / (total_patched_size+1),))
630    script.append(("run_program PACKAGE:applypatch "
631                   "/%s - %s %d %s:/tmp/patchtmp/%s.p") %
632                  (fn, tf.sha1, tf.size, sf.sha1, fn))
633
634  target_symlinks = CopySystemFiles(target_zip, None)
635
636  target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
637  temp_script = []
638  FixPermissions(temp_script)
639
640  # Note that this call will mess up the tree of Items, so make sure
641  # we're done with it.
642  source_symlinks = CopySystemFiles(source_zip, None)
643  source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
644
645  # Delete all the symlinks in source that aren't in target.  This
646  # needs to happen before verbatim files are unpacked, in case a
647  # symlink in the source is replaced by a real file in the target.
648  to_delete = []
649  for dest, link in source_symlinks:
650    if link not in target_symlinks_d:
651      to_delete.append(link)
652  DeleteFiles(script, to_delete)
653
654  if verbatim_targets:
655    pb_verbatim = progress_bar_total * \
656                  (total_verbatim_size /
657                   float(total_patched_size+total_verbatim_size+1))
658    script.append("show_progress %f 5" % (pb_verbatim,))
659    script.append("copy_dir PACKAGE:system SYSTEM:")
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  DeleteFiles(script, [i[1] for i in to_create])
672  script.extend(["symlink %s %s" % s for s in to_create])
673
674  # Now that the symlinks are created, we can set all the
675  # permissions.
676  script.extend(temp_script)
677
678  if OPTIONS.extra_script is not None:
679    script.append(OPTIONS.extra_script)
680
681  AddScript(script, 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    else:
700      return False
701    return True
702
703  args = common.ParseOptions(argv, __doc__,
704                             extra_opts="b:k:i:d:wne:",
705                             extra_long_opts=["board_config=",
706                                              "package_key=",
707                                              "incremental_from=",
708                                              "wipe_user_data",
709                                              "no_prereq",
710                                              "extra_script="],
711                             extra_option_handler=option_handler)
712
713  if len(args) != 2:
714    common.Usage(__doc__)
715    sys.exit(1)
716
717  if not OPTIONS.max_image_size:
718    print
719    print "  WARNING:  No board config specified; will not check image"
720    print "  sizes against limits.  Use -b to make sure the generated"
721    print "  images don't exceed partition sizes."
722    print
723
724  if OPTIONS.extra_script is not None:
725    OPTIONS.extra_script = open(OPTIONS.extra_script).read()
726
727  print "unzipping target target-files..."
728  OPTIONS.input_tmp = common.UnzipTemp(args[0])
729  OPTIONS.target_tmp = OPTIONS.input_tmp
730  input_zip = zipfile.ZipFile(args[0], "r")
731  if OPTIONS.package_key:
732    temp_zip_file = tempfile.NamedTemporaryFile()
733    output_zip = zipfile.ZipFile(temp_zip_file, "w",
734                                 compression=zipfile.ZIP_DEFLATED)
735  else:
736    output_zip = zipfile.ZipFile(args[1], "w",
737                                 compression=zipfile.ZIP_DEFLATED)
738
739  if OPTIONS.incremental_source is None:
740    WriteFullOTAPackage(input_zip, output_zip)
741  else:
742    print "unzipping source target-files..."
743    OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
744    source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
745    WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
746
747  output_zip.close()
748  if OPTIONS.package_key:
749    SignOutput(temp_zip_file.name, args[1])
750    temp_zip_file.close()
751
752  common.Cleanup()
753
754  print "done."
755
756
757if __name__ == '__main__':
758  try:
759    main(sys.argv[1:])
760  except common.ExternalError, e:
761    print
762    print "   ERROR: %s" % (e,)
763    print
764    sys.exit(1)
765