edify_generator.py revision 34b47bf42b4b9fad8e775a37e40922598bb7bd96
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      self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % (
185          p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device,
186          p.mount_point, mount_dict.get(p.fs_type, "")))
187      self.mounts.add(p.mount_point)
188
189  def UnpackPackageDir(self, src, dst):
190    """Unpack a given directory from the OTA package into the given
191    destination directory."""
192    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
193
194  def Comment(self, comment):
195    """Write a comment into the update script."""
196    self.script.append("")
197    for i in comment.split("\n"):
198      self.script.append("# " + i)
199    self.script.append("")
200
201  def Print(self, message):
202    """Log a message to the screen (if the logs are visible)."""
203    self.script.append('ui_print("%s");' % (message,))
204
205  def TunePartition(self, partition, *options):
206    fstab = self.fstab
207    if fstab:
208      p = fstab[partition]
209      if p.fs_type not in ("ext2", "ext3", "ext4"):
210        raise ValueError("Partition %s cannot be tuned\n" % (partition,))
211    self.script.append(
212        'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
213        '"%s") || abort("Failed to tune partition %s");' % (
214            p.device, partition))
215
216  def FormatPartition(self, partition):
217    """Format the given partition, specified by its mount point (eg,
218    "/system")."""
219
220    fstab = self.fstab
221    if fstab:
222      p = fstab[partition]
223      self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
224                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
225                          p.device, p.length, p.mount_point))
226
227  def WipeBlockDevice(self, partition):
228    if partition not in ("/system", "/vendor"):
229      raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
230    fstab = self.fstab
231    size = self.info.get(partition.lstrip("/") + "_size", None)
232    device = fstab[partition].device
233
234    self.script.append('wipe_block_device("%s", %s);' % (device, size))
235
236  def DeleteFiles(self, file_list):
237    """Delete all files in file_list."""
238    if not file_list:
239      return
240    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
241    self.script.append(self.WordWrap(cmd))
242
243  def RenameFile(self, srcfile, tgtfile):
244    """Moves a file from one location to another."""
245    if self.info.get("update_rename_support", False):
246      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
247    else:
248      raise ValueError("Rename not supported by update binary")
249
250  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
251    """Prepend an action with an apply_patch_check in order to
252       skip the action if the file exists.  Used when a patch
253       is later renamed."""
254    cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1))
255    self.script.append(self.WordWrap(cmd))
256
257  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
258    """Apply binary patches (in *patchpairs) to the given srcfile to
259    produce tgtfile (which may be "-" to indicate overwriting the
260    source file."""
261    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
262      raise ValueError("bad patches given to ApplyPatch")
263    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
264           % (srcfile, tgtfile, tgtsha1, tgtsize)]
265    for i in range(0, len(patchpairs), 2):
266      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
267    cmd.append(');')
268    cmd = "".join(cmd)
269    self.script.append(self.WordWrap(cmd))
270
271  def WriteRawImage(self, mount_point, fn, mapfn=None):
272    """Write the given package file into the partition for the given
273    mount point."""
274
275    fstab = self.fstab
276    if fstab:
277      p = fstab[mount_point]
278      partition_type = common.PARTITION_TYPES[p.fs_type]
279      args = {'device': p.device, 'fn': fn}
280      if partition_type == "MTD":
281        self.script.append(
282            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
283            % args)
284      elif partition_type == "EMMC":
285        if mapfn:
286          args["map"] = mapfn
287          self.script.append(
288              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
289        else:
290          self.script.append(
291              'package_extract_file("%(fn)s", "%(device)s");' % args)
292      else:
293        raise ValueError(
294            "don't know how to write \"%s\" partitions" % p.fs_type)
295
296  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
297    """Set file ownership and permissions."""
298    if not self.info.get("use_set_metadata", False):
299      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
300    else:
301      if capabilities is None:
302        capabilities = "0x0"
303      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
304          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
305      if selabel is not None:
306        cmd += ', "selabel", "%s"' % selabel
307      cmd += ');'
308      self.script.append(cmd)
309
310  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel,
311                              capabilities):
312    """Recursively set path ownership and permissions."""
313    if not self.info.get("use_set_metadata", False):
314      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
315                         % (uid, gid, dmode, fmode, fn))
316    else:
317      if capabilities is None:
318        capabilities = "0x0"
319      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
320          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
321          % (fn, uid, gid, dmode, fmode, capabilities)
322      if selabel is not None:
323        cmd += ', "selabel", "%s"' % selabel
324      cmd += ');'
325      self.script.append(cmd)
326
327  def MakeSymlinks(self, symlink_list):
328    """Create symlinks, given a list of (dest, link) pairs."""
329    by_dest = {}
330    for d, l in symlink_list:
331      by_dest.setdefault(d, []).append(l)
332
333    for dest, links in sorted(by_dest.iteritems()):
334      cmd = ('symlink("%s", ' % (dest,) +
335             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
336      self.script.append(self.WordWrap(cmd))
337
338  def AppendExtra(self, extra):
339    """Append text verbatim to the output script."""
340    self.script.append(extra)
341
342  def Unmount(self, mount_point):
343    self.script.append('unmount("%s");' % mount_point)
344    self.mounts.remove(mount_point)
345
346  def UnmountAll(self):
347    for p in sorted(self.mounts):
348      self.script.append('unmount("%s");' % (p,))
349    self.mounts = set()
350
351  def AddToZip(self, input_zip, output_zip, input_path=None):
352    """Write the accumulated script to the output_zip file.  input_zip
353    is used as the source for the 'updater' binary needed to run
354    script.  If input_path is not None, it will be used as a local
355    path for the binary instead of input_zip."""
356
357    self.UnmountAll()
358
359    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
360                       "\n".join(self.script) + "\n")
361
362    if input_path is None:
363      data = input_zip.read("OTA/bin/updater")
364    else:
365      data = open(input_path, "rb").read()
366    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
367                       data, perms=0o755)
368