ota_from_target_files.py revision ea5d7a9de7660bef5b9c68f6372a92d4b2f2f1f6
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"""
48
49import sys
50
51if sys.hexversion < 0x02040000:
52  print >> sys.stderr, "Python 2.4 or newer is required."
53  sys.exit(1)
54
55import copy
56import errno
57import os
58import re
59import sha
60import subprocess
61import tempfile
62import time
63import zipfile
64
65import common
66import edify_generator
67
68OPTIONS = common.OPTIONS
69OPTIONS.package_key = "build/target/product/security/testkey"
70OPTIONS.incremental_source = None
71OPTIONS.require_verbatim = set()
72OPTIONS.prohibit_verbatim = set(("system/build.prop",))
73OPTIONS.patch_threshold = 0.95
74OPTIONS.wipe_user_data = False
75OPTIONS.omit_prereq = False
76OPTIONS.extra_script = None
77OPTIONS.worker_threads = 3
78
79def MostPopularKey(d, default):
80  """Given a dict, return the key corresponding to the largest
81  value.  Returns 'default' if the dict is empty."""
82  x = [(v, k) for (k, v) in d.iteritems()]
83  if not x: return default
84  x.sort()
85  return x[-1][1]
86
87
88def IsSymlink(info):
89  """Return true if the zipfile.ZipInfo object passed in represents a
90  symlink."""
91  return (info.external_attr >> 16) == 0120777
92
93
94
95class Item:
96  """Items represent the metadata (user, group, mode) of files and
97  directories in the system image."""
98  ITEMS = {}
99  def __init__(self, name, dir=False):
100    self.name = name
101    self.uid = None
102    self.gid = None
103    self.mode = None
104    self.dir = dir
105
106    if name:
107      self.parent = Item.Get(os.path.dirname(name), dir=True)
108      self.parent.children.append(self)
109    else:
110      self.parent = None
111    if dir:
112      self.children = []
113
114  def Dump(self, indent=0):
115    if self.uid is not None:
116      print "%s%s %d %d %o" % ("  "*indent, self.name, self.uid, self.gid, self.mode)
117    else:
118      print "%s%s %s %s %s" % ("  "*indent, self.name, self.uid, self.gid, self.mode)
119    if self.dir:
120      print "%s%s" % ("  "*indent, self.descendants)
121      print "%s%s" % ("  "*indent, self.best_subtree)
122      for i in self.children:
123        i.Dump(indent=indent+1)
124
125  @classmethod
126  def Get(cls, name, dir=False):
127    if name not in cls.ITEMS:
128      cls.ITEMS[name] = Item(name, dir=dir)
129    return cls.ITEMS[name]
130
131  @classmethod
132  def GetMetadata(cls, input_zip):
133
134    try:
135      # See if the target_files contains a record of what the uid,
136      # gid, and mode is supposed to be.
137      output = input_zip.read("META/filesystem_config.txt")
138    except KeyError:
139      # Run the external 'fs_config' program to determine the desired
140      # uid, gid, and mode for every Item object.  Note this uses the
141      # one in the client now, which might not be the same as the one
142      # used when this target_files was built.
143      p = common.Run(["fs_config"], stdin=subprocess.PIPE,
144                     stdout=subprocess.PIPE, stderr=subprocess.PIPE)
145      suffix = { False: "", True: "/" }
146      input = "".join(["%s%s\n" % (i.name, suffix[i.dir])
147                       for i in cls.ITEMS.itervalues() if i.name])
148      output, error = p.communicate(input)
149      assert not error
150
151    for line in output.split("\n"):
152      if not line: continue
153      name, uid, gid, mode = line.split()
154      i = cls.ITEMS.get(name, None)
155      if i is not None:
156        i.uid = int(uid)
157        i.gid = int(gid)
158        i.mode = int(mode, 8)
159        if i.dir:
160          i.children.sort(key=lambda i: i.name)
161
162    # set metadata for the files generated by this script.
163    i = cls.ITEMS.get("system/recovery-from-boot.p", None)
164    if i: i.uid, i.gid, i.mode = 0, 0, 0644
165    i = cls.ITEMS.get("system/etc/install-recovery.sh", None)
166    if i: i.uid, i.gid, i.mode = 0, 0, 0544
167
168  def CountChildMetadata(self):
169    """Count up the (uid, gid, mode) tuples for all children and
170    determine the best strategy for using set_perm_recursive and
171    set_perm to correctly chown/chmod all the files to their desired
172    values.  Recursively calls itself for all descendants.
173
174    Returns a dict of {(uid, gid, dmode, fmode): count} counting up
175    all descendants of this node.  (dmode or fmode may be None.)  Also
176    sets the best_subtree of each directory Item to the (uid, gid,
177    dmode, fmode) tuple that will match the most descendants of that
178    Item.
179    """
180
181    assert self.dir
182    d = self.descendants = {(self.uid, self.gid, self.mode, None): 1}
183    for i in self.children:
184      if i.dir:
185        for k, v in i.CountChildMetadata().iteritems():
186          d[k] = d.get(k, 0) + v
187      else:
188        k = (i.uid, i.gid, None, i.mode)
189        d[k] = d.get(k, 0) + 1
190
191    # Find the (uid, gid, dmode, fmode) tuple that matches the most
192    # descendants.
193
194    # First, find the (uid, gid) pair that matches the most
195    # descendants.
196    ug = {}
197    for (uid, gid, _, _), count in d.iteritems():
198      ug[(uid, gid)] = ug.get((uid, gid), 0) + count
199    ug = MostPopularKey(ug, (0, 0))
200
201    # Now find the dmode and fmode that match the most descendants
202    # with that (uid, gid), and choose those.
203    best_dmode = (0, 0755)
204    best_fmode = (0, 0644)
205    for k, count in d.iteritems():
206      if k[:2] != ug: continue
207      if k[2] is not None and count >= best_dmode[0]: best_dmode = (count, k[2])
208      if k[3] is not None and count >= best_fmode[0]: best_fmode = (count, k[3])
209    self.best_subtree = ug + (best_dmode[1], best_fmode[1])
210
211    return d
212
213  def SetPermissions(self, script):
214    """Append set_perm/set_perm_recursive commands to 'script' to
215    set all permissions, users, and groups for the tree of files
216    rooted at 'self'."""
217
218    self.CountChildMetadata()
219
220    def recurse(item, current):
221      # current is the (uid, gid, dmode, fmode) tuple that the current
222      # item (and all its children) have already been set to.  We only
223      # need to issue set_perm/set_perm_recursive commands if we're
224      # supposed to be something different.
225      if item.dir:
226        if current != item.best_subtree:
227          script.SetPermissionsRecursive("/"+item.name, *item.best_subtree)
228          current = item.best_subtree
229
230        if item.uid != current[0] or item.gid != current[1] or \
231           item.mode != current[2]:
232          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
233
234        for i in item.children:
235          recurse(i, current)
236      else:
237        if item.uid != current[0] or item.gid != current[1] or \
238               item.mode != current[3]:
239          script.SetPermissions("/"+item.name, item.uid, item.gid, item.mode)
240
241    recurse(self, (-1, -1, -1, -1))
242
243
244def CopySystemFiles(input_zip, output_zip=None,
245                    substitute=None):
246  """Copies files underneath system/ in the input zip to the output
247  zip.  Populates the Item class with their metadata, and returns a
248  list of symlinks.  output_zip may be None, in which case the copy is
249  skipped (but the other side effects still happen).  substitute is an
250  optional dict of {output filename: contents} to be output instead of
251  certain input files.
252  """
253
254  symlinks = []
255
256  for info in input_zip.infolist():
257    if info.filename.startswith("SYSTEM/"):
258      basefilename = info.filename[7:]
259      if IsSymlink(info):
260        symlinks.append((input_zip.read(info.filename),
261                         "/system/" + basefilename))
262      else:
263        info2 = copy.copy(info)
264        fn = info2.filename = "system/" + basefilename
265        if substitute and fn in substitute and substitute[fn] is None:
266          continue
267        if output_zip is not None:
268          if substitute and fn in substitute:
269            data = substitute[fn]
270          else:
271            data = input_zip.read(info.filename)
272          output_zip.writestr(info2, data)
273        if fn.endswith("/"):
274          Item.Get(fn[:-1], dir=True)
275        else:
276          Item.Get(fn, dir=False)
277
278  symlinks.sort()
279  return symlinks
280
281
282def SignOutput(temp_zip_name, output_zip_name):
283  key_passwords = common.GetKeyPasswords([OPTIONS.package_key])
284  pw = key_passwords[OPTIONS.package_key]
285
286  common.SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
287                  whole_file=True)
288
289
290def AppendAssertions(script, input_zip):
291  device = GetBuildProp("ro.product.device", input_zip)
292  script.AssertDevice(device)
293
294
295def MakeRecoveryPatch(output_zip, recovery_img, boot_img):
296  """Generate a binary patch that creates the recovery image starting
297  with the boot image.  (Most of the space in these images is just the
298  kernel, which is identical for the two, so the resulting patch
299  should be efficient.)  Add it to the output zip, along with a shell
300  script that is run from init.rc on first boot to actually do the
301  patching and install the new recovery image.
302
303  recovery_img and boot_img should be File objects for the
304  corresponding images.
305
306  Returns an Item for the shell script, which must be made
307  executable.
308  """
309
310  d = common.Difference(recovery_img, boot_img)
311  _, _, patch = d.ComputePatch()
312  common.ZipWriteStr(output_zip, "recovery/recovery-from-boot.p", patch)
313  Item.Get("system/recovery-from-boot.p", dir=False)
314
315  # Images with different content will have a different first page, so
316  # we check to see if this recovery has already been installed by
317  # testing just the first 2k.
318  HEADER_SIZE = 2048
319  header_sha1 = sha.sha(recovery_img.data[:HEADER_SIZE]).hexdigest()
320  sh = """#!/system/bin/sh
321if ! applypatch -c MTD:recovery:%(header_size)d:%(header_sha1)s; then
322  log -t recovery "Installing new recovery image"
323  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
324else
325  log -t recovery "Recovery image already installed"
326fi
327""" % { 'boot_size': boot_img.size,
328        'boot_sha1': boot_img.sha1,
329        'header_size': HEADER_SIZE,
330        'header_sha1': header_sha1,
331        'recovery_size': recovery_img.size,
332        'recovery_sha1': recovery_img.sha1 }
333  common.ZipWriteStr(output_zip, "recovery/etc/install-recovery.sh", sh)
334  return Item.Get("system/etc/install-recovery.sh", dir=False)
335
336
337def WriteFullOTAPackage(input_zip, output_zip):
338  # TODO: how to determine this?  We don't know what version it will
339  # be installed on top of.  For now, we expect the API just won't
340  # change very often.
341  script = edify_generator.EdifyGenerator(3)
342
343  metadata = {"post-build": GetBuildProp("ro.build.fingerprint", input_zip),
344              "pre-device": GetBuildProp("ro.product.device", input_zip),
345              "post-timestamp": GetBuildProp("ro.build.date.utc", input_zip),
346              }
347
348  device_specific = common.DeviceSpecificParams(
349      input_zip=input_zip,
350      input_version=GetRecoveryAPIVersion(input_zip),
351      output_zip=output_zip,
352      script=script,
353      input_tmp=OPTIONS.input_tmp,
354      metadata=metadata)
355
356  if not OPTIONS.omit_prereq:
357    ts = GetBuildProp("ro.build.date.utc", input_zip)
358    script.AssertOlderBuild(ts)
359
360  AppendAssertions(script, input_zip)
361  device_specific.FullOTA_Assertions()
362
363  script.ShowProgress(0.5, 0)
364
365  if OPTIONS.wipe_user_data:
366    script.FormatPartition("userdata")
367
368  script.FormatPartition("system")
369  script.Mount("MTD", "system", "/system")
370  script.UnpackPackageDir("recovery", "/system")
371  script.UnpackPackageDir("system", "/system")
372
373  symlinks = CopySystemFiles(input_zip, output_zip)
374  script.MakeSymlinks(symlinks)
375
376  boot_img = common.File("boot.img", common.BuildBootableImage(
377      os.path.join(OPTIONS.input_tmp, "BOOT")))
378  recovery_img = common.File("recovery.img", common.BuildBootableImage(
379      os.path.join(OPTIONS.input_tmp, "RECOVERY")))
380  MakeRecoveryPatch(output_zip, recovery_img, boot_img)
381
382  Item.GetMetadata(input_zip)
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.UnmountAll()
399  script.AddToZip(input_zip, output_zip)
400  WriteMetadata(metadata, output_zip)
401
402
403def WriteMetadata(metadata, output_zip):
404  common.ZipWriteStr(output_zip, "META-INF/com/android/metadata",
405                     "".join(["%s=%s\n" % kv
406                              for kv in sorted(metadata.iteritems())]))
407
408
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] = common.File(fn, data)
420  return out
421
422
423def GetBuildProp(property, z):
424  """Return the fingerprint of the build of a given target-files
425  ZipFile object."""
426  bp = z.read("SYSTEM/build.prop")
427  if not property:
428    return bp
429  m = re.search(re.escape(property) + r"=(.*)\n", bp)
430  if not m:
431    raise common.ExternalError("couldn't find %s in build.prop" % (property,))
432  return m.group(1).strip()
433
434
435def GetRecoveryAPIVersion(zip):
436  """Returns the version of the recovery API.  Version 0 is the older
437  amend code (no separate binary)."""
438  try:
439    version = zip.read("META/recovery-api-version.txt")
440    return int(version)
441  except KeyError:
442    try:
443      # version one didn't have the recovery-api-version.txt file, but
444      # it did include an updater binary.
445      zip.getinfo("OTA/bin/updater")
446      return 1
447    except KeyError:
448      return 0
449
450
451def WriteIncrementalOTAPackage(target_zip, source_zip, output_zip):
452  source_version = GetRecoveryAPIVersion(source_zip)
453  target_version = GetRecoveryAPIVersion(target_zip)
454
455  if source_version == 0:
456    print ("WARNING: generating edify script for a source that "
457           "can't install it.")
458  script = edify_generator.EdifyGenerator(source_version)
459
460  metadata = {"pre-device": GetBuildProp("ro.product.device", source_zip),
461              "post-timestamp": GetBuildProp("ro.build.date.utc", target_zip),
462              }
463
464  device_specific = common.DeviceSpecificParams(
465      source_zip=source_zip,
466      source_version=source_version,
467      target_zip=target_zip,
468      target_version=target_version,
469      output_zip=output_zip,
470      script=script,
471      metadata=metadata)
472
473  print "Loading target..."
474  target_data = LoadSystemFiles(target_zip)
475  print "Loading source..."
476  source_data = LoadSystemFiles(source_zip)
477
478  verbatim_targets = []
479  patch_list = []
480  diffs = []
481  largest_source_size = 0
482  for fn in sorted(target_data.keys()):
483    tf = target_data[fn]
484    assert fn == tf.name
485    sf = source_data.get(fn, None)
486
487    if sf is None or fn in OPTIONS.require_verbatim:
488      # This file should be included verbatim
489      if fn in OPTIONS.prohibit_verbatim:
490        raise common.ExternalError("\"%s\" must be sent verbatim" % (fn,))
491      print "send", fn, "verbatim"
492      tf.AddToZip(output_zip)
493      verbatim_targets.append((fn, tf.size))
494    elif tf.sha1 != sf.sha1:
495      # File is different; consider sending as a patch
496      diffs.append(common.Difference(tf, sf))
497    else:
498      # Target file identical to source.
499      pass
500
501  common.ComputeDifferences(diffs)
502
503  for diff in diffs:
504    tf, sf, d = diff.GetPatch()
505    if d is None or len(d) > tf.size * OPTIONS.patch_threshold:
506      # patch is almost as big as the file; don't bother patching
507      tf.AddToZip(output_zip)
508      verbatim_targets.append((tf.name, tf.size))
509    else:
510      common.ZipWriteStr(output_zip, "patch/" + tf.name + ".p", d)
511      patch_list.append((tf.name, tf, sf, tf.size, sha.sha(d).hexdigest()))
512      largest_source_size = max(largest_source_size, sf.size)
513
514  source_fp = GetBuildProp("ro.build.fingerprint", source_zip)
515  target_fp = GetBuildProp("ro.build.fingerprint", target_zip)
516  metadata["pre-build"] = source_fp
517  metadata["post-build"] = target_fp
518
519  script.Mount("MTD", "system", "/system")
520  script.AssertSomeFingerprint(source_fp, target_fp)
521
522  source_boot = common.File("/tmp/boot.img",
523                            common.BuildBootableImage(
524                                os.path.join(OPTIONS.source_tmp, "BOOT")))
525  target_boot = common.File("/tmp/boot.img",
526                            common.BuildBootableImage(
527                                os.path.join(OPTIONS.target_tmp, "BOOT")))
528  updating_boot = (source_boot.data != target_boot.data)
529
530  source_recovery = common.File("system/recovery.img",
531                                common.BuildBootableImage(
532                                    os.path.join(OPTIONS.source_tmp, "RECOVERY")))
533  target_recovery = common.File("system/recovery.img",
534                                common.BuildBootableImage(
535                                    os.path.join(OPTIONS.target_tmp, "RECOVERY")))
536  updating_recovery = (source_recovery.data != target_recovery.data)
537
538  # Here's how we divide up the progress bar:
539  #  0.1 for verifying the start state (PatchCheck calls)
540  #  0.8 for applying patches (ApplyPatch calls)
541  #  0.1 for unpacking verbatim files, symlinking, and doing the
542  #      device-specific commands.
543
544  AppendAssertions(script, target_zip)
545  device_specific.IncrementalOTA_Assertions()
546
547  script.Print("Verifying current system...")
548
549  script.ShowProgress(0.1, 0)
550  total_verify_size = float(sum([i[2].size for i in patch_list]) + 1)
551  if updating_boot:
552    total_verify_size += source_boot.size
553  so_far = 0
554
555  for fn, tf, sf, size, patch_sha in patch_list:
556    script.PatchCheck("/"+fn, tf.sha1, sf.sha1)
557    so_far += sf.size
558    script.SetProgress(so_far / total_verify_size)
559
560  if updating_boot:
561    d = common.Difference(target_boot, source_boot)
562    _, _, d = d.ComputePatch()
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.PatchCheck("MTD:boot:%d:%s:%d:%s" %
569                      (source_boot.size, source_boot.sha1,
570                       target_boot.size, target_boot.sha1))
571    so_far += source_boot.size
572    script.SetProgress(so_far / total_verify_size)
573
574  if patch_list or updating_recovery or updating_boot:
575    script.CacheFreeSpaceCheck(largest_source_size)
576
577  device_specific.IncrementalOTA_VerifyEnd()
578
579  script.Comment("---- start making changes here ----")
580
581  if OPTIONS.wipe_user_data:
582    script.Print("Erasing user data...")
583    script.FormatPartition("userdata")
584
585  script.Print("Removing unneeded files...")
586  script.DeleteFiles(["/"+i[0] for i in verbatim_targets] +
587                     ["/"+i for i in sorted(source_data)
588                            if i not in target_data] +
589                     ["/system/recovery.img"])
590
591  script.ShowProgress(0.8, 0)
592  total_patch_size = float(sum([i[1].size for i in patch_list]) + 1)
593  if updating_boot:
594    total_patch_size += target_boot.size
595  so_far = 0
596
597  script.Print("Patching system files...")
598  for fn, tf, sf, size, _ in patch_list:
599    script.ApplyPatch("/"+fn, "-", tf.size, tf.sha1, sf.sha1, "patch/"+fn+".p")
600    so_far += tf.size
601    script.SetProgress(so_far / total_patch_size)
602
603  if updating_boot:
604    # Produce the boot image by applying a patch to the current
605    # contents of the boot partition, and write it back to the
606    # partition.
607    script.Print("Patching boot image...")
608    script.ApplyPatch("MTD:boot:%d:%s:%d:%s"
609                      % (source_boot.size, source_boot.sha1,
610                         target_boot.size, target_boot.sha1),
611                      "-",
612                      target_boot.size, target_boot.sha1,
613                      source_boot.sha1, "patch/boot.img.p")
614    so_far += target_boot.size
615    script.SetProgress(so_far / total_patch_size)
616    print "boot image changed; including."
617  else:
618    print "boot image unchanged; skipping."
619
620  if updating_recovery:
621    # Is it better to generate recovery as a patch from the current
622    # boot image, or from the previous recovery image?  For large
623    # updates with significant kernel changes, probably the former.
624    # For small updates where the kernel hasn't changed, almost
625    # certainly the latter.  We pick the first option.  Future
626    # complicated schemes may let us effectively use both.
627    #
628    # A wacky possibility: as long as there is room in the boot
629    # partition, include the binaries and image files from recovery in
630    # the boot image (though not in the ramdisk) so they can be used
631    # as fodder for constructing the recovery image.
632    MakeRecoveryPatch(output_zip, target_recovery, target_boot)
633    script.DeleteFiles(["/system/recovery-from-boot.p",
634                        "/system/etc/install-recovery.sh"])
635    print "recovery image changed; including as patch from boot."
636  else:
637    print "recovery image unchanged; skipping."
638
639  script.ShowProgress(0.1, 10)
640
641  target_symlinks = CopySystemFiles(target_zip, None)
642
643  target_symlinks_d = dict([(i[1], i[0]) for i in target_symlinks])
644  temp_script = script.MakeTemporary()
645  Item.GetMetadata(target_zip)
646  Item.Get("system").SetPermissions(temp_script)
647
648  # Note that this call will mess up the tree of Items, so make sure
649  # we're done with it.
650  source_symlinks = CopySystemFiles(source_zip, None)
651  source_symlinks_d = dict([(i[1], i[0]) for i in source_symlinks])
652
653  # Delete all the symlinks in source that aren't in target.  This
654  # needs to happen before verbatim files are unpacked, in case a
655  # symlink in the source is replaced by a real file in the target.
656  to_delete = []
657  for dest, link in source_symlinks:
658    if link not in target_symlinks_d:
659      to_delete.append(link)
660  script.DeleteFiles(to_delete)
661
662  if verbatim_targets:
663    script.Print("Unpacking new files...")
664    script.UnpackPackageDir("system", "/system")
665
666  if updating_recovery:
667    script.Print("Unpacking new recovery...")
668    script.UnpackPackageDir("recovery", "/system")
669
670  script.Print("Symlinks and permissions...")
671
672  # Create all the symlinks that don't already exist, or point to
673  # somewhere different than what we want.  Delete each symlink before
674  # creating it, since the 'symlink' command won't overwrite.
675  to_create = []
676  for dest, link in target_symlinks:
677    if link in source_symlinks_d:
678      if dest != source_symlinks_d[link]:
679        to_create.append((dest, link))
680    else:
681      to_create.append((dest, link))
682  script.DeleteFiles([i[1] for i in to_create])
683  script.MakeSymlinks(to_create)
684
685  # Now that the symlinks are created, we can set all the
686  # permissions.
687  script.AppendScript(temp_script)
688
689  # Do device-specific installation (eg, write radio image).
690  device_specific.IncrementalOTA_InstallEnd()
691
692  if OPTIONS.extra_script is not None:
693    scirpt.AppendExtra(OPTIONS.extra_script)
694
695  script.AddToZip(target_zip, output_zip)
696  WriteMetadata(metadata, output_zip)
697
698
699def main(argv):
700
701  def option_handler(o, a):
702    if o in ("-b", "--board_config"):
703      pass   # deprecated
704    elif o in ("-k", "--package_key"):
705      OPTIONS.package_key = a
706    elif o in ("-i", "--incremental_from"):
707      OPTIONS.incremental_source = a
708    elif o in ("-w", "--wipe_user_data"):
709      OPTIONS.wipe_user_data = True
710    elif o in ("-n", "--no_prereq"):
711      OPTIONS.omit_prereq = True
712    elif o in ("-e", "--extra_script"):
713      OPTIONS.extra_script = a
714    elif o in ("--worker_threads"):
715      OPTIONS.worker_threads = int(a)
716    else:
717      return False
718    return True
719
720  args = common.ParseOptions(argv, __doc__,
721                             extra_opts="b:k:i:d:wne:",
722                             extra_long_opts=["board_config=",
723                                              "package_key=",
724                                              "incremental_from=",
725                                              "wipe_user_data",
726                                              "no_prereq",
727                                              "extra_script=",
728                                              "worker_threads="],
729                             extra_option_handler=option_handler)
730
731  if len(args) != 2:
732    common.Usage(__doc__)
733    sys.exit(1)
734
735  if OPTIONS.extra_script is not None:
736    OPTIONS.extra_script = open(OPTIONS.extra_script).read()
737
738  print "unzipping target target-files..."
739  OPTIONS.input_tmp = common.UnzipTemp(args[0])
740
741  if OPTIONS.device_specific is None:
742    # look for the device-specific tools extension location in the input
743    try:
744      f = open(os.path.join(OPTIONS.input_tmp, "META", "tool-extensions.txt"))
745      ds = f.read().strip()
746      f.close()
747      if ds:
748        ds = os.path.normpath(ds)
749        print "using device-specific extensions in", ds
750        OPTIONS.device_specific = ds
751    except IOError, e:
752      if e.errno == errno.ENOENT:
753        # nothing specified in the file
754        pass
755      else:
756        raise
757
758  common.LoadMaxSizes()
759  if not OPTIONS.max_image_size:
760    print
761    print "  WARNING:  Failed to load max image sizes; will not enforce"
762    print "  image size limits."
763    print
764
765  OPTIONS.target_tmp = OPTIONS.input_tmp
766  input_zip = zipfile.ZipFile(args[0], "r")
767  if OPTIONS.package_key:
768    temp_zip_file = tempfile.NamedTemporaryFile()
769    output_zip = zipfile.ZipFile(temp_zip_file, "w",
770                                 compression=zipfile.ZIP_DEFLATED)
771  else:
772    output_zip = zipfile.ZipFile(args[1], "w",
773                                 compression=zipfile.ZIP_DEFLATED)
774
775  if OPTIONS.incremental_source is None:
776    WriteFullOTAPackage(input_zip, output_zip)
777  else:
778    print "unzipping source target-files..."
779    OPTIONS.source_tmp = common.UnzipTemp(OPTIONS.incremental_source)
780    source_zip = zipfile.ZipFile(OPTIONS.incremental_source, "r")
781    WriteIncrementalOTAPackage(input_zip, source_zip, output_zip)
782
783  output_zip.close()
784  if OPTIONS.package_key:
785    SignOutput(temp_zip_file.name, args[1])
786    temp_zip_file.close()
787
788  common.Cleanup()
789
790  print "done."
791
792
793if __name__ == '__main__':
794  try:
795    main(sys.argv[1:])
796  except common.ExternalError, e:
797    print
798    print "   ERROR: %s" % (e,)
799    print
800    sys.exit(1)
801