chiark / gitweb /
oops, include common. to call write_password_file()
[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 hashlib
23 import os
24 import re
25 import shutil
26 import socket
27 import sys
28 from optparse import OptionParser
29 import logging
30
31 import common
32 from common import FDroidPopen, BuildException
33
34 config = {}
35 options = None
36
37 def write_to_config(key, value):
38     '''write a key/value to the local config.py'''
39     with open('config.py', 'r') as f:
40         data = f.read()
41     pattern = key + '\s*=.*'
42     repl = key + ' = "' + value + '"'
43     data = re.sub(pattern, repl, data)
44     with open('config.py', 'w') as f:
45         f.writelines(data)
46
47
48 def genpassword():
49     '''generate a random password for when generating keys'''
50     h = hashlib.sha256()
51     h.update(os.urandom(16)) # salt
52     h.update(bytes(socket.getfqdn()))
53     return h.digest().encode('base64').strip()
54
55
56 def genkey(keystore, repo_keyalias, password, keydname):
57     '''generate a new keystore with a new key in it for signing repos'''
58     logging.info('Generating a new key in "' + keystore + '"...')
59     common.write_password_file("keystorepass", password)
60     common.write_password_file("keypass", password)
61     p = FDroidPopen(['keytool', '-genkey',
62                 '-keystore', keystore, '-alias', repo_keyalias,
63                 '-keyalg', 'RSA', '-keysize', '4096',
64                 '-sigalg', 'SHA256withRSA',
65                 '-validity', '10000',
66                 '-storepass:file', config['keystorepassfile'],
67                 '-keypass:file', config['keypassfile'],
68                 '-dname', keydname])
69     if p.returncode != 0:
70         raise BuildException("Failed to generate key", p.stdout)
71     # now show the lovely key that was just generated
72     p = FDroidPopen(['keytool', '-list', '-v',
73                 '-keystore', keystore, '-alias', repo_keyalias])
74     output = p.communicate(password)[0]
75     logging.info(output.lstrip().strip() + '\n\n')
76
77
78 def main():
79
80     global options, config
81
82     # Parse command line...
83     parser = OptionParser()
84     parser.add_option("-v", "--verbose", action="store_true", default=False,
85                       help="Spew out even more information than normal")
86     parser.add_option("-q", "--quiet", action="store_true", default=False,
87                       help="Restrict output to warnings and errors")
88     parser.add_option("-d", "--distinguished-name", default=None,
89                       help="X.509 'Distiguished Name' used when generating keys")
90     parser.add_option("--keystore", default=None,
91                       help="Path to the keystore for the repo signing key")
92     parser.add_option("--repo-keyalias", default=None,
93                       help="Alias of the repo signing key in the keystore")
94     (options, args) = parser.parse_args()
95
96     # find root install prefix
97     tmp = os.path.dirname(sys.argv[0])
98     if os.path.basename(tmp) == 'bin':
99         prefix = os.path.dirname(tmp)
100         examplesdir = prefix + '/share/doc/fdroidserver/examples'
101     else:
102         # we're running straight out of the git repo
103         prefix = tmp
104         examplesdir = prefix + '/examples'
105
106     fdroiddir = os.getcwd()
107
108     if not os.path.exists('config.py') and not os.path.exists('repo'):
109         # 'metadata' and 'tmp' are created in fdroid
110         os.mkdir('repo')
111         shutil.copy(os.path.join(examplesdir, 'fdroid-icon.png'), fdroiddir)
112         shutil.copyfile(os.path.join(examplesdir, 'config.py'), 'config.py')
113         os.chmod('config.py', 0o0600)
114     else:
115         logging.warn('Looks like this is already an F-Droid repo, cowardly refusing to overwrite it...')
116         logging.info('Try running `fdroid init` in an empty directory.')
117         sys.exit()
118
119     # now that we have a local config.py, read configuration...
120     config = common.read_config(options)
121
122     # track down where the Android SDK is
123     if os.path.isdir(config['sdk_path']):
124         logging.info('Using "' + config['sdk_path'] + '" for the Android SDK')
125         sdk_path = config['sdk_path']
126     elif 'ANDROID_HOME' in os.environ.keys():
127         sdk_path = os.environ['ANDROID_HOME']
128     else:
129         default_sdk_path = '/opt/android-sdk'
130         while True:
131             s = raw_input('Enter the path to the Android SDK (' + default_sdk_path + '): ')
132             if re.match('^\s*$', s) != None:
133                 sdk_path = default_sdk_path
134             else:
135                 sdk_path = s
136             if os.path.isdir(os.path.join(sdk_path, 'build-tools')):
137                 break
138             else:
139                 logging.info('"' + s + '" does not contain the Android SDK! Try again...')
140     if os.path.isdir(sdk_path):
141         write_to_config('sdk_path', sdk_path)
142
143     # try to find a working aapt, in all the recent possible paths
144     build_tools = os.path.join(sdk_path, 'build-tools')
145     aaptdirs = []
146     aaptdirs.append(os.path.join(build_tools, config['build_tools']))
147     aaptdirs.append(build_tools)
148     for f in sorted(os.listdir(build_tools), reverse=True):
149         if os.path.isdir(os.path.join(build_tools, f)):
150             aaptdirs.append(os.path.join(build_tools, f))
151     for d in aaptdirs:
152         if os.path.isfile(os.path.join(d, 'aapt')):
153             aapt = os.path.join(d, 'aapt')
154             break
155     if os.path.isfile(aapt):
156         dirname = os.path.basename(os.path.dirname(aapt))
157         if dirname == 'build-tools':
158             # this is the old layout, before versioned build-tools
159             write_to_config('build_tools', '')
160         else:
161             write_to_config('build_tools', dirname)
162
163     # track down where the Android NDK is
164     ndk_path = '/opt/android-ndk'
165     if os.path.isdir(config['ndk_path']):
166         ndk_path = config['ndk_path']
167     elif 'ANDROID_NDK' in os.environ.keys():
168         logging.info('using ANDROID_NDK')
169         ndk_path = os.environ['ANDROID_NDK']
170     if os.path.isdir(ndk_path):
171         write_to_config('ndk_path', ndk_path)
172     # the NDK is optional so we don't prompt the user for it if its not found
173
174     # find or generate the keystore for the repo signing key. First try the
175     # path written in the default config.py.  Then check if the user has
176     # specified a path from the command line, which will trump all others.
177     # Otherwise, create ~/.local/share/fdroidserver and stick it in there.
178     keystore = config['keystore']
179     if options.keystore:
180         if os.path.isfile(options.keystore):
181             keystore = options.keystore
182             write_to_config('keystore', keystore)
183         else:
184             logging.info('"' + options.keystore + '" does not exist or is not a file!')
185             sys.exit(1)
186     if options.repo_keyalias:
187         repo_keyalias = options.repo_keyalias
188         write_to_config('repo_keyalias', repo_keyalias)
189     if options.distinguished_name:
190         keydname = options.distinguished_name
191         write_to_config('keydname', keydname)
192     if not os.path.isfile(keystore):
193         # no existing or specified keystore, generate the whole thing
194         keystoredir = os.path.join(os.getenv('HOME'),
195                                    '.local', 'share', 'fdroidserver')
196         if not os.path.exists(keystoredir):
197             os.makedirs(keystoredir, mode=0o700)
198         keystore = os.path.join(keystoredir, 'keystore.jks')
199         write_to_config('keystore', keystore)
200         password = genpassword()
201         write_to_config('keystorepass', password)
202         write_to_config('keypass', password)
203         if not options.repo_keyalias:
204             repo_keyalias = socket.getfqdn()
205             write_to_config('repo_keyalias', repo_keyalias)
206         if not options.distinguished_name:
207             keydname = 'CN=' + repo_keyalias + ', OU=F-Droid'
208             write_to_config('keydname', keydname)
209         genkey(keystore, repo_keyalias, password, keydname)
210
211     logging.info('Built repo based in "' + fdroiddir + '"')
212     logging.info('with this config:')
213     logging.info('  Android SDK:\t\t\t' + sdk_path)
214     logging.info('  Android SDK Build Tools:\t' + os.path.dirname(aapt))
215     logging.info('  Android NDK (optional):\t' + ndk_path)
216     logging.info('  Keystore for signing key:\t' + keystore)
217     logging.info('\nTo complete the setup, add your APKs to "' +
218           os.path.join(fdroiddir, 'repo') + '"' +
219 '''
220 then run "fdroid update -c; fdroid update".  You might also want to edit
221 "config.py" to set the URL, repo name, and more.  You should also set up
222 a signing key.
223
224 For more info: https://f-droid.org/manual/fdroid.html#Simple-Binary-Repository
225 and https://f-droid.org/manual/fdroid.html#Signing
226 ''')