• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 re
16
17import common
18
19class EdifyGenerator(object):
20  """Class to generate scripts in the 'edify' recovery script language
21  used from donut onwards."""
22
23  def __init__(self, version, info, fstab=None):
24    self.script = []
25    self.mounts = set()
26    self._required_cache = 0
27    self.version = version
28    self.info = info
29    if fstab is None:
30      self.fstab = self.info.get("fstab", None)
31    else:
32      self.fstab = fstab
33
34  def MakeTemporary(self):
35    """Make a temporary script object whose commands can latter be
36    appended to the parent script with AppendScript().  Used when the
37    caller wants to generate script commands out-of-order."""
38    x = EdifyGenerator(self.version, self.info)
39    x.mounts = self.mounts
40    return x
41
42  @property
43  def required_cache(self):
44    """Return the minimum cache size to apply the update."""
45    return self._required_cache
46
47  @staticmethod
48  def WordWrap(cmd, linelen=80):
49    """'cmd' should be a function call with null characters after each
50    parameter (eg, "somefun(foo,\0bar,\0baz)").  This function wraps cmd
51    to a given line length, replacing nulls with spaces and/or newlines
52    to format it nicely."""
53    indent = cmd.index("(")+1
54    out = []
55    first = True
56    x = re.compile("^(.{,%d})\0" % (linelen-indent,))
57    while True:
58      if not first:
59        out.append(" " * indent)
60      first = False
61      m = x.search(cmd)
62      if not m:
63        parts = cmd.split("\0", 1)
64        out.append(parts[0]+"\n")
65        if len(parts) == 1:
66          break
67        else:
68          cmd = parts[1]
69          continue
70      out.append(m.group(1)+"\n")
71      cmd = cmd[m.end():]
72
73    return "".join(out).replace("\0", " ").rstrip("\n")
74
75  def AppendScript(self, other):
76    """Append the contents of another script (which should be created
77    with temporary=True) to this one."""
78    self.script.extend(other.script)
79
80  def AssertOemProperty(self, name, values):
81    """Assert that a property on the OEM paritition matches allowed values."""
82    if not name:
83      raise ValueError("must specify an OEM property")
84    if not values:
85      raise ValueError("must specify the OEM value")
86    get_prop_command = None
87    if common.OPTIONS.oem_no_mount:
88      get_prop_command = 'getprop("%s")' % name
89    else:
90      get_prop_command = 'file_getprop("/oem/oem.prop", "%s")' % name
91
92    cmd = ''
93    for value in values:
94      cmd += '%s == "%s" || ' % (get_prop_command, value)
95    cmd += (
96        'abort("E{code}: This package expects the value \\"{values}\\" for '
97        '\\"{name}\\"; this has value \\"" + '
98        '{get_prop_command} + "\\".");').format(
99            code=common.ErrorCode.OEM_PROP_MISMATCH,
100            get_prop_command=get_prop_command, name=name,
101            values='\\" or \\"'.join(values))
102    self.script.append(cmd)
103
104  def AssertSomeFingerprint(self, *fp):
105    """Assert that the current recovery build fingerprint is one of *fp."""
106    if not fp:
107      raise ValueError("must specify some fingerprints")
108    cmd = (' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"') % i
109                             for i in fp]) +
110           ' ||\n    abort("E%d: Package expects build fingerprint of %s; '
111           'this device has " + getprop("ro.build.fingerprint") + ".");') % (
112               common.ErrorCode.FINGERPRINT_MISMATCH, " or ".join(fp))
113    self.script.append(cmd)
114
115  def AssertSomeThumbprint(self, *fp):
116    """Assert that the current recovery build thumbprint is one of *fp."""
117    if not fp:
118      raise ValueError("must specify some thumbprints")
119    cmd = (' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"') % i
120                             for i in fp]) +
121           ' ||\n    abort("E%d: Package expects build thumbprint of %s; this '
122           'device has " + getprop("ro.build.thumbprint") + ".");') % (
123               common.ErrorCode.THUMBPRINT_MISMATCH, " or ".join(fp))
124    self.script.append(cmd)
125
126  def AssertFingerprintOrThumbprint(self, fp, tp):
127    """Assert that the current recovery build fingerprint is fp, or thumbprint
128       is tp."""
129    cmd = ('getprop("ro.build.fingerprint") == "{fp}" ||\n'
130           '    getprop("ro.build.thumbprint") == "{tp}" ||\n'
131           '    abort("Package expects build fingerprint of {fp} or '
132           'thumbprint of {tp}; this device has a fingerprint of " '
133           '+ getprop("ro.build.fingerprint") + " and a thumbprint of " '
134           '+ getprop("ro.build.thumbprint") + ".");').format(fp=fp, tp=tp)
135    self.script.append(cmd)
136
137  def AssertOlderBuild(self, timestamp, timestamp_text):
138    """Assert that the build on the device is older (or the same as)
139    the given timestamp."""
140    self.script.append(
141        ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
142         'abort("E%d: Can\'t install this package (%s) over newer '
143         'build (" + getprop("ro.build.date") + ").");') % (timestamp,
144             common.ErrorCode.OLDER_BUILD, timestamp_text))
145
146  def AssertDevice(self, device):
147    """Assert that the device identifier is the given string."""
148    cmd = ('getprop("ro.product.device") == "%s" || '
149           'abort("E%d: This package is for \\"%s\\" devices; '
150           'this is a \\"" + getprop("ro.product.device") + "\\".");') % (
151               device, common.ErrorCode.DEVICE_MISMATCH, device)
152    self.script.append(cmd)
153
154  def AssertSomeBootloader(self, *bootloaders):
155    """Asert that the bootloader version is one of *bootloaders."""
156    cmd = ("assert(" +
157           " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,)
158                         for b in bootloaders]) +
159           ");")
160    self.script.append(self.WordWrap(cmd))
161
162  def ShowProgress(self, frac, dur):
163    """Update the progress bar, advancing it over 'frac' over the next
164    'dur' seconds.  'dur' may be zero to advance it via SetProgress
165    commands instead of by time."""
166    self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
167
168  def SetProgress(self, frac):
169    """Set the position of the progress bar within the chunk defined
170    by the most recent ShowProgress call.  'frac' should be in
171    [0,1]."""
172    self.script.append("set_progress(%f);" % (frac,))
173
174  def PatchCheck(self, filename, *sha1):
175    """Check that the given file has one of the
176    given *sha1 hashes, checking the version saved in cache if the
177    file does not match."""
178    self.script.append(
179        'apply_patch_check("%s"' % (filename,) +
180        "".join([', "%s"' % (i,) for i in sha1]) +
181        ') || abort("E%d: \\"%s\\" has unexpected contents.");' % (
182            common.ErrorCode.BAD_PATCH_FILE, filename))
183
184  def Verify(self, filename):
185    """Check that the given file has one of the
186    given hashes (encoded in the filename)."""
187    self.script.append(
188        'apply_patch_check("{filename}") && '
189        'ui_print("    Verified.") || '
190        'ui_print("\\"{filename}\\" has unexpected contents.");'.format(
191            filename=filename))
192
193  def FileCheck(self, filename, *sha1):
194    """Check that the given file has one of the
195    given *sha1 hashes."""
196    self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
197                       "".join([', "%s"' % (i,) for i in sha1]) +
198                       '));')
199
200  def CacheFreeSpaceCheck(self, amount):
201    """Check that there's at least 'amount' space that can be made
202    available on /cache."""
203    self._required_cache = max(self._required_cache, amount)
204    self.script.append(('apply_patch_space(%d) || abort("E%d: Not enough free '
205                        'space on /cache to apply patches.");') % (
206                            amount,
207                            common.ErrorCode.INSUFFICIENT_CACHE_SPACE))
208
209  def Mount(self, mount_point, mount_options_by_format=""):
210    """Mount the partition with the given mount_point.
211      mount_options_by_format:
212      [fs_type=option[,option]...[|fs_type=option[,option]...]...]
213      where option is optname[=optvalue]
214      E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
215    """
216    fstab = self.fstab
217    if fstab:
218      p = fstab[mount_point]
219      mount_dict = {}
220      if mount_options_by_format is not None:
221        for option in mount_options_by_format.split("|"):
222          if "=" in option:
223            key, value = option.split("=", 1)
224            mount_dict[key] = value
225      mount_flags = mount_dict.get(p.fs_type, "")
226      if p.context is not None:
227        mount_flags = p.context + ("," + mount_flags if mount_flags else "")
228      self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % (
229          p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device,
230          p.mount_point, mount_flags))
231      self.mounts.add(p.mount_point)
232
233  def UnpackPackageDir(self, src, dst):
234    """Unpack a given directory from the OTA package into the given
235    destination directory."""
236    self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
237
238  def Comment(self, comment):
239    """Write a comment into the update script."""
240    self.script.append("")
241    for i in comment.split("\n"):
242      self.script.append("# " + i)
243    self.script.append("")
244
245  def Print(self, message):
246    """Log a message to the screen (if the logs are visible)."""
247    self.script.append('ui_print("%s");' % (message,))
248
249  def TunePartition(self, partition, *options):
250    fstab = self.fstab
251    if fstab:
252      p = fstab[partition]
253      if p.fs_type not in ("ext2", "ext3", "ext4"):
254        raise ValueError("Partition %s cannot be tuned\n" % (partition,))
255    self.script.append(
256        'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
257        '"%s") || abort("E%d: Failed to tune partition %s");' % (
258            p.device, common.ErrorCode.TUNE_PARTITION_FAILURE, partition))
259
260  def FormatPartition(self, partition):
261    """Format the given partition, specified by its mount point (eg,
262    "/system")."""
263
264    fstab = self.fstab
265    if fstab:
266      p = fstab[partition]
267      self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
268                         (p.fs_type, common.PARTITION_TYPES[p.fs_type],
269                          p.device, p.length, p.mount_point))
270
271  def WipeBlockDevice(self, partition):
272    if partition not in ("/system", "/vendor"):
273      raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
274    fstab = self.fstab
275    size = self.info.get(partition.lstrip("/") + "_size", None)
276    device = fstab[partition].device
277
278    self.script.append('wipe_block_device("%s", %s);' % (device, size))
279
280  def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
281    """Apply binary patches (in *patchpairs) to the given srcfile to
282    produce tgtfile (which may be "-" to indicate overwriting the
283    source file."""
284    if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
285      raise ValueError("bad patches given to ApplyPatch")
286    cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
287           % (srcfile, tgtfile, tgtsha1, tgtsize)]
288    for i in range(0, len(patchpairs), 2):
289      cmd.append(',\0%s,\0package_extract_file("%s")' % patchpairs[i:i+2])
290    cmd.append(') ||\n    abort("E%d: Failed to apply patch to %s");' % (
291        common.ErrorCode.APPLY_PATCH_FAILURE, srcfile))
292    cmd = "".join(cmd)
293    self.script.append(self.WordWrap(cmd))
294
295  def WriteRawImage(self, mount_point, fn, mapfn=None):
296    """Write the given package file into the partition for the given
297    mount point."""
298
299    fstab = self.fstab
300    if fstab:
301      p = fstab[mount_point]
302      partition_type = common.PARTITION_TYPES[p.fs_type]
303      args = {'device': p.device, 'fn': fn}
304      if partition_type == "EMMC":
305        if mapfn:
306          args["map"] = mapfn
307          self.script.append(
308              'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
309        else:
310          self.script.append(
311              'package_extract_file("%(fn)s", "%(device)s");' % args)
312      else:
313        raise ValueError(
314            "don't know how to write \"%s\" partitions" % p.fs_type)
315
316  def AppendExtra(self, extra):
317    """Append text verbatim to the output script."""
318    self.script.append(extra)
319
320  def Unmount(self, mount_point):
321    self.script.append('unmount("%s");' % mount_point)
322    self.mounts.remove(mount_point)
323
324  def UnmountAll(self):
325    for p in sorted(self.mounts):
326      self.script.append('unmount("%s");' % (p,))
327    self.mounts = set()
328
329  def AddToZip(self, input_zip, output_zip, input_path=None):
330    """Write the accumulated script to the output_zip file.  input_zip
331    is used as the source for the 'updater' binary needed to run
332    script.  If input_path is not None, it will be used as a local
333    path for the binary instead of input_zip."""
334
335    self.UnmountAll()
336
337    common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
338                       "\n".join(self.script) + "\n")
339
340    if input_path is None:
341      data = input_zip.read("OTA/bin/updater")
342    else:
343      data = open(input_path, "rb").read()
344    common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
345                       data, perms=0o755)
346