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