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