OSDN Git Service

2c6cfaff094e0de4dd07f0a377cef3b3925f51bc
[android-x86/build.git] / tools / releasetools / edify_generator.py
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
15 import re
16
17 import common
18
19 class 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, value):
81     """Assert that a property on the OEM paritition matches a value."""
82     if not name:
83       raise ValueError("must specify an OEM property")
84     if not value:
85       raise ValueError("must specify the OEM value")
86     if common.OPTIONS.oem_no_mount:
87       cmd = ('getprop("{name}") == "{value}" || '
88              'abort("E{code}: This package expects the value \\"{value}\\" for '
89              '\\"{name}\\"; this has value \\"" + '
90              'getprop("{name}") + "\\".");').format(
91                  code=common.ErrorCode.OEM_PROP_MISMATCH,
92                  name=name, value=value)
93     else:
94       cmd = ('file_getprop("/oem/oem.prop", "{name}") == "{value}" || '
95              'abort("E{code}: This package expects the value \\"{value}\\" for '
96              '\\"{name}\\" on the OEM partition; this has value \\"" + '
97              'file_getprop("/oem/oem.prop", "{name}") + "\\".");').format(
98                  code=common.ErrorCode.OEM_PROP_MISMATCH,
99                  name=name, value=value)
100     self.script.append(cmd)
101
102   def AssertSomeFingerprint(self, *fp):
103     """Assert that the current recovery build fingerprint is one of *fp."""
104     if not fp:
105       raise ValueError("must specify some fingerprints")
106     cmd = (' ||\n    '.join([('getprop("ro.build.fingerprint") == "%s"') % i
107                              for i in fp]) +
108            ' ||\n    abort("E%d: Package expects build fingerprint of %s; '
109            'this device has " + getprop("ro.build.fingerprint") + ".");') % (
110                common.ErrorCode.FINGERPRINT_MISMATCH, " or ".join(fp))
111     self.script.append(cmd)
112
113   def AssertSomeThumbprint(self, *fp):
114     """Assert that the current recovery build thumbprint is one of *fp."""
115     if not fp:
116       raise ValueError("must specify some thumbprints")
117     cmd = (' ||\n    '.join([('getprop("ro.build.thumbprint") == "%s"') % i
118                              for i in fp]) +
119            ' ||\n    abort("E%d: Package expects build thumbprint of %s; this '
120            'device has " + getprop("ro.build.thumbprint") + ".");') % (
121                common.ErrorCode.THUMBPRINT_MISMATCH, " or ".join(fp))
122     self.script.append(cmd)
123
124   def AssertOlderBuild(self, timestamp, timestamp_text):
125     """Assert that the build on the device is older (or the same as)
126     the given timestamp."""
127     self.script.append(
128         ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || '
129          'abort("E%d: Can\'t install this package (%s) over newer '
130          'build (" + getprop("ro.build.date") + ").");') % (timestamp,
131              common.ErrorCode.OLDER_BUILD, timestamp_text))
132
133   def AssertDevice(self, device):
134     """Assert that the device identifier is the given string."""
135     cmd = ('assert(' +
136            ' || '.join(['getprop("ro.product.device") == "%s" || getprop("ro.build.product") == "%s"'
137                          % (i, i) for i in device.split(",")]) +
138            ' || abort("E%d: This package is for device: %s; ' +
139            'this device is " + getprop("ro.product.device") + ".");' +
140            ');') % (common.ErrorCode.DEVICE_MISMATCH, device)
141     self.script.append(cmd)
142
143   def AssertSomeBootloader(self, *bootloaders):
144     """Assert that the bootloader version is one of *bootloaders."""
145     cmd = ("assert(" +
146            " || ".join(['getprop("ro.bootloader") == "%s"' % (b,)
147                          for b in bootloaders]) +
148            ' || abort("This package supports bootloader(s): ' +
149            ", ".join(["%s" % (b,) for b in bootloaders]) +
150            '; this device has bootloader " + getprop("ro.bootloader") + ".");' +
151            ");")
152     self.script.append(self.WordWrap(cmd))
153
154   def AssertSomeBaseband(self, *basebands):
155     """Assert that the baseband version is one of *basebands."""
156     cmd = ("assert(" +
157            " || ".join(['getprop("ro.baseband") == "%s"' % (b,)
158                          for b in basebands]) +
159            ' || abort("This package supports baseband(s): ' +
160            ", ".join(["%s" % (b,) for b in basebands]) +
161            '; this device has baseband " + getprop("ro.baseband") + ".");' +
162            ");")
163     self.script.append(self._WordWrap(cmd))
164
165   def RunBackup(self, command):
166     self.script.append(('run_program("/tmp/install/bin/backuptool.sh", "%s");' % command))
167
168   def ValidateSignatures(self, command):
169     self.script.append('package_extract_file("META-INF/org/cyanogenmod/releasekey", "/tmp/releasekey");')
170     # Exit code 124 == abort. run_program returns raw, so left-shift 8bit
171     self.script.append('run_program("/tmp/install/bin/otasigcheck.sh") != "31744" || abort("Can\'t install this package on top of incompatible data. Please try another package or run a factory reset");')
172
173   def ShowProgress(self, frac, dur):
174     """Update the progress bar, advancing it over 'frac' over the next
175     'dur' seconds.  'dur' may be zero to advance it via SetProgress
176     commands instead of by time."""
177     self.script.append("show_progress(%f, %d);" % (frac, int(dur)))
178
179   def SetProgress(self, frac):
180     """Set the position of the progress bar within the chunk defined
181     by the most recent ShowProgress call.  'frac' should be in
182     [0,1]."""
183     self.script.append("set_progress(%f);" % (frac,))
184
185   def PatchCheck(self, filename, *sha1):
186     """Check that the given file (or MTD reference) has one of the
187     given *sha1 hashes, checking the version saved in cache if the
188     file does not match."""
189     self.script.append(
190         'apply_patch_check("%s"' % (filename,) +
191         "".join([', "%s"' % (i,) for i in sha1]) +
192         ') || abort("E%d: \\"%s\\" has unexpected contents.");' % (
193             common.ErrorCode.BAD_PATCH_FILE, filename))
194
195   def Verify(self, filename):
196     """Check that the given file (or MTD reference) has one of the
197     given hashes (encoded in the filename)."""
198     self.script.append(
199         'apply_patch_check("{filename}") && '
200         'ui_print("    Verified.") || '
201         'ui_print("\\"{filename}\\" has unexpected contents.");'.format(
202             filename=filename))
203
204   def FileCheck(self, filename, *sha1):
205     """Check that the given file (or MTD reference) has one of the
206     given *sha1 hashes."""
207     self.script.append('assert(sha1_check(read_file("%s")' % (filename,) +
208                        "".join([', "%s"' % (i,) for i in sha1]) +
209                        '));')
210
211   def CacheFreeSpaceCheck(self, amount):
212     """Check that there's at least 'amount' space that can be made
213     available on /cache."""
214     self._required_cache = max(self._required_cache, amount)
215     self.script.append(('apply_patch_space(%d) || abort("E%d: Not enough free '
216                         'space on /cache to apply patches.");') % (
217                             amount,
218                             common.ErrorCode.INSUFFICIENT_CACHE_SPACE))
219
220   def Mount(self, mount_point, mount_options_by_format=""):
221     """Mount the partition with the given mount_point.
222       mount_options_by_format:
223       [fs_type=option[,option]...[|fs_type=option[,option]...]...]
224       where option is optname[=optvalue]
225       E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover
226     """
227     fstab = self.fstab
228     if fstab:
229       p = fstab[mount_point]
230       mount_dict = {}
231       if mount_options_by_format is not None:
232         for option in mount_options_by_format.split("|"):
233           if "=" in option:
234             key, value = option.split("=", 1)
235             mount_dict[key] = value
236       mount_flags = mount_dict.get(p.fs_type, "")
237       if p.context is not None:
238         mount_flags = p.context + ("," + mount_flags if mount_flags else "")
239       self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % (
240           p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device,
241           p.mount_point, mount_flags))
242       self.mounts.add(p.mount_point)
243
244   def Unmount(self, mount_point):
245     """Unmount the partition with the given mount_point."""
246     if mount_point in self.mounts:
247       self.mounts.remove(mount_point)
248       self.script.append('unmount("%s");' % (mount_point,))
249
250   def UnpackPackageDir(self, src, dst):
251     """Unpack a given directory from the OTA package into the given
252     destination directory."""
253     self.script.append('package_extract_dir("%s", "%s");' % (src, dst))
254
255   def Comment(self, comment):
256     """Write a comment into the update script."""
257     self.script.append("")
258     for i in comment.split("\n"):
259       self.script.append("# " + i)
260     self.script.append("")
261
262   def Print(self, message):
263     """Log a message to the screen (if the logs are visible)."""
264     self.script.append('ui_print("%s");' % (message,))
265
266   def TunePartition(self, partition, *options):
267     fstab = self.fstab
268     if fstab:
269       p = fstab[partition]
270       if p.fs_type not in ("ext2", "ext3", "ext4"):
271         raise ValueError("Partition %s cannot be tuned\n" % (partition,))
272     self.script.append(
273         'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) +
274         '"%s") || abort("E%d: Failed to tune partition %s");' % (
275             p.device, common.ErrorCode.TUNE_PARTITION_FAILURE, partition))
276
277   def FormatPartition(self, partition):
278     """Format the given partition, specified by its mount point (eg,
279     "/system")."""
280
281     fstab = self.fstab
282     if fstab:
283       p = fstab[partition]
284       self.script.append('format("%s", "%s", "%s", "%s", "%s");' %
285                          (p.fs_type, common.PARTITION_TYPES[p.fs_type],
286                           p.device, p.length, p.mount_point))
287
288   def WipeBlockDevice(self, partition):
289     if partition not in ("/system", "/vendor"):
290       raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,))
291     fstab = self.fstab
292     size = self.info.get(partition.lstrip("/") + "_size", None)
293     device = fstab[partition].device
294
295     self.script.append('wipe_block_device("%s", %s);' % (device, size))
296
297   def DeleteFiles(self, file_list):
298     """Delete all files in file_list."""
299     if not file_list:
300       return
301     cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");"
302     self.script.append(self.WordWrap(cmd))
303
304   def DeleteFilesIfNotMatching(self, file_list):
305     """Delete the file in file_list if not matching the checksum."""
306     if not file_list:
307       return
308     for name, sha1 in file_list:
309       cmd = ('sha1_check(read_file("{name}"), "{sha1}") || '
310              'delete("{name}");'.format(name=name, sha1=sha1))
311       self.script.append(self.WordWrap(cmd))
312
313   def RenameFile(self, srcfile, tgtfile):
314     """Moves a file from one location to another."""
315     if self.info.get("update_rename_support", False):
316       self.script.append('rename("%s", "%s");' % (srcfile, tgtfile))
317     else:
318       raise ValueError("Rename not supported by update binary")
319
320   def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1):
321     """Prepend an action with an apply_patch_check in order to
322        skip the action if the file exists.  Used when a patch
323        is later renamed."""
324     cmd = ('sha1_check(read_file("%s"), %s) ||' % (tgtfile, tgtsha1))
325     self.script.append(self.WordWrap(cmd))
326
327   def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs):
328     """Apply binary patches (in *patchpairs) to the given srcfile to
329     produce tgtfile (which may be "-" to indicate overwriting the
330     source file."""
331     if len(patchpairs) % 2 != 0 or len(patchpairs) == 0:
332       raise ValueError("bad patches given to ApplyPatch")
333     cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d'
334            % (srcfile, tgtfile, tgtsha1, tgtsize)]
335     for i in range(0, len(patchpairs), 2):
336       cmd.append(',\0%s,\0package_extract_file("%s")' % patchpairs[i:i+2])
337     cmd.append(') ||\n    abort("E%d: Failed to apply patch to %s");' % (
338         common.ErrorCode.APPLY_PATCH_FAILURE, srcfile))
339     cmd = "".join(cmd)
340     self.script.append(self.WordWrap(cmd))
341
342   def WriteRawImage(self, mount_point, fn, mapfn=None):
343     """Write the given package file into the partition for the given
344     mount point."""
345
346     fstab = self.fstab
347     if fstab:
348       p = fstab[mount_point]
349       partition_type = common.PARTITION_TYPES[p.fs_type]
350       args = {'device': p.device, 'fn': fn}
351       if partition_type == "MTD":
352         self.script.append(
353             'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");'
354             % args)
355       elif partition_type == "OSIP":
356         self.script.append(
357             'write_osip_image(package_extract_file("%(fn)s"), "%(device)s");'
358             % args)
359       elif partition_type == "EMMC":
360         if mapfn:
361           args["map"] = mapfn
362           self.script.append(
363               'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args)
364         else:
365           self.script.append(
366               'package_extract_file("%(fn)s", "%(device)s");' % args)
367       else:
368         raise ValueError(
369             "don't know how to write \"%s\" partitions" % p.fs_type)
370
371   def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities):
372     """Set file ownership and permissions."""
373     if not self.info.get("use_set_metadata", False):
374       self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn))
375     else:
376       cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o' \
377           % (fn, uid, gid, mode)
378       if capabilities is not None:
379         cmd += ', "capabilities", %s' % ( capabilities )
380       if selabel is not None:
381         cmd += ', "selabel", "%s"' % selabel
382       cmd += ');'
383       self.script.append(cmd)
384
385   def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel,
386                               capabilities):
387     """Recursively set path ownership and permissions."""
388     if not self.info.get("use_set_metadata", False):
389       self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");'
390                          % (uid, gid, dmode, fmode, fn))
391     else:
392       cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \
393           '"dmode", 0%o, "fmode", 0%o' \
394           % (fn, uid, gid, dmode, fmode)
395       if capabilities is not None:
396         cmd += ', "capabilities", "%s"' % ( capabilities )
397       if selabel is not None:
398         cmd += ', "selabel", "%s"' % selabel
399       cmd += ');'
400       self.script.append(cmd)
401
402   def MakeSymlinks(self, symlink_list):
403     """Create symlinks, given a list of (dest, link) pairs."""
404     by_dest = {}
405     for d, l in symlink_list:
406       by_dest.setdefault(d, []).append(l)
407
408     for dest, links in sorted(by_dest.iteritems()):
409       cmd = ('symlink("%s", ' % (dest,) +
410              ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");")
411       self.script.append(self.WordWrap(cmd))
412
413   def AppendExtra(self, extra):
414     """Append text verbatim to the output script."""
415     self.script.append(extra)
416
417   def Unmount(self, mount_point):
418     self.script.append('unmount("%s");' % mount_point)
419     self.mounts.remove(mount_point)
420
421   def UnmountAll(self):
422     for p in sorted(self.mounts):
423       self.script.append('unmount("%s");' % (p,))
424     self.mounts = set()
425
426   def AddToZip(self, input_zip, output_zip, input_path=None):
427     """Write the accumulated script to the output_zip file.  input_zip
428     is used as the source for the 'updater' binary needed to run
429     script.  If input_path is not None, it will be used as a local
430     path for the binary instead of input_zip."""
431
432     self.UnmountAll()
433
434     common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script",
435                        "\n".join(self.script) + "\n")
436
437     if input_path is None:
438       data = input_zip.read("OTA/bin/updater")
439     else:
440       data = open(input_path, "rb").read()
441     common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary",
442                        data, perms=0o755)