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