chiark / gitweb /
Fix --android-home in 'fdroid init' when ANDROID_HOME is set
[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     fdroiddir = os.getcwd()
128     test_config = dict()
129     common.fill_config_defaults(test_config)
130
131     # track down where the Android SDK is, the default is to use the path set
132     # in ANDROID_HOME if that exists, otherwise None
133     if options.android_home is not None:
134         test_config['sdk_path'] = options.android_home
135     elif not common.test_sdk_exists(test_config):
136         # if neither --android-home nor the default sdk_path exist, prompt the user
137         default_sdk_path = '/opt/android-sdk'
138         while not options.no_prompt:
139             try:
140                 s = raw_input('Enter the path to the Android SDK ('
141                               + default_sdk_path + ') here:\n> ')
142             except KeyboardInterrupt:
143                 print('')
144                 sys.exit(1)
145             if re.match('^\s*$', s) is not None:
146                 test_config['sdk_path'] = default_sdk_path
147             else:
148                 test_config['sdk_path'] = s
149             if common.test_sdk_exists(test_config):
150                 break
151     if not common.test_sdk_exists(test_config):
152         sys.exit(3)
153
154     if not os.path.exists('config.py'):
155         # 'metadata' and 'tmp' are created in fdroid
156         if not os.path.exists('repo'):
157             os.mkdir('repo')
158         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
159         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
160         os.chmod('config.py', 0o0600)
161         # If android_home is None, test_config['sdk_path'] will be used and
162         # "$ANDROID_HOME" may be used if the env var is set up correctly.
163         # If android_home is not None, the path given from the command line
164         # will be directly written in the config.
165         write_to_config(test_config, 'sdk_path', options.android_home)
166     else:
167         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
168         logging.info('Try running `fdroid init` in an empty directory.')
169         sys.exit()
170
171     # try to find a working aapt, in all the recent possible paths
172     build_tools = os.path.join(test_config['sdk_path'], 'build-tools')
173     aaptdirs = []
174     aaptdirs.append(os.path.join(build_tools, test_config['build_tools']))
175     aaptdirs.append(build_tools)
176     for f in os.listdir(build_tools):
177         if os.path.isdir(os.path.join(build_tools, f)):
178             aaptdirs.append(os.path.join(build_tools, f))
179     for d in sorted(aaptdirs, reverse=True):
180         if os.path.isfile(os.path.join(d, 'aapt')):
181             aapt = os.path.join(d, 'aapt')
182             break
183     if os.path.isfile(aapt):
184         dirname = os.path.basename(os.path.dirname(aapt))
185         if dirname == 'build-tools':
186             # this is the old layout, before versioned build-tools
187             test_config['build_tools'] = ''
188         else:
189             test_config['build_tools'] = dirname
190         write_to_config(test_config, 'build_tools')
191     if not common.test_build_tools_exists(test_config):
192         sys.exit(3)
193
194     # now that we have a local config.py, read configuration...
195     config = common.read_config(options)
196
197     # track down where the Android NDK is
198     ndk_path = '/opt/android-ndk'
199     if os.path.isdir(config['ndk_path']):
200         ndk_path = config['ndk_path']
201     elif 'ANDROID_NDK' in os.environ.keys():
202         logging.info('using ANDROID_NDK')
203         ndk_path = os.environ['ANDROID_NDK']
204     if os.path.isdir(ndk_path):
205         write_to_config(test_config, 'ndk_path')
206     # the NDK is optional so we don't prompt the user for it if its not found
207
208     # find or generate the keystore for the repo signing key. First try the
209     # path written in the default config.py.  Then check if the user has
210     # specified a path from the command line, which will trump all others.
211     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.  If
212     # keystore is set to NONE, that means that Java will look for keys in a
213     # Hardware Security Module aka Smartcard.
214     keystore = config['keystore']
215     if options.keystore:
216         keystore = os.path.abspath(options.keystore)
217         if options.keystore == 'NONE':
218             keystore = options.keystore
219         else:
220             keystore = os.path.abspath(options.keystore)
221             if not os.path.exists(keystore):
222                 logging.info('"' + keystore
223                              + '" does not exist, creating a new keystore there.')
224     write_to_config(test_config, 'keystore', keystore)
225     repo_keyalias = None
226     if options.repo_keyalias:
227         repo_keyalias = options.repo_keyalias
228         write_to_config(test_config, 'repo_keyalias', repo_keyalias)
229     if options.distinguished_name:
230         keydname = options.distinguished_name
231         write_to_config(test_config, 'keydname', keydname)
232     if keystore == 'NONE':  # we're using a smartcard
233         write_to_config(test_config, 'repo_keyalias', '1')  # seems to be the default
234         disable_in_config('keypass', 'never used with smartcard')
235         write_to_config(test_config, 'smartcardoptions',
236                         ('-storetype PKCS11 -providerName SunPKCS11-OpenSC '
237                          + '-providerClass sun.security.pkcs11.SunPKCS11 '
238                          + '-providerArg opensc-fdroid.cfg'))
239         # find opensc-pkcs11.so
240         if not os.path.exists('opensc-fdroid.cfg'):
241             if os.path.exists('/usr/lib/opensc-pkcs11.so'):
242                 opensc_so = '/usr/lib/opensc-pkcs11.so'
243             elif os.path.exists('/usr/lib64/opensc-pkcs11.so'):
244                 opensc_so = '/usr/lib64/opensc-pkcs11.so'
245             else:
246                 files = glob.glob('/usr/lib/' + os.uname()[4] + '-*-gnu/opensc-pkcs11.so')
247                 if len(files) > 0:
248                     opensc_so = files[0]
249                 else:
250                     opensc_so = '/usr/lib/opensc-pkcs11.so'
251                     logging.warn('No OpenSC PKCS#11 module found, ' +
252                                  'install OpenSC then edit "opensc-fdroid.cfg"!')
253             with open(os.path.join(examplesdir, 'opensc-fdroid.cfg'), 'r') as f:
254                 opensc_fdroid = f.read()
255             opensc_fdroid = re.sub('^library.*', 'library = ' + opensc_so, opensc_fdroid,
256                                    flags=re.MULTILINE)
257             with open('opensc-fdroid.cfg', 'w') as f:
258                 f.write(opensc_fdroid)
259     elif not os.path.exists(keystore):
260         # no existing or specified keystore, generate the whole thing
261         keystoredir = os.path.dirname(keystore)
262         if not os.path.exists(keystoredir):
263             os.makedirs(keystoredir, mode=0o700)
264         password = genpassword()
265         write_to_config(test_config, 'keystorepass', password)
266         write_to_config(test_config, 'keypass', password)
267         if options.repo_keyalias is None:
268             repo_keyalias = socket.getfqdn()
269             write_to_config(test_config, 'repo_keyalias', repo_keyalias)
270         if not options.distinguished_name:
271             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
272             write_to_config(test_config, 'keydname', keydname)
273         genkey(keystore, repo_keyalias, password, keydname)
274
275     logging.info('Built repo based in "' + fdroiddir + '"')
276     logging.info('with this config:')
277     logging.info('  Android SDK:\t\t\t' + config['sdk_path'])
278     logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
279     logging.info('  Android NDK (optional):\t' + ndk_path)
280     logging.info('  Keystore for signing key:\t' + keystore)
281     if repo_keyalias is not None:
282         logging.info('  Alias for key in store:\t' + repo_keyalias)
283     logging.info('\nTo complete the setup, add your APKs to "' +
284                  os.path.join(fdroiddir, 'repo') + '"' + '''
285 then run "fdroid update -c; fdroid update".  You might also want to edit
286 "config.py" to set the URL, repo name, and more.  You should also set up
287 a signing key (a temporary one might have been automatically generated).
288
289 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
290 and https://f-droid.org/manual/fdroid.html#Signing
291 ''')