• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
15import re
16
17import common
18
19class 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