edify_generator.py revision b4cfca530cb2173554df1a30c60f8548eb7d3ed6
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  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, value):
81    """Assert that a property on the OEM paritition matches a value."""
82    if not name:
83      raise ValueError("must specify an OEM property")
84    if not value:
85      raise ValueError("must specify the OEM value")
86    cmd = ('file_getprop("/oem/oem.prop", "{name}") == "{value}" || '
87           'abort("This package expects the value \\"{value}\\" for '
88           '\\"{name}\\" on the OEM partition; this has value \\"" + '
89           'file_getprop("/oem/oem.prop", "{name}") + "\\".");').format(
90               name=name, value=value)
91    self.script.append(cmd)
92
93  def AssertSomeFingerprint(self, *fp):
94    """Assert that the current recovery build fingerprint is one of *fp."""
95    if not fp:
96      raise ValueError("must specify some fingerprints")
97    cmd = (' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"') % i
98                             for i in fp]) +
99           ' ||\n    abort("Package expects build fingerprint of %s; this '
100           'device has " + getprop("ro.build.fingerprint") + ".");') % (
101               " or ".join(fp))
102    self.script.append(cmd)
103
104  def AssertSomeThumbprint(self, *fp):
105    """Assert that the current recovery build thumbprint is one of *fp."""
106    if not fp:
107      raise ValueError("must specify some thumbprints")
108    cmd = (' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"') % i
109                             for i in fp]) +
110           ' ||\n    abort("Package expects build thumbprint of %s; this '
111           'device has " + getprop("ro.build.thumbprint") + ".");') % (
112               " or ".join(fp))
113    self.script.append(cmd)
114
115  def AssertOlderBuild(self, timestamp, timestamp_text):
116    """Assert that the build on the device is older (or the same as)
117    the given timestamp."""
118    self.script.append(
119        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
120         'abort("Can\'t install this package (%s) over newer '
121         'build (" + getprop("ro.build.date") + ").");') % (timestamp,
122                                                            timestamp_text))
123
124  def AssertDevice(self, device):
125    """Assert that the device identifier is the given string."""
126    cmd = ('getprop("ro.product.device") == "%s" || '
127           'abort("This package is for \\"%s\\" devices; '
128           'this is a \\"" + getprop("ro.product.device") + "\\".");') % (
129               device, device)
130    self.script.append(cmd)
131
132  def AssertSomeBootloader(self, *bootloaders):
133    """Asert that the bootloader version is one of *bootloaders."""
134    cmd = ("assert(" +
135           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
136                         for b in bootloaders]) +
137           ");")
138    self.script.append(self.WordWrap(cmd))
139
140  def ShowProgress(self, frac, dur):
141    """Update the progress bar, advancing it over 'frac' over the next
142    'dur' seconds.  'dur' may be zero to advance it via SetProgress
143    commands instead of by time."""
144    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
145
146  def SetProgress(self, frac):
147    """Set the position of the progress bar within the chunk defined
148    by the most recent ShowProgress call.  'frac' should be in
149    [0,1]."""
150    self.script.append("set_progress(%f);" % (frac,))
151
152  def PatchCheck(self, filename, *sha1):
153    """Check that the given file (or MTD reference) has one of the
154    given *sha1 hashes, checking the version saved in cache if the
155    file does not match."""
156    self.script.append(
157        'apply_patch_check("%s"' % (filename,) +
158        "".join([', "%s"' % (i,) for i in sha1]) +
159        ') || abort("\\"%s\\" has unexpected contents.");' % (filename,))
160
161  def Verify(self, filename):
162    """Check that the given file (or MTD reference) has one of the
163    given hashes (encoded in the filename)."""
164    self.script.append(
165        'apply_patch_check("{filename}") && '
166        'ui_print("    Verified.") || '
167        'ui_print("\\"{filename}\\" has unexpected contents.");'.format(
168            filename=filename))
169
170  def FileCheck(self, filename, *sha1):
171    """Check that the given file (or MTD reference) has one of the
172    given *sha1 hashes."""
173    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
174                       "".join([', "%s"' % (i,) for i in sha1]) +
175                       '));')
176
177  def CacheFreeSpaceCheck(self, amount):
178    """Check that there's at least 'amount' space that can be made
179    available on /cache."""
180    self._required_cache = max(self._required_cache, amount)
181    self.script.append(('apply_patch_space(%d) || abort("Not enough free space '
182                        'on /cache to apply patches.");') % (amount,))
183
184  def Mount(self, mount_point, mount_options_by_format=""):
185    """Mount the partition with the given mount_point.
186      mount_options_by_format:
187      [fs_type=option[,option]...[|fs_type=option[,option]...]...]
188      where option is optname[=optvalue]
189      E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
190    """
191    fstab = self.fstab
192    if fstab:
193      p = fstab[mount_point]
194      mount_dict = {}
195      if mount_options_by_format is not None:
196        for option in mount_options_by_format.split("|"):
197          if "=" in option:
198            key, value = option.split("=", 1)
199            mount_dict[key] = value
200      mount_flags = mount_dict.get(p.fs_type, "")
201      if p.context is not None:
202        mount_flags = p.context + ("," + mount_flags if mount_flags else "")
203      self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % (
204          p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device,
205          p.mount_point, mount_flags))
206      self.mounts.add(p.mount_point)
207
208  def UnpackPackageDir(self, src, dst):
209    """Unpack a given directory from the OTA package into the given
210    destination directory."""
211    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
212
213  def Comment(self, comment):
214    """Write a comment into the update script."""
215    self.script.append("")
216    for i in comment.split("\n"):
217      self.script.append("# " + i)
218    self.script.append("")
219
220  def Print(self, message):
221    """Log a message to the screen (if the logs are visible)."""
222    self.script.append('ui_print("%s");' % (message,))
223
224  def TunePartition(self, partition, *options):
225    fstab = self.fstab
226    if fstab:
227      p = fstab[partition]
228      if p.fs_type not in ("ext2", "ext3", "ext4"):
229        raise ValueError("Partition %s cannot be tuned\n" % (partition,))
230    self.script.append(
231        'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
232        '"%s") || abort("Failed to tune partition %s");' % (
233            p.device, partition))
234
235  def FormatPartition(self, partition):
236    """Format the given partition, specified by its mount point (eg,
237    "/system")."""
238
239    fstab = self.fstab
240    if fstab:
241      p = fstab[partition]
242      self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
243                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
244                          p.device, p.length, p.mount_point))
245
246  def WipeBlockDevice(self, partition):
247    if partition not in ("/system", "/vendor"):
248      raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
249    fstab = self.fstab
250    size = self.info.get(partition.lstrip("/") + "_size", None)
251    device = fstab[partition].device
252
253    self.script.append('wipe_block_device("%s", %s);' % (device, size))
254
255  def DeleteFiles(self, file_list):
256    """Delete all files in file_list."""
257    if not file_list:
258      return
259    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
260    self.script.append(self.WordWrap(cmd))
261
262  def DeleteFilesIfNotMatching(self, file_list):
263    """Delete the file in file_list if not matching the checksum."""
264    if not file_list:
265      return
266    for name, sha1 in file_list:
267      cmd = ('sha1_check(read_file("{name}"), "{sha1}") || '
268             'delete("{name}");'.format(name=name, sha1=sha1))
269      self.script.append(self.WordWrap(cmd))
270
271  def RenameFile(self, srcfile, tgtfile):
272    """Moves a file from one location to another."""
273    if self.info.get("update_rename_support", False):
274      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
275    else:
276      raise ValueError("Rename not supported by update binary")
277
278  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
279    """Prepend an action with an apply_patch_check in order to
280       skip the action if the file exists.  Used when a patch
281       is later renamed."""
282    cmd = ('sha1_check(read_file("%s"), %s) ||' % (tgtfile, tgtsha1))
283    self.script.append(self.WordWrap(cmd))
284
285  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
286    """Apply binary patches (in *patchpairs) to the given srcfile to
287    produce tgtfile (which may be "-" to indicate overwriting the
288    source file."""
289    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
290      raise ValueError("bad patches given to ApplyPatch")
291    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
292           % (srcfile, tgtfile, tgtsha1, tgtsize)]
293    for i in range(0, len(patchpairs), 2):
294      cmd.append(',\0%s,\0package_extract_file("%s")' % patchpairs[i:i+2])
295    cmd.append(') ||\n    abort("Failed to apply patch to %s");' % (srcfile,))
296    cmd = "".join(cmd)
297    self.script.append(self.WordWrap(cmd))
298
299  def WriteRawImage(self, mount_point, fn, mapfn=None):
300    """Write the given package file into the partition for the given
301    mount point."""
302
303    fstab = self.fstab
304    if fstab:
305      p = fstab[mount_point]
306      partition_type = common.PARTITION_TYPES[p.fs_type]
307      args = {'device': p.device, 'fn': fn}
308      if partition_type == "MTD":
309        self.script.append(
310            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
311            % args)
312      elif partition_type == "EMMC":
313        if mapfn:
314          args["map"] = mapfn
315          self.script.append(
316              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
317        else:
318          self.script.append(
319              'package_extract_file("%(fn)s", "%(device)s");' % args)
320      else:
321        raise ValueError(
322            "don't know how to write \"%s\" partitions" % p.fs_type)
323
324  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
325    """Set file ownership and permissions."""
326    if not self.info.get("use_set_metadata", False):
327      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
328    else:
329      if capabilities is None:
330        capabilities = "0x0"
331      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
332          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
333      if selabel is not None:
334        cmd += ', "selabel", "%s"' % selabel
335      cmd += ');'
336      self.script.append(cmd)
337
338  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel,
339                              capabilities):
340    """Recursively set path ownership and permissions."""
341    if not self.info.get("use_set_metadata", False):
342      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
343                         % (uid, gid, dmode, fmode, fn))
344    else:
345      if capabilities is None:
346        capabilities = "0x0"
347      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
348          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
349          % (fn, uid, gid, dmode, fmode, capabilities)
350      if selabel is not None:
351        cmd += ', "selabel", "%s"' % selabel
352      cmd += ');'
353      self.script.append(cmd)
354
355  def MakeSymlinks(self, symlink_list):
356    """Create symlinks, given a list of (dest, link) pairs."""
357    by_dest = {}
358    for d, l in symlink_list:
359      by_dest.setdefault(d, []).append(l)
360
361    for dest, links in sorted(by_dest.iteritems()):
362      cmd = ('symlink("%s", ' % (dest,) +
363             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
364      self.script.append(self.WordWrap(cmd))
365
366  def AppendExtra(self, extra):
367    """Append text verbatim to the output script."""
368    self.script.append(extra)
369
370  def Unmount(self, mount_point):
371    self.script.append('unmount("%s");' % mount_point)
372    self.mounts.remove(mount_point)
373
374  def UnmountAll(self):
375    for p in sorted(self.mounts):
376      self.script.append('unmount("%s");' % (p,))
377    self.mounts = set()
378
379  def AddToZip(self, input_zip, output_zip, input_path=None):
380    """Write the accumulated script to the output_zip file.  input_zip
381    is used as the source for the 'updater' binary needed to run
382    script.  If input_path is not None, it will be used as a local
383    path for the binary instead of input_zip."""
384
385    self.UnmountAll()
386
387    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
388                       "\n".join(self.script) + "\n")
389
390    if input_path is None:
391      data = input_zip.read("OTA/bin/updater")
392    else:
393      data = open(input_path, "rb").read()
394    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
395                       data, perms=0o755)
396