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