|
1 #!/usr/bin/env python |
|
2 |
|
3 # This Source Code Form is subject to the terms of the Mozilla Public |
|
4 # License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
|
6 |
|
7 """ |
|
8 Sign Android packages using an Android debug keystore, creating the |
|
9 keystore if it does not exist. |
|
10 |
|
11 This and |zip| can be combined to replace the Android |apkbuilder| |
|
12 tool, which was deprecated in SDK r22. |
|
13 |
|
14 Exits with code 0 if creating the keystore and every signing succeeded, |
|
15 or with code 1 if any creation or signing failed. |
|
16 """ |
|
17 |
|
18 from argparse import ArgumentParser |
|
19 import errno |
|
20 import logging |
|
21 import os |
|
22 import subprocess |
|
23 import sys |
|
24 |
|
25 |
|
26 log = logging.getLogger(os.path.basename(__file__)) |
|
27 log.setLevel(logging.INFO) |
|
28 sh = logging.StreamHandler(stream=sys.stdout) |
|
29 sh.setFormatter(logging.Formatter('%(name)s: %(message)s')) |
|
30 log.addHandler(sh) |
|
31 |
|
32 |
|
33 class DebugKeystore: |
|
34 """ |
|
35 A thin abstraction on top of an Android debug key store. |
|
36 """ |
|
37 def __init__(self, keystore): |
|
38 self._keystore = os.path.abspath(os.path.expanduser(keystore)) |
|
39 self._alias = 'androiddebugkey' |
|
40 self.verbose = False |
|
41 self.keytool = 'keytool' |
|
42 self.jarsigner = 'jarsigner' |
|
43 |
|
44 @property |
|
45 def keystore(self): |
|
46 return self._keystore |
|
47 |
|
48 @property |
|
49 def alias(self): |
|
50 return self._alias |
|
51 |
|
52 def _check(self, args): |
|
53 try: |
|
54 if self.verbose: |
|
55 subprocess.check_call(args) |
|
56 else: |
|
57 subprocess.check_output(args) |
|
58 except OSError as ex: |
|
59 if ex.errno != errno.ENOENT: |
|
60 raise |
|
61 raise Exception("Could not find executable '%s'" % args[0]) |
|
62 |
|
63 def keystore_contains_alias(self): |
|
64 args = [ self.keytool, |
|
65 '-list', |
|
66 '-keystore', self.keystore, |
|
67 '-storepass', 'android', |
|
68 '-alias', self.alias, |
|
69 ] |
|
70 if self.verbose: |
|
71 args.append('-v') |
|
72 contains = True |
|
73 try: |
|
74 self._check(args) |
|
75 except subprocess.CalledProcessError as e: |
|
76 contains = False |
|
77 if self.verbose: |
|
78 log.info('Keystore %s %s alias %s' % |
|
79 (self.keystore, |
|
80 'contains' if contains else 'does not contain', |
|
81 self.alias)) |
|
82 return contains |
|
83 |
|
84 def create_alias_in_keystore(self): |
|
85 try: |
|
86 path = os.path.dirname(self.keystore) |
|
87 os.makedirs(path) |
|
88 except OSError as exception: |
|
89 if exception.errno != errno.EEXIST: |
|
90 raise |
|
91 |
|
92 args = [ self.keytool, |
|
93 '-genkeypair', |
|
94 '-keystore', self.keystore, |
|
95 '-storepass', 'android', |
|
96 '-alias', self.alias, |
|
97 '-keypass', 'android', |
|
98 '-dname', 'CN=Android Debug,O=Android,C=US', |
|
99 '-keyalg', 'RSA', |
|
100 '-validity', '365', |
|
101 ] |
|
102 if self.verbose: |
|
103 args.append('-v') |
|
104 self._check(args) |
|
105 if self.verbose: |
|
106 log.info('Created alias %s in keystore %s' % |
|
107 (self.alias, self.keystore)) |
|
108 |
|
109 def sign(self, apk): |
|
110 if not self.keystore_contains_alias(): |
|
111 self.create_alias_in_keystore() |
|
112 |
|
113 args = [ self.jarsigner, |
|
114 '-digestalg', 'SHA1', |
|
115 '-sigalg', 'MD5withRSA', |
|
116 '-keystore', self.keystore, |
|
117 '-storepass', 'android', |
|
118 apk, |
|
119 self.alias, |
|
120 ] |
|
121 if self.verbose: |
|
122 args.append('-verbose') |
|
123 self._check(args) |
|
124 if self.verbose: |
|
125 log.info('Signed %s with alias %s from keystore %s' % |
|
126 (apk, self.alias, self.keystore)) |
|
127 |
|
128 |
|
129 def parse_args(argv): |
|
130 parser = ArgumentParser(description='Sign Android packages using an Android debug keystore.') |
|
131 parser.add_argument('apks', nargs='+', |
|
132 metavar='APK', |
|
133 help='Android packages to be signed') |
|
134 parser.add_argument('-v', '--verbose', |
|
135 dest='verbose', |
|
136 default=False, |
|
137 action='store_true', |
|
138 help='verbose output') |
|
139 parser.add_argument('--keytool', |
|
140 metavar='PATH', |
|
141 default='keytool', |
|
142 help='path to Java keytool') |
|
143 parser.add_argument('--jarsigner', |
|
144 metavar='PATH', |
|
145 default='jarsigner', |
|
146 help='path to Java jarsigner') |
|
147 parser.add_argument('--keystore', |
|
148 metavar='PATH', |
|
149 default='~/.android/debug.keystore', |
|
150 help='path to keystore (default: ~/.android/debug.keystore)') |
|
151 parser.add_argument('-f', '--force-create-keystore', |
|
152 dest='force', |
|
153 default=False, |
|
154 action='store_true', |
|
155 help='force creating keystore') |
|
156 return parser.parse_args(argv) |
|
157 |
|
158 |
|
159 def main(): |
|
160 args = parse_args(sys.argv[1:]) |
|
161 |
|
162 keystore = DebugKeystore(args.keystore) |
|
163 keystore.verbose = args.verbose |
|
164 keystore.keytool = args.keytool |
|
165 keystore.jarsigner = args.jarsigner |
|
166 |
|
167 if args.force: |
|
168 try: |
|
169 keystore.create_alias_in_keystore() |
|
170 except subprocess.CalledProcessError as e: |
|
171 log.error('Failed to force-create alias %s in keystore %s' % |
|
172 (keystore.alias, keystore.keystore)) |
|
173 log.error(e) |
|
174 return 1 |
|
175 |
|
176 for apk in args.apks: |
|
177 try: |
|
178 keystore.sign(apk) |
|
179 except subprocess.CalledProcessError as e: |
|
180 log.error('Failed to sign %s', apk) |
|
181 log.error(e) |
|
182 return 1 |
|
183 |
|
184 return 0 |
|
185 |
|
186 if __name__ == '__main__': |
|
187 sys.exit(main()) |