chiark / gitweb /
Remove SilentPopen for consistency with SdkToolsPopen
[fdroidserver.git] / fdroidserver / init.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # update.py - part of the FDroid server tools
5 # Copyright (C) 2010-2013, Ciaran Gultnieks, ciaran@ciarang.com
6 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
7 # Copyright (C) 2013 Hans-Christoph Steiner <hans@eds.org>
8 #
9 # This program is free software: you can redistribute it and/or modify
10 # it under the terms of the GNU Affero General Public License as published by
11 # the Free Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU Affero General Public License for more details.
18 #
19 # You should have received a copy of the GNU Affero General Public License
20 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
21
22 import glob
23 import hashlib
24 import os
25 import re
26 import shutil
27 import socket
28 import sys
29 from optparse import OptionParser
30 import logging
31
32 import common
33 from common import FDroidPopen, BuildException
34
35 config = {}
36 options = None
37
38
39 def write_to_config(thisconfig, key, value=None):
40     '''write a key/value to the local config.py'''
41     if value is None:
42         origkey = key + '_orig'
43         value = thisconfig[origkey] if origkey in thisconfig else thisconfig[key]
44     with open('config.py', 'r') as f:
45         data = f.read()
46     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
47     repl = '\n' + key + ' = "' + value + '"'
48     data = re.sub(pattern, repl, data)
49     with open('config.py', 'w') as f:
50         f.writelines(data)
51
52
53 def disable_in_config(key, value):
54     '''write a key/value to the local config.py, then comment it out'''
55     with open('config.py', 'r') as f:
56         data = f.read()
57     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
58     repl = '\n#' + key + ' = "' + value + '"'
59     data = re.sub(pattern, repl, data)
60     with open('config.py', 'w') as f:
61         f.writelines(data)
62
63
64 def genpassword():
65     '''generate a random password for when generating keys'''
66     h = hashlib.sha256()
67     h.update(os.urandom(16))  # salt
68     h.update(bytes(socket.getfqdn()))
69     return h.digest().encode('base64').strip()
70
71
72 def genkey(keystore, repo_keyalias, password, keydname):
73     '''generate a new keystore with a new key in it for signing repos'''
74     logging.info('Generating a new key in "' + keystore + '"...')
75     common.write_password_file("keystorepass", password)
76     common.write_password_file("keypass", password)
77     p = FDroidPopen(['keytool', '-genkey',
78                      '-keystore', keystore, '-alias', repo_keyalias,
79                      '-keyalg', 'RSA', '-keysize', '4096',
80                      '-sigalg', 'SHA256withRSA',
81                      '-validity', '10000',
82                      '-storepass:file', config['keystorepassfile'],
83                      '-keypass:file', config['keypassfile'],
84                      '-dname', keydname])
85     # TODO keypass should be sent via stdin
86     if p.returncode != 0:
87         raise BuildException("Failed to generate key", p.output)
88     # now show the lovely key that was just generated
89     p = FDroidPopen(['keytool', '-list', '-v',
90                      '-keystore', keystore, '-alias', repo_keyalias,
91                      '-storepass:file', config['keystorepassfile']])
92     logging.info(p.output.strip() + '\n\n')
93
94
95 def main():
96
97     global options, config
98
99     # Parse command line...
100     parser = OptionParser()
101     parser.add_option("-v", "--verbose", action="store_true", default=False,
102                       help="Spew out even more information than normal")
103     parser.add_option("-q", "--quiet", action="store_true", default=False,
104                       help="Restrict output to warnings and errors")
105     parser.add_option("-d", "--distinguished-name", default=None,
106                       help="X.509 'Distiguished Name' used when generating keys")
107     parser.add_option("--keystore", default=None,
108                       help="Path to the keystore for the repo signing key")
109     parser.add_option("--repo-keyalias", default=None,
110                       help="Alias of the repo signing key in the keystore")
111     parser.add_option("--android-home", default=None,
112                       help="Path to the Android SDK (sometimes set in ANDROID_HOME)")
113     parser.add_option("--no-prompt", action="store_true", default=False,
114                       help="Do not prompt for Android SDK path, just fail")
115     (options, args) = parser.parse_args()
116
117     # find root install prefix
118     tmp = os.path.dirname(sys.argv[0])
119     if os.path.basename(tmp) == 'bin':
120         prefix = os.path.dirname(tmp)
121         examplesdir = prefix + '/share/doc/fdroidserver/examples'
122     else:
123         # we're running straight out of the git repo
124         prefix = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
125         examplesdir = prefix + '/examples'
126
127     aapt = None
128     fdroiddir = os.getcwd()
129     test_config = dict()
130     common.fill_config_defaults(test_config)
131
132     # track down where the Android SDK is, the default is to use the path set
133     # in ANDROID_HOME if that exists, otherwise None
134     if options.android_home is not None:
135         test_config['sdk_path'] = options.android_home
136     elif not common.test_sdk_exists(test_config):
137         if os.path.isfile('/usr/bin/aapt'):
138             # remove sdk_path and build_tools, they are not required
139             test_config.pop('sdk_path', None)
140             test_config.pop('build_tools', None)
141             # make sure at least aapt is found, since this can't do anything without it
142             test_config['aapt'] = common.find_sdk_tools_cmd('aapt')
143         else:
144             # if neither --android-home nor the default sdk_path exist, prompt the user
145             default_sdk_path = '/opt/android-sdk'
146             while not options.no_prompt:
147                 try:
148                     s = raw_input('Enter the path to the Android SDK ('
149                                   + default_sdk_path + ') here:\n> ')
150                 except KeyboardInterrupt:
151                     print('')
152                     sys.exit(1)
153                 if re.match('^\s*$', s) is not None:
154                     test_config['sdk_path'] = default_sdk_path
155                 else:
156                     test_config['sdk_path'] = s
157                 if common.test_sdk_exists(test_config):
158                     break
159     if not common.test_sdk_exists(test_config):
160         sys.exit(3)
161
162     if not os.path.exists('config.py'):
163         # 'metadata' and 'tmp' are created in fdroid
164         if not os.path.exists('repo'):
165             os.mkdir('repo')
166         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
167         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
168         os.chmod('config.py', 0o0600)
169         # If android_home is None, test_config['sdk_path'] will be used and
170         # "$ANDROID_HOME" may be used if the env var is set up correctly.
171         # If android_home is not None, the path given from the command line
172         # will be directly written in the config.
173         if 'sdk_path' in test_config:
174             write_to_config(test_config, 'sdk_path', options.android_home)
175     else:
176         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
177         logging.info('Try running `fdroid init` in an empty directory.')
178         sys.exit()
179
180     if 'aapt' not in test_config or not os.path.isfile(test_config['aapt']):
181         # try to find a working aapt, in all the recent possible paths
182         build_tools = os.path.join(test_config['sdk_path'], 'build-tools')
183         aaptdirs = []
184         aaptdirs.append(os.path.join(build_tools, test_config['build_tools']))
185         aaptdirs.append(build_tools)
186         for f in os.listdir(build_tools):
187             if os.path.isdir(os.path.join(build_tools, f)):
188                 aaptdirs.append(os.path.join(build_tools, f))
189         for d in sorted(aaptdirs, reverse=True):
190             if os.path.isfile(os.path.join(d, 'aapt')):
191                 aapt = os.path.join(d, 'aapt')
192                 break
193         if os.path.isfile(aapt):
194             dirname = os.path.basename(os.path.dirname(aapt))
195             if dirname == 'build-tools':
196                 # this is the old layout, before versioned build-tools
197                 test_config['build_tools'] = ''
198             else:
199                 test_config['build_tools'] = dirname
200             write_to_config(test_config, 'build_tools')
201         common.ensure_build_tools_exists(test_config)
202
203     # now that we have a local config.py, read configuration...
204     config = common.read_config(options)
205
206     # track down where the Android NDK is
207     ndk_path = '/opt/android-ndk'
208     if os.path.isdir(config['ndk_path']):
209         ndk_path = config['ndk_path']
210     elif 'ANDROID_NDK' in os.environ.keys():
211         logging.info('using ANDROID_NDK')
212         ndk_path = os.environ['ANDROID_NDK']
213     if os.path.isdir(ndk_path):
214         write_to_config(test_config, 'ndk_path')
215     # the NDK is optional so we don't prompt the user for it if its not found
216
217     # find or generate the keystore for the repo signing key. First try the
218     # path written in the default config.py.  Then check if the user has
219     # specified a path from the command line, which will trump all others.
220     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.  If
221     # keystore is set to NONE, that means that Java will look for keys in a
222     # Hardware Security Module aka Smartcard.
223     keystore = config['keystore']
224     if options.keystore:
225         keystore = os.path.abspath(options.keystore)
226         if options.keystore == 'NONE':
227             keystore = options.keystore
228         else:
229             keystore = os.path.abspath(options.keystore)
230             if not os.path.exists(keystore):
231                 logging.info('"' + keystore
232                              + '" does not exist, creating a new keystore there.')
233     write_to_config(test_config, 'keystore', keystore)
234     repo_keyalias = None
235     if options.repo_keyalias:
236         repo_keyalias = options.repo_keyalias
237         write_to_config(test_config, 'repo_keyalias', repo_keyalias)
238     if options.distinguished_name:
239         keydname = options.distinguished_name
240         write_to_config(test_config, 'keydname', keydname)
241     if keystore == 'NONE':  # we're using a smartcard
242         write_to_config(test_config, 'repo_keyalias', '1')  # seems to be the default
243         disable_in_config('keypass', 'never used with smartcard')
244         write_to_config(test_config, 'smartcardoptions',
245                         ('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
246                          + '-providerClass sun.security.pkcs11.SunPKCS11 '
247                          + '-providerArg opensc-fdroid.cfg'))
248         # find opensc-pkcs11.so
249         if not os.path.exists('opensc-fdroid.cfg'):
250             if os.path.exists('/usr/lib/opensc-pkcs11.so'):
251                 opensc_so = '/usr/lib/opensc-pkcs11.so'
252             elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
253                 opensc_so = '/usr/lib64/opensc-pkcs11.so'
254             else:
255                 files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
256                 if len(files) > 0:
257                     opensc_so = files[0]
258                 else:
259                     opensc_so = '/usr/lib/opensc-pkcs11.so'
260                     logging.warn('No OpenSC PKCS#11 module found, ' +
261                                  'install OpenSC then edit "opensc-fdroid.cfg"!')
262             with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
263                 opensc_fdroid = f.read()
264             opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
265                                    flags=re.MULTILINE)
266             with open('opensc-fdroid.cfg', 'w') as f:
267                 f.write(opensc_fdroid)
268     elif not os.path.exists(keystore):
269         # no existing or specified keystore, generate the whole thing
270         keystoredir = os.path.dirname(keystore)
271         if not os.path.exists(keystoredir):
272             os.makedirs(keystoredir, mode=0o700)
273         password = genpassword()
274         write_to_config(test_config, 'keystorepass', password)
275         write_to_config(test_config, 'keypass', password)
276         if options.repo_keyalias is None:
277             repo_keyalias = socket.getfqdn()
278             write_to_config(test_config, 'repo_keyalias', repo_keyalias)
279         if not options.distinguished_name:
280             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
281             write_to_config(test_config, 'keydname', keydname)
282         genkey(keystore, repo_keyalias, password, keydname)
283
284     logging.info('Built repo based in "' + fdroiddir + '"')
285     logging.info('with this config:')
286     logging.info('  Android SDK:\t\t\t' + config['sdk_path'])
287     if aapt:
288         logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
289     logging.info('  Android NDK (optional):\t' + ndk_path)
290     logging.info('  Keystore for signing key:\t' + keystore)
291     if repo_keyalias is not None:
292         logging.info('  Alias for key in store:\t' + repo_keyalias)
293     logging.info('\nTo complete the setup, add your APKs to "' +
294                  os.path.join(fdroiddir, 'repo') + '"' + '''
295 then run "fdroid update -c; fdroid update".  You might also want to edit
296 "config.py" to set the URL, repo name, and more.  You should also set up
297 a signing key (a temporary one might have been automatically generated).
298
299 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
300 and https://f-droid.org/manual/fdroid.html#Signing
301 ''')