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):
     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     get_prop_command = None
     87     if common.OPTIONS.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 UnpackPackageDir(self, src, dst):
    234     """Unpack a given directory from the OTA package into the given
    235     destination directory."""
    236     self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
    237 
    238   def Comment(self, comment):
    239     """Write a comment into the update script."""
    240     self.script.append("")
    241     for i in comment.split("\n"):
    242       self.script.append("# " + i)
    243     self.script.append("")
    244 
    245   def Print(self, message):
    246     """Log a message to the screen (if the logs are visible)."""
    247     self.script.append('ui_print("%s");' % (message,))
    248 
    249   def TunePartition(self, partition, *options):
    250     fstab = self.fstab
    251     if fstab:
    252       p = fstab[partition]
    253       if p.fs_type not in ("ext2", "ext3", "ext4"):
    254         raise ValueError("Partition %s cannot be tuned\n" % (partition,))
    255     self.script.append(
    256         'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
    257         '"%s") || abort("E%d: Failed to tune partition %s");' % (
    258             p.device, common.ErrorCode.TUNE_PARTITION_FAILURE, partition))
    259 
    260   def FormatPartition(self, partition):
    261     """Format the given partition, specified by its mount point (eg,
    262     "/system")."""
    263 
    264     fstab = self.fstab
    265     if fstab:
    266       p = fstab[partition]
    267       self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
    268                          (p.fs_type, common.PARTITION_TYPES[p.fs_type],
    269                           p.device, p.length, p.mount_point))
    270 
    271   def WipeBlockDevice(self, partition):
    272     if partition not in ("/system", "/vendor"):
    273       raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
    274     fstab = self.fstab
    275     size = self.info.get(partition.lstrip("/") + "_size", None)
    276     device = fstab[partition].device
    277 
    278     self.script.append('wipe_block_device("%s", %s);' % (device, size))
    279 
    280   def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
    281     """Apply binary patches (in *patchpairs) to the given srcfile to
    282     produce tgtfile (which may be "-" to indicate overwriting the
    283     source file."""
    284     if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
    285       raise ValueError("bad patches given to ApplyPatch")
    286     cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
    287            % (srcfile, tgtfile, tgtsha1, tgtsize)]
    288     for i in range(0, len(patchpairs), 2):
    289       cmd.append(',\0%s,\0package_extract_file("%s")' % patchpairs[i:i+2])
    290     cmd.append(') ||\n    abort("E%d: Failed to apply patch to %s");' % (
    291         common.ErrorCode.APPLY_PATCH_FAILURE, srcfile))
    292     cmd = "".join(cmd)
    293     self.script.append(self.WordWrap(cmd))
    294 
    295   def WriteRawImage(self, mount_point, fn, mapfn=None):
    296     """Write the given package file into the partition for the given
    297     mount point."""
    298 
    299     fstab = self.fstab
    300     if fstab:
    301       p = fstab[mount_point]
    302       partition_type = common.PARTITION_TYPES[p.fs_type]
    303       args = {'device': p.device, 'fn': fn}
    304       if partition_type == "EMMC":
    305         if mapfn:
    306           args["map"] = mapfn
    307           self.script.append(
    308               'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
    309         else:
    310           self.script.append(
    311               'package_extract_file("%(fn)s", "%(device)s");' % args)
    312       else:
    313         raise ValueError(
    314             "don't know how to write \"%s\" partitions" % p.fs_type)
    315 
    316   def AppendExtra(self, extra):
    317     """Append text verbatim to the output script."""
    318     self.script.append(extra)
    319 
    320   def Unmount(self, mount_point):
    321     self.script.append('unmount("%s");' % mount_point)
    322     self.mounts.remove(mount_point)
    323 
    324   def UnmountAll(self):
    325     for p in sorted(self.mounts):
    326       self.script.append('unmount("%s");' % (p,))
    327     self.mounts = set()
    328 
    329   def AddToZip(self, input_zip, output_zip, input_path=None):
    330     """Write the accumulated script to the output_zip file.  input_zip
    331     is used as the source for the 'updater' binary needed to run
    332     script.  If input_path is not None, it will be used as a local
    333     path for the binary instead of input_zip."""
    334 
    335     self.UnmountAll()
    336 
    337     common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
    338                        "\n".join(self.script) + "\n")
    339 
    340     if input_path is None:
    341       data = input_zip.read("OTA/bin/updater")
    342     else:
    343       data = open(input_path, "rb").read()
    344     common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
    345                        data, perms=0o755)
    346