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 os 16import re 17 18import common 19 20class EdifyGenerator(object): 21 """Class to generate scripts in the 'edify' recovery script language 22 used from donut onwards.""" 23 24 def __init__(self, version, info): 25 self.script = [] 26 self.mounts = set() 27 self.version = version 28 self.info = info 29 30 def MakeTemporary(self): 31 """Make a temporary script object whose commands can latter be 32 appended to the parent script with AppendScript(). Used when the 33 caller wants to generate script commands out-of-order.""" 34 x = EdifyGenerator(self.version, self.info) 35 x.mounts = self.mounts 36 return x 37 38 @staticmethod 39 def _WordWrap(cmd, linelen=80): 40 """'cmd' should be a function call with null characters after each 41 parameter (eg, "somefun(foo,\0bar,\0baz)"). This function wraps cmd 42 to a given line length, replacing nulls with spaces and/or newlines 43 to format it nicely.""" 44 indent = cmd.index("(")+1 45 out = [] 46 first = True 47 x = re.compile("^(.{,%d})\0" % (linelen-indent,)) 48 while True: 49 if not first: 50 out.append(" " * indent) 51 first = False 52 m = x.search(cmd) 53 if not m: 54 parts = cmd.split("\0", 1) 55 out.append(parts[0]+"\n") 56 if len(parts) == 1: 57 break 58 else: 59 cmd = parts[1] 60 continue 61 out.append(m.group(1)+"\n") 62 cmd = cmd[m.end():] 63 64 return "".join(out).replace("\0", " ").rstrip("\n") 65 66 def AppendScript(self, other): 67 """Append the contents of another script (which should be created 68 with temporary=True) to this one.""" 69 self.script.extend(other.script) 70 71 def AssertOemProperty(self, name, value): 72 """Assert that a property on the OEM paritition matches a value.""" 73 if not name: 74 raise ValueError("must specify an OEM property") 75 if not value: 76 raise ValueError("must specify the OEM value") 77 cmd = ('file_getprop("/oem/oem.prop", "%s") == "%s" || ' 78 'abort("This package expects the value \\"%s\\" for ' 79 '\\"%s\\" on the OEM partition; ' 80 'this has value \\"" + file_getprop("/oem/oem.prop") + "\\".");' 81 ) % (name, value, name, value) 82 self.script.append(cmd) 83 84 def AssertSomeFingerprint(self, *fp): 85 """Assert that the current recovery build fingerprint is one of *fp.""" 86 if not fp: 87 raise ValueError("must specify some fingerprints") 88 cmd = ( 89 ' ||\n '.join([('getprop("ro.build.fingerprint") == "%s"') 90 % i for i in fp]) + 91 ' ||\n abort("Package expects build fingerprint of %s; this ' 92 'device has " + getprop("ro.build.fingerprint") + ".");' 93 ) % (" or ".join(fp),) 94 self.script.append(cmd) 95 96 def AssertSomeThumbprint(self, *fp): 97 """Assert that the current recovery build thumbprint is one of *fp.""" 98 if not fp: 99 raise ValueError("must specify some thumbprints") 100 cmd = ( 101 ' ||\n '.join([('getprop("ro.build.thumbprint") == "%s"') 102 % i for i in fp]) + 103 ' ||\n abort("Package expects build thumbprint of %s; this ' 104 'device has " + getprop("ro.build.thumbprint") + ".");' 105 ) % (" or ".join(fp),) 106 self.script.append(cmd) 107 108 def AssertOlderBuild(self, timestamp, timestamp_text): 109 """Assert that the build on the device is older (or the same as) 110 the given timestamp.""" 111 self.script.append( 112 ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || ' 113 'abort("Can\'t install this package (%s) over newer ' 114 'build (" + getprop("ro.build.date") + ").");' 115 ) % (timestamp, timestamp_text)) 116 117 def AssertDevice(self, device): 118 """Assert that the device identifier is the given string.""" 119 cmd = ('getprop("ro.product.device") == "%s" || ' 120 'abort("This package is for \\"%s\\" devices; ' 121 'this is a \\"" + getprop("ro.product.device") + "\\".");' 122 ) % (device, device) 123 self.script.append(cmd) 124 125 def AssertSomeBootloader(self, *bootloaders): 126 """Asert that the bootloader version is one of *bootloaders.""" 127 cmd = ("assert(" + 128 " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,) 129 for b in bootloaders]) + 130 ");") 131 self.script.append(self._WordWrap(cmd)) 132 133 def ShowProgress(self, frac, dur): 134 """Update the progress bar, advancing it over 'frac' over the next 135 'dur' seconds. 'dur' may be zero to advance it via SetProgress 136 commands instead of by time.""" 137 self.script.append("show_progress(%f, %d);" % (frac, int(dur))) 138 139 def SetProgress(self, frac): 140 """Set the position of the progress bar within the chunk defined 141 by the most recent ShowProgress call. 'frac' should be in 142 [0,1].""" 143 self.script.append("set_progress(%f);" % (frac,)) 144 145 def PatchCheck(self, filename, *sha1): 146 """Check that the given file (or MTD reference) has one of the 147 given *sha1 hashes, checking the version saved in cache if the 148 file does not match.""" 149 self.script.append( 150 'apply_patch_check("%s"' % (filename,) + 151 "".join([', "%s"' % (i,) for i in sha1]) + 152 ') || abort("\\"%s\\" has unexpected contents.");' % (filename,)) 153 154 def FileCheck(self, filename, *sha1): 155 """Check that the given file (or MTD reference) has one of the 156 given *sha1 hashes.""" 157 self.script.append('assert(sha1_check(read_file("%s")' % (filename,) + 158 "".join([', "%s"' % (i,) for i in sha1]) + 159 '));') 160 161 def CacheFreeSpaceCheck(self, amount): 162 """Check that there's at least 'amount' space that can be made 163 available on /cache.""" 164 self.script.append(('apply_patch_space(%d) || abort("Not enough free space ' 165 'on /system to apply patches.");') % (amount,)) 166 167 def Mount(self, mount_point, mount_options_by_format=""): 168 """Mount the partition with the given mount_point. 169 mount_options_by_format: 170 [fs_type=option[,option]...[|fs_type=option[,option]...]...] 171 where option is optname[=optvalue] 172 E.g. ext4=barrier=1,nodelalloc,errors=panic|f2fs=errors=recover 173 """ 174 fstab = self.info.get("fstab", None) 175 if fstab: 176 p = fstab[mount_point] 177 mount_dict = {} 178 if mount_options_by_format is not None: 179 for option in mount_options_by_format.split("|"): 180 if "=" in option: 181 key, value = option.split("=", 1) 182 mount_dict[key] = value 183 self.script.append('mount("%s", "%s", "%s", "%s", "%s");' % 184 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 185 p.device, p.mount_point, mount_dict.get(p.fs_type, ""))) 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('tune2fs(' + 211 "".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 reserve_size = 0 220 fstab = self.info.get("fstab", None) 221 if fstab: 222 p = fstab[partition] 223 self.script.append('format("%s", "%s", "%s", "%s", "%s");' % 224 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 225 p.device, p.length, p.mount_point)) 226 227 def WipeBlockDevice(self, partition): 228 if partition not in ("/system", "/vendor"): 229 raise ValueError(("WipeBlockDevice doesn't work on %s\n") % (partition,)) 230 fstab = self.info.get("fstab", None) 231 size = self.info.get(partition.lstrip("/") + "_size", None) 232 device = fstab[partition].device 233 234 self.script.append('wipe_block_device("%s", %s);' % (device, size)) 235 236 def DeleteFiles(self, file_list): 237 """Delete all files in file_list.""" 238 if not file_list: 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("don't know how to write \"%s\" partitions" % (p.fs_type,)) 293 294 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 295 """Set file ownership and permissions.""" 296 if not self.info.get("use_set_metadata", False): 297 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 298 else: 299 if capabilities is None: capabilities = "0x0" 300 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 301 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 302 if selabel is not None: 303 cmd += ', "selabel", "%s"' % ( selabel ) 304 cmd += ');' 305 self.script.append(cmd) 306 307 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, 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: capabilities = "0x0" 314 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 315 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 316 % (fn, uid, gid, dmode, fmode, capabilities) 317 if selabel is not None: 318 cmd += ', "selabel", "%s"' % ( selabel ) 319 cmd += ');' 320 self.script.append(cmd) 321 322 def MakeSymlinks(self, symlink_list): 323 """Create symlinks, given a list of (dest, link) pairs.""" 324 by_dest = {} 325 for d, l in symlink_list: 326 by_dest.setdefault(d, []).append(l) 327 328 for dest, links in sorted(by_dest.iteritems()): 329 cmd = ('symlink("%s", ' % (dest,) + 330 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 331 self.script.append(self._WordWrap(cmd)) 332 333 def AppendExtra(self, extra): 334 """Append text verbatim to the output script.""" 335 self.script.append(extra) 336 337 def Unmount(self, mount_point): 338 self.script.append('unmount("%s");' % (mount_point,)) 339 self.mounts.remove(mount_point); 340 341 def UnmountAll(self): 342 for p in sorted(self.mounts): 343 self.script.append('unmount("%s");' % (p,)) 344 self.mounts = set() 345 346 def AddToZip(self, input_zip, output_zip, input_path=None): 347 """Write the accumulated script to the output_zip file. input_zip 348 is used as the source for the 'updater' binary needed to run 349 script. If input_path is not None, it will be used as a local 350 path for the binary instead of input_zip.""" 351 352 self.UnmountAll() 353 354 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 355 "\n".join(self.script) + "\n") 356 357 if input_path is None: 358 data = input_zip.read("OTA/bin/updater") 359 else: 360 data = open(input_path, "rb").read() 361 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 362 data, perms=0755) 363