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