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