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