edify_generator.py revision c8d446bcde877ec94f8e68dd5af68fe34eb1b1f9
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, checking the version saved in cache if the
116    file does not match."""
117    self.script.append('assert(apply_patch_check("%s"' % (filename,) +
118                       "".join([', "%s"' % (i,) for i in sha1]) +
119                       '));')
120
121  def FileCheck(self, filename, *sha1):
122    """Check that the given file (or MTD reference) has one of the
123    given *sha1 hashes."""
124    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
125                       "".join([', "%s"' % (i,) for i in sha1]) +
126                       '));')
127
128  def CacheFreeSpaceCheck(self, amount):
129    """Check that there's at least 'amount' space that can be made
130    available on /cache."""
131    self.script.append("assert(apply_patch_space(%d));" % (amount,))
132
133  def Mount(self, kind, what, path):
134    """Mount the given 'what' at the given path.  'what' should be a
135    partition name if kind is "MTD", or a block device if kind is
136    "vfat".  No other values of 'kind' are supported."""
137    self.script.append('mount("%s", "%s", "%s");' % (kind, what, path))
138    self.mounts.add(path)
139
140  def UnpackPackageDir(self, src, dst):
141    """Unpack a given directory from the OTA package into the given
142    destination directory."""
143    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
144
145  def Comment(self, comment):
146    """Write a comment into the update script."""
147    self.script.append("")
148    for i in comment.split("\n"):
149      self.script.append("# " + i)
150    self.script.append("")
151
152  def Print(self, message):
153    """Log a message to the screen (if the logs are visible)."""
154    self.script.append('ui_print("%s");' % (message,))
155
156  def FormatPartition(self, partition):
157    """Format the given MTD partition."""
158    self.script.append('format("MTD", "%s");' % (partition,))
159
160  def DeleteFiles(self, file_list):
161    """Delete all files in file_list."""
162    if not file_list: return
163    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
164    self.script.append(self._WordWrap(cmd))
165
166  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
167    """Apply binary patches (in *patchpairs) to the given srcfile to
168    produce tgtfile (which may be "-" to indicate overwriting the
169    source file."""
170    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
171      raise ValueError("bad patches given to ApplyPatch")
172    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
173           % (srcfile, tgtfile, tgtsha1, tgtsize)]
174    for i in range(0, len(patchpairs), 2):
175      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
176    cmd.append(');')
177    cmd = "".join(cmd)
178    self.script.append(self._WordWrap(cmd))
179
180  def WriteFirmwareImage(self, kind, fn):
181    """Arrange to update the given firmware image (kind must be
182    "hboot" or "radio") when recovery finishes."""
183    if self.version == 1:
184      self.script.append(
185          ('assert(package_extract_file("%(fn)s", "/tmp/%(kind)s.img"),\n'
186           '       write_firmware_image("/tmp/%(kind)s.img", "%(kind)s"));')
187          % {'kind': kind, 'fn': fn})
188    else:
189      self.script.append(
190          'write_firmware_image("PACKAGE:%s", "%s");' % (fn, kind))
191
192  def WriteRawImage(self, partition, fn):
193    """Write the given package file into the given MTD partition."""
194    self.script.append(
195        ('assert(package_extract_file("%(fn)s", "/tmp/%(partition)s.img"),\n'
196         '       write_raw_image("/tmp/%(partition)s.img", "%(partition)s"),\n'
197         '       delete("/tmp/%(partition)s.img"));')
198        % {'partition': partition, 'fn': fn})
199
200  def SetPermissions(self, fn, uid, gid, mode):
201    """Set file ownership and permissions."""
202    self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
203
204  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode):
205    """Recursively set path ownership and permissions."""
206    self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
207                       % (uid, gid, dmode, fmode, fn))
208
209  def MakeSymlinks(self, symlink_list):
210    """Create symlinks, given a list of (dest, link) pairs."""
211    by_dest = {}
212    for d, l in symlink_list:
213      by_dest.setdefault(d, []).append(l)
214
215    for dest, links in sorted(by_dest.iteritems()):
216      cmd = ('symlink("%s", ' % (dest,) +
217             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
218      self.script.append(self._WordWrap(cmd))
219
220  def AppendExtra(self, extra):
221    """Append text verbatim to the output script."""
222    self.script.append(extra)
223
224  def UnmountAll(self):
225    for p in sorted(self.mounts):
226      self.script.append('unmount("%s");' % (p,))
227    self.mounts = set()
228
229  def AddToZip(self, input_zip, output_zip, input_path=None):
230    """Write the accumulated script to the output_zip file.  input_zip
231    is used as the source for the 'updater' binary needed to run
232    script.  If input_path is not None, it will be used as a local
233    path for the binary instead of input_zip."""
234
235    self.UnmountAll()
236
237    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
238                       "\n".join(self.script) + "\n")
239
240    if input_path is None:
241      data = input_zip.read("OTA/bin/updater")
242    else:
243      data = open(os.path.join(input_path, "updater")).read()
244    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
245                       data, perms=0755)
246