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