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