edify_generator.py revision 84006eacd05e3f2784a0ecdf36609767f2c34044
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 /cache 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 DeleteFilesIfNotMatching(self, file_list):
247    """Delete the file in file_list if not matching the checksum."""
248    if not file_list:
249      return
250    for name, sha1 in file_list:
251      cmd = ('sha1_check(read_file("{name}"), "{sha1}") || '
252             'delete("{name}");'.format(name=name, sha1=sha1))
253      self.script.append(self.WordWrap(cmd))
254
255  def RenameFile(self, srcfile, tgtfile):
256    """Moves a file from one location to another."""
257    if self.info.get("update_rename_support", False):
258      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
259    else:
260      raise ValueError("Rename not supported by update binary")
261
262  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
263    """Prepend an action with an apply_patch_check in order to
264       skip the action if the file exists.  Used when a patch
265       is later renamed."""
266    cmd = ('sha1_check(read_file("%s"), %s) ||' % (tgtfile, tgtsha1))
267    self.script.append(self.WordWrap(cmd))
268
269  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
270    """Apply binary patches (in *patchpairs) to the given srcfile to
271    produce tgtfile (which may be "-" to indicate overwriting the
272    source file."""
273    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
274      raise ValueError("bad patches given to ApplyPatch")
275    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
276           % (srcfile, tgtfile, tgtsha1, tgtsize)]
277    for i in range(0, len(patchpairs), 2):
278      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
279    cmd.append(');')
280    cmd = "".join(cmd)
281    self.script.append(self.WordWrap(cmd))
282
283  def WriteRawImage(self, mount_point, fn, mapfn=None):
284    """Write the given package file into the partition for the given
285    mount point."""
286
287    fstab = self.fstab
288    if fstab:
289      p = fstab[mount_point]
290      partition_type = common.PARTITION_TYPES[p.fs_type]
291      args = {'device': p.device, 'fn': fn}
292      if partition_type == "MTD":
293        self.script.append(
294            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
295            % args)
296      elif partition_type == "EMMC":
297        if mapfn:
298          args["map"] = mapfn
299          self.script.append(
300              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
301        else:
302          self.script.append(
303              'package_extract_file("%(fn)s", "%(device)s");' % args)
304      else:
305        raise ValueError(
306            "don't know how to write \"%s\" partitions" % p.fs_type)
307
308  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
309    """Set file ownership and permissions."""
310    if not self.info.get("use_set_metadata", False):
311      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
312    else:
313      if capabilities is None:
314        capabilities = "0x0"
315      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
316          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
317      if selabel is not None:
318        cmd += ', "selabel", "%s"' % selabel
319      cmd += ');'
320      self.script.append(cmd)
321
322  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel,
323                              capabilities):
324    """Recursively set path ownership and permissions."""
325    if not self.info.get("use_set_metadata", False):
326      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
327                         % (uid, gid, dmode, fmode, fn))
328    else:
329      if capabilities is None:
330        capabilities = "0x0"
331      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
332          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
333          % (fn, uid, gid, dmode, fmode, capabilities)
334      if selabel is not None:
335        cmd += ', "selabel", "%s"' % selabel
336      cmd += ');'
337      self.script.append(cmd)
338
339  def MakeSymlinks(self, symlink_list):
340    """Create symlinks, given a list of (dest, link) pairs."""
341    by_dest = {}
342    for d, l in symlink_list:
343      by_dest.setdefault(d, []).append(l)
344
345    for dest, links in sorted(by_dest.iteritems()):
346      cmd = ('symlink("%s", ' % (dest,) +
347             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
348      self.script.append(self.WordWrap(cmd))
349
350  def AppendExtra(self, extra):
351    """Append text verbatim to the output script."""
352    self.script.append(extra)
353
354  def Unmount(self, mount_point):
355    self.script.append('unmount("%s");' % mount_point)
356    self.mounts.remove(mount_point)
357
358  def UnmountAll(self):
359    for p in sorted(self.mounts):
360      self.script.append('unmount("%s");' % (p,))
361    self.mounts = set()
362
363  def AddToZip(self, input_zip, output_zip, input_path=None):
364    """Write the accumulated script to the output_zip file.  input_zip
365    is used as the source for the 'updater' binary needed to run
366    script.  If input_path is not None, it will be used as a local
367    path for the binary instead of input_zip."""
368
369    self.UnmountAll()
370
371    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
372                       "\n".join(self.script) + "\n")
373
374    if input_path is None:
375      data = input_zip.read("OTA/bin/updater")
376    else:
377      data = open(input_path, "rb").read()
378    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
379                       data, perms=0o755)
380