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