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