common.py revision 5ecba70ce5302c34996bc70a0f8dc9ee6cbb2dea
1# Copyright (C) 2008 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 errno 16import getopt 17import getpass 18import imp 19import os 20import re 21import shutil 22import subprocess 23import sys 24import tempfile 25import zipfile 26 27# missing in Python 2.4 and before 28if not hasattr(os, "SEEK_SET"): 29 os.SEEK_SET = 0 30 31class Options(object): pass 32OPTIONS = Options() 33OPTIONS.search_path = "out/host/linux-x86" 34OPTIONS.max_image_size = {} 35OPTIONS.verbose = False 36OPTIONS.tempfiles = [] 37OPTIONS.device_specific = None 38OPTIONS.extras = {} 39 40class ExternalError(RuntimeError): pass 41 42 43def Run(args, **kwargs): 44 """Create and return a subprocess.Popen object, printing the command 45 line on the terminal if -v was specified.""" 46 if OPTIONS.verbose: 47 print " running: ", " ".join(args) 48 return subprocess.Popen(args, **kwargs) 49 50 51def LoadMaxSizes(): 52 """Load the maximum allowable images sizes from the input 53 target_files size.""" 54 OPTIONS.max_image_size = {} 55 try: 56 for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")): 57 pieces = line.split() 58 if len(pieces) != 2: continue 59 image = pieces[0] 60 size = int(pieces[1]) 61 OPTIONS.max_image_size[image + ".img"] = size 62 except IOError, e: 63 if e.errno == errno.ENOENT: 64 pass 65 66 67def BuildAndAddBootableImage(sourcedir, targetname, output_zip): 68 """Take a kernel, cmdline, and ramdisk directory from the input (in 69 'sourcedir'), and turn them into a boot image. Put the boot image 70 into the output zip file under the name 'targetname'. Returns 71 targetname on success or None on failure (if sourcedir does not 72 appear to contain files for the requested image).""" 73 74 print "creating %s..." % (targetname,) 75 76 img = BuildBootableImage(sourcedir) 77 if img is None: 78 return None 79 80 CheckSize(img, targetname) 81 ZipWriteStr(output_zip, targetname, img) 82 return targetname 83 84def BuildBootableImage(sourcedir): 85 """Take a kernel, cmdline, and ramdisk directory from the input (in 86 'sourcedir'), and turn them into a boot image. Return the image 87 data, or None if sourcedir does not appear to contains files for 88 building the requested image.""" 89 90 if (not os.access(os.path.join(sourcedir, "RAMDISK"), os.F_OK) or 91 not os.access(os.path.join(sourcedir, "kernel"), os.F_OK)): 92 return None 93 94 ramdisk_img = tempfile.NamedTemporaryFile() 95 img = tempfile.NamedTemporaryFile() 96 97 p1 = Run(["mkbootfs", os.path.join(sourcedir, "RAMDISK")], 98 stdout=subprocess.PIPE) 99 p2 = Run(["minigzip"], 100 stdin=p1.stdout, stdout=ramdisk_img.file.fileno()) 101 102 p2.wait() 103 p1.wait() 104 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,) 105 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,) 106 107 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")] 108 109 fn = os.path.join(sourcedir, "cmdline") 110 if os.access(fn, os.F_OK): 111 cmd.append("--cmdline") 112 cmd.append(open(fn).read().rstrip("\n")) 113 114 fn = os.path.join(sourcedir, "base") 115 if os.access(fn, os.F_OK): 116 cmd.append("--base") 117 cmd.append(open(fn).read().rstrip("\n")) 118 119 cmd.extend(["--ramdisk", ramdisk_img.name, 120 "--output", img.name]) 121 122 p = Run(cmd, stdout=subprocess.PIPE) 123 p.communicate() 124 assert p.returncode == 0, "mkbootimg of %s image failed" % ( 125 os.path.basename(sourcedir),) 126 127 img.seek(os.SEEK_SET, 0) 128 data = img.read() 129 130 ramdisk_img.close() 131 img.close() 132 133 return data 134 135 136def AddRecovery(output_zip): 137 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"), 138 "recovery.img", output_zip) 139 140def AddBoot(output_zip): 141 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"), 142 "boot.img", output_zip) 143 144def UnzipTemp(filename): 145 """Unzip the given archive into a temporary directory and return the name.""" 146 147 tmp = tempfile.mkdtemp(prefix="targetfiles-") 148 OPTIONS.tempfiles.append(tmp) 149 p = Run(["unzip", "-o", "-q", filename, "-d", tmp], stdout=subprocess.PIPE) 150 p.communicate() 151 if p.returncode != 0: 152 raise ExternalError("failed to unzip input target-files \"%s\"" % 153 (filename,)) 154 return tmp 155 156 157def GetKeyPasswords(keylist): 158 """Given a list of keys, prompt the user to enter passwords for 159 those which require them. Return a {key: password} dict. password 160 will be None if the key has no password.""" 161 162 no_passwords = [] 163 need_passwords = [] 164 devnull = open("/dev/null", "w+b") 165 for k in sorted(keylist): 166 # An empty-string key is used to mean don't re-sign this package. 167 # Obviously we don't need a password for this non-key. 168 if not k: 169 no_passwords.append(k) 170 continue 171 172 p = Run(["openssl", "pkcs8", "-in", k+".pk8", 173 "-inform", "DER", "-nocrypt"], 174 stdin=devnull.fileno(), 175 stdout=devnull.fileno(), 176 stderr=subprocess.STDOUT) 177 p.communicate() 178 if p.returncode == 0: 179 no_passwords.append(k) 180 else: 181 need_passwords.append(k) 182 devnull.close() 183 184 key_passwords = PasswordManager().GetPasswords(need_passwords) 185 key_passwords.update(dict.fromkeys(no_passwords, None)) 186 return key_passwords 187 188 189def SignFile(input_name, output_name, key, password, align=None, 190 whole_file=False): 191 """Sign the input_name zip/jar/apk, producing output_name. Use the 192 given key and password (the latter may be None if the key does not 193 have a password. 194 195 If align is an integer > 1, zipalign is run to align stored files in 196 the output zip on 'align'-byte boundaries. 197 198 If whole_file is true, use the "-w" option to SignApk to embed a 199 signature that covers the whole file in the archive comment of the 200 zip file. 201 """ 202 203 if align == 0 or align == 1: 204 align = None 205 206 if align: 207 temp = tempfile.NamedTemporaryFile() 208 sign_name = temp.name 209 else: 210 sign_name = output_name 211 212 cmd = ["java", "-Xmx512m", "-jar", 213 os.path.join(OPTIONS.search_path, "framework", "signapk.jar")] 214 if whole_file: 215 cmd.append("-w") 216 cmd.extend([key + ".x509.pem", key + ".pk8", 217 input_name, sign_name]) 218 219 p = Run(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE) 220 if password is not None: 221 password += "\n" 222 p.communicate(password) 223 if p.returncode != 0: 224 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,)) 225 226 if align: 227 p = Run(["zipalign", "-f", str(align), sign_name, output_name]) 228 p.communicate() 229 if p.returncode != 0: 230 raise ExternalError("zipalign failed: return code %s" % (p.returncode,)) 231 temp.close() 232 233 234def CheckSize(data, target): 235 """Check the data string passed against the max size limit, if 236 any, for the given target. Raise exception if the data is too big. 237 Print a warning if the data is nearing the maximum size.""" 238 limit = OPTIONS.max_image_size.get(target, None) 239 if limit is None: return 240 241 size = len(data) 242 pct = float(size) * 100.0 / limit 243 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit) 244 if pct >= 99.0: 245 raise ExternalError(msg) 246 elif pct >= 95.0: 247 print 248 print " WARNING: ", msg 249 print 250 elif OPTIONS.verbose: 251 print " ", msg 252 253 254COMMON_DOCSTRING = """ 255 -p (--path) <dir> 256 Prepend <dir>/bin to the list of places to search for binaries 257 run by this script, and expect to find jars in <dir>/framework. 258 259 -s (--device_specific) <file> 260 Path to the python module containing device-specific 261 releasetools code. 262 263 -x (--extra) <key=value> 264 Add a key/value pair to the 'extras' dict, which device-specific 265 extension code may look at. 266 267 -v (--verbose) 268 Show command lines being executed. 269 270 -h (--help) 271 Display this usage message and exit. 272""" 273 274def Usage(docstring): 275 print docstring.rstrip("\n") 276 print COMMON_DOCSTRING 277 278 279def ParseOptions(argv, 280 docstring, 281 extra_opts="", extra_long_opts=(), 282 extra_option_handler=None): 283 """Parse the options in argv and return any arguments that aren't 284 flags. docstring is the calling module's docstring, to be displayed 285 for errors and -h. extra_opts and extra_long_opts are for flags 286 defined by the caller, which are processed by passing them to 287 extra_option_handler.""" 288 289 try: 290 opts, args = getopt.getopt( 291 argv, "hvp:s:x:" + extra_opts, 292 ["help", "verbose", "path=", "device_specific=", "extra="] + 293 list(extra_long_opts)) 294 except getopt.GetoptError, err: 295 Usage(docstring) 296 print "**", str(err), "**" 297 sys.exit(2) 298 299 path_specified = False 300 301 for o, a in opts: 302 if o in ("-h", "--help"): 303 Usage(docstring) 304 sys.exit() 305 elif o in ("-v", "--verbose"): 306 OPTIONS.verbose = True 307 elif o in ("-p", "--path"): 308 OPTIONS.search_path = a 309 elif o in ("-s", "--device_specific"): 310 OPTIONS.device_specific = a 311 elif o in ("-x", "--extra"): 312 key, value = a.split("=", 1) 313 OPTIONS.extras[key] = value 314 else: 315 if extra_option_handler is None or not extra_option_handler(o, a): 316 assert False, "unknown option \"%s\"" % (o,) 317 318 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") + 319 os.pathsep + os.environ["PATH"]) 320 321 return args 322 323 324def Cleanup(): 325 for i in OPTIONS.tempfiles: 326 if os.path.isdir(i): 327 shutil.rmtree(i) 328 else: 329 os.remove(i) 330 331 332class PasswordManager(object): 333 def __init__(self): 334 self.editor = os.getenv("EDITOR", None) 335 self.pwfile = os.getenv("ANDROID_PW_FILE", None) 336 337 def GetPasswords(self, items): 338 """Get passwords corresponding to each string in 'items', 339 returning a dict. (The dict may have keys in addition to the 340 values in 'items'.) 341 342 Uses the passwords in $ANDROID_PW_FILE if available, letting the 343 user edit that file to add more needed passwords. If no editor is 344 available, or $ANDROID_PW_FILE isn't define, prompts the user 345 interactively in the ordinary way. 346 """ 347 348 current = self.ReadFile() 349 350 first = True 351 while True: 352 missing = [] 353 for i in items: 354 if i not in current or not current[i]: 355 missing.append(i) 356 # Are all the passwords already in the file? 357 if not missing: return current 358 359 for i in missing: 360 current[i] = "" 361 362 if not first: 363 print "key file %s still missing some passwords." % (self.pwfile,) 364 answer = raw_input("try to edit again? [y]> ").strip() 365 if answer and answer[0] not in 'yY': 366 raise RuntimeError("key passwords unavailable") 367 first = False 368 369 current = self.UpdateAndReadFile(current) 370 371 def PromptResult(self, current): 372 """Prompt the user to enter a value (password) for each key in 373 'current' whose value is fales. Returns a new dict with all the 374 values. 375 """ 376 result = {} 377 for k, v in sorted(current.iteritems()): 378 if v: 379 result[k] = v 380 else: 381 while True: 382 result[k] = getpass.getpass("Enter password for %s key> " 383 % (k,)).strip() 384 if result[k]: break 385 return result 386 387 def UpdateAndReadFile(self, current): 388 if not self.editor or not self.pwfile: 389 return self.PromptResult(current) 390 391 f = open(self.pwfile, "w") 392 os.chmod(self.pwfile, 0600) 393 f.write("# Enter key passwords between the [[[ ]]] brackets.\n") 394 f.write("# (Additional spaces are harmless.)\n\n") 395 396 first_line = None 397 sorted = [(not v, k, v) for (k, v) in current.iteritems()] 398 sorted.sort() 399 for i, (_, k, v) in enumerate(sorted): 400 f.write("[[[ %s ]]] %s\n" % (v, k)) 401 if not v and first_line is None: 402 # position cursor on first line with no password. 403 first_line = i + 4 404 f.close() 405 406 p = Run([self.editor, "+%d" % (first_line,), self.pwfile]) 407 _, _ = p.communicate() 408 409 return self.ReadFile() 410 411 def ReadFile(self): 412 result = {} 413 if self.pwfile is None: return result 414 try: 415 f = open(self.pwfile, "r") 416 for line in f: 417 line = line.strip() 418 if not line or line[0] == '#': continue 419 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line) 420 if not m: 421 print "failed to parse password file: ", line 422 else: 423 result[m.group(2)] = m.group(1) 424 f.close() 425 except IOError, e: 426 if e.errno != errno.ENOENT: 427 print "error reading password file: ", str(e) 428 return result 429 430 431def ZipWriteStr(zip, filename, data, perms=0644): 432 # use a fixed timestamp so the output is repeatable. 433 zinfo = zipfile.ZipInfo(filename=filename, 434 date_time=(2009, 1, 1, 0, 0, 0)) 435 zinfo.compress_type = zip.compression 436 zinfo.external_attr = perms << 16 437 zip.writestr(zinfo, data) 438 439 440class DeviceSpecificParams(object): 441 module = None 442 def __init__(self, **kwargs): 443 """Keyword arguments to the constructor become attributes of this 444 object, which is passed to all functions in the device-specific 445 module.""" 446 for k, v in kwargs.iteritems(): 447 setattr(self, k, v) 448 self.extras = OPTIONS.extras 449 450 if self.module is None: 451 path = OPTIONS.device_specific 452 if not path: return 453 try: 454 if os.path.isdir(path): 455 info = imp.find_module("releasetools", [path]) 456 else: 457 d, f = os.path.split(path) 458 b, x = os.path.splitext(f) 459 if x == ".py": 460 f = b 461 info = imp.find_module(f, [d]) 462 self.module = imp.load_module("device_specific", *info) 463 except ImportError: 464 print "unable to load device-specific module; assuming none" 465 466 def _DoCall(self, function_name, *args, **kwargs): 467 """Call the named function in the device-specific module, passing 468 the given args and kwargs. The first argument to the call will be 469 the DeviceSpecific object itself. If there is no module, or the 470 module does not define the function, return the value of the 471 'default' kwarg (which itself defaults to None).""" 472 if self.module is None or not hasattr(self.module, function_name): 473 return kwargs.get("default", None) 474 return getattr(self.module, function_name)(*((self,) + args), **kwargs) 475 476 def FullOTA_Assertions(self): 477 """Called after emitting the block of assertions at the top of a 478 full OTA package. Implementations can add whatever additional 479 assertions they like.""" 480 return self._DoCall("FullOTA_Assertions") 481 482 def FullOTA_InstallEnd(self): 483 """Called at the end of full OTA installation; typically this is 484 used to install the image for the device's baseband processor.""" 485 return self._DoCall("FullOTA_InstallEnd") 486 487 def IncrementalOTA_Assertions(self): 488 """Called after emitting the block of assertions at the top of an 489 incremental OTA package. Implementations can add whatever 490 additional assertions they like.""" 491 return self._DoCall("IncrementalOTA_Assertions") 492 493 def IncrementalOTA_VerifyEnd(self): 494 """Called at the end of the verification phase of incremental OTA 495 installation; additional checks can be placed here to abort the 496 script before any changes are made.""" 497 return self._DoCall("IncrementalOTA_VerifyEnd") 498 499 def IncrementalOTA_InstallEnd(self): 500 """Called at the end of incremental OTA installation; typically 501 this is used to install the image for the device's baseband 502 processor.""" 503 return self._DoCall("IncrementalOTA_InstallEnd") 504