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 os
16import re
17
18import common
19
20class EdifyGenerator(object):
21  """Class to generate scripts in the 'edify' recovery script language
22  used from donut onwards."""
23
24  def __init__(self, version, info):
25    self.script = []
26    self.mounts = set()
27    self.version = version
28    self.info = info
29
30  def MakeTemporary(self):
31    """Make a temporary script object whose commands can latter be
32    appended to the parent script with AppendScript().  Used when the
33    caller wants to generate script commands out-of-order."""
34    x = EdifyGenerator(self.version, self.info)
35    x.mounts = self.mounts
36    return x
37
38  @staticmethod
39  def _WordWrap(cmd, linelen=80):
40    """'cmd' should be a function call with null characters after each
41    parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
42    to a given line length, replacing nulls with spaces and/or newlines
43    to format it nicely."""
44    indent = cmd.index("(")+1
45    out = []
46    first = True
47    x = re.compile("^(.{,%d})\0" % (linelen-indent,))
48    while True:
49      if not first:
50        out.append(" " * indent)
51      first = False
52      m = x.search(cmd)
53      if not m:
54        parts = cmd.split("\0", 1)
55        out.append(parts[0]+"\n")
56        if len(parts) == 1:
57          break
58        else:
59          cmd = parts[1]
60          continue
61      out.append(m.group(1)+"\n")
62      cmd = cmd[m.end():]
63
64    return "".join(out).replace("\0", " ").rstrip("\n")
65
66  def AppendScript(self, other):
67    """Append the contents of another script (which should be created
68    with temporary=True) to this one."""
69    self.script.extend(other.script)
70
71  def AssertOemProperty(self, name, value):
72    """Assert that a property on the OEM paritition matches a value."""
73    if not name:
74      raise ValueError("must specify an OEM property")
75    if not value:
76      raise ValueError("must specify the OEM value")
77    cmd = ('file_getprop("/oem/oem.prop", "%s") == "%s" || '
78           'abort("This package expects the value \\"%s\\"  for '
79           '\\"%s\\" on the OEM partition; '
80           'this has value \\"" + file_getprop("/oem/oem.prop") + "\\".");'
81           ) % (name, value, name, value)
82    self.script.append(cmd)
83
84  def AssertSomeFingerprint(self, *fp):
85    """Assert that the current recovery build fingerprint is one of *fp."""
86    if not fp:
87      raise ValueError("must specify some fingerprints")
88    cmd = (
89           ' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"')
90                        % i for i in fp]) +
91           ' ||\n    abort("Package expects build fingerprint of %s; this '
92           'device has " + getprop("ro.build.fingerprint") + ".");'
93           ) % (" or ".join(fp),)
94    self.script.append(cmd)
95
96  def AssertSomeThumbprint(self, *fp):
97    """Assert that the current recovery build thumbprint is one of *fp."""
98    if not fp:
99      raise ValueError("must specify some thumbprints")
100    cmd = (
101           ' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"')
102                        % i for i in fp]) +
103           ' ||\n    abort("Package expects build thumbprint of %s; this '
104           'device has " + getprop("ro.build.thumbprint") + ".");'
105           ) % (" or ".join(fp),)
106    self.script.append(cmd)
107
108  def AssertOlderBuild(self, timestamp, timestamp_text):
109    """Assert that the build on the device is older (or the same as)
110    the given timestamp."""
111    self.script.append(
112        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
113         'abort("Can\'t install this package (%s) over newer '
114         'build (" + getprop("ro.build.date") + ").");'
115         ) % (timestamp, timestamp_text))
116
117  def AssertDevice(self, device):
118    """Assert that the device identifier is the given string."""
119    cmd = ('getprop("ro.product.device") == "%s" || '
120           'abort("This package is for \\"%s\\" devices; '
121           'this is a \\"" + getprop("ro.product.device") + "\\".");'
122           ) % (device, device)
123    self.script.append(cmd)
124
125  def AssertSomeBootloader(self, *bootloaders):
126    """Asert that the bootloader version is one of *bootloaders."""
127    cmd = ("assert(" +
128           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
129                         for b in bootloaders]) +
130           ");")
131    self.script.append(self._WordWrap(cmd))
132
133  def ShowProgress(self, frac, dur):
134    """Update the progress bar, advancing it over 'frac' over the next
135    'dur' seconds.  'dur' may be zero to advance it via SetProgress
136    commands instead of by time."""
137    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
138
139  def SetProgress(self, frac):
140    """Set the position of the progress bar within the chunk defined
141    by the most recent ShowProgress call.  'frac' should be in
142    [0,1]."""
143    self.script.append("set_progress(%f);" % (frac,))
144
145  def PatchCheck(self, filename, *sha1):
146    """Check that the given file (or MTD reference) has one of the
147    given *sha1 hashes, checking the version saved in cache if the
148    file does not match."""
149    self.script.append(
150        'apply_patch_check("%s"' % (filename,) +
151        "".join([', "%s"' % (i,) for i in sha1]) +
152        ') || abort("\\"%s\\" has unexpected contents.");' % (filename,))
153
154  def FileCheck(self, filename, *sha1):
155    """Check that the given file (or MTD reference) has one of the
156    given *sha1 hashes."""
157    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
158                       "".join([', "%s"' % (i,) for i in sha1]) +
159                       '));')
160
161  def CacheFreeSpaceCheck(self, amount):
162    """Check that there's at least 'amount' space that can be made
163    available on /cache."""
164    self.script.append(('apply_patch_space(%d) || abort("Not enough free space '
165                        'on /system to apply patches.");') % (amount,))
166
167  def Mount(self, mount_point, mount_options_by_format=""):
168    """Mount the partition with the given mount_point.
169      mount_options_by_format:
170      [fs_type=option[,option]...[|fs_type=option[,option]...]...]
171      where option is optname[=optvalue]
172      E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
173    """
174    fstab = self.info.get("fstab", None)
175    if fstab:
176      p = fstab[mount_point]
177      mount_dict = {}
178      if mount_options_by_format is not None:
179        for option in mount_options_by_format.split("|"):
180          if "=" in option:
181            key, value = option.split("=", 1)
182            mount_dict[key] = value
183      self.script.append('mount("%s", "%s", "%s", "%s", "%s");' %
184                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
185                          p.device, p.mount_point, mount_dict.get(p.fs_type, "")))
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('tune2fs(' +
211                       "".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    reserve_size = 0
220    fstab = self.info.get("fstab", None)
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.info.get("fstab", None)
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: 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("don't know how to write \"%s\" partitions" % (p.fs_type,))
293
294  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
295    """Set file ownership and permissions."""
296    if not self.info.get("use_set_metadata", False):
297      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
298    else:
299      if capabilities is None: capabilities = "0x0"
300      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
301          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
302      if selabel is not None:
303        cmd += ', "selabel", "%s"' % ( selabel )
304      cmd += ');'
305      self.script.append(cmd)
306
307  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, 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: capabilities = "0x0"
314      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
315          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
316          % (fn, uid, gid, dmode, fmode, capabilities)
317      if selabel is not None:
318        cmd += ', "selabel", "%s"' % ( selabel )
319      cmd += ');'
320      self.script.append(cmd)
321
322  def MakeSymlinks(self, symlink_list):
323    """Create symlinks, given a list of (dest, link) pairs."""
324    by_dest = {}
325    for d, l in symlink_list:
326      by_dest.setdefault(d, []).append(l)
327
328    for dest, links in sorted(by_dest.iteritems()):
329      cmd = ('symlink("%s", ' % (dest,) +
330             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
331      self.script.append(self._WordWrap(cmd))
332
333  def AppendExtra(self, extra):
334    """Append text verbatim to the output script."""
335    self.script.append(extra)
336
337  def Unmount(self, mount_point):
338    self.script.append('unmount("%s");' % (mount_point,))
339    self.mounts.remove(mount_point);
340
341  def UnmountAll(self):
342    for p in sorted(self.mounts):
343      self.script.append('unmount("%s");' % (p,))
344    self.mounts = set()
345
346  def AddToZip(self, input_zip, output_zip, input_path=None):
347    """Write the accumulated script to the output_zip file.  input_zip
348    is used as the source for the 'updater' binary needed to run
349    script.  If input_path is not None, it will be used as a local
350    path for the binary instead of input_zip."""
351
352    self.UnmountAll()
353
354    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
355                       "\n".join(self.script) + "\n")
356
357    if input_path is None:
358      data = input_zip.read("OTA/bin/updater")
359    else:
360      data = open(input_path, "rb").read()
361    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
362                       data, perms=0755)
363