edify_generator.py revision 25568486e5777f416d2fcb6cc7aa96caafc66880
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 DeleteFiles(self, file_list):
194    """Delete all files in file_list."""
195    if not file_list: return
196    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
197    self.script.append(self._WordWrap(cmd))
198
199  def RenameFile(self, srcfile, tgtfile):
200    """Moves a file from one location to another."""
201    if self.info.get("update_rename_support", False):
202      self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
203    else:
204      raise ValueError("Rename not supported by update binary")
205
206  def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
207    """Prepend an action with an apply_patch_check in order to
208       skip the action if the file exists.  Used when a patch
209       is later renamed."""
210    cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1))
211    self.script.append(self._WordWrap(cmd))
212
213  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
214    """Apply binary patches (in *patchpairs) to the given srcfile to
215    produce tgtfile (which may be "-" to indicate overwriting the
216    source file."""
217    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
218      raise ValueError("bad patches given to ApplyPatch")
219    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
220           % (srcfile, tgtfile, tgtsha1, tgtsize)]
221    for i in range(0, len(patchpairs), 2):
222      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
223    cmd.append(');')
224    cmd = "".join(cmd)
225    self.script.append(self._WordWrap(cmd))
226
227  def WriteRawImage(self, mount_point, fn):
228    """Write the given package file into the partition for the given
229    mount point."""
230
231    fstab = self.info["fstab"]
232    if fstab:
233      p = fstab[mount_point]
234      partition_type = common.PARTITION_TYPES[p.fs_type]
235      args = {'device': p.device, 'fn': fn}
236      if partition_type == "MTD":
237        self.script.append(
238            'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
239            % args)
240      elif partition_type == "EMMC":
241        self.script.append(
242            'package_extract_file("%(fn)s", "%(device)s");' % args)
243      else:
244        raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,))
245
246  def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
247    """Set file ownership and permissions."""
248    if not self.info.get("use_set_metadata", False):
249      self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
250    else:
251      if capabilities is None: capabilities = "0x0"
252      cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \
253          '"capabilities", %s' % (fn, uid, gid, mode, capabilities)
254      if selabel is not None:
255        cmd += ', "selabel", "%s"' % ( selabel )
256      cmd += ');'
257      self.script.append(cmd)
258
259  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities):
260    """Recursively set path ownership and permissions."""
261    if not self.info.get("use_set_metadata", False):
262      self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
263                         % (uid, gid, dmode, fmode, fn))
264    else:
265      if capabilities is None: capabilities = "0x0"
266      cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
267          '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \
268          % (fn, uid, gid, dmode, fmode, capabilities)
269      if selabel is not None:
270        cmd += ', "selabel", "%s"' % ( selabel )
271      cmd += ');'
272      self.script.append(cmd)
273
274  def MakeSymlinks(self, symlink_list):
275    """Create symlinks, given a list of (dest, link) pairs."""
276    by_dest = {}
277    for d, l in symlink_list:
278      by_dest.setdefault(d, []).append(l)
279
280    for dest, links in sorted(by_dest.iteritems()):
281      cmd = ('symlink("%s", ' % (dest,) +
282             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
283      self.script.append(self._WordWrap(cmd))
284
285  def AppendExtra(self, extra):
286    """Append text verbatim to the output script."""
287    self.script.append(extra)
288
289  def UnmountAll(self):
290    for p in sorted(self.mounts):
291      self.script.append('unmount("%s");' % (p,))
292    self.mounts = set()
293
294  def AddToZip(self, input_zip, output_zip, input_path=None):
295    """Write the accumulated script to the output_zip file.  input_zip
296    is used as the source for the 'updater' binary needed to run
297    script.  If input_path is not None, it will be used as a local
298    path for the binary instead of input_zip."""
299
300    self.UnmountAll()
301
302    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
303                       "\n".join(self.script) + "\n")
304
305    if input_path is None:
306      data = input_zip.read("OTA/bin/updater")
307    else:
308      data = open(input_path, "rb").read()
309    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
310                       data, perms=0755)
311
312  def Syspatch(self, filename, size, target_sha, source_sha, patchfile):
313    """Applies a compressed binary patch to a block device."""
314    call = 'syspatch("%s", "%s", "%s", "%s", "%s");'
315    self.script.append(call % (filename, size, target_sha, source_sha, patchfile))
316