ota_from_target_files.py revision 881dd40ffb683fed465df955f3fd21812fae59aa
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      Deprecated.
26
27  -k  (--package_key)  <key>
28      Key to use to sign the package (default is
29      "build/target/product/security/testkey").
30
31  -i  (--incremental_from)  <file>
32      Generate an incremental OTA using the given target-files zip as
33      the starting build.
34
35  -w  (--wipe_user_data)
36      Generate an OTA package that will wipe the user data partition
37      when installed.
38
39  -n  (--no_prereq)
40      Omit the timestamp prereq check normally included at the top of
41      the build scripts (used for developer OTA packages which
42      legitimately need to go back and forth).
43
44  -e  (--extra_script)  <file>
45      Insert the contents of file at the end of the update script.
46
47  -m  (--script_mode)  <mode>
48      Specify 'amend' or 'edify' scripts, or 'auto' to pick
49      automatically (this is the default).
50
51"""
52
53import sys
54
55if sys.hexversion < 0x02040000:
56  print >> sys.stderr, "Python 2.4 or newer is required."
57  sys.exit(1)
58
59import copy
60import os
61import re
62import sha
63import subprocess
64import tempfile
65import time
66import zipfile
67
68import common
69import amend_generator
70import edify_generator
71import both_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                  whole_file=True)
278
279
280def AppendAssertions(script, input_zip):
281  device = GetBuildProp("ro.product.device", input_zip)
282  script.AssertDevice(device)
283
284
285def MakeRecoveryPatch(output_zip, recovery_img, boot_img):
286  """Generate a binary patch that creates the recovery image starting
287  with the boot image.  (Most of the space in these images is just the
288  kernel, which is identical for the two, so the resulting patch
289  should be efficient.)  Add it to the output zip, along with a shell
290  script that is run from init.rc on first boot to actually do the
291  patching and install the new recovery image.
292
293  recovery_img and boot_img should be File objects for the
294  corresponding images.
295
296  Returns an Item for the shell script, which must be made
297  executable.
298  """
299
300  patch = Difference(recovery_img, boot_img, "imgdiff")
301  common.ZipWriteStr(output_zip, "system/recovery-from-boot.p", patch)
302  Item.Get("system/recovery-from-boot.p", dir=False)
303
304  # Images with different content will have a different first page, so
305  # we check to see if this recovery has already been installed by
306  # testing just the first 2k.
307  HEADER_SIZE = 2048
308  header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()
309  sh = """#!/system/bin/sh
310if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then
311  log -t recovery "Installing new recovery image"
312  applypatch MTD:boot:%(boot_size)d:%(boot_sha1)s MTD:recovery %(recovery_sha1)s %(recovery_size)d %(boot_sha1)s:/system/recovery-from-boot.p
313else
314  log -t recovery "Recovery image already installed"
315fi
316""" % { 'boot_size': boot_img.size,
317        'boot_sha1': boot_img.sha1,
318        'header_size': HEADER_SIZE,
319        'header_sha1': header_sha1,
320        'recovery_size': recovery_img.size,
321        'recovery_sha1': recovery_img.sha1 }
322  common.ZipWriteStr(output_zip, "system/etc/install-recovery.sh", sh)
323  return Item.Get("system/etc/install-recovery.sh", dir=False)
324
325
326def WriteFullOTAPackage(input_zip, output_zip):
327  if OPTIONS.script_mode == "auto":
328    script = both_generator.BothGenerator(2)
329  elif OPTIONS.script_mode == "amend":
330    script = amend_generator.AmendGenerator()
331  else:
332    # TODO: how to determine this?  We don't know what version it will
333    # be installed on top of.  For now, we expect the API just won't
334    # change very often.
335    script = edify_generator.EdifyGenerator(2)
336
337  device_specific = common.DeviceSpecificParams(
338      input_zip=input_zip,
339      output_zip=output_zip,
340      script=script,
341      input_tmp=OPTIONS.input_tmp)
342
343  if not OPTIONS.omit_prereq:
344    ts = GetBuildProp("ro.build.date.utc", input_zip)
345    script.AssertOlderBuild(ts)
346
347  AppendAssertions(script, input_zip)
348  device_specific.FullOTA_Assertions()
349
350  script.ShowProgress(0.5, 0)
351
352  if OPTIONS.wipe_user_data:
353    script.FormatPartition("userdata")
354
355  script.FormatPartition("system")
356  script.Mount("MTD", "system", "/system")
357  script.UnpackPackageDir("system", "/system")
358
359  symlinks = CopySystemFiles(input_zip, output_zip)
360  script.MakeSymlinks(symlinks)
361
362  boot_img = File("boot.img", common.BuildBootableImage(
363      os.path.join(OPTIONS.input_tmp, "BOOT")))
364  recovery_img = File("recovery.img", common.BuildBootableImage(
365      os.path.join(OPTIONS.input_tmp, "RECOVERY")))
366  i = MakeRecoveryPatch(output_zip, recovery_img, boot_img)
367
368  Item.GetMetadata()
369
370  # GetMetadata uses the data in android_filesystem_config.h to assign
371  # the uid/gid/mode of all files.  We want to override that for the
372  # recovery patching shell script to make it executable.
373  i.uid = 0
374  i.gid = 0
375  i.mode = 0544
376  Item.Get("system").SetPermissions(script)
377
378  common.CheckSize(boot_img.data, "boot.img")
379  common.ZipWriteStr(output_zip, "boot.img", boot_img.data)
380  script.ShowProgress(0.2, 0)
381
382  script.ShowProgress(0.2, 10)
383  script.WriteRawImage("boot", "boot.img")
384
385  script.ShowProgress(0.1, 0)
386  device_specific.FullOTA_InstallEnd()
387
388  if OPTIONS.extra_script is not None:
389    script.AppendExtra(OPTIONS.extra_script)
390
391  script.AddToZip(input_zip, output_zip)
392
393
394class File(object):
395  def __init__(self, name, data):
396    self.name = name
397    self.data = data
398    self.size = len(data)
399    self.sha1 = sha.sha(data).hexdigest()
400
401  def WriteToTemp(self):
402    t = tempfile.NamedTemporaryFile()
403    t.write(self.data)
404    t.flush()
405    return t
406
407  def AddToZip(self, z):
408    common.ZipWriteStr(z, self.name, self.data)
409
410
411def LoadSystemFiles(z):
412  """Load all the files from SYSTEM/... in a given target-files
413  ZipFile, and return a dict of {filename: File object}."""
414  out = {}
415  for info in z.infolist():
416    if info.filename.startswith("SYSTEM/") and not IsSymlink(info):
417      fn = "system/" + info.filename[7:]
418      data = z.read(info.filename)
419      out[fn] = File(fn, data)
420  return out
421
422
423def Difference(tf, sf, diff_program):
424  """Return the patch (as a string of data) needed to turn sf into tf.
425  diff_program is the name of an external program (or list, if
426  additional arguments are desired) to run to generate the diff.
427  """
428
429  ttemp = tf.WriteToTemp()
430  stemp = sf.WriteToTemp()
431
432  ext = os.path.splitext(tf.name)[1]
433
434  try:
435    ptemp = tempfile.NamedTemporaryFile()
436    if isinstance(diff_program, list):
437      cmd = copy.copy(diff_program)
438    else:
439      cmd = [diff_program]
440    cmd.append(stemp.name)
441    cmd.append(ttemp.name)
442    cmd.append(ptemp.name)
443    p = common.Run(cmd)
444    _, err = p.communicate()
445    if err or p.returncode != 0:
446      print "WARNING: failure running %s:\n%s\n" % (diff_program, err)
447      return None
448    diff = ptemp.read()
449  finally:
450    ptemp.close()
451    stemp.close()
452    ttemp.close()
453
454  return diff
455
456
457def GetBuildProp(property, z):
458  """Return the fingerprint of the build of a given target-files
459  ZipFile object."""
460  bp = z.read("SYSTEM/build.prop")
461  if not property:
462    return bp
463  m = re.search(re.escape(property) + r"=(.*)\n", bp)
464  if not m:
465    raise common.ExternalError("couldn't find %s in build.prop" % (property,))
466  return m.group(1).strip()
467
468
469def GetRecoveryAPIVersion(zip):
470  """Returns the version of the recovery API.  Version 0 is the older
471  amend code (no separate binary)."""
472  try:
473    version = zip.read("META/recovery-api-version.txt")
474    return int(version)
475  except KeyError:
476    try:
477      # version one didn't have the recovery-api-version.txt file, but
478      # it did include an updater binary.
479      zip.getinfo("OTA/bin/updater")
480      return 1
481    except KeyError:
482      return 0
483
484
485DIFF_METHOD_BY_EXT = {
486    ".gz" : "imgdiff",
487    ".zip" : ["imgdiff", "-z"],
488    ".jar" : ["imgdiff", "-z"],
489    ".apk" : ["imgdiff", "-z"],
490    }
491
492
493def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
494  source_version = GetRecoveryAPIVersion(source_zip)
495
496  if OPTIONS.script_mode == 'amend':
497    script = amend_generator.AmendGenerator()
498  elif OPTIONS.script_mode == 'edify':
499    if source_version == 0:
500      print ("WARNING: generating edify script for a source that "
501             "can't install it.")
502    script = edify_generator.EdifyGenerator(source_version)
503  elif OPTIONS.script_mode == 'auto':
504    if source_version > 0:
505      script = edify_generator.EdifyGenerator(source_version)
506    else:
507      script = amend_generator.AmendGenerator()
508  else:
509    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
510
511  device_specific = common.DeviceSpecificParams(
512      source_zip=source_zip,
513      target_zip=target_zip,
514      output_zip=output_zip,
515      script=script)
516
517  print "Loading target..."
518  target_data = LoadSystemFiles(target_zip)
519  print "Loading source..."
520  source_data = LoadSystemFiles(source_zip)
521
522  verbatim_targets = []
523  patch_list = []
524  largest_source_size = 0
525  for fn in sorted(target_data.keys()):
526    tf = target_data[fn]
527    sf = source_data.get(fn, None)
528
529    if sf is None or fn in OPTIONS.require_verbatim:
530      # This file should be included verbatim
531      if fn in OPTIONS.prohibit_verbatim:
532        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
533      print "send", fn, "verbatim"
534      tf.AddToZip(output_zip)
535      verbatim_targets.append((fn, tf.size))
536    elif tf.sha1 != sf.sha1:
537      # File is different; consider sending as a patch
538      ext = os.path.splitext(tf.name)[1]
539      diff_method = DIFF_METHOD_BY_EXT.get(ext, "bsdiff")
540      d = Difference(tf, sf, diff_method)
541      if d is not None:
542        print fn, tf.size, len(d), (float(len(d)) / tf.size)
543      if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
544        # patch is almost as big as the file; don't bother patching
545        tf.AddToZip(output_zip)
546        verbatim_targets.append((fn, tf.size))
547      else:
548        common.ZipWriteStr(output_zip, "patch/" + fn + ".p", d)
549        patch_list.append((fn, tf, sf, tf.size))
550        largest_source_size = max(largest_source_size, sf.size)
551    else:
552      # Target file identical to source.
553      pass
554
555  total_verbatim_size = sum([i[1] for i in verbatim_targets])
556  total_patched_size = sum([i[3] for i in patch_list])
557
558  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
559  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
560
561  script.Mount("MTD", "system", "/system")
562  script.AssertSomeFingerprint(source_fp, target_fp)
563
564  source_boot = File("/tmp/boot.img",
565                     common.BuildBootableImage(
566      os.path.join(OPTIONS.source_tmp, "BOOT")))
567  target_boot = File("/tmp/boot.img",
568                     common.BuildBootableImage(
569      os.path.join(OPTIONS.target_tmp, "BOOT")))
570  updating_boot = (source_boot.data != target_boot.data)
571
572  source_recovery = File("system/recovery.img",
573                         common.BuildBootableImage(
574      os.path.join(OPTIONS.source_tmp, "RECOVERY")))
575  target_recovery = File("system/recovery.img",
576                         common.BuildBootableImage(
577      os.path.join(OPTIONS.target_tmp, "RECOVERY")))
578  updating_recovery = (source_recovery.data != target_recovery.data)
579
580  # Here's how we divide up the progress bar:
581  #  0.1 for verifying the start state (PatchCheck calls)
582  #  0.8 for applying patches (ApplyPatch calls)
583  #  0.1 for unpacking verbatim files, symlinking, and doing the
584  #      device-specific commands.
585
586  AppendAssertions(script, target_zip)
587  device_specific.IncrementalOTA_Assertions()
588
589  script.Print("Verifying current system...")
590
591  script.ShowProgress(0.1, 0)
592  total_verify_size = float(sum([i[2].size for i in patch_list]) + 1)
593  if updating_boot:
594    total_verify_size += source_boot.size
595  so_far = 0
596
597  for fn, tf, sf, size in patch_list:
598    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
599    so_far += sf.size
600    script.SetProgress(so_far / total_verify_size)
601
602  if updating_boot:
603    d = Difference(target_boot, source_boot, "imgdiff")
604    print "boot      target: %d  source: %d  diff: %d" % (
605        target_boot.size, source_boot.size, len(d))
606
607    common.ZipWriteStr(output_zip, "patch/boot.img.p", d)
608
609    script.PatchCheck("MTD:boot:%d:%s:%d:%s" %
610                      (source_boot.size, source_boot.sha1,
611                       target_boot.size, target_boot.sha1))
612    so_far += source_boot.size
613    script.SetProgress(so_far / total_verify_size)
614
615  if patch_list or updating_recovery or updating_boot:
616    script.CacheFreeSpaceCheck(largest_source_size)
617    script.Print("Unpacking patches...")
618    script.UnpackPackageDir("patch", "/tmp/patchtmp")
619
620  device_specific.IncrementalOTA_VerifyEnd()
621
622  script.Comment("---- start making changes here ----")
623
624  if OPTIONS.wipe_user_data:
625    script.Print("Erasing user data...")
626    script.FormatPartition("userdata")
627
628  script.Print("Removing unneeded files...")
629  script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
630                     ["/"+i for i in sorted(source_data)
631                            if i not in target_data] +
632                     ["/system/recovery.img"])
633
634  script.ShowProgress(0.8, 0)
635  total_patch_size = float(sum([i[1].size for i in patch_list]) + 1)
636  if updating_boot:
637    total_patch_size += target_boot.size
638  so_far = 0
639
640  script.Print("Patching system files...")
641  for fn, tf, sf, size in patch_list:
642    script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1,
643                      sf.sha1, "/tmp/patchtmp/"+fn+".p")
644    so_far += tf.size
645    script.SetProgress(so_far / total_patch_size)
646
647  if updating_boot:
648    # Produce the boot image by applying a patch to the current
649    # contents of the boot partition, and write it back to the
650    # partition.
651    script.Print("Patching boot image...")
652    script.ApplyPatch("MTD:boot:%d:%s:%d:%s"
653                      % (source_boot.size, source_boot.sha1,
654                         target_boot.size, target_boot.sha1),
655                      "-",
656                      target_boot.size, target_boot.sha1,
657                      source_boot.sha1, "/tmp/patchtmp/boot.img.p")
658    so_far += target_boot.size
659    script.SetProgress(so_far / total_patch_size)
660    print "boot image changed; including."
661  else:
662    print "boot image unchanged; skipping."
663
664  if updating_recovery:
665    # Is it better to generate recovery as a patch from the current
666    # boot image, or from the previous recovery image?  For large
667    # updates with significant kernel changes, probably the former.
668    # For small updates where the kernel hasn't changed, almost
669    # certainly the latter.  We pick the first option.  Future
670    # complicated schemes may let us effectively use both.
671    #
672    # A wacky possibility: as long as there is room in the boot
673    # partition, include the binaries and image files from recovery in
674    # the boot image (though not in the ramdisk) so they can be used
675    # as fodder for constructing the recovery image.
676    recovery_sh_item = MakeRecoveryPatch(output_zip,
677                                         target_recovery, target_boot)
678    print "recovery image changed; including as patch from boot."
679  else:
680    print "recovery image unchanged; skipping."
681
682  script.ShowProgress(0.1, 10)
683
684  target_symlinks = CopySystemFiles(target_zip, None)
685
686  target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
687  temp_script = script.MakeTemporary()
688  Item.GetMetadata()
689  if updating_recovery:
690    recovery_sh_item.uid = 0
691    recovery_sh_item.gid = 0
692    recovery_sh_item.mode = 0544
693  Item.Get("system").SetPermissions(temp_script)
694
695  # Note that this call will mess up the tree of Items, so make sure
696  # we're done with it.
697  source_symlinks = CopySystemFiles(source_zip, None)
698  source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
699
700  # Delete all the symlinks in source that aren't in target.  This
701  # needs to happen before verbatim files are unpacked, in case a
702  # symlink in the source is replaced by a real file in the target.
703  to_delete = []
704  for dest, link in source_symlinks:
705    if link not in target_symlinks_d:
706      to_delete.append(link)
707  script.DeleteFiles(to_delete)
708
709  if verbatim_targets:
710    script.Print("Unpacking new files...")
711    script.UnpackPackageDir("system", "/system")
712
713  script.Print("Symlinks and permissions...")
714
715  # Create all the symlinks that don't already exist, or point to
716  # somewhere different than what we want.  Delete each symlink before
717  # creating it, since the 'symlink' command won't overwrite.
718  to_create = []
719  for dest, link in target_symlinks:
720    if link in source_symlinks_d:
721      if dest != source_symlinks_d[link]:
722        to_create.append((dest, link))
723    else:
724      to_create.append((dest, link))
725  script.DeleteFiles([i[1] for i in to_create])
726  script.MakeSymlinks(to_create)
727
728  # Now that the symlinks are created, we can set all the
729  # permissions.
730  script.AppendScript(temp_script)
731
732  # Do device-specific installation (eg, write radio image).
733  device_specific.IncrementalOTA_InstallEnd()
734
735  if OPTIONS.extra_script is not None:
736    scirpt.AppendExtra(OPTIONS.extra_script)
737
738  script.AddToZip(target_zip, output_zip)
739
740
741def main(argv):
742
743  def option_handler(o, a):
744    if o in ("-b", "--board_config"):
745      pass   # deprecated
746    elif o in ("-k", "--package_key"):
747      OPTIONS.package_key = a
748    elif o in ("-i", "--incremental_from"):
749      OPTIONS.incremental_source = a
750    elif o in ("-w", "--wipe_user_data"):
751      OPTIONS.wipe_user_data = True
752    elif o in ("-n", "--no_prereq"):
753      OPTIONS.omit_prereq = True
754    elif o in ("-e", "--extra_script"):
755      OPTIONS.extra_script = a
756    elif o in ("-m", "--script_mode"):
757      OPTIONS.script_mode = a
758    else:
759      return False
760    return True
761
762  args = common.ParseOptions(argv, __doc__,
763                             extra_opts="b:k:i:d:wne:m:",
764                             extra_long_opts=["board_config=",
765                                              "package_key=",
766                                              "incremental_from=",
767                                              "wipe_user_data",
768                                              "no_prereq",
769                                              "extra_script=",
770                                              "script_mode="],
771                             extra_option_handler=option_handler)
772
773  if len(args) != 2:
774    common.Usage(__doc__)
775    sys.exit(1)
776
777  if OPTIONS.script_mode not in ("amend", "edify", "auto"):
778    raise ValueError('unknown script mode "%s"' % (OPTIONS.script_mode,))
779
780  if OPTIONS.extra_script is not None:
781    OPTIONS.extra_script = open(OPTIONS.extra_script).read()
782
783  print "unzipping target target-files..."
784  OPTIONS.input_tmp = common.UnzipTemp(args[0])
785
786  common.LoadMaxSizes()
787  if not OPTIONS.max_image_size:
788    print
789    print "  WARNING:  Failed to load max image sizes; will not enforce"
790    print "  image size limits."
791    print
792
793  OPTIONS.target_tmp = OPTIONS.input_tmp
794  input_zip = zipfile.ZipFile(args[0], "r")
795  if OPTIONS.package_key:
796    temp_zip_file = tempfile.NamedTemporaryFile()
797    output_zip = zipfile.ZipFile(temp_zip_file, "w",
798                                 compression=zipfile.ZIP_DEFLATED)
799  else:
800    output_zip = zipfile.ZipFile(args[1], "w",
801                                 compression=zipfile.ZIP_DEFLATED)
802
803  if OPTIONS.incremental_source is None:
804    WriteFullOTAPackage(input_zip, output_zip)
805  else:
806    print "unzipping source target-files..."
807    OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
808    source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
809    WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
810
811  output_zip.close()
812  if OPTIONS.package_key:
813    SignOutput(temp_zip_file.name, args[1])
814    temp_zip_file.close()
815
816  common.Cleanup()
817
818  print "done."
819
820
821if __name__ == '__main__':
822  try:
823    main(sys.argv[1:])
824  except common.ExternalError, e:
825    print
826    print "   ERROR: %s" % (e,)
827    print
828    sys.exit(1)
829