chiark / gitweb /
init: split out defconfig and sdk test to run before config is loaded
[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 def write_to_config(key, value):
39     '''write a key/value to the local config.py'''
40     with open('config.py', 'r') as f:
41         data = f.read()
42     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
43     repl = '\n' + key + ' = "' + value + '"'
44     data = re.sub(pattern, repl, data)
45     with open('config.py', 'w') as f:
46         f.writelines(data)
47
48 def disable_in_config(key, value):
49     '''write a key/value to the local config.py, then comment it out'''
50     with open('config.py', 'r') as f:
51         data = f.read()
52     pattern = '\n[\s#]*' + key + '\s*=\s*"[^"]*"'
53     repl = '\n#' + key + ' = "' + value + '"'
54     data = re.sub(pattern, repl, data)
55     with open('config.py', 'w') as f:
56         f.writelines(data)
57
58
59 def genpassword():
60     '''generate a random password for when generating keys'''
61     h = hashlib.sha256()
62     h.update(os.urandom(16)) # salt
63     h.update(bytes(socket.getfqdn()))
64     return h.digest().encode('base64').strip()
65
66
67 def genkey(keystore, repo_keyalias, password, keydname):
68     '''generate a new keystore with a new key in it for signing repos'''
69     logging.info('Generating a new key in "' + keystore + '"...')
70     common.write_password_file("keystorepass", password)
71     common.write_password_file("keypass", password)
72     p = FDroidPopen(['keytool', '-genkey',
73                 '-keystore', keystore, '-alias', repo_keyalias,
74                 '-keyalg', 'RSA', '-keysize', '4096',
75                 '-sigalg', 'SHA256withRSA',
76                 '-validity', '10000',
77                 '-storepass:file', config['keystorepassfile'],
78                 '-keypass:file', config['keypassfile'],
79                 '-dname', keydname])
80     # TODO keypass should be sent via stdin
81     if p.returncode != 0:
82         raise BuildException("Failed to generate key", p.stdout)
83     # now show the lovely key that was just generated
84     p = FDroidPopen(['keytool', '-list', '-v',
85                      '-keystore', keystore, '-alias', repo_keyalias,
86                      '-storepass:file', config['keystorepassfile']])
87     logging.info(p.stdout.strip() + '\n\n')
88
89
90 def main():
91
92     global options, config
93
94     # Parse command line...
95     parser = OptionParser()
96     parser.add_option("-v", "--verbose", action="store_true", default=False,
97                       help="Spew out even more information than normal")
98     parser.add_option("-q", "--quiet", action="store_true", default=False,
99                       help="Restrict output to warnings and errors")
100     parser.add_option("-d", "--distinguished-name", default=None,
101                       help="X.509 'Distiguished Name' used when generating keys")
102     parser.add_option("--keystore", default=None,
103                       help="Path to the keystore for the repo signing key")
104     parser.add_option("--repo-keyalias", default=None,
105                       help="Alias of the repo signing key in the keystore")
106     (options, args) = parser.parse_args()
107
108     common.test_sdk_exists(common.get_default_config())
109
110     # find root install prefix
111     tmp = os.path.dirname(sys.argv[0])
112     if os.path.basename(tmp) == 'bin':
113         prefix = os.path.dirname(tmp)
114         examplesdir = prefix + '/share/doc/fdroidserver/examples'
115     else:
116         # we're running straight out of the git repo
117         prefix = tmp
118         examplesdir = prefix + '/examples'
119
120     fdroiddir = os.getcwd()
121
122     if not os.path.exists('config.py') and not os.path.exists('repo'):
123         # 'metadata' and 'tmp' are created in fdroid
124         os.mkdir('repo')
125         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
126         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
127         os.chmod('config.py', 0o0600)
128     else:
129         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
130         logging.info('Try running `fdroid init` in an empty directory.')
131         sys.exit()
132
133     # now that we have a local config.py, read configuration...
134     config = common.read_config(options)
135
136     # track down where the Android SDK is
137     if os.path.isdir(config['sdk_path']):
138         logging.info('Using "' + config['sdk_path'] + '" for the Android SDK')
139         sdk_path = config['sdk_path']
140     elif 'ANDROID_HOME' in os.environ.keys():
141         sdk_path = os.environ['ANDROID_HOME']
142     else:
143         default_sdk_path = '/opt/android-sdk'
144         while True:
145             s = raw_input('Enter the path to the Android SDK (' + default_sdk_path + '): ')
146             if re.match('^\s*$', s) != None:
147                 sdk_path = default_sdk_path
148             else:
149                 sdk_path = s
150             if os.path.isdir(os.path.join(sdk_path, 'build-tools')):
151                 break
152             else:
153                 logging.info('"' + s + '" does not contain the Android SDK! Try again...')
154     if os.path.isdir(sdk_path):
155         write_to_config('sdk_path', sdk_path)
156
157     # try to find a working aapt, in all the recent possible paths
158     build_tools = os.path.join(sdk_path, 'build-tools')
159     aaptdirs = []
160     aaptdirs.append(os.path.join(build_tools, config['build_tools']))
161     aaptdirs.append(build_tools)
162     for f in sorted(os.listdir(build_tools), reverse=True):
163         if os.path.isdir(os.path.join(build_tools, f)):
164             aaptdirs.append(os.path.join(build_tools, f))
165     for d in aaptdirs:
166         if os.path.isfile(os.path.join(d, 'aapt')):
167             aapt = os.path.join(d, 'aapt')
168             break
169     if os.path.isfile(aapt):
170         dirname = os.path.basename(os.path.dirname(aapt))
171         if dirname == 'build-tools':
172             # this is the old layout, before versioned build-tools
173             write_to_config('build_tools', '')
174         else:
175             write_to_config('build_tools', dirname)
176
177     # track down where the Android NDK is
178     ndk_path = '/opt/android-ndk'
179     if os.path.isdir(config['ndk_path']):
180         ndk_path = config['ndk_path']
181     elif 'ANDROID_NDK' in os.environ.keys():
182         logging.info('using ANDROID_NDK')
183         ndk_path = os.environ['ANDROID_NDK']
184     if os.path.isdir(ndk_path):
185         write_to_config('ndk_path', ndk_path)
186     # the NDK is optional so we don't prompt the user for it if its not found
187
188     # find or generate the keystore for the repo signing key. First try the
189     # path written in the default config.py.  Then check if the user has
190     # specified a path from the command line, which will trump all others.
191     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.  If
192     # keystore is set to NONE, that means that Java will look for keys in a
193     # Hardware Security Module aka Smartcard.
194     keystore = config['keystore']
195     if options.keystore:
196         keystore = os.path.abspath(options.keystore)
197         if options.keystore == 'NONE':
198             keystore = options.keystore
199         else:
200             keystore = os.path.abspath(options.keystore)
201             if not os.path.exists(keystore):
202                 logging.info('"' + keystore
203                              + '" does not exist, creating a new keystore there.')
204     write_to_config('keystore', keystore)
205     repo_keyalias = None
206     if options.repo_keyalias:
207         repo_keyalias = options.repo_keyalias
208         write_to_config('repo_keyalias', repo_keyalias)
209     if options.distinguished_name:
210         keydname = options.distinguished_name
211         write_to_config('keydname', keydname)
212     if keystore == 'NONE': # we're using a smartcard
213         write_to_config('repo_keyalias', '1')  # seems to be the default
214         disable_in_config('keypass', 'never used with smartcard')
215         write_to_config('smartcardoptions',
216                         ('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
217                          + '-providerClass sun.security.pkcs11.SunPKCS11 '
218                          + '-providerArg opensc-fdroid.cfg'))
219         # find opensc-pkcs11.so
220         if not os.path.exists('opensc-fdroid.cfg'):
221             if os.path.exists('/usr/lib/opensc-pkcs11.so'):
222                 opensc_so = '/usr/lib/opensc-pkcs11.so'
223             elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
224                 opensc_so = '/usr/lib64/opensc-pkcs11.so'
225             else:
226                 files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
227                 if len(files) > 0:
228                     opensc_so = files[0]
229                 else:
230                     opensc_so = '/usr/lib/opensc-pkcs11.so'
231                     logging.warn('No OpenSC PKCS#11 module found, ' +
232                                  'install OpenSC then edit "opensc-fdroid.cfg"!')
233             with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
234                 opensc_fdroid = f.read()
235             opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
236                                    flags=re.MULTILINE)
237             with open('opensc-fdroid.cfg', 'w') as f:
238                 f.write(opensc_fdroid)
239     elif not os.path.exists(keystore):
240         # no existing or specified keystore, generate the whole thing
241         keystoredir = os.path.dirname(keystore)
242         if not os.path.exists(keystoredir):
243             os.makedirs(keystoredir, mode=0o700)
244         password = genpassword()
245         write_to_config('keystorepass', password)
246         write_to_config('keypass', password)
247         if options.repo_keyalias == None:
248             repo_keyalias = socket.getfqdn()
249             write_to_config('repo_keyalias', repo_keyalias)
250         if not options.distinguished_name:
251             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
252             write_to_config('keydname', keydname)
253         genkey(keystore, repo_keyalias, password, keydname)
254
255     logging.info('Built repo based in "' + fdroiddir + '"')
256     logging.info('with this config:')
257     logging.info('  Android SDK:\t\t\t' + sdk_path)
258     logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
259     logging.info('  Android NDK (optional):\t' + ndk_path)
260     logging.info('  Keystore for signing key:\t' + keystore)
261     if repo_keyalias != None:
262         logging.info('  Alias for key in store:\t' + repo_keyalias)
263     logging.info('\nTo complete the setup, add your APKs to "' +
264           os.path.join(fdroiddir, 'repo') + '"' +
265 '''
266 then run "fdroid update -c; fdroid update".  You might also want to edit
267 "config.py" to set the URL, repo name, and more.  You should also set up
268 a signing key (a temporary one might have been automatically generated).
269
270 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
271 and https://f-droid.org/manual/fdroid.html#Signing
272 ''')