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