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