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