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