Home | History | Annotate | Download | only in releasetools
      1 # Copyright (C) 2009 The Android Open Source Project
      2 #
      3 # Licensed under the Apache License, Version 2.0 (the "License");
      4 # you may not use this file except in compliance with the License.
      5 # You may obtain a copy of the License at
      6 #
      7 #      http://www.apache.org/licenses/LICENSE-2.0
      8 #
      9 # Unless required by applicable law or agreed to in writing, software
     10 # distributed under the License is distributed on an "AS IS" BASIS,
     11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 # See the License for the specific language governing permissions and
     13 # limitations under the License.
     14 
     15 import os
     16 import re
     17 
     18 import common
     19 
     20 class EdifyGenerator(object):
     21   """Class to generate scripts in the 'edify' recovery script language
     22   used from donut onwards."""
     23 
     24   def __init__(self, version, info):
     25     self.script = []
     26     self.mounts = set()
     27     self.version = version
     28     self.info = info
     29 
     30   def MakeTemporary(self):
     31     """Make a temporary script object whose commands can latter be
     32     appended to the parent script with AppendScript().  Used when the
     33     caller wants to generate script commands out-of-order."""
     34     x = EdifyGenerator(self.version, self.info)
     35     x.mounts = self.mounts
     36     return x
     37 
     38   @staticmethod
     39   def _WordWrap(cmd, linelen=80):
     40     """'cmd' should be a function call with null characters after each
     41     parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
     42     to a given line length, replacing nulls with spaces and/or newlines
     43     to format it nicely."""
     44     indent = cmd.index("(")+1
     45     out = []
     46     first = True
     47     x = re.compile("^(.{,%d})\0" % (linelen-indent,))
     48     while True:
     49       if not first:
     50         out.append(" " * indent)
     51       first = False
     52       m = x.search(cmd)
     53       if not m:
     54         parts = cmd.split("\0", 1)
     55         out.append(parts[0]+"\n")
     56         if len(parts) == 1:
     57           break
     58         else:
     59           cmd = parts[1]
     60           continue
     61       out.append(m.group(1)+"\n")
     62       cmd = cmd[m.end():]
     63 
     64     return "".join(out).replace("\0", " ").rstrip("\n")
     65 
     66   def AppendScript(self, other):
     67     """Append the contents of another script (which should be created
     68     with temporary=True) to this one."""
     69     self.script.extend(other.script)
     70 
     71   def AssertOemProperty(self, name, value):
     72     """Assert that a property on the OEM paritition matches a value."""
     73     if not name:
     74       raise ValueError("must specify an OEM property")
     75     if not value:
     76       raise ValueError("must specify the OEM value")
     77     cmd = ('file_getprop("/oem/oem.prop", "%s") == "%s" || '
     78            'abort("This package expects the value \\"%s\\"  for '
     79            '\\"%s\\" on the OEM partition; '
     80            'this has value \\"" + file_getprop("/oem/oem.prop") + "\\".");'
     81            ) % (name, value, name, value)
     82     self.script.append(cmd)
     83 
     84   def AssertSomeFingerprint(self, *fp):
     85     """Assert that the current recovery build fingerprint is one of *fp."""
     86     if not fp:
     87       raise ValueError("must specify some fingerprints")
     88     cmd = (
     89            ' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"')
     90                         % i for i in fp]) +
     91            ' ||\n    abort("Package expects build fingerprint of %s; this '
     92            'device has " + getprop("ro.build.fingerprint") + ".");'
     93            ) % (" or ".join(fp),)
     94     self.script.append(cmd)
     95 
     96   def AssertSomeThumbprint(self, *fp):
     97     """Assert that the current recovery build thumbprint is one of *fp."""
     98     if not fp:
     99       raise ValueError("must specify some thumbprints")
    100     cmd = (
    101            ' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"')
    102                         % i for i in fp]) +
    103            ' ||\n    abort("Package expects build thumbprint of %s; this '
    104            'device has " + getprop("ro.build.thumbprint") + ".");'
    105            ) % (" or ".join(fp),)
    106     self.script.append(cmd)
    107 
    108   def AssertOlderBuild(self, timestamp, timestamp_text):
    109     """Assert that the build on the device is older (or the same as)
    110     the given timestamp."""
    111     self.script.append(
    112         ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
    113          'abort("Can\'t install this package (%s) over newer '
    114          'build (" + getprop("ro.build.date") + ").");'
    115          ) % (timestamp, timestamp_text))
    116 
    117   def AssertDevice(self, device):
    118     """Assert that the device identifier is the given string."""
    119     cmd = ('getprop("ro.product.device") == "%s" || '
    120            'abort("This package is for \\"%s\\" devices; '
    121            'this is a \\"" + getprop("ro.product.device") + "\\".");'
    122            ) % (device, device)
    123     self.script.append(cmd)
    124 
    125   def AssertSomeBootloader(self, *bootloaders):
    126     """Asert that the bootloader version is one of *bootloaders."""
    127     cmd = ("assert(" +
    128            " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
    129                          for b in bootloaders]) +
    130            ");")
    131     self.script.append(self._WordWrap(cmd))
    132 
    133   def ShowProgress(self, frac, dur):
    134     """Update the progress bar, advancing it over 'frac' over the next
    135     'dur' seconds.  'dur' may be zero to advance it via SetProgress
    136     commands instead of by time."""
    137     self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
    138 
    139   def SetProgress(self, frac):
    140     """Set the position of the progress bar within the chunk defined
    141     by the most recent ShowProgress call.  'frac' should be in
    142     [0,1]."""
    143     self.script.append("set_progress(%f);" % (frac,))
    144 
    145   def PatchCheck(self, filename, *sha1):
    146     """Check that the given file (or MTD reference) has one of the
    147     given *sha1 hashes, checking the version saved in cache if the
    148     file does not match."""
    149     self.script.append(
    150         'apply_patch_check("%s"' % (filename,) +
    151         "".join([', "%s"' % (i,) for i in sha1]) +
    152         ') || abort("\\"%s\\" has unexpected contents.");' % (filename,))
    153 
    154   def FileCheck(self, filename, *sha1):
    155     """Check that the given file (or MTD reference) has one of the
    156     given *sha1 hashes."""
    157     self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
    158                        "".join([', "%s"' % (i,) for i in sha1]) +
    159                        '));')
    160 
    161   def CacheFreeSpaceCheck(self, amount):
    162     """Check that there's at least 'amount' space that can be made
    163     available on /cache."""
    164     self.script.append(('apply_patch_space(%d) || abort("Not enough free space '
    165                         'on /system to apply patches.");') % (amount,))
    166 
    167   def Mount(self, mount_point, mount_options_by_format=""):
    168     """Mount the partition with the given mount_point.
    169       mount_options_by_format:
    170       [fs_type=option[,option]...[|fs_type=option[,option]...]...]
    171       where option is optname[=optvalue]
    172       E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
    173     """
    174     fstab = self.info.get("fstab", None)
    175     if fstab:
    176       p = fstab[mount_point]
    177       mount_dict = {}
    178       if mount_options_by_format is not None:
    179         for option in mount_options_by_format.split("|"):
    180           if "=" in option:
    181             key, value = option.split("=", 1)
    182             mount_dict[key] = value
    183       self.script.append('mount("%s", "%s", "%s", "%s", "%s");' %
    184                          (p.fs_type, common.PARTITION_TYPES[p.fs_type],
    185                           p.device, p.mount_point, mount_dict.get(p.fs_type, "")))
    186       self.mounts.add(p.mount_point)
    187 
    188   def UnpackPackageDir(self, src, dst):
    189     """Unpack a given directory from the OTA package into the given
    190     destination directory."""
    191     self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
    192 
    193   def Comment(self, comment):
    194     """Write a comment into the update script."""
    195     self.script.append("")
    196     for i in comment.split("\n"):
    197       self.script.append("# " + i)
    198     self.script.append("")
    199 
    200   def Print(self, message):
    201     """Log a message to the screen (if the logs are visible)."""
    202     self.script.append('ui_print("%s");' % (message,))
    203 
    204   def FormatPartition(self, partition):
    205     """Format the given partition, specified by its mount point (eg,
    206     "/system")."""
    207 
    208     reserve_size = 0
    209     fstab = self.info.get("fstab", None)
    210     if fstab:
    211       p = fstab[partition]
    212       self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
    213                          (p.fs_type, common.PARTITION_TYPES[p.fs_type],
    214                           p.device, p.length, p.mount_point))
    215 
    216   def WipeBlockDevice(self, partition):
    217     if partition not in ("/system", "/vendor"):
    218       raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
    219     fstab = self.info.get("fstab", None)
    220     size = self.info.get(partition.lstrip("/") + "_size", None)
    221     device = fstab[partition].device
    222 
    223     self.script.append('wipe_block_device("%s", %s);' % (device, size))
    224 
    225   def DeleteFiles(self, file_list):
    226     """Delete all files in file_list."""
    227     if not file_list: return
    228     cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
    229     self.script.append(self._WordWrap(cmd))
    230 
    231   def RenameFile(self, srcfile, tgtfile):
    232     """Moves a file from one location to another."""
    233     if self.info.get("update_rename_support", False):
    234       self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
    235     else:
    236       raise ValueError("Rename not supported by update binary")
    237 
    238   def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
    239     """Prepend an action with an apply_patch_check in order to
    240        skip the action if the file exists.  Used when a patch
    241        is later renamed."""
    242     cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1))
    243     self.script.append(self._WordWrap(cmd))
    244 
    245   def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
    246     """Apply binary patches (in *patchpairs) to the given srcfile to
    247     produce tgtfile (which may be "-" to indicate overwriting the
    248     source file."""
    249     if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
    250       raise ValueError("bad patches given to ApplyPatch")
    251     cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
    252            % (srcfile, tgtfile, tgtsha1, tgtsize)]
    253     for i in range(0, len(patchpairs), 2):
    254       cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
    255     cmd.append(');')
    256     cmd = "".join(cmd)
    257     self.script.append(self._WordWrap(cmd))
    258 
    259   def WriteRawImage(self, mount_point, fn, mapfn=None):
    260     """Write the given package file into the partition for the given
    261     mount point."""
    262 
    263     fstab = self.info["fstab"]
    264     if fstab:
    265       p = fstab[mount_point]
    266       partition_type = common.PARTITION_TYPES[p.fs_type]
    267       args = {'device': p.device, 'fn': fn}
    268       if partition_type == "MTD":
    269         self.script.append(
    270             'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
    271             % args)
    272       elif partition_type == "EMMC":
    273         if mapfn:
    274           args["map"] = mapfn
    275           self.script.append(
    276               'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
    277         else:
    278           self.script.append(
    279               'package_extract_file("%(fn)s", "%(device)s");' % args)
    280       else:
    281         raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,))
    282 
    283   def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
    284     """Set file ownership and permissions."""
    285     if not self.info.get("use_set_metadata", False):
    286       self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
    287     else:
    288       if capabilities is None: capabilities = "0x0"
    289       cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
    290           '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
    291       if selabel is not None:
    292         cmd += ', "selabel", "%s"' % ( selabel )
    293       cmd += ');'
    294       self.script.append(cmd)
    295 
    296   def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities):
    297     """Recursively set path ownership and permissions."""
    298     if not self.info.get("use_set_metadata", False):
    299       self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
    300                          % (uid, gid, dmode, fmode, fn))
    301     else:
    302       if capabilities is None: capabilities = "0x0"
    303       cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
    304           '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
    305           % (fn, uid, gid, dmode, fmode, capabilities)
    306       if selabel is not None:
    307         cmd += ', "selabel", "%s"' % ( selabel )
    308       cmd += ');'
    309       self.script.append(cmd)
    310 
    311   def MakeSymlinks(self, symlink_list):
    312     """Create symlinks, given a list of (dest, link) pairs."""
    313     by_dest = {}
    314     for d, l in symlink_list:
    315       by_dest.setdefault(d, []).append(l)
    316 
    317     for dest, links in sorted(by_dest.iteritems()):
    318       cmd = ('symlink("%s", ' % (dest,) +
    319              ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
    320       self.script.append(self._WordWrap(cmd))
    321 
    322   def AppendExtra(self, extra):
    323     """Append text verbatim to the output script."""
    324     self.script.append(extra)
    325 
    326   def Unmount(self, mount_point):
    327     self.script.append('unmount("%s");' % (mount_point,))
    328     self.mounts.remove(mount_point);
    329 
    330   def UnmountAll(self):
    331     for p in sorted(self.mounts):
    332       self.script.append('unmount("%s");' % (p,))
    333     self.mounts = set()
    334 
    335   def AddToZip(self, input_zip, output_zip, input_path=None):
    336     """Write the accumulated script to the output_zip file.  input_zip
    337     is used as the source for the 'updater' binary needed to run
    338     script.  If input_path is not None, it will be used as a local
    339     path for the binary instead of input_zip."""
    340 
    341     self.UnmountAll()
    342 
    343     common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
    344                        "\n".join(self.script) + "\n")
    345 
    346     if input_path is None:
    347       data = input_zip.read("OTA/bin/updater")
    348     else:
    349       data = open(input_path, "rb").read()
    350     common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
    351                        data, perms=0755)
    352