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 DeleteFilesIfNotMatching(self, file_list): 247 """Delete the file in file_list if not matching the checksum.""" 248 if not file_list: 249 return 250 for name, sha1 in file_list: 251 cmd = ('sha1_check(read_file("{name}"), "{sha1}") || ' 252 'delete("{name}");'.format(name=name, sha1=sha1)) 253 self.script.append(self.WordWrap(cmd)) 254 255 def RenameFile(self, srcfile, tgtfile): 256 """Moves a file from one location to another.""" 257 if self.info.get("update_rename_support", False): 258 self.script.append('rename("%s", "%s");' % (srcfile, tgtfile)) 259 else: 260 raise ValueError("Rename not supported by update binary") 261 262 def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1): 263 """Prepend an action with an apply_patch_check in order to 264 skip the action if the file exists. Used when a patch 265 is later renamed.""" 266 cmd = ('sha1_check(read_file("%s"), %s) ||' % (tgtfile, tgtsha1)) 267 self.script.append(self.WordWrap(cmd)) 268 269 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 270 """Apply binary patches (in *patchpairs) to the given srcfile to 271 produce tgtfile (which may be "-" to indicate overwriting the 272 source file.""" 273 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 274 raise ValueError("bad patches given to ApplyPatch") 275 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 276 % (srcfile, tgtfile, tgtsha1, tgtsize)] 277 for i in range(0, len(patchpairs), 2): 278 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 279 cmd.append(');') 280 cmd = "".join(cmd) 281 self.script.append(self.WordWrap(cmd)) 282 283 def WriteRawImage(self, mount_point, fn, mapfn=None): 284 """Write the given package file into the partition for the given 285 mount point.""" 286 287 fstab = self.fstab 288 if fstab: 289 p = fstab[mount_point] 290 partition_type = common.PARTITION_TYPES[p.fs_type] 291 args = {'device': p.device, 'fn': fn} 292 if partition_type == "MTD": 293 self.script.append( 294 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 295 % args) 296 elif partition_type == "EMMC": 297 if mapfn: 298 args["map"] = mapfn 299 self.script.append( 300 'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args) 301 else: 302 self.script.append( 303 'package_extract_file("%(fn)s", "%(device)s");' % args) 304 else: 305 raise ValueError( 306 "don't know how to write \"%s\" partitions" % p.fs_type) 307 308 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 309 """Set file ownership and permissions.""" 310 if not self.info.get("use_set_metadata", False): 311 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 312 else: 313 if capabilities is None: 314 capabilities = "0x0" 315 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 316 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 317 if selabel is not None: 318 cmd += ', "selabel", "%s"' % selabel 319 cmd += ');' 320 self.script.append(cmd) 321 322 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, 323 capabilities): 324 """Recursively set path ownership and permissions.""" 325 if not self.info.get("use_set_metadata", False): 326 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 327 % (uid, gid, dmode, fmode, fn)) 328 else: 329 if capabilities is None: 330 capabilities = "0x0" 331 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 332 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 333 % (fn, uid, gid, dmode, fmode, capabilities) 334 if selabel is not None: 335 cmd += ', "selabel", "%s"' % selabel 336 cmd += ');' 337 self.script.append(cmd) 338 339 def MakeSymlinks(self, symlink_list): 340 """Create symlinks, given a list of (dest, link) pairs.""" 341 by_dest = {} 342 for d, l in symlink_list: 343 by_dest.setdefault(d, []).append(l) 344 345 for dest, links in sorted(by_dest.iteritems()): 346 cmd = ('symlink("%s", ' % (dest,) + 347 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 348 self.script.append(self.WordWrap(cmd)) 349 350 def AppendExtra(self, extra): 351 """Append text verbatim to the output script.""" 352 self.script.append(extra) 353 354 def Unmount(self, mount_point): 355 self.script.append('unmount("%s");' % mount_point) 356 self.mounts.remove(mount_point) 357 358 def UnmountAll(self): 359 for p in sorted(self.mounts): 360 self.script.append('unmount("%s");' % (p,)) 361 self.mounts = set() 362 363 def AddToZip(self, input_zip, output_zip, input_path=None): 364 """Write the accumulated script to the output_zip file. input_zip 365 is used as the source for the 'updater' binary needed to run 366 script. If input_path is not None, it will be used as a local 367 path for the binary instead of input_zip.""" 368 369 self.UnmountAll() 370 371 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 372 "\n".join(self.script) + "\n") 373 374 if input_path is None: 375 data = input_zip.read("OTA/bin/updater") 376 else: 377 data = open(input_path, "rb").read() 378 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 379 data, perms=0o755) 380