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