edify_generator.py revision 6e836116f764cf5cebf1654df2f17d8222554f6e
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 AssertOemProperty(self, name, value): 72 """Assert that a property on the OEM paritition matches a value.""" 73 if not name: 74 raise ValueError("must specify an OEM property") 75 if not value: 76 raise ValueError("must specify the OEM value") 77 cmd = ('file_getprop("/oem/oem.prop", "%s") == "%s" || ' 78 'abort("This package expects the value \\"%s\\" for ' 79 '\\"%s\\" on the OEM partition; ' 80 'this has value \\"" + file_getprop("/oem/oem.prop") + "\\".");' 81 ) % (name, value, name, value) 82 self.script.append(cmd) 83 84 def AssertSomeFingerprint(self, *fp): 85 """Assert that the current system build fingerprint is one of *fp.""" 86 if not fp: 87 raise ValueError("must specify some fingerprints") 88 cmd = ( 89 ' ||\n '.join([('file_getprop("/system/build.prop", ' 90 '"ro.build.fingerprint") == "%s"') 91 % i for i in fp]) + 92 ' ||\n abort("Package expects build fingerprint of %s; this ' 93 'device has " + getprop("ro.build.fingerprint") + ".");' 94 ) % (" or ".join(fp),) 95 self.script.append(cmd) 96 97 def AssertSomeThumbprint(self, *fp): 98 """Assert that the current system build thumbprint is one of *fp.""" 99 if not fp: 100 raise ValueError("must specify some thumbprints") 101 cmd = ( 102 ' ||\n '.join([('file_getprop("/system/build.prop", ' 103 '"ro.build.thumbprint") == "%s"') 104 % i for i in fp]) + 105 ' ||\n abort("Package expects build thumbprint of %s; this ' 106 'device has " + getprop("ro.build.thumbprint") + ".");' 107 ) % (" or ".join(fp),) 108 self.script.append(cmd) 109 110 def AssertOlderBuild(self, timestamp, timestamp_text): 111 """Assert that the build on the device is older (or the same as) 112 the given timestamp.""" 113 self.script.append( 114 ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || ' 115 'abort("Can\'t install this package (%s) over newer ' 116 'build (" + getprop("ro.build.date") + ").");' 117 ) % (timestamp, timestamp_text)) 118 119 def AssertDevice(self, device): 120 """Assert that the device identifier is the given string.""" 121 cmd = ('getprop("ro.product.device") == "%s" || ' 122 'abort("This package is for \\"%s\\" devices; ' 123 'this is a \\"" + getprop("ro.product.device") + "\\".");' 124 ) % (device, device) 125 self.script.append(cmd) 126 127 def AssertSomeBootloader(self, *bootloaders): 128 """Asert that the bootloader version is one of *bootloaders.""" 129 cmd = ("assert(" + 130 " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,) 131 for b in bootloaders]) + 132 ");") 133 self.script.append(self._WordWrap(cmd)) 134 135 def ShowProgress(self, frac, dur): 136 """Update the progress bar, advancing it over 'frac' over the next 137 'dur' seconds. 'dur' may be zero to advance it via SetProgress 138 commands instead of by time.""" 139 self.script.append("show_progress(%f, %d);" % (frac, int(dur))) 140 141 def SetProgress(self, frac): 142 """Set the position of the progress bar within the chunk defined 143 by the most recent ShowProgress call. 'frac' should be in 144 [0,1].""" 145 self.script.append("set_progress(%f);" % (frac,)) 146 147 def PatchCheck(self, filename, *sha1): 148 """Check that the given file (or MTD reference) has one of the 149 given *sha1 hashes, checking the version saved in cache if the 150 file does not match.""" 151 self.script.append( 152 'apply_patch_check("%s"' % (filename,) + 153 "".join([', "%s"' % (i,) for i in sha1]) + 154 ') || abort("\\"%s\\" has unexpected contents.");' % (filename,)) 155 156 def FileCheck(self, filename, *sha1): 157 """Check that the given file (or MTD reference) has one of the 158 given *sha1 hashes.""" 159 self.script.append('assert(sha1_check(read_file("%s")' % (filename,) + 160 "".join([', "%s"' % (i,) for i in sha1]) + 161 '));') 162 163 def CacheFreeSpaceCheck(self, amount): 164 """Check that there's at least 'amount' space that can be made 165 available on /cache.""" 166 self.script.append(('apply_patch_space(%d) || abort("Not enough free space ' 167 'on /system to apply patches.");') % (amount,)) 168 169 def Mount(self, mount_point): 170 """Mount the partition with the given mount_point.""" 171 fstab = self.info.get("fstab", None) 172 if fstab: 173 p = fstab[mount_point] 174 self.script.append('mount("%s", "%s", "%s", "%s");' % 175 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 176 p.device, p.mount_point)) 177 self.mounts.add(p.mount_point) 178 179 def UnpackPackageDir(self, src, dst): 180 """Unpack a given directory from the OTA package into the given 181 destination directory.""" 182 self.script.append('package_extract_dir("%s", "%s");' % (src, dst)) 183 184 def Comment(self, comment): 185 """Write a comment into the update script.""" 186 self.script.append("") 187 for i in comment.split("\n"): 188 self.script.append("# " + i) 189 self.script.append("") 190 191 def Print(self, message): 192 """Log a message to the screen (if the logs are visible).""" 193 self.script.append('ui_print("%s");' % (message,)) 194 195 def FormatPartition(self, partition): 196 """Format the given partition, specified by its mount point (eg, 197 "/system").""" 198 199 reserve_size = 0 200 fstab = self.info.get("fstab", None) 201 if fstab: 202 p = fstab[partition] 203 self.script.append('format("%s", "%s", "%s", "%s", "%s");' % 204 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 205 p.device, p.length, p.mount_point)) 206 207 def WipeBlockDevice(self, partition): 208 if partition != "/system": 209 raise ValueError(("WipeBlockDevice currently only works " 210 "on /system, not %s\n") % (partition,)) 211 fstab = self.info.get("fstab", None) 212 size = self.info.get("system_size", None) 213 device = fstab[partition].device 214 215 self.script.append('wipe_block_device("%s", %s);' % (device, size)) 216 217 def DeleteFiles(self, file_list): 218 """Delete all files in file_list.""" 219 if not file_list: return 220 cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");" 221 self.script.append(self._WordWrap(cmd)) 222 223 def RenameFile(self, srcfile, tgtfile): 224 """Moves a file from one location to another.""" 225 if self.info.get("update_rename_support", False): 226 self.script.append('rename("%s", "%s");' % (srcfile, tgtfile)) 227 else: 228 raise ValueError("Rename not supported by update binary") 229 230 def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1): 231 """Prepend an action with an apply_patch_check in order to 232 skip the action if the file exists. Used when a patch 233 is later renamed.""" 234 cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1)) 235 self.script.append(self._WordWrap(cmd)) 236 237 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 238 """Apply binary patches (in *patchpairs) to the given srcfile to 239 produce tgtfile (which may be "-" to indicate overwriting the 240 source file.""" 241 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 242 raise ValueError("bad patches given to ApplyPatch") 243 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 244 % (srcfile, tgtfile, tgtsha1, tgtsize)] 245 for i in range(0, len(patchpairs), 2): 246 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 247 cmd.append(');') 248 cmd = "".join(cmd) 249 self.script.append(self._WordWrap(cmd)) 250 251 def WriteRawImage(self, mount_point, fn, mapfn=None): 252 """Write the given package file into the partition for the given 253 mount point.""" 254 255 fstab = self.info["fstab"] 256 if fstab: 257 p = fstab[mount_point] 258 partition_type = common.PARTITION_TYPES[p.fs_type] 259 args = {'device': p.device, 'fn': fn} 260 if partition_type == "MTD": 261 self.script.append( 262 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 263 % args) 264 elif partition_type == "EMMC": 265 if mapfn: 266 args["map"] = mapfn 267 self.script.append( 268 'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args) 269 else: 270 self.script.append( 271 'package_extract_file("%(fn)s", "%(device)s");' % args) 272 else: 273 raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,)) 274 275 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 276 """Set file ownership and permissions.""" 277 if not self.info.get("use_set_metadata", False): 278 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 279 else: 280 if capabilities is None: capabilities = "0x0" 281 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 282 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 283 if selabel is not None: 284 cmd += ', "selabel", "%s"' % ( selabel ) 285 cmd += ');' 286 self.script.append(cmd) 287 288 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities): 289 """Recursively set path ownership and permissions.""" 290 if not self.info.get("use_set_metadata", False): 291 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 292 % (uid, gid, dmode, fmode, fn)) 293 else: 294 if capabilities is None: capabilities = "0x0" 295 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 296 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 297 % (fn, uid, gid, dmode, fmode, capabilities) 298 if selabel is not None: 299 cmd += ', "selabel", "%s"' % ( selabel ) 300 cmd += ');' 301 self.script.append(cmd) 302 303 def MakeSymlinks(self, symlink_list): 304 """Create symlinks, given a list of (dest, link) pairs.""" 305 by_dest = {} 306 for d, l in symlink_list: 307 by_dest.setdefault(d, []).append(l) 308 309 for dest, links in sorted(by_dest.iteritems()): 310 cmd = ('symlink("%s", ' % (dest,) + 311 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 312 self.script.append(self._WordWrap(cmd)) 313 314 def AppendExtra(self, extra): 315 """Append text verbatim to the output script.""" 316 self.script.append(extra) 317 318 def UnmountAll(self): 319 for p in sorted(self.mounts): 320 self.script.append('unmount("%s");' % (p,)) 321 self.mounts = set() 322 323 def AddToZip(self, input_zip, output_zip, input_path=None): 324 """Write the accumulated script to the output_zip file. input_zip 325 is used as the source for the 'updater' binary needed to run 326 script. If input_path is not None, it will be used as a local 327 path for the binary instead of input_zip.""" 328 329 self.UnmountAll() 330 331 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 332 "\n".join(self.script) + "\n") 333 334 if input_path is None: 335 data = input_zip.read("OTA/bin/updater") 336 else: 337 data = open(input_path, "rb").read() 338 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 339 data, perms=0755) 340 341 def Syspatch(self, filename, target_mapfile, target_sha, 342 source_mapfile, source_sha, patchfile): 343 """Applies a compressed binary patch to a block device.""" 344 call = 'syspatch("%s", "%s", "%s", "%s", "%s", "%s");' 345 self.script.append(call % (filename, target_mapfile, target_sha, 346 source_mapfile, source_sha, patchfile)) 347