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