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