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