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