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 15 import os 16 import re 17 18 import common 19 20 class 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 192 def ApplyPatch(self, srcfile, tgtfile, tgtsize, tgtsha1, *patchpairs): 193 """Apply binary patches (in *patchpairs) to the given srcfile to 194 produce tgtfile (which may be "-" to indicate overwriting the 195 source file.""" 196 if len(patchpairs) % 2 != 0 or len(patchpairs) == 0: 197 raise ValueError("bad patches given to ApplyPatch") 198 cmd = ['apply_patch("%s",\0"%s",\0%s,\0%d' 199 % (srcfile, tgtfile, tgtsha1, tgtsize)] 200 for i in range(0, len(patchpairs), 2): 201 cmd.append(',\0%s, package_extract_file("%s")' % patchpairs[i:i+2]) 202 cmd.append(');') 203 cmd = "".join(cmd) 204 self.script.append(self._WordWrap(cmd)) 205 206 def WriteRawImage(self, mount_point, fn): 207 """Write the given package file into the partition for the given 208 mount point.""" 209 210 fstab = self.info["fstab"] 211 if fstab: 212 p = fstab[mount_point] 213 partition_type = common.PARTITION_TYPES[p.fs_type] 214 args = {'device': p.device, 'fn': fn} 215 if partition_type == "MTD": 216 self.script.append( 217 'write_raw_image(package_extract_file("%(fn)s"), "%(device)s");' 218 % args) 219 elif partition_type == "EMMC": 220 self.script.append( 221 'package_extract_file("%(fn)s", "%(device)s");' % args) 222 else: 223 raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,)) 224 225 def SetPermissions(self, fn, uid, gid, mode, selabel, capabilities): 226 """Set file ownership and permissions.""" 227 if not self.info.get("use_set_metadata", False): 228 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 229 else: 230 if capabilities is None: capabilities = "0x0" 231 cmd = 'set_metadata("%s", "uid", %d, "gid", %d, "mode", 0%o, ' \ 232 '"capabilities", %s' % (fn, uid, gid, mode, capabilities) 233 if selabel is not None: 234 cmd += ', "selabel", "%s"' % ( selabel ) 235 cmd += ');' 236 self.script.append(cmd) 237 238 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode, selabel, capabilities): 239 """Recursively set path ownership and permissions.""" 240 if not self.info.get("use_set_metadata", False): 241 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 242 % (uid, gid, dmode, fmode, fn)) 243 else: 244 if capabilities is None: capabilities = "0x0" 245 cmd = 'set_metadata_recursive("%s", "uid", %d, "gid", %d, ' \ 246 '"dmode", 0%o, "fmode", 0%o, "capabilities", %s' \ 247 % (fn, uid, gid, dmode, fmode, capabilities) 248 if selabel is not None: 249 cmd += ', "selabel", "%s"' % ( selabel ) 250 cmd += ');' 251 self.script.append(cmd) 252 253 def MakeSymlinks(self, symlink_list): 254 """Create symlinks, given a list of (dest, link) pairs.""" 255 by_dest = {} 256 for d, l in symlink_list: 257 by_dest.setdefault(d, []).append(l) 258 259 for dest, links in sorted(by_dest.iteritems()): 260 cmd = ('symlink("%s", ' % (dest,) + 261 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 262 self.script.append(self._WordWrap(cmd)) 263 264 def AppendExtra(self, extra): 265 """Append text verbatim to the output script.""" 266 self.script.append(extra) 267 268 def UnmountAll(self): 269 for p in sorted(self.mounts): 270 self.script.append('unmount("%s");' % (p,)) 271 self.mounts = set() 272 273 def AddToZip(self, input_zip, output_zip, input_path=None): 274 """Write the accumulated script to the output_zip file. input_zip 275 is used as the source for the 'updater' binary needed to run 276 script. If input_path is not None, it will be used as a local 277 path for the binary instead of input_zip.""" 278 279 self.UnmountAll() 280 281 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 282 "\n".join(self.script) + "\n") 283 284 if input_path is None: 285 data = input_zip.read("OTA/bin/updater") 286 else: 287 data = open(os.path.join(input_path, "updater")).read() 288 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 289 data, perms=0755) 290