edify_generator.py revision 5fad2039bbd4e55d671106c8b39a6e451b85ac23
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 AssertSomeFingerprint(self, *fp): 72 """Assert that the current system build fingerprint is one of *fp.""" 73 if not fp: 74 raise ValueError("must specify some fingerprints") 75 cmd = ( 76 ' ||\n '.join([('file_getprop("/system/build.prop", ' 77 '"ro.build.fingerprint") == "%s"') 78 % i for i in fp]) + 79 ' ||\n abort("Package expects build fingerprint of %s; this ' 80 'device has " + getprop("ro.build.fingerprint") + ".");' 81 ) % (" or ".join(fp),) 82 self.script.append(cmd) 83 84 def AssertRecoveryFingerprint(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 AssertOlderBuild(self, timestamp, timestamp_text): 97 """Assert that the build on the device is older (or the same as) 98 the given timestamp.""" 99 self.script.append( 100 ('(!less_than_int(%s, getprop("ro.build.date.utc"))) || ' 101 'abort("Can\'t install this package (%s) over newer ' 102 'build (" + getprop("ro.build.date") + ").");' 103 ) % (timestamp, timestamp_text)) 104 105 def AssertDevice(self, device): 106 """Assert that the device identifier is the given string.""" 107 cmd = ('getprop("ro.product.device") == "%s" || ' 108 'abort("This package is for \\"%s\\" devices; ' 109 'this is a \\"" + getprop("ro.product.device") + "\\".");' 110 ) % (device, device) 111 self.script.append(cmd) 112 113 def AssertSomeBootloader(self, *bootloaders): 114 """Asert that the bootloader version is one of *bootloaders.""" 115 cmd = ("assert(" + 116 " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,) 117 for b in bootloaders]) + 118 ");") 119 self.script.append(self._WordWrap(cmd)) 120 121 def ShowProgress(self, frac, dur): 122 """Update the progress bar, advancing it over 'frac' over the next 123 'dur' seconds. 'dur' may be zero to advance it via SetProgress 124 commands instead of by time.""" 125 self.script.append("show_progress(%f, %d);" % (frac, int(dur))) 126 127 def SetProgress(self, frac): 128 """Set the position of the progress bar within the chunk defined 129 by the most recent ShowProgress call. 'frac' should be in 130 [0,1].""" 131 self.script.append("set_progress(%f);" % (frac,)) 132 133 def PatchCheck(self, filename, *sha1): 134 """Check that the given file (or MTD reference) has one of the 135 given *sha1 hashes, checking the version saved in cache if the 136 file does not match.""" 137 self.script.append( 138 'apply_patch_check("%s"' % (filename,) + 139 "".join([', "%s"' % (i,) for i in sha1]) + 140 ') || abort("\\"%s\\" has unexpected contents.");' % (filename,)) 141 142 def FileCheck(self, filename, *sha1): 143 """Check that the given file (or MTD reference) has one of the 144 given *sha1 hashes.""" 145 self.script.append('assert(sha1_check(read_file("%s")' % (filename,) + 146 "".join([', "%s"' % (i,) for i in sha1]) + 147 '));') 148 149 def CacheFreeSpaceCheck(self, amount): 150 """Check that there's at least 'amount' space that can be made 151 available on /cache.""" 152 self.script.append(('apply_patch_space(%d) || abort("Not enough free space ' 153 'on /system to apply patches.");') % (amount,)) 154 155 def Mount(self, mount_point): 156 """Mount the partition with the given mount_point.""" 157 fstab = self.info.get("fstab", None) 158 if fstab: 159 p = fstab[mount_point] 160 self.script.append('mount("%s", "%s", "%s", "%s");' % 161 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 162 p.device, p.mount_point)) 163 self.mounts.add(p.mount_point) 164 165 def UnpackPackageDir(self, src, dst): 166 """Unpack a given directory from the OTA package into the given 167 destination directory.""" 168 self.script.append('package_extract_dir("%s", "%s");' % (src, dst)) 169 170 def Comment(self, comment): 171 """Write a comment into the update script.""" 172 self.script.append("") 173 for i in comment.split("\n"): 174 self.script.append("# " + i) 175 self.script.append("") 176 177 def Print(self, message): 178 """Log a message to the screen (if the logs are visible).""" 179 self.script.append('ui_print("%s");' % (message,)) 180 181 def FormatPartition(self, partition): 182 """Format the given partition, specified by its mount point (eg, 183 "/system").""" 184 185 reserve_size = 0 186 fstab = self.info.get("fstab", None) 187 if fstab: 188 p = fstab[partition] 189 self.script.append('format("%s", "%s", "%s", "%s", "%s");' % 190 (p.fs_type, common.PARTITION_TYPES[p.fs_type], 191 p.device, p.length, p.mount_point)) 192 193 def WipeBlockDevice(self, partition): 194 if partition != "/system": 195 raise ValueError(("WipeBlockDevice currently only works " 196 "on /system, not %s\n") % (partition,)) 197 fstab = self.info.get("fstab", None) 198 size = self.info.get("system_size", None) 199 device = fstab[partition].device 200 201 self.script.append('wipe_block_device("%s", %s);' % (device, size)) 202 203 def DeleteFiles(self, file_list): 204 """Delete all files in file_list.""" 205 if not file_list: return 206 cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");" 207 self.script.append(self._WordWrap(cmd)) 208 209 def RenameFile(self, srcfile, tgtfile): 210 """Moves a file from one location to another.""" 211 if self.info.get("update_rename_support", False): 212 self.script.append('rename("%s", "%s");' % (srcfile, tgtfile)) 213 else: 214 raise ValueError("Rename not supported by update binary") 215 216 def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1): 217 """Prepend an action with an apply_patch_check in order to 218 skip the action if the file exists. Used when a patch 219 is later renamed.""" 220 cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1)) 221 self.script.append(self._WordWrap(cmd)) 222 223 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 224 """Apply binary patches (in *patchpairs) to the given srcfile to 225 produce tgtfile (which may be "-" to indicate overwriting the 226 source file.""" 227 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 228 raise ValueError("bad patches given to ApplyPatch") 229 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 230 % (srcfile, tgtfile, tgtsha1, tgtsize)] 231 for i in range(0, len(patchpairs), 2): 232 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 233 cmd.append(');') 234 cmd = "".join(cmd) 235 self.script.append(self._WordWrap(cmd)) 236 237 def WriteRawImage(self, mount_point, fn, mapfn=None): 238 """Write the given package file into the partition for the given 239 mount point.""" 240 241 fstab = self.info["fstab"] 242 if fstab: 243 p = fstab[mount_point] 244 partition_type = common.PARTITION_TYPES[p.fs_type] 245 args = {'device': p.device, 'fn': fn} 246 if partition_type == "MTD": 247 self.script.append( 248 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 249 % args) 250 elif partition_type == "EMMC": 251 if mapfn: 252 args["map"] = mapfn 253 self.script.append( 254 'package_extract_file("%(fn)s", "%(device)s", "%(map)s");' % args) 255 else: 256 self.script.append( 257 'package_extract_file("%(fn)s", "%(device)s");' % args) 258 else: 259 raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,)) 260 261 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 262 """Set file ownership and permissions.""" 263 if not self.info.get("use_set_metadata", False): 264 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 265 else: 266 if capabilities is None: capabilities = "0x0" 267 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 268 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 269 if selabel is not None: 270 cmd += ', "selabel", "%s"' % ( selabel ) 271 cmd += ');' 272 self.script.append(cmd) 273 274 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities): 275 """Recursively set path ownership and permissions.""" 276 if not self.info.get("use_set_metadata", False): 277 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 278 % (uid, gid, dmode, fmode, fn)) 279 else: 280 if capabilities is None: capabilities = "0x0" 281 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 282 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 283 % (fn, uid, gid, dmode, fmode, capabilities) 284 if selabel is not None: 285 cmd += ', "selabel", "%s"' % ( selabel ) 286 cmd += ');' 287 self.script.append(cmd) 288 289 def MakeSymlinks(self, symlink_list): 290 """Create symlinks, given a list of (dest, link) pairs.""" 291 by_dest = {} 292 for d, l in symlink_list: 293 by_dest.setdefault(d, []).append(l) 294 295 for dest, links in sorted(by_dest.iteritems()): 296 cmd = ('symlink("%s", ' % (dest,) + 297 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 298 self.script.append(self._WordWrap(cmd)) 299 300 def AppendExtra(self, extra): 301 """Append text verbatim to the output script.""" 302 self.script.append(extra) 303 304 def UnmountAll(self): 305 for p in sorted(self.mounts): 306 self.script.append('unmount("%s");' % (p,)) 307 self.mounts = set() 308 309 def AddToZip(self, input_zip, output_zip, input_path=None): 310 """Write the accumulated script to the output_zip file. input_zip 311 is used as the source for the 'updater' binary needed to run 312 script. If input_path is not None, it will be used as a local 313 path for the binary instead of input_zip.""" 314 315 self.UnmountAll() 316 317 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 318 "\n".join(self.script) + "\n") 319 320 if input_path is None: 321 data = input_zip.read("OTA/bin/updater") 322 else: 323 data = open(input_path, "rb").read() 324 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 325 data, perms=0755) 326 327 def Syspatch(self, filename, target_mapfile, target_sha, 328 source_mapfile, source_sha, patchfile): 329 """Applies a compressed binary patch to a block device.""" 330 call = 'syspatch("%s", "%s", "%s", "%s", "%s", "%s");' 331 self.script.append(call % (filename, target_mapfile, target_sha, 332 source_mapfile, source_sha, patchfile)) 333