edify_generator.py revision c19a8d5590a4ffd42b37ceaca2d779b48e481f99
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)
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 = ('assert(' +
76           ' ||\0'.join([('file_getprop("/system/build.prop", '
77                         '"ro.build.fingerprint") == "%s"')
78                        % i for i in fp]) +
79           ');')
80    self.script.append(self._WordWrap(cmd))
81
82  def AssertOlderBuild(self, timestamp):
83    """Assert that the build on the device is older (or the same as)
84    the given timestamp."""
85    self.script.append(('assert(!less_than_int(%s, '
86                        'getprop("ro.build.date.utc")));') % (timestamp,))
87
88  def AssertDevice(self, device):
89    """Assert that the device identifier is the given string."""
90    cmd = ('assert(getprop("ro.product.device") == "%s" ||\0'
91           'getprop("ro.build.product") == "%s");' % (device, device))
92    self.script.append(self._WordWrap(cmd))
93
94  def AssertSomeBootloader(self, *bootloaders):
95    """Asert that the bootloader version is one of *bootloaders."""
96    cmd = ("assert(" +
97           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
98                         for b in bootloaders]) +
99           ");")
100    self.script.append(self._WordWrap(cmd))
101
102  def ShowProgress(self, frac, dur):
103    """Update the progress bar, advancing it over 'frac' over the next
104    'dur' seconds.  'dur' may be zero to advance it via SetProgress
105    commands instead of by time."""
106    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
107
108  def SetProgress(self, frac):
109    """Set the position of the progress bar within the chunk defined
110    by the most recent ShowProgress call.  'frac' should be in
111    [0,1]."""
112    self.script.append("set_progress(%f);" % (frac,))
113
114  def PatchCheck(self, filename, *sha1):
115    """Check that the given file (or MTD reference) has one of the
116    given *sha1 hashes, checking the version saved in cache if the
117    file does not match."""
118    self.script.append('assert(apply_patch_check("%s"' % (filename,) +
119                       "".join([', "%s"' % (i,) for i in sha1]) +
120                       '));')
121
122  def FileCheck(self, filename, *sha1):
123    """Check that the given file (or MTD reference) has one of the
124    given *sha1 hashes."""
125    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
126                       "".join([', "%s"' % (i,) for i in sha1]) +
127                       '));')
128
129  def CacheFreeSpaceCheck(self, amount):
130    """Check that there's at least 'amount' space that can be made
131    available on /cache."""
132    self.script.append("assert(apply_patch_space(%d));" % (amount,))
133
134  def Mount(self, what, mount_point):
135    """Mount the given 'what' at the given path.  'what' should be a
136    partition name for an MTD partition, or a block device for
137    anything else."""
138    what = self.info.get("partition_path", "") + what
139    self.script.append('mount("%s", "%s", "%s", "%s");' %
140                       (self.info["fs_type"], self.info["partition_type"],
141                        what, mount_point))
142    self.mounts.add(mount_point)
143
144  def UnpackPackageDir(self, src, dst):
145    """Unpack a given directory from the OTA package into the given
146    destination directory."""
147    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
148
149  def Comment(self, comment):
150    """Write a comment into the update script."""
151    self.script.append("")
152    for i in comment.split("\n"):
153      self.script.append("# " + i)
154    self.script.append("")
155
156  def Print(self, message):
157    """Log a message to the screen (if the logs are visible)."""
158    self.script.append('ui_print("%s");' % (message,))
159
160  def FormatPartition(self, partition):
161    """Format the given partition."""
162    partition = self.info.get("partition_path", "") + partition
163    self.script.append('format("%s", "%s", "%s");' %
164                       (self.info["fs_type"], self.info["partition_type"],
165                        partition))
166
167  def DeleteFiles(self, file_list):
168    """Delete all files in file_list."""
169    if not file_list: return
170    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
171    self.script.append(self._WordWrap(cmd))
172
173  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
174    """Apply binary patches (in *patchpairs) to the given srcfile to
175    produce tgtfile (which may be "-" to indicate overwriting the
176    source file."""
177    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
178      raise ValueError("bad patches given to ApplyPatch")
179    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
180           % (srcfile, tgtfile, tgtsha1, tgtsize)]
181    for i in range(0, len(patchpairs), 2):
182      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
183    cmd.append(');')
184    cmd = "".join(cmd)
185    self.script.append(self._WordWrap(cmd))
186
187  def WriteFirmwareImage(self, kind, fn):
188    """Arrange to update the given firmware image (kind must be
189    "hboot" or "radio") when recovery finishes."""
190    if self.version == 1:
191      self.script.append(
192          ('assert(package_extract_file("%(fn)s", "/tmp/%(kind)s.img"),\n'
193           '       write_firmware_image("/tmp/%(kind)s.img", "%(kind)s"));')
194          % {'kind': kind, 'fn': fn})
195    else:
196      self.script.append(
197          'write_firmware_image("PACKAGE:%s", "%s");' % (fn, kind))
198
199  def WriteRawImage(self, partition, fn):
200    """Write the given package file into the given partition."""
201
202    if self.info["partition_type"] == "MTD":
203      self.script.append(
204          ('assert(package_extract_file("%(fn)s", "/tmp/%(partition)s.img"),\n'
205           '       write_raw_image("/tmp/%(partition)s.img", "%(partition)s"),\n'
206           '       delete("/tmp/%(partition)s.img"));')
207          % {'partition': partition, 'fn': fn})
208    elif self.info["partition_type"] == "EMMC":
209      self.script.append(
210          ('package_extract_file("%(fn)s", "%(dir)s%(partition)s");')
211          % {'partition': partition, 'fn': fn,
212             'dir': self.info.get("partition_path", ""),
213             })
214    else:
215      raise ValueError("don't know how to write \"%s\" partitions" %
216                       (self.info["partition_type"],))
217
218  def SetPermissions(self, fn, uid, gid, mode):
219    """Set file ownership and permissions."""
220    self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
221
222  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode):
223    """Recursively set path ownership and permissions."""
224    self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
225                       % (uid, gid, dmode, fmode, fn))
226
227  def MakeSymlinks(self, symlink_list):
228    """Create symlinks, given a list of (dest, link) pairs."""
229    by_dest = {}
230    for d, l in symlink_list:
231      by_dest.setdefault(d, []).append(l)
232
233    for dest, links in sorted(by_dest.iteritems()):
234      cmd = ('symlink("%s", ' % (dest,) +
235             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
236      self.script.append(self._WordWrap(cmd))
237
238  def AppendExtra(self, extra):
239    """Append text verbatim to the output script."""
240    self.script.append(extra)
241
242  def UnmountAll(self):
243    for p in sorted(self.mounts):
244      self.script.append('unmount("%s");' % (p,))
245    self.mounts = set()
246
247  def AddToZip(self, input_zip, output_zip, input_path=None):
248    """Write the accumulated script to the output_zip file.  input_zip
249    is used as the source for the 'updater' binary needed to run
250    script.  If input_path is not None, it will be used as a local
251    path for the binary instead of input_zip."""
252
253    self.UnmountAll()
254
255    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
256                       "\n".join(self.script) + "\n")
257
258    if input_path is None:
259      data = input_zip.read("OTA/bin/updater")
260    else:
261      data = open(os.path.join(input_path, "updater")).read()
262    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
263                       data, perms=0755)
264