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