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