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