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