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