edify_generator.py revision 5a48209541d5eed602bfb8e2c4ff51e31443daf2
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):
25    self.script = []
26    self.mounts = set()
27    self.version = version
28
29  def MakeTemporary(self):
30    """Make a temporary script object whose commands can latter be
31    appended to the parent script with AppendScript().  Used when the
32    caller wants to generate script commands out-of-order."""
33    x = EdifyGenerator(self.version)
34    x.mounts = self.mounts
35    return x
36
37  @staticmethod
38  def _WordWrap(cmd, linelen=80):
39    """'cmd' should be a function call with null characters after each
40    parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
41    to a given line length, replacing nulls with spaces and/or newlines
42    to format it nicely."""
43    indent = cmd.index("(")+1
44    out = []
45    first = True
46    x = re.compile("^(.{,%d})\0" % (linelen-indent,))
47    while True:
48      if not first:
49        out.append(" " * indent)
50      first = False
51      m = x.search(cmd)
52      if not m:
53        parts = cmd.split("\0", 1)
54        out.append(parts[0]+"\n")
55        if len(parts) == 1:
56          break
57        else:
58          cmd = parts[1]
59          continue
60      out.append(m.group(1)+"\n")
61      cmd = cmd[m.end():]
62
63    return "".join(out).replace("\0", " ").rstrip("\n")
64
65  def AppendScript(self, other):
66    """Append the contents of another script (which should be created
67    with temporary=True) to this one."""
68    self.script.extend(other.script)
69
70  def AssertSomeFingerprint(self, *fp):
71    """Assert that the current system build fingerprint is one of *fp."""
72    if not fp:
73      raise ValueError("must specify some fingerprints")
74    cmd = ('assert(' +
75           ' ||\0'.join([('file_getprop("/system/build.prop", '
76                         '"ro.build.fingerprint") == "%s"')
77                        % i for i in fp]) +
78           ');')
79    self.script.append(self._WordWrap(cmd))
80
81  def AssertOlderBuild(self, timestamp):
82    """Assert that the build on the device is older (or the same as)
83    the given timestamp."""
84    self.script.append(('assert(!less_than_int(%s, '
85                        'getprop("ro.build.date.utc")));') % (timestamp,))
86
87  def AssertDevice(self, device):
88    """Assert that the device identifier is the given string."""
89    cmd = ('assert(getprop("ro.product.device") == "%s" ||\0'
90           'getprop("ro.build.product") == "%s");' % (device, device))
91    self.script.append(self._WordWrap(cmd))
92
93  def AssertSomeBootloader(self, *bootloaders):
94    """Asert that the bootloader version is one of *bootloaders."""
95    cmd = ("assert(" +
96           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
97                         for b in bootloaders]) +
98           ");")
99    self.script.append(self._WordWrap(cmd))
100
101  def ShowProgress(self, frac, dur):
102    """Update the progress bar, advancing it over 'frac' over the next
103    'dur' seconds.  'dur' may be zero to advance it via SetProgress
104    commands instead of by time."""
105    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
106
107  def SetProgress(self, frac):
108    """Set the position of the progress bar within the chunk defined
109    by the most recent ShowProgress call.  'frac' should be in
110    [0,1]."""
111    self.script.append("set_progress(%f);" % (frac,))
112
113  def PatchCheck(self, filename, *sha1):
114    """Check that the given file (or MTD reference) has one of the
115    given *sha1 hashes."""
116    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
117                       "".join([', "%s"' % (i,) for i in sha1]) +
118                       '));')
119
120  def CacheFreeSpaceCheck(self, amount):
121    """Check that there's at least 'amount' space that can be made
122    available on /cache."""
123    self.script.append("assert(apply_patch_space(%d));" % (amount,))
124
125  def Mount(self, kind, what, path):
126    """Mount the given 'what' at the given path.  'what' should be a
127    partition name if kind is "MTD", or a block device if kind is
128    "vfat".  No other values of 'kind' are supported."""
129    self.script.append('mount("%s", "%s", "%s");' % (kind, what, path))
130    self.mounts.add(path)
131
132  def UnpackPackageDir(self, src, dst):
133    """Unpack a given directory from the OTA package into the given
134    destination directory."""
135    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
136
137  def Comment(self, comment):
138    """Write a comment into the update script."""
139    self.script.append("")
140    for i in comment.split("\n"):
141      self.script.append("# " + i)
142    self.script.append("")
143
144  def Print(self, message):
145    """Log a message to the screen (if the logs are visible)."""
146    self.script.append('ui_print("%s");' % (message,))
147
148  def FormatPartition(self, partition):
149    """Format the given MTD partition."""
150    self.script.append('format("MTD", "%s");' % (partition,))
151
152  def DeleteFiles(self, file_list):
153    """Delete all files in file_list."""
154    if not file_list: return
155    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
156    self.script.append(self._WordWrap(cmd))
157
158  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
159    """Apply binary patches (in *patchpairs) to the given srcfile to
160    produce tgtfile (which may be "-" to indicate overwriting the
161    source file."""
162    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
163      raise ValueError("bad patches given to ApplyPatch")
164    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
165           % (srcfile, tgtfile, tgtsha1, tgtsize)]
166    for i in range(0, len(patchpairs), 2):
167      cmd.append(',\0"%s:%s"' % patchpairs[i:i+2])
168    cmd.append(');')
169    cmd = "".join(cmd)
170    self.script.append(self._WordWrap(cmd))
171
172  def WriteFirmwareImage(self, kind, fn):
173    """Arrange to update the given firmware image (kind must be
174    "hboot" or "radio") when recovery finishes."""
175    if self.version == 1:
176      self.script.append(
177          ('assert(package_extract_file("%(fn)s", "/tmp/%(kind)s.img"),\n'
178           '       write_firmware_image("/tmp/%(kind)s.img", "%(kind)s"));')
179          % {'kind': kind, 'fn': fn})
180    else:
181      self.script.append(
182          'write_firmware_image("PACKAGE:%s", "%s");' % (fn, kind))
183
184  def WriteRawImage(self, partition, fn):
185    """Write the given package file into the given MTD partition."""
186    self.script.append(
187        ('assert(package_extract_file("%(fn)s", "/tmp/%(partition)s.img"),\n'
188         '       write_raw_image("/tmp/%(partition)s.img", "%(partition)s"),\n'
189         '       delete("/tmp/%(partition)s.img"));')
190        % {'partition': partition, 'fn': fn})
191
192  def SetPermissions(self, fn, uid, gid, mode):
193    """Set file ownership and permissions."""
194    self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
195
196  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode):
197    """Recursively set path ownership and permissions."""
198    self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
199                       % (uid, gid, dmode, fmode, fn))
200
201  def MakeSymlinks(self, symlink_list):
202    """Create symlinks, given a list of (dest, link) pairs."""
203    by_dest = {}
204    for d, l in symlink_list:
205      by_dest.setdefault(d, []).append(l)
206
207    for dest, links in sorted(by_dest.iteritems()):
208      cmd = ('symlink("%s", ' % (dest,) +
209             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
210      self.script.append(self._WordWrap(cmd))
211
212  def AppendExtra(self, extra):
213    """Append text verbatim to the output script."""
214    self.script.append(extra)
215
216  def UnmountAll(self):
217    for p in sorted(self.mounts):
218      self.script.append('unmount("%s");' % (p,))
219    self.mounts = set()
220
221  def AddToZip(self, input_zip, output_zip, input_path=None):
222    """Write the accumulated script to the output_zip file.  input_zip
223    is used as the source for the 'updater' binary needed to run
224    script.  If input_path is not None, it will be used as a local
225    path for the binary instead of input_zip."""
226
227    self.UnmountAll()
228
229    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
230                       "\n".join(self.script) + "\n")
231
232    if input_path is None:
233      data = input_zip.read("OTA/bin/updater")
234    else:
235      data = open(os.path.join(input_path, "updater")).read()
236    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
237                       data, perms=0755)
238