edify_generator.py revision e7b103751c909dbf3da36e4ba88b3a0746ce8f4c
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 /cache 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 self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % ( 181 p.fs_type, common.PARTITION_TYPES[p.fs_type], p.device, 182 p.mount_point, mount_dict.get(p.fs_type, ""))) 183 self.mounts.add(p.mount_point) 184 185 def UnpackPackageDir(self, src, dst): 186 """Unpack a given directory from the OTA package into the given 187 destination directory.""" 188 self.script.append('package_extract_dir("%s", "%s");' % (src, dst)) 189 190 def Comment(self, comment): 191 """Write a comment into the update script.""" 192 self.script.append("") 193 for i in comment.split("\n"): 194 self.script.append("# " + i) 195 self.script.append("") 196 197 def Print(self, message): 198 """Log a message to the screen (if the logs are visible).""" 199 self.script.append('ui_print("%s");' % (message,)) 200 201 def TunePartition(self, partition, *options): 202 fstab = self.info.get("fstab", None) 203 if fstab: 204 p = fstab[partition] 205 if p.fs_type not in ("ext2", "ext3", "ext4"): 206 raise ValueError("Partition %s cannot be tuned\n" % (partition,)) 207 self.script.append( 208 'tune2fs(' + "".join(['"%s", ' % (i,) for i in options]) + 209 '"%s") || abort("Failed to tune partition %s");' % ( 210 p.device, partition)) 211 212 def FormatPartition(self, partition): 213 """Format the given partition, specified by its mount point (eg, 214 "/system").""" 215 216 fstab = self.info.get("fstab", None) 217 if fstab: 218 p = fstab[partition] 219 self.script.append('format("%s", "%s", "%s", "%s", "%s");' % 220 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 221 p.device, p.length, p.mount_point)) 222 223 def WipeBlockDevice(self, partition): 224 if partition not in ("/system", "/vendor"): 225 raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,)) 226 fstab = self.info.get("fstab", None) 227 size = self.info.get(partition.lstrip("/") + "_size", None) 228 device = fstab[partition].device 229 230 self.script.append('wipe_block_device("%s", %s);' % (device, size)) 231 232 def DeleteFiles(self, file_list): 233 """Delete all files in file_list.""" 234 if not file_list: 235 return 236 cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");" 237 self.script.append(self.WordWrap(cmd)) 238 239 def RenameFile(self, srcfile, tgtfile): 240 """Moves a file from one location to another.""" 241 if self.info.get("update_rename_support", False): 242 self.script.append('rename("%s", "%s");' % (srcfile, tgtfile)) 243 else: 244 raise ValueError("Rename not supported by update binary") 245 246 def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1): 247 """Prepend an action with an apply_patch_check in order to 248 skip the action if the file exists. Used when a patch 249 is later renamed.""" 250 cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1)) 251 self.script.append(self.WordWrap(cmd)) 252 253 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 254 """Apply binary patches (in *patchpairs) to the given srcfile to 255 produce tgtfile (which may be "-" to indicate overwriting the 256 source file.""" 257 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 258 raise ValueError("bad patches given to ApplyPatch") 259 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 260 % (srcfile, tgtfile, tgtsha1, tgtsize)] 261 for i in range(0, len(patchpairs), 2): 262 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 263 cmd.append(');') 264 cmd = "".join(cmd) 265 self.script.append(self.WordWrap(cmd)) 266 267 def WriteRawImage(self, mount_point, fn, mapfn=None): 268 """Write the given package file into the partition for the given 269 mount point.""" 270 271 fstab = self.info["fstab"] 272 if fstab: 273 p = fstab[mount_point] 274 partition_type = common.PARTITION_TYPES[p.fs_type] 275 args = {'device': p.device, 'fn': fn} 276 if partition_type == "MTD": 277 self.script.append( 278 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 279 % args) 280 elif partition_type == "EMMC": 281 if mapfn: 282 args["map"] = mapfn 283 self.script.append( 284 'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args) 285 else: 286 self.script.append( 287 'package_extract_file("%(fn)s", "%(device)s");' % args) 288 else: 289 raise ValueError( 290 "don't know how to write \"%s\" partitions" % p.fs_type) 291 292 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 293 """Set file ownership and permissions.""" 294 if not self.info.get("use_set_metadata", False): 295 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 296 else: 297 if capabilities is None: 298 capabilities = "0x0" 299 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 300 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 301 if selabel is not None: 302 cmd += ', "selabel", "%s"' % selabel 303 cmd += ');' 304 self.script.append(cmd) 305 306 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, 307 capabilities): 308 """Recursively set path ownership and permissions.""" 309 if not self.info.get("use_set_metadata", False): 310 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 311 % (uid, gid, dmode, fmode, fn)) 312 else: 313 if capabilities is None: 314 capabilities = "0x0" 315 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 316 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 317 % (fn, uid, gid, dmode, fmode, capabilities) 318 if selabel is not None: 319 cmd += ', "selabel", "%s"' % selabel 320 cmd += ');' 321 self.script.append(cmd) 322 323 def MakeSymlinks(self, symlink_list): 324 """Create symlinks, given a list of (dest, link) pairs.""" 325 by_dest = {} 326 for d, l in symlink_list: 327 by_dest.setdefault(d, []).append(l) 328 329 for dest, links in sorted(by_dest.iteritems()): 330 cmd = ('symlink("%s", ' % (dest,) + 331 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 332 self.script.append(self.WordWrap(cmd)) 333 334 def AppendExtra(self, extra): 335 """Append text verbatim to the output script.""" 336 self.script.append(extra) 337 338 def Unmount(self, mount_point): 339 self.script.append('unmount("%s");' % mount_point) 340 self.mounts.remove(mount_point) 341 342 def UnmountAll(self): 343 for p in sorted(self.mounts): 344 self.script.append('unmount("%s");' % (p,)) 345 self.mounts = set() 346 347 def AddToZip(self, input_zip, output_zip, input_path=None): 348 """Write the accumulated script to the output_zip file. input_zip 349 is used as the source for the 'updater' binary needed to run 350 script. If input_path is not None, it will be used as a local 351 path for the binary instead of input_zip.""" 352 353 self.UnmountAll() 354 355 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 356 "\n".join(self.script) + "\n") 357 358 if input_path is None: 359 data = input_zip.read("OTA/bin/updater") 360 else: 361 data = open(input_path, "rb").read() 362 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 363 data, perms=0o755) 364