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 re
     16 
     17 import common
     18 
     19 class EdifyGenerator(object):
     20   """Class to generate scripts in the 'edify' recovery script language
     21   used from donut onwards."""
     22 
     23   def __init__(self, version, info, fstab=None):
     24     self.script = []
     25     self.mounts = set()
     26     self._required_cache = 0
     27     self.version = version
     28     self.info = info
     29     if fstab is None:
     30       self.fstab = self.info.get("fstab", None)
     31     else:
     32       self.fstab = fstab
     33 
     34   @property
     35   def required_cache(self):
     36     """Return the minimum cache size to apply the update."""
     37     return self._required_cache
     38 
     39   @staticmethod
     40   def WordWrap(cmd, linelen=80):
     41     """'cmd' should be a function call with null characters after each
     42     parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
     43     to a given line length, replacing nulls with spaces and/or newlines
     44     to format it nicely."""
     45     indent = cmd.index("(")+1
     46     out = []
     47     first = True
     48     x = re.compile("^(.{,%d})\0" % (linelen-indent,))
     49     while True:
     50       if not first:
     51         out.append(" " * indent)
     52       first = False
     53       m = x.search(cmd)
     54       if not m:
     55         parts = cmd.split("\0", 1)
     56         out.append(parts[0]+"\n")
     57         if len(parts) == 1:
     58           break
     59         else:
     60           cmd = parts[1]
     61           continue
     62       out.append(m.group(1)+"\n")
     63       cmd = cmd[m.end():]
     64 
     65     return "".join(out).replace("\0", " ").rstrip("\n")
     66 
     67   def AppendScript(self, other):
     68     """Append the contents of another script (which should be created
     69     with temporary=True) to this one."""
     70     self.script.extend(other.script)
     71 
     72   def AssertOemProperty(self, name, values, oem_no_mount):
     73     """Assert that a property on the OEM paritition matches allowed values."""
     74     if not name:
     75       raise ValueError("must specify an OEM property")
     76     if not values:
     77       raise ValueError("must specify the OEM value")
     78 
     79     if oem_no_mount:
     80       get_prop_command = 'getprop("%s")' % name
     81     else:
     82       get_prop_command = 'file_getprop("/oem/oem.prop", "%s")' % name
     83 
     84     cmd = ''
     85     for value in values:
     86       cmd += '%s == "%s" || ' % (get_prop_command, value)
     87     cmd += (
     88         'abort("E{code}: This package expects the value \\"{values}\\" for '
     89         '\\"{name}\\"; this has value \\"" + '
     90         '{get_prop_command} + "\\".");').format(
     91             code=common.ErrorCode.OEM_PROP_MISMATCH,
     92             get_prop_command=get_prop_command, name=name,
     93             values='\\" or \\"'.join(values))
     94     self.script.append(cmd)
     95 
     96   def AssertSomeFingerprint(self, *fp):
     97     """Assert that the current recovery build fingerprint is one of *fp."""
     98     if not fp:
     99       raise ValueError("must specify some fingerprints")
    100     cmd = (' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"') % i
    101                              for i in fp]) +
    102            ' ||\n    abort("E%d: Package expects build fingerprint of %s; '
    103            'this device has " + getprop("ro.build.fingerprint") + ".");') % (
    104                common.ErrorCode.FINGERPRINT_MISMATCH, " or ".join(fp))
    105     self.script.append(cmd)
    106 
    107   def AssertSomeThumbprint(self, *fp):
    108     """Assert that the current recovery build thumbprint is one of *fp."""
    109     if not fp:
    110       raise ValueError("must specify some thumbprints")
    111     cmd = (' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"') % i
    112                              for i in fp]) +
    113            ' ||\n    abort("E%d: Package expects build thumbprint of %s; this '
    114            'device has " + getprop("ro.build.thumbprint") + ".");') % (
    115                common.ErrorCode.THUMBPRINT_MISMATCH, " or ".join(fp))
    116     self.script.append(cmd)
    117 
    118   def AssertFingerprintOrThumbprint(self, fp, tp):
    119     """Assert that the current recovery build fingerprint is fp, or thumbprint
    120        is tp."""
    121     cmd = ('getprop("ro.build.fingerprint") == "{fp}" ||\n'
    122            '    getprop("ro.build.thumbprint") == "{tp}" ||\n'
    123            '    abort("Package expects build fingerprint of {fp} or '
    124            'thumbprint of {tp}; this device has a fingerprint of " '
    125            '+ getprop("ro.build.fingerprint") + " and a thumbprint of " '
    126            '+ getprop("ro.build.thumbprint") + ".");').format(fp=fp, tp=tp)
    127     self.script.append(cmd)
    128 
    129   def AssertOlderBuild(self, timestamp, timestamp_text):
    130     """Assert that the build on the device is older (or the same as)
    131     the given timestamp."""
    132     self.script.append(
    133         ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
    134          'abort("E%d: Can\'t install this package (%s) over newer '
    135          'build (" + getprop("ro.build.date") + ").");') % (
    136              timestamp, common.ErrorCode.OLDER_BUILD, timestamp_text))
    137 
    138   def AssertDevice(self, device):
    139     """Assert that the device identifier is the given string."""
    140     cmd = ('getprop("ro.product.device") == "%s" || '
    141            'abort("E%d: This package is for \\"%s\\" devices; '
    142            'this is a \\"" + getprop("ro.product.device") + "\\".");') % (
    143                device, common.ErrorCode.DEVICE_MISMATCH, device)
    144     self.script.append(cmd)
    145 
    146   def AssertSomeBootloader(self, *bootloaders):
    147     """Asert that the bootloader version is one of *bootloaders."""
    148     cmd = ("assert(" +
    149            " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
    150                          for b in bootloaders]) +
    151            ");")
    152     self.script.append(self.WordWrap(cmd))
    153 
    154   def ShowProgress(self, frac, dur):
    155     """Update the progress bar, advancing it over 'frac' over the next
    156     'dur' seconds.  'dur' may be zero to advance it via SetProgress
    157     commands instead of by time."""
    158     self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
    159 
    160   def SetProgress(self, frac):
    161     """Set the position of the progress bar within the chunk defined
    162     by the most recent ShowProgress call.  'frac' should be in
    163     [0,1]."""
    164     self.script.append("set_progress(%f);" % (frac,))
    165 
    166   def PatchCheck(self, filename, *sha1):  # pylint: disable=unused-argument
    167     """Checks that the given partition has the desired checksum.
    168 
    169     The call to this function is being deprecated in favor of
    170     PatchPartitionCheck(). It will try to parse and handle the old format,
    171     unless the format is unknown.
    172     """
    173     tokens = filename.split(':')
    174     assert len(tokens) == 6 and tokens[0] == 'EMMC', \
    175         "Failed to handle unknown format. Use PatchPartitionCheck() instead."
    176     source = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[2], tokens[3])
    177     target = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[4], tokens[5])
    178     self.PatchPartitionCheck(target, source)
    179 
    180   def PatchPartitionCheck(self, target, source):
    181     """Checks whether updater can patch the given partitions.
    182 
    183     It checks the checksums of the given partitions. If none of them matches the
    184     expected checksum, updater will additionally look for a backup on /cache.
    185     """
    186     self.script.append(self.WordWrap((
    187         'patch_partition_check("{target}",\0"{source}") ||\n    abort('
    188         '"E{code}: \\"{target}\\" or \\"{source}\\" has unexpected '
    189         'contents.");').format(
    190             target=target, source=source,
    191             code=common.ErrorCode.BAD_PATCH_FILE)))
    192 
    193   def CacheFreeSpaceCheck(self, amount):
    194     """Check that there's at least 'amount' space that can be made
    195     available on /cache."""
    196     self._required_cache = max(self._required_cache, amount)
    197     self.script.append(('apply_patch_space(%d) || abort("E%d: Not enough free '
    198                         'space on /cache to apply patches.");') % (
    199                             amount,
    200                             common.ErrorCode.INSUFFICIENT_CACHE_SPACE))
    201 
    202   def Mount(self, mount_point, mount_options_by_format=""):
    203     """Mount the partition with the given mount_point.
    204       mount_options_by_format:
    205       [fs_type=option[,option]...[|fs_type=option[,option]...]...]
    206       where option is optname[=optvalue]
    207       E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
    208     """
    209     fstab = self.fstab
    210     if fstab:
    211       p = fstab[mount_point]
    212       mount_dict = {}
    213       if mount_options_by_format is not None:
    214         for option in mount_options_by_format.split("|"):
    215           if "=" in option:
    216             key, value = option.split("=", 1)
    217             mount_dict[key] = value
    218       mount_flags = mount_dict.get(p.fs_type, "")
    219       if p.context is not None:
    220         mount_flags = p.context + ("," + mount_flags if mount_flags else "")
    221       self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % (
    222           p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device,
    223           p.mount_point, mount_flags))
    224       self.mounts.add(p.mount_point)
    225 
    226   def Comment(self, comment):
    227     """Write a comment into the update script."""
    228     self.script.append("")
    229     for i in comment.split("\n"):
    230       self.script.append("# " + i)
    231     self.script.append("")
    232 
    233   def Print(self, message):
    234     """Log a message to the screen (if the logs are visible)."""
    235     self.script.append('ui_print("%s");' % (message,))
    236 
    237   def TunePartition(self, partition, *options):
    238     fstab = self.fstab
    239     if fstab:
    240       p = fstab[partition]
    241       if p.fs_type not in ("ext2", "ext3", "ext4"):
    242         raise ValueError("Partition %s cannot be tuned\n" % (partition,))
    243     self.script.append(
    244         'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
    245         '"%s") || abort("E%d: Failed to tune partition %s");' % (
    246             p.device, common.ErrorCode.TUNE_PARTITION_FAILURE, partition))
    247 
    248   def FormatPartition(self, partition):
    249     """Format the given partition, specified by its mount point (eg,
    250     "/system")."""
    251 
    252     fstab = self.fstab
    253     if fstab:
    254       p = fstab[partition]
    255       self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
    256                          (p.fs_type, common.PARTITION_TYPES[p.fs_type],
    257                           p.device, p.length, p.mount_point))
    258 
    259   def WipeBlockDevice(self, partition):
    260     if partition not in ("/system", "/vendor"):
    261       raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
    262     fstab = self.fstab
    263     size = self.info.get(partition.lstrip("/") + "_size", None)
    264     device = fstab[partition].device
    265 
    266     self.script.append('wipe_block_device("%s", %s);' % (device, size))
    267 
    268   def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
    269     """Apply binary patches (in *patchpairs) to the given srcfile to
    270     produce tgtfile (which may be "-" to indicate overwriting the
    271     source file.
    272 
    273     This edify function is being deprecated in favor of PatchPartition(). It
    274     will try to redirect calls to PatchPartition() if possible. On unknown /
    275     invalid inputs, raises an exception.
    276     """
    277     tokens = srcfile.split(':')
    278     assert (len(tokens) == 6 and tokens[0] == 'EMMC' and tgtfile == '-' and
    279             len(patchpairs) == 2), \
    280         "Failed to handle unknown format. Use PatchPartition() instead."
    281 
    282     # Also sanity check the args.
    283     assert tokens[3] == patchpairs[0], \
    284         "Found mismatching values for source SHA-1: {} vs {}".format(
    285             tokens[3], patchpairs[0])
    286     assert int(tokens[4]) == tgtsize, \
    287         "Found mismatching values for target size: {} vs {}".format(
    288             tokens[4], tgtsize)
    289     assert tokens[5] == tgtsha1, \
    290         "Found mismatching values for target SHA-1: {} vs {}".format(
    291             tokens[5], tgtsha1)
    292 
    293     source = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[2], tokens[3])
    294     target = '{}:{}:{}:{}'.format(tokens[0], tokens[1], tokens[4], tokens[5])
    295     patch = patchpairs[1]
    296     self.PatchPartition(target, source, patch)
    297 
    298   def PatchPartition(self, target, source, patch):
    299     """Applies the patch to the source partition and writes it to target."""
    300     self.script.append(self.WordWrap((
    301         'patch_partition("{target}",\0"{source}",\0'
    302         'package_extract_file("{patch}")) ||\n'
    303         '    abort("E{code}: Failed to apply patch to {source}");').format(
    304             target=target, source=source, patch=patch,
    305             code=common.ErrorCode.APPLY_PATCH_FAILURE)))
    306 
    307   def WriteRawImage(self, mount_point, fn, mapfn=None):
    308     """Write the given package file into the partition for the given
    309     mount point."""
    310 
    311     fstab = self.fstab
    312     if fstab:
    313       p = fstab[mount_point]
    314       partition_type = common.PARTITION_TYPES[p.fs_type]
    315       args = {'device': p.device, 'fn': fn}
    316       if partition_type == "EMMC":
    317         if mapfn:
    318           args["map"] = mapfn
    319           self.script.append(
    320               'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
    321         else:
    322           self.script.append(
    323               'package_extract_file("%(fn)s", "%(device)s");' % args)
    324       else:
    325         raise ValueError(
    326             "don't know how to write \"%s\" partitions" % p.fs_type)
    327 
    328   def AppendExtra(self, extra):
    329     """Append text verbatim to the output script."""
    330     self.script.append(extra)
    331 
    332   def Unmount(self, mount_point):
    333     self.script.append('unmount("%s");' % mount_point)
    334     self.mounts.remove(mount_point)
    335 
    336   def UnmountAll(self):
    337     for p in sorted(self.mounts):
    338       self.script.append('unmount("%s");' % (p,))
    339     self.mounts = set()
    340 
    341   def AddToZip(self, input_zip, output_zip, input_path=None):
    342     """Write the accumulated script to the output_zip file.  input_zip
    343     is used as the source for the 'updater' binary needed to run
    344     script.  If input_path is not None, it will be used as a local
    345     path for the binary instead of input_zip."""
    346 
    347     self.UnmountAll()
    348 
    349     common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
    350                        "\n".join(self.script) + "\n")
    351 
    352     if input_path is None:
    353       data = input_zip.read("OTA/bin/updater")
    354     else:
    355       data = open(input_path, "rb").read()
    356     common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
    357                        data, perms=0o755)
    358