edify_generator.py revision 6e836116f764cf5cebf1654df2f17d8222554f6e
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 system build fingerprint is one of *fp."""
86    if not fp:
87      raise ValueError("must specify some fingerprints")
88    cmd = (
89           ' ||\n    '.join([('file_getprop("/system/build.prop", '
90                         '"ro.build.fingerprint") == "%s"')
91                        % i for i in fp]) +
92           ' ||\n    abort("Package expects build fingerprint of %s; this '
93           'device has " + getprop("ro.build.fingerprint") + ".");'
94           ) % (" or ".join(fp),)
95    self.script.append(cmd)
96
97  def AssertSomeThumbprint(self, *fp):
98    """Assert that the current system build thumbprint is one of *fp."""
99    if not fp:
100      raise ValueError("must specify some thumbprints")
101    cmd = (
102           ' ||\n    '.join([('file_getprop("/system/build.prop", '
103                         '"ro.build.thumbprint") == "%s"')
104                        % i for i in fp]) +
105           ' ||\n    abort("Package expects build thumbprint of %s; this '
106           'device has " + getprop("ro.build.thumbprint") + ".");'
107           ) % (" or ".join(fp),)
108    self.script.append(cmd)
109
110  def AssertOlderBuild(self, timestamp, timestamp_text):
111    """Assert that the build on the device is older (or the same as)
112    the given timestamp."""
113    self.script.append(
114        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
115         'abort("Can\'t install this package (%s) over newer '
116         'build (" + getprop("ro.build.date") + ").");'
117         ) % (timestamp, timestamp_text))
118
119  def AssertDevice(self, device):
120    """Assert that the device identifier is the given string."""
121    cmd = ('getprop("ro.product.device") == "%s" || '
122           'abort("This package is for \\"%s\\" devices; '
123           'this is a \\"" + getprop("ro.product.device") + "\\".");'
124           ) % (device, device)
125    self.script.append(cmd)
126
127  def AssertSomeBootloader(self, *bootloaders):
128    """Asert that the bootloader version is one of *bootloaders."""
129    cmd = ("assert(" +
130           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
131                         for b in bootloaders]) +
132           ");")
133    self.script.append(self._WordWrap(cmd))
134
135  def ShowProgress(self, frac, dur):
136    """Update the progress bar, advancing it over 'frac' over the next
137    'dur' seconds.  'dur' may be zero to advance it via SetProgress
138    commands instead of by time."""
139    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
140
141  def SetProgress(self, frac):
142    """Set the position of the progress bar within the chunk defined
143    by the most recent ShowProgress call.  'frac' should be in
144    [0,1]."""
145    self.script.append("set_progress(%f);" % (frac,))
146
147  def PatchCheck(self, filename, *sha1):
148    """Check that the given file (or MTD reference) has one of the
149    given *sha1 hashes, checking the version saved in cache if the
150    file does not match."""
151    self.script.append(
152        'apply_patch_check("%s"' % (filename,) +
153        "".join([', "%s"' % (i,) for i in sha1]) +
154        ') || abort("\\"%s\\" has unexpected contents.");' % (filename,))
155
156  def FileCheck(self, filename, *sha1):
157    """Check that the given file (or MTD reference) has one of the
158    given *sha1 hashes."""
159    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
160                       "".join([', "%s"' % (i,) for i in sha1]) +
161                       '));')
162
163  def CacheFreeSpaceCheck(self, amount):
164    """Check that there's at least 'amount' space that can be made
165    available on /cache."""
166    self.script.append(('apply_patch_space(%d) || abort("Not enough free space '
167                        'on /system to apply patches.");') % (amount,))
168
169  def Mount(self, mount_point):
170    """Mount the partition with the given mount_point."""
171    fstab = self.info.get("fstab", None)
172    if fstab:
173      p = fstab[mount_point]
174      self.script.append('mount("%s", "%s", "%s", "%s");' %
175                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
176                          p.device, p.mount_point))
177      self.mounts.add(p.mount_point)
178
179  def UnpackPackageDir(self, src, dst):
180    """Unpack a given directory from the OTA package into the given
181    destination directory."""
182    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
183
184  def Comment(self, comment):
185    """Write a comment into the update script."""
186    self.script.append("")
187    for i in comment.split("\n"):
188      self.script.append("# " + i)
189    self.script.append("")
190
191  def Print(self, message):
192    """Log a message to the screen (if the logs are visible)."""
193    self.script.append('ui_print("%s");' % (message,))
194
195  def FormatPartition(self, partition):
196    """Format the given partition, specified by its mount point (eg,
197    "/system")."""
198
199    reserve_size = 0
200    fstab = self.info.get("fstab", None)
201    if fstab:
202      p = fstab[partition]
203      self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
204                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
205                          p.device, p.length, p.mount_point))
206
207  def WipeBlockDevice(self, partition):
208    if partition != "/system":
209      raise ValueError(("WipeBlockDevice currently only works "
210                        "on /system, not %s\n") % (partition,))
211    fstab = self.info.get("fstab", None)
212    size = self.info.get("system_size", None)
213    device = fstab[partition].device
214
215    self.script.append('wipe_block_device("%s", %s);' % (device, size))
216
217  def DeleteFiles(self, file_list):
218    """Delete all files in file_list."""
219    if not file_list: return
220    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
221    self.script.append(self._WordWrap(cmd))
222
223  def RenameFile(self, srcfile, tgtfile):
224    """Moves a file from one location to another."""
225    if self.info.get("update_rename_support", False):
226      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
227    else:
228      raise ValueError("Rename not supported by update binary")
229
230  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
231    """Prepend an action with an apply_patch_check in order to
232       skip the action if the file exists.  Used when a patch
233       is later renamed."""
234    cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1))
235    self.script.append(self._WordWrap(cmd))
236
237  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
238    """Apply binary patches (in *patchpairs) to the given srcfile to
239    produce tgtfile (which may be "-" to indicate overwriting the
240    source file."""
241    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
242      raise ValueError("bad patches given to ApplyPatch")
243    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
244           % (srcfile, tgtfile, tgtsha1, tgtsize)]
245    for i in range(0, len(patchpairs), 2):
246      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
247    cmd.append(');')
248    cmd = "".join(cmd)
249    self.script.append(self._WordWrap(cmd))
250
251  def WriteRawImage(self, mount_point, fn, mapfn=None):
252    """Write the given package file into the partition for the given
253    mount point."""
254
255    fstab = self.info["fstab"]
256    if fstab:
257      p = fstab[mount_point]
258      partition_type = common.PARTITION_TYPES[p.fs_type]
259      args = {'device': p.device, 'fn': fn}
260      if partition_type == "MTD":
261        self.script.append(
262            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
263            % args)
264      elif partition_type == "EMMC":
265        if mapfn:
266          args["map"] = mapfn
267          self.script.append(
268              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
269        else:
270          self.script.append(
271              'package_extract_file("%(fn)s", "%(device)s");' % args)
272      else:
273        raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,))
274
275  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
276    """Set file ownership and permissions."""
277    if not self.info.get("use_set_metadata", False):
278      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
279    else:
280      if capabilities is None: capabilities = "0x0"
281      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
282          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
283      if selabel is not None:
284        cmd += ', "selabel", "%s"' % ( selabel )
285      cmd += ');'
286      self.script.append(cmd)
287
288  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities):
289    """Recursively set path ownership and permissions."""
290    if not self.info.get("use_set_metadata", False):
291      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
292                         % (uid, gid, dmode, fmode, fn))
293    else:
294      if capabilities is None: capabilities = "0x0"
295      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
296          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
297          % (fn, uid, gid, dmode, fmode, capabilities)
298      if selabel is not None:
299        cmd += ', "selabel", "%s"' % ( selabel )
300      cmd += ');'
301      self.script.append(cmd)
302
303  def MakeSymlinks(self, symlink_list):
304    """Create symlinks, given a list of (dest, link) pairs."""
305    by_dest = {}
306    for d, l in symlink_list:
307      by_dest.setdefault(d, []).append(l)
308
309    for dest, links in sorted(by_dest.iteritems()):
310      cmd = ('symlink("%s", ' % (dest,) +
311             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
312      self.script.append(self._WordWrap(cmd))
313
314  def AppendExtra(self, extra):
315    """Append text verbatim to the output script."""
316    self.script.append(extra)
317
318  def UnmountAll(self):
319    for p in sorted(self.mounts):
320      self.script.append('unmount("%s");' % (p,))
321    self.mounts = set()
322
323  def AddToZip(self, input_zip, output_zip, input_path=None):
324    """Write the accumulated script to the output_zip file.  input_zip
325    is used as the source for the 'updater' binary needed to run
326    script.  If input_path is not None, it will be used as a local
327    path for the binary instead of input_zip."""
328
329    self.UnmountAll()
330
331    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
332                       "\n".join(self.script) + "\n")
333
334    if input_path is None:
335      data = input_zip.read("OTA/bin/updater")
336    else:
337      data = open(input_path, "rb").read()
338    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
339                       data, perms=0755)
340
341  def Syspatch(self, filename, target_mapfile, target_sha,
342               source_mapfile, source_sha, patchfile):
343    """Applies a compressed binary patch to a block device."""
344    call = 'syspatch("%s", "%s", "%s", "%s", "%s", "%s");'
345    self.script.append(call % (filename, target_mapfile, target_sha,
346                               source_mapfile, source_sha, patchfile))
347