edify_generator.py revision 560d535cffcb38d475ba6af6ac516e61ba8fec56
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 = ('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, mount_point):
135    """Mount the partition with the given mount_point."""
136    fstab = self.info.get("fstab", None)
137    if fstab:
138      p = fstab[mount_point]
139      self.script.append('mount("%s", "%s", "%s", "%s");' %
140                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
141                          p.device, p.mount_point))
142      self.mounts.add(p.mount_point)
143    else:
144      what = mount_point.lstrip("/")
145      what = self.info.get("partition_path", "") + what
146      self.script.append('mount("%s", "%s", "%s", "%s");' %
147                         (self.info["fs_type"], self.info["partition_type"],
148                          what, mount_point))
149      self.mounts.add(mount_point)
150
151  def UnpackPackageDir(self, src, dst):
152    """Unpack a given directory from the OTA package into the given
153    destination directory."""
154    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
155
156  def Comment(self, comment):
157    """Write a comment into the update script."""
158    self.script.append("")
159    for i in comment.split("\n"):
160      self.script.append("# " + i)
161    self.script.append("")
162
163  def Print(self, message):
164    """Log a message to the screen (if the logs are visible)."""
165    self.script.append('ui_print("%s");' % (message,))
166
167  def FormatPartition(self, partition):
168    """Format the given partition, specified by its mount point (eg,
169    "/system")."""
170
171    reserve_size = 0
172    fstab = self.info.get("fstab", None)
173    if fstab:
174      p = fstab[partition]
175      self.script.append('format("%s", "%s", "%s", "%s");' %
176                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
177                          p.device, p.length))
178    else:
179      # older target-files without per-partition types
180      partition = self.info.get("partition_path", "") + partition
181      self.script.append('format("%s", "%s", "%s", "%s");' %
182                         (self.info["fs_type"], self.info["partition_type"],
183                          partition, reserve_size))
184
185  def DeleteFiles(self, file_list):
186    """Delete all files in file_list."""
187    if not file_list: return
188    cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
189    self.script.append(self._WordWrap(cmd))
190
191  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
192    """Apply binary patches (in *patchpairs) to the given srcfile to
193    produce tgtfile (which may be "-" to indicate overwriting the
194    source file."""
195    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
196      raise ValueError("bad patches given to ApplyPatch")
197    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
198           % (srcfile, tgtfile, tgtsha1, tgtsize)]
199    for i in range(0, len(patchpairs), 2):
200      cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2])
201    cmd.append(');')
202    cmd = "".join(cmd)
203    self.script.append(self._WordWrap(cmd))
204
205  def WriteFirmwareImage(self, kind, fn):
206    """Arrange to update the given firmware image (kind must be
207    "hboot" or "radio") when recovery finishes."""
208    if self.version == 1:
209      self.script.append(
210          ('assert(package_extract_file("%(fn)s", "/tmp/%(kind)s.img"),\n'
211           '       write_firmware_image("/tmp/%(kind)s.img", "%(kind)s"));')
212          % {'kind': kind, 'fn': fn})
213    else:
214      self.script.append(
215          'write_firmware_image("PACKAGE:%s", "%s");' % (fn, kind))
216
217  def WriteRawImage(self, mount_point, fn):
218    """Write the given package file into the partition for the given
219    mount point."""
220
221    fstab = self.info["fstab"]
222    if fstab:
223      p = fstab[mount_point]
224      partition_type = common.PARTITION_TYPES[p.fs_type]
225      args = {'device': p.device, 'fn': fn}
226      if partition_type == "MTD":
227        self.script.append(
228            ('assert(package_extract_file("%(fn)s", "/tmp/%(device)s.img"),\n'
229             '       write_raw_image("/tmp/%(device)s.img", "%(device)s"),\n'
230             '       delete("/tmp/%(device)s.img"));') % args)
231      elif partition_type == "EMMC":
232        self.script.append(
233            'package_extract_file("%(fn)s", "%(device)s");' % args)
234      else:
235        raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,))
236    else:
237      # backward compatibility with older target-files that lack recovery.fstab
238      if self.info["partition_type"] == "MTD":
239        self.script.append(
240            ('assert(package_extract_file("%(fn)s", "/tmp/%(partition)s.img"),\n'
241             '       write_raw_image("/tmp/%(partition)s.img", "%(partition)s"),\n'
242             '       delete("/tmp/%(partition)s.img"));')
243            % {'partition': partition, 'fn': fn})
244      elif self.info["partition_type"] == "EMMC":
245        self.script.append(
246            ('package_extract_file("%(fn)s", "%(dir)s%(partition)s");')
247            % {'partition': partition, 'fn': fn,
248               'dir': self.info.get("partition_path", ""),
249               })
250      else:
251        raise ValueError("don't know how to write \"%s\" partitions" %
252                         (self.info["partition_type"],))
253
254  def SetPermissions(self, fn, uid, gid, mode):
255    """Set file ownership and permissions."""
256    self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
257
258  def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode):
259    """Recursively set path ownership and permissions."""
260    self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
261                       % (uid, gid, dmode, fmode, fn))
262
263  def MakeSymlinks(self, symlink_list):
264    """Create symlinks, given a list of (dest, link) pairs."""
265    by_dest = {}
266    for d, l in symlink_list:
267      by_dest.setdefault(d, []).append(l)
268
269    for dest, links in sorted(by_dest.iteritems()):
270      cmd = ('symlink("%s", ' % (dest,) +
271             ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
272      self.script.append(self._WordWrap(cmd))
273
274  def RetouchBinaries(self, file_list):
275    """Execute the retouch instructions in files listed."""
276    cmd = ('retouch_binaries(' +
277           ', '.join(['"' + i[0] + '", "' + i[1] + '"' for i in file_list]) +
278           ');')
279    self.script.append(self._WordWrap(cmd))
280
281  def UndoRetouchBinaries(self, file_list):
282    """Undo the retouching (retouch to zero offset)."""
283    cmd = ('undo_retouch_binaries(' +
284           ', '.join(['"' + i[0] + '", "' + i[1] + '"' for i in file_list]) +
285           ');')
286    self.script.append(self._WordWrap(cmd))
287
288  def AppendExtra(self, extra):
289    """Append text verbatim to the output script."""
290    self.script.append(extra)
291
292  def UnmountAll(self):
293    for p in sorted(self.mounts):
294      self.script.append('unmount("%s");' % (p,))
295    self.mounts = set()
296
297  def AddToZip(self, input_zip, output_zip, input_path=None):
298    """Write the accumulated script to the output_zip file.  input_zip
299    is used as the source for the 'updater' binary needed to run
300    script.  If input_path is not None, it will be used as a local
301    path for the binary instead of input_zip."""
302
303    self.UnmountAll()
304
305    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
306                       "\n".join(self.script) + "\n")
307
308    if input_path is None:
309      data = input_zip.read("OTA/bin/updater")
310    else:
311      data = open(os.path.join(input_path, "updater")).read()
312    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
313                       data, perms=0755)
314