chiark / gitweb /
init: --android-home for forcing the path to the Android SDK
[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     parser.add_option("--android-home", default=None,
107                       help="Path to the Android SDK (sometimes set in ANDROID_HOME)")
108     (options, args) = parser.parse_args()
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     test_config = common.get_default_config()
122
123     # track down where the Android SDK is, the default is to use the path set
124     # in ANDROID_HOME if that exists, otherwise None
125     if options.android_home != None:
126         test_config['sdk_path'] = options.android_home
127     elif not common.test_sdk_exists(test_config):
128         # if neither --android-home nor the default sdk_path exist, prompt the user
129         default_sdk_path = '/opt/android-sdk'
130         while True:
131             s = raw_input('Enter the path to the Android SDK (' + default_sdk_path + ') here:\n> ')
132             if re.match('^\s*$', s) != None:
133                 test_config['sdk_path'] = default_sdk_path
134             else:
135                 test_config['sdk_path'] = s
136             if common.test_sdk_exists(test_config):
137                 break
138     if not common.test_sdk_exists(test_config):
139         sys.exit(3)
140
141     if not os.path.exists('config.py'):
142         # 'metadata' and 'tmp' are created in fdroid
143         if not os.path.exists('repo'):
144             os.mkdir('repo')
145         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
146         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
147         os.chmod('config.py', 0o0600)
148         write_to_config('sdk_path', test_config['sdk_path'])
149     else:
150         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
151         logging.info('Try running `fdroid init` in an empty directory.')
152         sys.exit()
153
154     # now that we have a local config.py, read configuration...
155     config = common.read_config(options)
156
157     # try to find a working aapt, in all the recent possible paths
158     build_tools = os.path.join(config['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' + config['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 ''')