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