edify_generator.py revision 258bf46ea6bb4f25d01fab1b783238589e5bbec4
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 # map recovery.fstab's fs_types to mount/format "partition types" 25 PARTITION_TYPES = { "yaffs2": "MTD", "mtd": "MTD", "ext4": "EMMC" } 26 27 def __init__(self, version, info): 28 self.script = [] 29 self.mounts = set() 30 self.version = version 31 self.info = info 32 33 def MakeTemporary(self): 34 """Make a temporary script object whose commands can latter be 35 appended to the parent script with AppendScript(). Used when the 36 caller wants to generate script commands out-of-order.""" 37 x = EdifyGenerator(self.version, self.info) 38 x.mounts = self.mounts 39 return x 40 41 @staticmethod 42 def _WordWrap(cmd, linelen=80): 43 """'cmd' should be a function call with null characters after each 44 parameter (eg, "somefun(foo,\0bar,\0baz)"). This function wraps cmd 45 to a given line length, replacing nulls with spaces and/or newlines 46 to format it nicely.""" 47 indent = cmd.index("(")+1 48 out = [] 49 first = True 50 x = re.compile("^(.{,%d})\0" % (linelen-indent,)) 51 while True: 52 if not first: 53 out.append(" " * indent) 54 first = False 55 m = x.search(cmd) 56 if not m: 57 parts = cmd.split("\0", 1) 58 out.append(parts[0]+"\n") 59 if len(parts) == 1: 60 break 61 else: 62 cmd = parts[1] 63 continue 64 out.append(m.group(1)+"\n") 65 cmd = cmd[m.end():] 66 67 return "".join(out).replace("\0", " ").rstrip("\n") 68 69 def AppendScript(self, other): 70 """Append the contents of another script (which should be created 71 with temporary=True) to this one.""" 72 self.script.extend(other.script) 73 74 def AssertSomeFingerprint(self, *fp): 75 """Assert that the current system build fingerprint is one of *fp.""" 76 if not fp: 77 raise ValueError("must specify some fingerprints") 78 cmd = ('assert(' + 79 ' ||\0'.join([('file_getprop("/system/build.prop", ' 80 '"ro.build.fingerprint") == "%s"') 81 % i for i in fp]) + 82 ');') 83 self.script.append(self._WordWrap(cmd)) 84 85 def AssertOlderBuild(self, timestamp): 86 """Assert that the build on the device is older (or the same as) 87 the given timestamp.""" 88 self.script.append(('assert(!less_than_int(%s, ' 89 'getprop("ro.build.date.utc")));') % (timestamp,)) 90 91 def AssertDevice(self, device): 92 """Assert that the device identifier is the given string.""" 93 cmd = ('assert(getprop("ro.product.device") == "%s" ||\0' 94 'getprop("ro.build.product") == "%s");' % (device, device)) 95 self.script.append(self._WordWrap(cmd)) 96 97 def AssertSomeBootloader(self, *bootloaders): 98 """Asert that the bootloader version is one of *bootloaders.""" 99 cmd = ("assert(" + 100 " ||\0".join(['getprop("ro.bootloader") == "%s"' % (b,) 101 for b in bootloaders]) + 102 ");") 103 self.script.append(self._WordWrap(cmd)) 104 105 def ShowProgress(self, frac, dur): 106 """Update the progress bar, advancing it over 'frac' over the next 107 'dur' seconds. 'dur' may be zero to advance it via SetProgress 108 commands instead of by time.""" 109 self.script.append("show_progress(%f, %d);" % (frac, int(dur))) 110 111 def SetProgress(self, frac): 112 """Set the position of the progress bar within the chunk defined 113 by the most recent ShowProgress call. 'frac' should be in 114 [0,1].""" 115 self.script.append("set_progress(%f);" % (frac,)) 116 117 def PatchCheck(self, filename, *sha1): 118 """Check that the given file (or MTD reference) has one of the 119 given *sha1 hashes, checking the version saved in cache if the 120 file does not match.""" 121 self.script.append('assert(apply_patch_check("%s"' % (filename,) + 122 "".join([', "%s"' % (i,) for i in sha1]) + 123 '));') 124 125 def FileCheck(self, filename, *sha1): 126 """Check that the given file (or MTD reference) has one of the 127 given *sha1 hashes.""" 128 self.script.append('assert(sha1_check(read_file("%s")' % (filename,) + 129 "".join([', "%s"' % (i,) for i in sha1]) + 130 '));') 131 132 def CacheFreeSpaceCheck(self, amount): 133 """Check that there's at least 'amount' space that can be made 134 available on /cache.""" 135 self.script.append("assert(apply_patch_space(%d));" % (amount,)) 136 137 def Mount(self, mount_point): 138 """Mount the partition with the given mount_point.""" 139 fstab = self.info.get("fstab", None) 140 if fstab: 141 p = fstab[mount_point] 142 self.script.append('mount("%s", "%s", "%s", "%s");' % 143 (p.fs_type, self.PARTITION_TYPES[p.fs_type], 144 p.device, p.mount_point)) 145 self.mounts.add(p.mount_point) 146 else: 147 what = mount_point.lstrip("/") 148 what = self.info.get("partition_path", "") + what 149 self.script.append('mount("%s", "%s", "%s", "%s");' % 150 (self.info["fs_type"], self.info["partition_type"], 151 what, mount_point)) 152 self.mounts.add(mount_point) 153 154 def UnpackPackageDir(self, src, dst): 155 """Unpack a given directory from the OTA package into the given 156 destination directory.""" 157 self.script.append('package_extract_dir("%s", "%s");' % (src, dst)) 158 159 def Comment(self, comment): 160 """Write a comment into the update script.""" 161 self.script.append("") 162 for i in comment.split("\n"): 163 self.script.append("# " + i) 164 self.script.append("") 165 166 def Print(self, message): 167 """Log a message to the screen (if the logs are visible).""" 168 self.script.append('ui_print("%s");' % (message,)) 169 170 def FormatPartition(self, partition): 171 """Format the given partition, specified by its mount point (eg, 172 "/system").""" 173 174 fstab = self.info.get("fstab", None) 175 if fstab: 176 p = fstab[partition] 177 self.script.append('format("%s", "%s", "%s");' % 178 (p.fs_type, self.PARTITION_TYPES[p.fs_type], p.device)) 179 else: 180 # older target-files without per-partition types 181 partition = self.info.get("partition_path", "") + partition 182 self.script.append('format("%s", "%s", "%s");' % 183 (self.info["fs_type"], self.info["partition_type"], 184 partition)) 185 186 def DeleteFiles(self, file_list): 187 """Delete all files in file_list.""" 188 if not file_list: return 189 cmd = "delete(" + ",\0".join(['"%s"' % (i,) for i in file_list]) + ");" 190 self.script.append(self._WordWrap(cmd)) 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 WriteFirmwareImage(self, kind, fn): 207 """Arrange to update the given firmware image (kind must be 208 "hboot" or "radio") when recovery finishes.""" 209 if self.version == 1: 210 self.script.append( 211 ('assert(package_extract_file("%(fn)s", "/tmp/%(kind)s.img"),\n' 212 ' write_firmware_image("/tmp/%(kind)s.img", "%(kind)s"));') 213 % {'kind': kind, 'fn': fn}) 214 else: 215 self.script.append( 216 'write_firmware_image("PACKAGE:%s", "%s");' % (fn, kind)) 217 218 def WriteRawImage(self, mount_point, fn): 219 """Write the given package file into the partition for the given 220 mount point.""" 221 222 fstab = self.info["fstab"] 223 if fstab: 224 p = fstab[mount_point] 225 partition_type = self.PARTITION_TYPES[p.fs_type] 226 args = {'device': p.device, 'fn': fn} 227 if partition_type == "MTD": 228 self.script.append( 229 ('assert(package_extract_file("%(fn)s", "/tmp/%(device)s.img"),\n' 230 ' write_raw_image("/tmp/%(device)s.img", "%(device)s"),\n' 231 ' delete("/tmp/%(device)s.img"));') % args) 232 elif partition_type == "EMMC": 233 self.script.append( 234 'package_extract_file("%(fn)s", "%(device)s");' % args) 235 else: 236 raise ValueError("don't know how to write \"%s\" partitions" % (p.fs_type,)) 237 else: 238 # backward compatibility with older target-files that lack recovery.fstab 239 if self.info["partition_type"] == "MTD": 240 self.script.append( 241 ('assert(package_extract_file("%(fn)s", "/tmp/%(partition)s.img"),\n' 242 ' write_raw_image("/tmp/%(partition)s.img", "%(partition)s"),\n' 243 ' delete("/tmp/%(partition)s.img"));') 244 % {'partition': partition, 'fn': fn}) 245 elif self.info["partition_type"] == "EMMC": 246 self.script.append( 247 ('package_extract_file("%(fn)s", "%(dir)s%(partition)s");') 248 % {'partition': partition, 'fn': fn, 249 'dir': self.info.get("partition_path", ""), 250 }) 251 else: 252 raise ValueError("don't know how to write \"%s\" partitions" % 253 (self.info["partition_type"],)) 254 255 def SetPermissions(self, fn, uid, gid, mode): 256 """Set file ownership and permissions.""" 257 self.script.append('set_perm(%d, %d, 0%o, "%s");' % (uid, gid, mode, fn)) 258 259 def SetPermissionsRecursive(self, fn, uid, gid, dmode, fmode): 260 """Recursively set path ownership and permissions.""" 261 self.script.append('set_perm_recursive(%d, %d, 0%o, 0%o, "%s");' 262 % (uid, gid, dmode, fmode, fn)) 263 264 def MakeSymlinks(self, symlink_list): 265 """Create symlinks, given a list of (dest, link) pairs.""" 266 by_dest = {} 267 for d, l in symlink_list: 268 by_dest.setdefault(d, []).append(l) 269 270 for dest, links in sorted(by_dest.iteritems()): 271 cmd = ('symlink("%s", ' % (dest,) + 272 ",\0".join(['"' + i + '"' for i in sorted(links)]) + ");") 273 self.script.append(self._WordWrap(cmd)) 274 275 def AppendExtra(self, extra): 276 """Append text verbatim to the output script.""" 277 self.script.append(extra) 278 279 def UnmountAll(self): 280 for p in sorted(self.mounts): 281 self.script.append('unmount("%s");' % (p,)) 282 self.mounts = set() 283 284 def AddToZip(self, input_zip, output_zip, input_path=None): 285 """Write the accumulated script to the output_zip file. input_zip 286 is used as the source for the 'updater' binary needed to run 287 script. If input_path is not None, it will be used as a local 288 path for the binary instead of input_zip.""" 289 290 self.UnmountAll() 291 292 common.ZipWriteStr(output_zip, "META-INF/com/google/android/updater-script", 293 "\n".join(self.script) + "\n") 294 295 if input_path is None: 296 data = input_zip.read("OTA/bin/updater") 297 else: 298 data = open(os.path.join(input_path, "updater")).read() 299 common.ZipWriteStr(output_zip, "META-INF/com/google/android/update-binary", 300 data, perms=0755) 301