edify_generator.py revision 25568486e5777f416d2fcb6cc7aa96caafc66880
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 DeleteFiles(self, file_list): 194 """Delete all files in file_list.""" 195 if not file_list: return 196 cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");" 197 self.script.append(self._WordWrap(cmd)) 198 199 def RenameFile(self, srcfile, tgtfile): 200 """Moves a file from one location to another.""" 201 if self.info.get("update_rename_support", False): 202 self.script.append('rename("%s", "%s");' % (srcfile, tgtfile)) 203 else: 204 raise ValueError("Rename not supported by update binary") 205 206 def SkipNextActionIfTargetExists(self, tgtfile, tgtsha1): 207 """Prepend an action with an apply_patch_check in order to 208 skip the action if the file exists. Used when a patch 209 is later renamed.""" 210 cmd = ('sha1_check(read_file("%s"), %s) || ' % (tgtfile, tgtsha1)) 211 self.script.append(self._WordWrap(cmd)) 212 213 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 214 """Apply binary patches (in *patchpairs) to the given srcfile to 215 produce tgtfile (which may be "-" to indicate overwriting the 216 source file.""" 217 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 218 raise ValueError("bad patches given to ApplyPatch") 219 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 220 % (srcfile, tgtfile, tgtsha1, tgtsize)] 221 for i in range(0, len(patchpairs), 2): 222 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 223 cmd.append(');') 224 cmd = "".join(cmd) 225 self.script.append(self._WordWrap(cmd)) 226 227 def WriteRawImage(self, mount_point, fn): 228 """Write the given package file into the partition for the given 229 mount point.""" 230 231 fstab = self.info["fstab"] 232 if fstab: 233 p = fstab[mount_point] 234 partition_type = common.PARTITION_TYPES[p.fs_type] 235 args = {'device': p.device, 'fn': fn} 236 if partition_type == "MTD": 237 self.script.append( 238 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 239 % args) 240 elif partition_type == "EMMC": 241 self.script.append( 242 'package_extract_file("%(fn)s", "%(device)s");' % args) 243 else: 244 raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,)) 245 246 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 247 """Set file ownership and permissions.""" 248 if not self.info.get("use_set_metadata", False): 249 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 250 else: 251 if capabilities is None: capabilities = "0x0" 252 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 253 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 254 if selabel is not None: 255 cmd += ', "selabel", "%s"' % ( selabel ) 256 cmd += ');' 257 self.script.append(cmd) 258 259 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities): 260 """Recursively set path ownership and permissions.""" 261 if not self.info.get("use_set_metadata", False): 262 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 263 % (uid, gid, dmode, fmode, fn)) 264 else: 265 if capabilities is None: capabilities = "0x0" 266 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 267 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 268 % (fn, uid, gid, dmode, fmode, capabilities) 269 if selabel is not None: 270 cmd += ', "selabel", "%s"' % ( selabel ) 271 cmd += ');' 272 self.script.append(cmd) 273 274 def MakeSymlinks(self, symlink_list): 275 """Create symlinks, given a list of (dest, link) pairs.""" 276 by_dest = {} 277 for d, l in symlink_list: 278 by_dest.setdefault(d, []).append(l) 279 280 for dest, links in sorted(by_dest.iteritems()): 281 cmd = ('symlink("%s", ' % (dest,) + 282 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 283 self.script.append(self._WordWrap(cmd)) 284 285 def AppendExtra(self, extra): 286 """Append text verbatim to the output script.""" 287 self.script.append(extra) 288 289 def UnmountAll(self): 290 for p in sorted(self.mounts): 291 self.script.append('unmount("%s");' % (p,)) 292 self.mounts = set() 293 294 def AddToZip(self, input_zip, output_zip, input_path=None): 295 """Write the accumulated script to the output_zip file. input_zip 296 is used as the source for the 'updater' binary needed to run 297 script. If input_path is not None, it will be used as a local 298 path for the binary instead of input_zip.""" 299 300 self.UnmountAll() 301 302 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 303 "\n".join(self.script) + "\n") 304 305 if input_path is None: 306 data = input_zip.read("OTA/bin/updater") 307 else: 308 data = open(input_path, "rb").read() 309 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 310 data, perms=0755) 311 312 def Syspatch(self, filename, size, target_sha, source_sha, patchfile): 313 """Applies a compressed binary patch to a block device.""" 314 call = 'syspatch("%s", "%s", "%s", "%s", "%s");' 315 self.script.append(call % (filename, size, target_sha, source_sha, patchfile)) 316