1 # Copyright (C) 2008 The Android Open Source Project
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
26 # missing in Python 2.4 and before
27 if not hasattr(os, "SEEK_SET"):
30 class Options(object): pass
32 OPTIONS.search_path = "out/host/linux-x86"
33 OPTIONS.max_image_size = {}
34 OPTIONS.verbose = False
35 OPTIONS.tempfiles = []
38 class ExternalError(RuntimeError): pass
41 def Run(args, **kwargs):
42 """Create and return a subprocess.Popen object, printing the command
43 line on the terminal if -v was specified."""
45 print " running: ", " ".join(args)
46 return subprocess.Popen(args, **kwargs)
49 def LoadBoardConfig(fn):
50 """Parse a board_config.mk file looking for lines that specify the
51 maximum size of various images, and parse them into the
52 OPTIONS.max_image_size dict."""
53 OPTIONS.max_image_size = {}
56 m = re.match(r"BOARD_(BOOT|RECOVERY|SYSTEM|USERDATA)IMAGE_MAX_SIZE"
57 r"\s*:=\s*(\d+)", line)
60 OPTIONS.max_image_size[m.group(1).lower() + ".img"] = int(m.group(2))
63 def 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)."""
70 print "creating %s..." % (targetname,)
72 img = BuildBootableImage(sourcedir)
76 CheckSize(img, targetname)
77 ZipWriteStr(output_zip, targetname, img)
80 def 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."""
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)):
90 ramdisk_img = tempfile.NamedTemporaryFile()
91 img = tempfile.NamedTemporaryFile()
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())
100 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
101 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
103 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
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"))
110 fn = os.path.join(sourcedir, "base")
111 if os.access(fn, os.F_OK):
113 cmd.append(open(fn).read().rstrip("\n"))
115 cmd.extend(["--ramdisk", ramdisk_img.name,
116 "--output", img.name])
118 p = Run(cmd, stdout=subprocess.PIPE)
120 assert p.returncode == 0, "mkbootimg of %s image failed" % (
121 os.path.basename(sourcedir),)
123 img.seek(os.SEEK_SET, 0)
132 def AddRecovery(output_zip):
133 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
134 "recovery.img", output_zip)
136 def AddBoot(output_zip):
137 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
138 "boot.img", output_zip)
140 def UnzipTemp(filename):
141 """Unzip the given archive into a temporary directory and return the name."""
143 tmp = tempfile.mkdtemp(prefix="targetfiles-")
144 OPTIONS.tempfiles.append(tmp)
145 p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
147 if p.returncode != 0:
148 raise ExternalError("failed to unzip input target-files \"%s\"" %
153 def 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."""
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.
165 no_passwords.append(k)
168 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
169 "-inform", "DER", "-nocrypt"],
170 stdin=devnull.fileno(),
171 stdout=devnull.fileno(),
172 stderr=subprocess.STDOUT)
174 if p.returncode == 0:
175 no_passwords.append(k)
177 need_passwords.append(k)
180 key_passwords = PasswordManager().GetPasswords(need_passwords)
181 key_passwords.update(dict.fromkeys(no_passwords, None))
185 def 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
190 If align is an integer > 1, zipalign is run to align stored files in
191 the output zip on 'align'-byte boundaries.
193 if align == 0 or align == 1:
197 temp = tempfile.NamedTemporaryFile()
198 sign_name = temp.name
200 sign_name = output_name
202 p = Run(["java", "-jar",
203 os.path.join(OPTIONS.search_path, "framework", "signapk.jar"),
206 input_name, sign_name],
207 stdin=subprocess.PIPE,
208 stdout=subprocess.PIPE)
209 if password is not None:
211 p.communicate(password)
212 if p.returncode != 0:
213 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
216 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
218 if p.returncode != 0:
219 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
223 def 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
231 pct = float(size) * 100.0 / limit
232 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
234 raise ExternalError(msg)
237 print " WARNING: ", msg
239 elif OPTIONS.verbose:
243 COMMON_DOCSTRING = """
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.
249 Show command lines being executed.
252 Display this usage message and exit.
255 def Usage(docstring):
256 print docstring.rstrip("\n")
257 print COMMON_DOCSTRING
260 def ParseOptions(argv,
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."""
271 opts, args = getopt.getopt(
272 argv, "hvp:" + extra_opts,
273 ["help", "verbose", "path="] + list(extra_long_opts))
274 except getopt.GetoptError, err:
276 print "**", str(err), "**"
279 path_specified = False
282 if o in ("-h", "--help"):
285 elif o in ("-v", "--verbose"):
286 OPTIONS.verbose = True
287 elif o in ("-p", "--path"):
288 OPTIONS.search_path = a
290 if extra_option_handler is None or not extra_option_handler(o, a):
291 assert False, "unknown option \"%s\"" % (o,)
293 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
294 os.pathsep + os.environ["PATH"])
300 for i in OPTIONS.tempfiles:
307 class PasswordManager(object):
309 self.editor = os.getenv("EDITOR", None)
310 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
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
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.
323 current = self.ReadFile()
329 if i not in current or not current[i]:
331 # Are all the passwords already in the file?
332 if not missing: return current
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")
344 current = self.UpdateAndReadFile(current)
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
352 for k, v in sorted(current.iteritems()):
357 result[k] = getpass.getpass("Enter password for %s key> "
362 def UpdateAndReadFile(self, current):
363 if not self.editor or not self.pwfile:
364 return self.PromptResult(current)
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")
372 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
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.
381 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
382 _, _ = p.communicate()
384 return self.ReadFile()
388 if self.pwfile is None: return result
390 f = open(self.pwfile, "r")
393 if not line or line[0] == '#': continue
394 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
396 print "failed to parse password file: ", line
398 result[m.group(2)] = m.group(1)
401 if e.errno != errno.ENOENT:
402 print "error reading password file: ", str(e)
406 def 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)