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)
50 """Load the maximum allowable images sizes from the input
52 OPTIONS.max_image_size = {}
54 for line in open(os.path.join(OPTIONS.input_tmp, "META", "imagesizes.txt")):
56 if len(pieces) != 2: continue
59 OPTIONS.max_image_size[image + ".img"] = size
61 if e.errno == errno.ENOENT:
65 def 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)."""
72 print "creating %s..." % (targetname,)
74 img = BuildBootableImage(sourcedir)
78 CheckSize(img, targetname)
79 ZipWriteStr(output_zip, targetname, img)
82 def 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."""
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)):
92 ramdisk_img = tempfile.NamedTemporaryFile()
93 img = tempfile.NamedTemporaryFile()
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())
102 assert p1.returncode == 0, "mkbootfs of %s ramdisk failed" % (targetname,)
103 assert p2.returncode == 0, "minigzip of %s ramdisk failed" % (targetname,)
105 cmd = ["mkbootimg", "--kernel", os.path.join(sourcedir, "kernel")]
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"))
112 fn = os.path.join(sourcedir, "base")
113 if os.access(fn, os.F_OK):
115 cmd.append(open(fn).read().rstrip("\n"))
117 cmd.extend(["--ramdisk", ramdisk_img.name,
118 "--output", img.name])
120 p = Run(cmd, stdout=subprocess.PIPE)
122 assert p.returncode == 0, "mkbootimg of %s image failed" % (
123 os.path.basename(sourcedir),)
125 img.seek(os.SEEK_SET, 0)
134 def AddRecovery(output_zip):
135 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "RECOVERY"),
136 "recovery.img", output_zip)
138 def AddBoot(output_zip):
139 BuildAndAddBootableImage(os.path.join(OPTIONS.input_tmp, "BOOT"),
140 "boot.img", output_zip)
142 def UnzipTemp(filename):
143 """Unzip the given archive into a temporary directory and return the name."""
145 tmp = tempfile.mkdtemp(prefix="targetfiles-")
146 OPTIONS.tempfiles.append(tmp)
147 p = Run(["unzip", "-q", filename, "-d", tmp], stdout=subprocess.PIPE)
149 if p.returncode != 0:
150 raise ExternalError("failed to unzip input target-files \"%s\"" %
155 def 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."""
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.
167 no_passwords.append(k)
170 p = Run(["openssl", "pkcs8", "-in", k+".pk8",
171 "-inform", "DER", "-nocrypt"],
172 stdin=devnull.fileno(),
173 stdout=devnull.fileno(),
174 stderr=subprocess.STDOUT)
176 if p.returncode == 0:
177 no_passwords.append(k)
179 need_passwords.append(k)
182 key_passwords = PasswordManager().GetPasswords(need_passwords)
183 key_passwords.update(dict.fromkeys(no_passwords, None))
187 def 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
192 If align is an integer > 1, zipalign is run to align stored files in
193 the output zip on 'align'-byte boundaries.
195 if align == 0 or align == 1:
199 temp = tempfile.NamedTemporaryFile()
200 sign_name = temp.name
202 sign_name = output_name
204 p = Run(["java", "-jar",
205 os.path.join(OPTIONS.search_path, "framework", "signapk.jar"),
208 input_name, sign_name],
209 stdin=subprocess.PIPE,
210 stdout=subprocess.PIPE)
211 if password is not None:
213 p.communicate(password)
214 if p.returncode != 0:
215 raise ExternalError("signapk.jar failed: return code %s" % (p.returncode,))
218 p = Run(["zipalign", "-f", str(align), sign_name, output_name])
220 if p.returncode != 0:
221 raise ExternalError("zipalign failed: return code %s" % (p.returncode,))
225 def 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
233 pct = float(size) * 100.0 / limit
234 msg = "%s size (%d) is %.2f%% of limit (%d)" % (target, size, pct, limit)
236 raise ExternalError(msg)
239 print " WARNING: ", msg
241 elif OPTIONS.verbose:
245 COMMON_DOCSTRING = """
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.
251 Show command lines being executed.
254 Display this usage message and exit.
257 def Usage(docstring):
258 print docstring.rstrip("\n")
259 print COMMON_DOCSTRING
262 def ParseOptions(argv,
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."""
273 opts, args = getopt.getopt(
274 argv, "hvp:" + extra_opts,
275 ["help", "verbose", "path="] + list(extra_long_opts))
276 except getopt.GetoptError, err:
278 print "**", str(err), "**"
281 path_specified = False
284 if o in ("-h", "--help"):
287 elif o in ("-v", "--verbose"):
288 OPTIONS.verbose = True
289 elif o in ("-p", "--path"):
290 OPTIONS.search_path = a
292 if extra_option_handler is None or not extra_option_handler(o, a):
293 assert False, "unknown option \"%s\"" % (o,)
295 os.environ["PATH"] = (os.path.join(OPTIONS.search_path, "bin") +
296 os.pathsep + os.environ["PATH"])
302 for i in OPTIONS.tempfiles:
309 class PasswordManager(object):
311 self.editor = os.getenv("EDITOR", None)
312 self.pwfile = os.getenv("ANDROID_PW_FILE", None)
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
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.
325 current = self.ReadFile()
331 if i not in current or not current[i]:
333 # Are all the passwords already in the file?
334 if not missing: return current
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")
346 current = self.UpdateAndReadFile(current)
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
354 for k, v in sorted(current.iteritems()):
359 result[k] = getpass.getpass("Enter password for %s key> "
364 def UpdateAndReadFile(self, current):
365 if not self.editor or not self.pwfile:
366 return self.PromptResult(current)
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")
374 sorted = [(not v, k, v) for (k, v) in current.iteritems()]
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.
383 p = Run([self.editor, "+%d" % (first_line,), self.pwfile])
384 _, _ = p.communicate()
386 return self.ReadFile()
390 if self.pwfile is None: return result
392 f = open(self.pwfile, "r")
395 if not line or line[0] == '#': continue
396 m = re.match(r"^\[\[\[\s*(.*?)\s*\]\]\]\s*(\S+)$", line)
398 print "failed to parse password file: ", line
400 result[m.group(2)] = m.group(1)
403 if e.errno != errno.ENOENT:
404 print "error reading password file: ", str(e)
408 def 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)