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