chiark / gitweb /
PEP8 fixes
[fdroidserver.git] / fdroidserver / nightly.py
1 #!/usr/bin/env python3
2 #
3 # nightly.py - part of the FDroid server tools
4 # Copyright (C) 2017 Hans-Christoph Steiner <hans@eds.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU Affero General Public License for more details.
15 #
16 # You should have received a copy of the GNU Affero General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 import base64
20 import datetime
21 import git
22 import hashlib
23 import logging
24 import os
25 import paramiko
26 import platform
27 import shutil
28 import subprocess
29 import sys
30 import tempfile
31 from urllib.parse import urlparse
32 from argparse import ArgumentParser
33
34 from . import _
35 from . import common
36
37
38 # hard coded defaults for Android ~/.android/debug.keystore files
39 # https://developers.google.com/android/guides/client-auth
40 KEYSTORE_FILE = os.path.join(os.getenv('HOME'), '.android', 'debug.keystore')
41 PASSWORD = 'android'
42 KEY_ALIAS = 'androiddebugkey'
43 DISTINGUISHED_NAME = 'CN=Android Debug,O=Android,C=US'
44
45 # standard suffix for naming fdroid git repos
46 NIGHTLY = '-nightly'
47
48
49 def _ssh_key_from_debug_keystore():
50     tmp_dir = tempfile.mkdtemp(prefix='.')
51     privkey = os.path.join(tmp_dir, '.privkey')
52     key_pem = os.path.join(tmp_dir, '.key.pem')
53     p12 = os.path.join(tmp_dir, '.keystore.p12')
54     subprocess.check_call([common.config['keytool'], '-importkeystore',
55                            '-srckeystore', KEYSTORE_FILE, '-srcalias', KEY_ALIAS,
56                            '-srcstorepass', PASSWORD, '-srckeypass', PASSWORD,
57                            '-destkeystore', p12, '-destalias', KEY_ALIAS,
58                            '-deststorepass', PASSWORD, '-destkeypass', PASSWORD,
59                            '-deststoretype', 'PKCS12'])
60     subprocess.check_call(['openssl', 'pkcs12', '-in', p12, '-out', key_pem,
61                            '-passin', 'pass:' + PASSWORD, '-passout', 'pass:' + PASSWORD])
62     subprocess.check_call(['openssl', 'rsa', '-in', key_pem, '-out', privkey,
63                            '-passin', 'pass:' + PASSWORD])
64     os.remove(key_pem)
65     os.remove(p12)
66     os.chmod(privkey, 0o600)  # os.umask() should cover this, but just in case
67
68     rsakey = paramiko.RSAKey.from_private_key_file(privkey)
69     fingerprint = base64.b64encode(hashlib.sha256(rsakey.asbytes()).digest()).decode('ascii').rstrip('=')
70     ssh_private_key_file = os.path.join(tmp_dir, 'debug_keystore_' + fingerprint + '_id_rsa')
71     os.rename(privkey, ssh_private_key_file)
72
73     pub = rsakey.get_name() + ' ' + rsakey.get_base64() + ' ' + ssh_private_key_file
74     with open(ssh_private_key_file + '.pub', 'w') as fp:
75         fp.write(pub)
76
77     logging.info(_('\nSSH Public Key to be used as Deploy Key:') + '\n' + pub)
78
79     return ssh_private_key_file
80
81
82 def main():
83
84     parser = ArgumentParser(usage="%(prog)s")
85     common.setup_global_opts(parser)
86     parser.add_argument("--show-secret-var", action="store_true", default=False,
87                         help=_("Print the secret variable to the terminal for easy copy/paste"))
88     parser.add_argument("--file", default='app/build/outputs/apk/*.apk',
89                         help=_('The the file to be included in the repo (path or glob)'))
90     parser.add_argument("--no-checksum", action="store_true", default=False,
91                         help=_("Don't use rsync checksums"))
92     # TODO add --with-btlog
93     options = parser.parse_args()
94     common.read_config(None)
95
96     # force a tighter umask since this writes private key material
97     umask = os.umask(0o077)
98
99     if 'CI' in os.environ:
100         v = os.getenv('DEBUG_KEYSTORE')
101         debug_keystore = None
102         if v:
103             debug_keystore = base64.b64decode(v)
104         if not debug_keystore:
105             logging.error(_('DEBUG_KEYSTORE is not set or the value is incomplete'))
106             sys.exit(1)
107         os.makedirs(os.path.dirname(KEYSTORE_FILE), exist_ok=True)
108         if os.path.exists(KEYSTORE_FILE):
109             logging.warning(_('overwriting existing {path}').format(path=KEYSTORE_FILE))
110         with open(KEYSTORE_FILE, 'wb') as fp:
111             fp.write(debug_keystore)
112
113         repo_basedir = os.path.join(os.getcwd(), 'fdroid')
114         repodir = os.path.join(repo_basedir, 'repo')
115         cibase = os.getcwd()
116         os.makedirs(repodir, exist_ok=True)
117
118         if 'CI_PROJECT_PATH' in os.environ and 'CI_PROJECT_URL' in os.environ:
119             # we are in GitLab CI
120             repo_git_base = os.getenv('CI_PROJECT_PATH') + NIGHTLY
121             clone_url = os.getenv('CI_PROJECT_URL') + NIGHTLY
122             repo_base = clone_url + '/raw/master/fdroid'
123             servergitmirror = 'git@' + urlparse(clone_url).netloc + ':' + repo_git_base
124             deploy_key_url = clone_url + '/settings/repository'
125             git_user_name = os.getenv('GITLAB_USER_NAME')
126             git_user_email = os.getenv('GITLAB_USER_EMAIL')
127         elif 'TRAVIS_REPO_SLUG' in os.environ:
128             # we are in Travis CI
129             repo_git_base = os.getenv('TRAVIS_REPO_SLUG') + NIGHTLY
130             clone_url = 'https://github.com/' + repo_git_base
131             _branch = os.getenv('TRAVIS_BRANCH')
132             repo_base = 'https://raw.githubusercontent.com/' + repo_git_base + '/' + _branch + '/fdroid'
133             servergitmirror = 'git@github.com:' + repo_git_base
134             deploy_key_url = ('https://github.com/' + repo_git_base + '/settings/keys'
135                               + '\nhttps://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys')
136             git_user_name = repo_git_base
137             git_user_email = os.getenv('USER') + '@' + platform.node()
138         elif 'CIRCLE_REPOSITORY_URL' in os.environ \
139              and 'CIRCLE_PROJECT_USERNAME' in os.environ \
140              and 'CIRCLE_PROJECT_REPONAME' in os.environ:
141             # we are in Circle CI
142             repo_git_base = (os.getenv('CIRCLE_PROJECT_USERNAME')
143                              + '/' + os.getenv('CIRCLE_PROJECT_REPONAME') + NIGHTLY)
144             clone_url = os.getenv('CIRCLE_REPOSITORY_URL') + NIGHTLY
145             repo_base = clone_url + '/raw/master/fdroid'
146             servergitmirror = 'git@' + urlparse(clone_url).netloc + ':' + repo_git_base
147             deploy_key_url = ('https://github.com/' + repo_git_base + '/settings/keys'
148                               + '\nhttps://developer.github.com/v3/guides/managing-deploy-keys/#deploy-keys')
149             git_user_name = os.getenv('CIRCLE_USERNAME')
150             git_user_email = git_user_name + '@' + platform.node()
151         else:
152             print(_('ERROR: unsupported CI type, patches welcome!'))
153             sys.exit(1)
154
155         repo_url = repo_base + '/repo'
156         git_mirror_path = os.path.join(repo_basedir, 'git-mirror')
157         git_mirror_repodir = os.path.join(git_mirror_path, 'fdroid', 'repo')
158         if not os.path.isdir(git_mirror_repodir):
159             logging.debug(_('cloning {url}').format(url=clone_url))
160             try:
161                 git.Repo.clone_from(clone_url, git_mirror_path)
162             except Exception:
163                 pass
164         if not os.path.isdir(git_mirror_repodir):
165             os.makedirs(git_mirror_repodir, mode=0o755)
166
167         mirror_git_repo = git.Repo.init(git_mirror_path)
168         writer = mirror_git_repo.config_writer()
169         writer.set_value('user', 'name', git_user_name)
170         writer.set_value('user', 'email', git_user_email)
171         writer.release()
172         for remote in mirror_git_repo.remotes:
173             mirror_git_repo.delete_remote(remote)
174
175         readme_path = os.path.join(git_mirror_path, 'README.md')
176         readme = '''
177 # {repo_git_base}
178
179 [![{repo_url}](icon.png)]({repo_url})
180
181 Last updated: {date}'''.format(repo_git_base=repo_git_base,
182                                repo_url=repo_url,
183                                date=datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S UTC'))
184         with open(readme_path, 'w') as fp:
185             fp.write(readme)
186         mirror_git_repo.git.add(all=True)
187         mirror_git_repo.index.commit("update README")
188
189         icon_path = os.path.join(repo_basedir, 'icon.png')
190         try:
191             import qrcode
192             img = qrcode.make('Some data here')
193             with open(icon_path, 'wb') as fp:
194                 fp.write(img)
195         except Exception:
196             exampleicon = os.path.join(common.get_examples_dir(), 'fdroid-icon.png')
197             shutil.copy(exampleicon, icon_path)
198         mirror_git_repo.git.add(all=True)
199         mirror_git_repo.index.commit("update repo/website icon")
200
201         os.chdir(repo_basedir)
202         common.local_rsync(options, git_mirror_repodir + '/', 'repo/')
203
204         ssh_private_key_file = _ssh_key_from_debug_keystore()
205         # this is needed for GitPython to find the SSH key
206         ssh_dir = os.path.join(os.getenv('HOME'), '.ssh')
207         os.makedirs(ssh_dir, exist_ok=True)
208         ssh_config = os.path.join(ssh_dir, 'config')
209         logging.debug(_('adding IdentityFile to {path}').format(path=ssh_config))
210         with open(ssh_config, 'a') as fp:
211             fp.write('\n\nHost *\n\tIdentityFile %s\n' % ssh_private_key_file)
212
213         config = ''
214         config += "identity_file = '%s'\n" % ssh_private_key_file
215         config += "repo_name = '%s'\n" % repo_git_base
216         config += "repo_url = '%s'\n" % repo_url
217         config += "repo_icon = 'icon.png'\n"
218         config += "archive_name = '%s'\n" % (repo_git_base + ' archive')
219         config += "archive_url = '%s'\n" % (repo_base + '/archive')
220         config += "archive_icon = 'icon.png'\n"
221         config += "servergitmirrors = '%s'\n" % servergitmirror
222         config += "keystore = '%s'\n" % KEYSTORE_FILE
223         config += "repo_keyalias = '%s'\n" % KEY_ALIAS
224         config += "keystorepass = '%s'\n" % PASSWORD
225         config += "keypass = '%s'\n" % PASSWORD
226         config += "keydname = '%s'\n" % DISTINGUISHED_NAME
227         config += "make_current_version_link = False\n"
228         config += "accepted_formats = ('txt', 'yml')\n"
229         # TODO add update_stats = True
230         with open('config.py', 'w') as fp:
231             fp.write(config)
232         os.chmod('config.py', 0o600)
233
234         for root, dirs, files in os.walk(cibase):
235             for d in ('fdroid', '.git', '.gradle'):
236                 if d in dirs:
237                     dirs.remove(d)
238             for f in files:
239                 if f.endswith('-debug.apk'):
240                     apkfilename = os.path.join(root, f)
241                     logging.debug(_('copying {apkfilename} into {path}')
242                                   .format(apkfilename=apkfilename, path=repodir))
243                     destapk = os.path.join(repodir, os.path.basename(f))
244                     shutil.copyfile(apkfilename, destapk)
245                     shutil.copystat(apkfilename, destapk)
246                     os.chmod(destapk, 0o644)
247
248         if options.verbose:
249             logging.debug(_('attempting bare ssh connection to test deploy key:'))
250             try:
251                 subprocess.check_call(['ssh', '-Tvi', ssh_private_key_file,
252                                        '-oIdentitiesOnly=yes', '-oStrictHostKeyChecking=no',
253                                        servergitmirror.split(':')[0]])
254             except subprocess.CalledProcessError:
255                 pass
256
257         subprocess.check_call(['fdroid', 'update', '--rename-apks', '--verbose'], cwd=repo_basedir)
258         try:
259             subprocess.check_call(['fdroid', 'server', 'update', '--verbose'], cwd=repo_basedir)
260         except subprocess.CalledProcessError:
261             logging.error(_('cannot publish update, did you set the deploy key?')
262                           + '\n' + deploy_key_url)
263             sys.exit(1)
264         if shutil.rmtree.avoids_symlink_attacks:
265             shutil.rmtree(os.path.dirname(ssh_private_key_file))
266
267     else:
268         ssh_dir = os.path.join(os.getenv('HOME'), '.ssh')
269         os.makedirs(os.path.dirname(ssh_dir), exist_ok=True)
270         privkey = _ssh_key_from_debug_keystore()
271         ssh_private_key_file = os.path.join(ssh_dir, os.path.basename(privkey))
272         os.rename(privkey, ssh_private_key_file)
273         os.rename(privkey + '.pub', ssh_private_key_file + '.pub')
274         if shutil.rmtree.avoids_symlink_attacks:
275             shutil.rmtree(os.path.dirname(privkey))
276
277         if options.show_secret_var:
278             with open(KEYSTORE_FILE, 'rb') as fp:
279                 debug_keystore = base64.standard_b64encode(fp.read()).decode('ascii')
280             print(_('\n{path} encoded for the DEBUG_KEYSTORE secret variable:')
281                   .format(path=KEYSTORE_FILE))
282             print(debug_keystore)
283
284     os.umask(umask)
285
286
287 if __name__ == "__main__":
288     main()