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