chiark / gitweb /
handle gradle-plugin 3.0 output apk location
[fdroidserver.git] / fdroidserver / build.py
1 #!/usr/bin/env python3
2 #
3 # build.py - part of the FDroid server tools
4 # Copyright (C) 2010-2014, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import sys
21 import os
22 import shutil
23 import glob
24 import subprocess
25 import re
26 import tarfile
27 import traceback
28 import time
29 import requests
30 import tempfile
31 from configparser import ConfigParser
32 from argparse import ArgumentParser
33 import logging
34
35 from . import common
36 from . import net
37 from . import metadata
38 from . import scanner
39 from . import vmtools
40 from .common import FDroidPopen, SdkToolsPopen
41 from .exception import FDroidException, BuildException, VCSException
42
43 try:
44     import paramiko
45 except ImportError:
46     pass
47
48
49 # Note that 'force' here also implies test mode.
50 def build_server(app, build, vcs, build_dir, output_dir, log_dir, force):
51     """Do a build on the builder vm.
52
53     :param app: app metadata dict
54     :param build:
55     :param vcs: version control system controller object
56     :param build_dir: local source-code checkout of app
57     :param output_dir: target folder for the build result
58     :param force:
59     """
60
61     global buildserverid
62
63     try:
64         paramiko
65     except NameError:
66         raise BuildException("Paramiko is required to use the buildserver")
67     if options.verbose:
68         logging.getLogger("paramiko").setLevel(logging.INFO)
69     else:
70         logging.getLogger("paramiko").setLevel(logging.WARN)
71
72     sshinfo = vmtools.get_clean_builder('builder')
73
74     try:
75         if not buildserverid:
76             buildserverid = subprocess.check_output(['vagrant', 'ssh', '-c',
77                                                      'cat /home/vagrant/buildserverid'],
78                                                     cwd='builder').rstrip()
79
80         # Open SSH connection...
81         logging.info("Connecting to virtual machine...")
82         sshs = paramiko.SSHClient()
83         sshs.set_missing_host_key_policy(paramiko.AutoAddPolicy())
84         sshs.connect(sshinfo['hostname'], username=sshinfo['user'],
85                      port=sshinfo['port'], timeout=300,
86                      look_for_keys=False, key_filename=sshinfo['idfile'])
87
88         homedir = '/home/' + sshinfo['user']
89
90         # Get an SFTP connection...
91         ftp = sshs.open_sftp()
92         ftp.get_channel().settimeout(60)
93
94         # Put all the necessary files in place...
95         ftp.chdir(homedir)
96
97         # Helper to copy the contents of a directory to the server...
98         def send_dir(path):
99             root = os.path.dirname(path)
100             main = os.path.basename(path)
101             ftp.mkdir(main)
102             for r, d, f in os.walk(path):
103                 rr = os.path.relpath(r, root)
104                 ftp.chdir(rr)
105                 for dd in d:
106                     ftp.mkdir(dd)
107                 for ff in f:
108                     lfile = os.path.join(root, rr, ff)
109                     if not os.path.islink(lfile):
110                         ftp.put(lfile, ff)
111                         ftp.chmod(ff, os.stat(lfile).st_mode)
112                 for i in range(len(rr.split('/'))):
113                     ftp.chdir('..')
114             ftp.chdir('..')
115
116         logging.info("Preparing server for build...")
117         serverpath = os.path.abspath(os.path.dirname(__file__))
118         ftp.mkdir('fdroidserver')
119         ftp.chdir('fdroidserver')
120         ftp.put(os.path.join(serverpath, '..', 'fdroid'), 'fdroid')
121         ftp.chmod('fdroid', 0o755)
122         send_dir(os.path.join(serverpath))
123         ftp.chdir(homedir)
124
125         ftp.put(os.path.join(serverpath, '..', 'buildserver',
126                              'config.buildserver.py'), 'config.py')
127         ftp.chmod('config.py', 0o600)
128
129         # Copy over the ID (head commit hash) of the fdroidserver in use...
130         subprocess.call('git rev-parse HEAD >' +
131                         os.path.join(os.getcwd(), 'tmp', 'fdroidserverid'),
132                         shell=True, cwd=serverpath)
133         ftp.put('tmp/fdroidserverid', 'fdroidserverid')
134
135         # Copy the metadata - just the file for this app...
136         ftp.mkdir('metadata')
137         ftp.mkdir('srclibs')
138         ftp.chdir('metadata')
139         ftp.put(app.metadatapath, os.path.basename(app.metadatapath))
140
141         # And patches if there are any...
142         if os.path.exists(os.path.join('metadata', app.id)):
143             send_dir(os.path.join('metadata', app.id))
144
145         ftp.chdir(homedir)
146         # Create the build directory...
147         ftp.mkdir('build')
148         ftp.chdir('build')
149         ftp.mkdir('extlib')
150         ftp.mkdir('srclib')
151         # Copy any extlibs that are required...
152         if build.extlibs:
153             ftp.chdir(homedir + '/build/extlib')
154             for lib in build.extlibs:
155                 lib = lib.strip()
156                 libsrc = os.path.join('build/extlib', lib)
157                 if not os.path.exists(libsrc):
158                     raise BuildException("Missing extlib {0}".format(libsrc))
159                 lp = lib.split('/')
160                 for d in lp[:-1]:
161                     if d not in ftp.listdir():
162                         ftp.mkdir(d)
163                     ftp.chdir(d)
164                 ftp.put(libsrc, lp[-1])
165                 for _ in lp[:-1]:
166                     ftp.chdir('..')
167         # Copy any srclibs that are required...
168         srclibpaths = []
169         if build.srclibs:
170             for lib in build.srclibs:
171                 srclibpaths.append(
172                     common.getsrclib(lib, 'build/srclib', basepath=True, prepare=False))
173
174         # If one was used for the main source, add that too.
175         basesrclib = vcs.getsrclib()
176         if basesrclib:
177             srclibpaths.append(basesrclib)
178         for name, number, lib in srclibpaths:
179             logging.info("Sending srclib '%s'" % lib)
180             ftp.chdir(homedir + '/build/srclib')
181             if not os.path.exists(lib):
182                 raise BuildException("Missing srclib directory '" + lib + "'")
183             fv = '.fdroidvcs-' + name
184             ftp.put(os.path.join('build/srclib', fv), fv)
185             send_dir(lib)
186             # Copy the metadata file too...
187             ftp.chdir(homedir + '/srclibs')
188             ftp.put(os.path.join('srclibs', name + '.txt'),
189                     name + '.txt')
190         # Copy the main app source code
191         # (no need if it's a srclib)
192         if (not basesrclib) and os.path.exists(build_dir):
193             ftp.chdir(homedir + '/build')
194             fv = '.fdroidvcs-' + app.id
195             ftp.put(os.path.join('build', fv), fv)
196             send_dir(build_dir)
197
198         # Execute the build script...
199         logging.info("Starting build...")
200         chan = sshs.get_transport().open_session()
201         chan.get_pty()
202         cmdline = os.path.join(homedir, 'fdroidserver', 'fdroid')
203         cmdline += ' build --on-server'
204         if force:
205             cmdline += ' --force --test'
206         if options.verbose:
207             cmdline += ' --verbose'
208         if options.skipscan:
209             cmdline += ' --skip-scan'
210         cmdline += " %s:%s" % (app.id, build.versionCode)
211         chan.exec_command('bash --login -c "' + cmdline + '"')
212
213         output = bytes()
214         output += get_android_tools_version_log(build.ndk_path()).encode()
215         while not chan.exit_status_ready():
216             while chan.recv_ready():
217                 output += chan.recv(1024)
218             time.sleep(0.1)
219         logging.info("...getting exit status")
220         returncode = chan.recv_exit_status()
221         while True:
222             get = chan.recv(1024)
223             if len(get) == 0:
224                 break
225             output += get
226         if returncode != 0:
227             raise BuildException(
228                 "Build.py failed on server for {0}:{1}".format(
229                     app.id, build.versionName), str(output, 'utf-8'))
230
231         # Retreive logs...
232         toolsversion_log = common.get_toolsversion_logname(app, build)
233         try:
234             ftp.chdir(os.path.join(homedir, log_dir))
235             ftp.get(toolsversion_log, os.path.join(log_dir, toolsversion_log))
236             logging.debug('retrieved %s', toolsversion_log)
237         except Exception as e:
238             logging.warn('could not get %s from builder vm: %s' % (toolsversion_log, e))
239
240         # Retrieve the built files...
241         logging.info("Retrieving build output...")
242         if force:
243             ftp.chdir(homedir + '/tmp')
244         else:
245             ftp.chdir(homedir + '/unsigned')
246         apkfile = common.get_release_filename(app, build)
247         tarball = common.getsrcname(app, build)
248         try:
249             ftp.get(apkfile, os.path.join(output_dir, apkfile))
250             if not options.notarball:
251                 ftp.get(tarball, os.path.join(output_dir, tarball))
252         except Exception:
253             raise BuildException(
254                 "Build failed for %s:%s - missing output files".format(
255                     app.id, build.versionName), output)
256         ftp.close()
257
258     finally:
259         # Suspend the build server.
260         vm = vmtools.get_build_vm('builder')
261         vm.suspend()
262
263
264 def force_gradle_build_tools(build_dir, build_tools):
265     for root, dirs, files in os.walk(build_dir):
266         for filename in files:
267             if not filename.endswith('.gradle'):
268                 continue
269             path = os.path.join(root, filename)
270             if not os.path.isfile(path):
271                 continue
272             logging.debug("Forcing build-tools %s in %s" % (build_tools, path))
273             common.regsub_file(r"""(\s*)buildToolsVersion([\s=]+).*""",
274                                r"""\1buildToolsVersion\2'%s'""" % build_tools,
275                                path)
276
277
278 def transform_first_char(string, method):
279     """Uses method() on the first character of string."""
280     if len(string) == 0:
281         return string
282     if len(string) == 1:
283         return method(string)
284     return method(string[0]) + string[1:]
285
286
287 def has_native_code(apkobj):
288     """aapt checks if there are architecture folders under the lib/ folder
289     so we are simulating the same behaviour"""
290     arch_re = re.compile("^lib/(.*)/.*$")
291     arch = [file for file in apkobj.get_files() if arch_re.match(file)]
292     return False if not arch else True
293
294
295 def get_apk_metadata_aapt(apkfile):
296     """aapt function to extract versionCode, versionName, packageName and nativecode"""
297     vercode = None
298     version = None
299     foundid = None
300     nativecode = None
301
302     p = SdkToolsPopen(['aapt', 'dump', 'badging', apkfile], output=False)
303
304     for line in p.output.splitlines():
305         if line.startswith("package:"):
306             pat = re.compile(".*name='([a-zA-Z0-9._]*)'.*")
307             m = pat.match(line)
308             if m:
309                 foundid = m.group(1)
310             pat = re.compile(".*versionCode='([0-9]*)'.*")
311             m = pat.match(line)
312             if m:
313                 vercode = m.group(1)
314             pat = re.compile(".*versionName='([^']*)'.*")
315             m = pat.match(line)
316             if m:
317                 version = m.group(1)
318         elif line.startswith("native-code:"):
319             nativecode = line[12:]
320
321     return vercode, version, foundid, nativecode
322
323
324 def get_apk_metadata_androguard(apkfile):
325     """androguard function to extract versionCode, versionName, packageName and nativecode"""
326     try:
327         from androguard.core.bytecodes.apk import APK
328         apkobject = APK(apkfile)
329     except ImportError:
330         raise BuildException("androguard library is not installed and aapt binary not found")
331     except FileNotFoundError:
332         raise BuildException("Could not open apk file for metadata analysis")
333
334     if not apkobject.is_valid_APK():
335         raise BuildException("Invalid APK provided")
336
337     foundid = apkobject.get_package()
338     vercode = apkobject.get_androidversion_code()
339     version = apkobject.get_androidversion_name()
340     nativecode = has_native_code(apkobject)
341
342     return vercode, version, foundid, nativecode
343
344
345 def get_metadata_from_apk(app, build, apkfile):
346     """get the required metadata from the built APK"""
347
348     if common.SdkToolsPopen(['aapt', 'version'], output=False):
349         vercode, version, foundid, nativecode = get_apk_metadata_aapt(apkfile)
350     else:
351         vercode, version, foundid, nativecode = get_apk_metadata_androguard(apkfile)
352
353     # Ignore empty strings or any kind of space/newline chars that we don't
354     # care about
355     if nativecode is not None:
356         nativecode = nativecode.strip()
357         nativecode = None if not nativecode else nativecode
358
359     if build.buildjni and build.buildjni != ['no']:
360         if nativecode is None:
361             raise BuildException("Native code should have been built but none was packaged")
362     if build.novcheck:
363         vercode = build.versionCode
364         version = build.versionName
365     if not version or not vercode:
366         raise BuildException("Could not find version information in build in output")
367     if not foundid:
368         raise BuildException("Could not find package ID in output")
369     if foundid != app.id:
370         raise BuildException("Wrong package ID - build " + foundid + " but expected " + app.id)
371
372     return vercode, version
373
374
375 def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh):
376     """Do a build locally."""
377     ndk_path = build.ndk_path()
378     if build.ndk or (build.buildjni and build.buildjni != ['no']):
379         if not ndk_path:
380             logging.critical("Android NDK version '%s' could not be found!" % build.ndk or 'r12b')
381             logging.critical("Configured versions:")
382             for k, v in config['ndk_paths'].items():
383                 if k.endswith("_orig"):
384                     continue
385                 logging.critical("  %s: %s" % (k, v))
386             raise FDroidException()
387         elif not os.path.isdir(ndk_path):
388             logging.critical("Android NDK '%s' is not a directory!" % ndk_path)
389             raise FDroidException()
390
391     common.set_FDroidPopen_env(build)
392
393     # create ..._toolsversion.log when running in builder vm
394     if onserver:
395         # before doing anything, run the sudo commands to setup the VM
396         if build.sudo:
397             logging.info("Running 'sudo' commands in %s" % os.getcwd())
398
399             p = FDroidPopen(['sudo', 'bash', '-x', '-c', build.sudo])
400             if p.returncode != 0:
401                 raise BuildException("Error running sudo command for %s:%s" %
402                                      (app.id, build.versionName), p.output)
403
404         log_path = os.path.join(log_dir,
405                                 common.get_toolsversion_logname(app, build))
406         with open(log_path, 'w') as f:
407             f.write(get_android_tools_version_log(build.ndk_path()))
408     else:
409         if build.sudo:
410             logging.warning('%s:%s runs this on the buildserver with sudo:\n\t%s'
411                             % (app.id, build.versionName, build.sudo))
412
413     # Prepare the source code...
414     root_dir, srclibpaths = common.prepare_source(vcs, app, build,
415                                                   build_dir, srclib_dir,
416                                                   extlib_dir, onserver, refresh)
417
418     # We need to clean via the build tool in case the binary dirs are
419     # different from the default ones
420     p = None
421     gradletasks = []
422     bmethod = build.build_method()
423     if bmethod == 'maven':
424         logging.info("Cleaning Maven project...")
425         cmd = [config['mvn3'], 'clean', '-Dandroid.sdk.path=' + config['sdk_path']]
426
427         if '@' in build.maven:
428             maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1])
429             maven_dir = os.path.normpath(maven_dir)
430         else:
431             maven_dir = root_dir
432
433         p = FDroidPopen(cmd, cwd=maven_dir)
434
435     elif bmethod == 'gradle':
436
437         logging.info("Cleaning Gradle project...")
438
439         if build.preassemble:
440             gradletasks += build.preassemble
441
442         flavours = build.gradle
443         if flavours == ['yes']:
444             flavours = []
445
446         flavours_cmd = ''.join([transform_first_char(flav, str.upper) for flav in flavours])
447
448         gradletasks += ['assemble' + flavours_cmd + 'Release']
449
450         if config['force_build_tools']:
451             force_gradle_build_tools(build_dir, config['build_tools'])
452             for name, number, libpath in srclibpaths:
453                 force_gradle_build_tools(libpath, config['build_tools'])
454
455         cmd = [config['gradle']]
456         if build.gradleprops:
457             cmd += ['-P' + kv for kv in build.gradleprops]
458
459         cmd += ['clean']
460
461         p = FDroidPopen(cmd, cwd=root_dir)
462
463     elif bmethod == 'kivy':
464         pass
465
466     elif bmethod == 'buildozer':
467         pass
468
469     elif bmethod == 'ant':
470         logging.info("Cleaning Ant project...")
471         p = FDroidPopen(['ant', 'clean'], cwd=root_dir)
472
473     if p is not None and p.returncode != 0:
474         raise BuildException("Error cleaning %s:%s" %
475                              (app.id, build.versionName), p.output)
476
477     for root, dirs, files in os.walk(build_dir):
478
479         def del_dirs(dl):
480             for d in dl:
481                 if d in dirs:
482                     shutil.rmtree(os.path.join(root, d))
483
484         def del_files(fl):
485             for f in fl:
486                 if f in files:
487                     os.remove(os.path.join(root, f))
488
489         if 'build.gradle' in files:
490             # Even when running clean, gradle stores task/artifact caches in
491             # .gradle/ as binary files. To avoid overcomplicating the scanner,
492             # manually delete them, just like `gradle clean` should have removed
493             # the build/ dirs.
494             del_dirs(['build', '.gradle'])
495             del_files(['gradlew', 'gradlew.bat'])
496
497         if 'pom.xml' in files:
498             del_dirs(['target'])
499
500         if any(f in files for f in ['ant.properties', 'project.properties', 'build.xml']):
501             del_dirs(['bin', 'gen'])
502
503         if 'jni' in dirs:
504             del_dirs(['obj'])
505
506     if options.skipscan:
507         if build.scandelete:
508             raise BuildException("Refusing to skip source scan since scandelete is present")
509     else:
510         # Scan before building...
511         logging.info("Scanning source for common problems...")
512         count = scanner.scan_source(build_dir, build)
513         if count > 0:
514             if force:
515                 logging.warn('Scanner found %d problems' % count)
516             else:
517                 raise BuildException("Can't build due to %d errors while scanning" % count)
518
519     if not options.notarball:
520         # Build the source tarball right before we build the release...
521         logging.info("Creating source tarball...")
522         tarname = common.getsrcname(app, build)
523         tarball = tarfile.open(os.path.join(tmp_dir, tarname), "w:gz")
524
525         def tarexc(f):
526             return any(f.endswith(s) for s in ['.svn', '.git', '.hg', '.bzr'])
527         tarball.add(build_dir, tarname, exclude=tarexc)
528         tarball.close()
529
530     # Run a build command if one is required...
531     if build.build:
532         logging.info("Running 'build' commands in %s" % root_dir)
533         cmd = common.replace_config_vars(build.build, build)
534
535         # Substitute source library paths into commands...
536         for name, number, libpath in srclibpaths:
537             libpath = os.path.relpath(libpath, root_dir)
538             cmd = cmd.replace('$$' + name + '$$', libpath)
539
540         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
541
542         if p.returncode != 0:
543             raise BuildException("Error running build command for %s:%s" %
544                                  (app.id, build.versionName), p.output)
545
546     # Build native stuff if required...
547     if build.buildjni and build.buildjni != ['no']:
548         logging.info("Building the native code")
549         jni_components = build.buildjni
550
551         if jni_components == ['yes']:
552             jni_components = ['']
553         cmd = [os.path.join(ndk_path, "ndk-build"), "-j1"]
554         for d in jni_components:
555             if d:
556                 logging.info("Building native code in '%s'" % d)
557             else:
558                 logging.info("Building native code in the main project")
559             manifest = os.path.join(root_dir, d, 'AndroidManifest.xml')
560             if os.path.exists(manifest):
561                 # Read and write the whole AM.xml to fix newlines and avoid
562                 # the ndk r8c or later 'wordlist' errors. The outcome of this
563                 # under gnu/linux is the same as when using tools like
564                 # dos2unix, but the native python way is faster and will
565                 # work in non-unix systems.
566                 manifest_text = open(manifest, 'U').read()
567                 open(manifest, 'w').write(manifest_text)
568                 # In case the AM.xml read was big, free the memory
569                 del manifest_text
570             p = FDroidPopen(cmd, cwd=os.path.join(root_dir, d))
571             if p.returncode != 0:
572                 raise BuildException("NDK build failed for %s:%s" % (app.id, build.versionName), p.output)
573
574     p = None
575     # Build the release...
576     if bmethod == 'maven':
577         logging.info("Building Maven project...")
578
579         if '@' in build.maven:
580             maven_dir = os.path.join(root_dir, build.maven.split('@', 1)[1])
581         else:
582             maven_dir = root_dir
583
584         mvncmd = [config['mvn3'], '-Dandroid.sdk.path=' + config['sdk_path'],
585                   '-Dmaven.jar.sign.skip=true', '-Dmaven.test.skip=true',
586                   '-Dandroid.sign.debug=false', '-Dandroid.release=true',
587                   'package']
588         if build.target:
589             target = build.target.split('-')[1]
590             common.regsub_file(r'<platform>[0-9]*</platform>',
591                                r'<platform>%s</platform>' % target,
592                                os.path.join(root_dir, 'pom.xml'))
593             if '@' in build.maven:
594                 common.regsub_file(r'<platform>[0-9]*</platform>',
595                                    r'<platform>%s</platform>' % target,
596                                    os.path.join(maven_dir, 'pom.xml'))
597
598         p = FDroidPopen(mvncmd, cwd=maven_dir)
599
600         bindir = os.path.join(root_dir, 'target')
601
602     elif bmethod == 'kivy':
603         logging.info("Building Kivy project...")
604
605         spec = os.path.join(root_dir, 'buildozer.spec')
606         if not os.path.exists(spec):
607             raise BuildException("Expected to find buildozer-compatible spec at {0}"
608                                  .format(spec))
609
610         defaults = {'orientation': 'landscape', 'icon': '',
611                     'permissions': '', 'android.api': "18"}
612         bconfig = ConfigParser(defaults, allow_no_value=True)
613         bconfig.read(spec)
614
615         distdir = os.path.join('python-for-android', 'dist', 'fdroid')
616         if os.path.exists(distdir):
617             shutil.rmtree(distdir)
618
619         modules = bconfig.get('app', 'requirements').split(',')
620
621         cmd = 'ANDROIDSDK=' + config['sdk_path']
622         cmd += ' ANDROIDNDK=' + ndk_path
623         cmd += ' ANDROIDNDKVER=' + build.ndk
624         cmd += ' ANDROIDAPI=' + str(bconfig.get('app', 'android.api'))
625         cmd += ' VIRTUALENV=virtualenv'
626         cmd += ' ./distribute.sh'
627         cmd += ' -m ' + "'" + ' '.join(modules) + "'"
628         cmd += ' -d fdroid'
629         p = subprocess.Popen(cmd, cwd='python-for-android', shell=True)
630         if p.returncode != 0:
631             raise BuildException("Distribute build failed")
632
633         cid = bconfig.get('app', 'package.domain') + '.' + bconfig.get('app', 'package.name')
634         if cid != app.id:
635             raise BuildException("Package ID mismatch between metadata and spec")
636
637         orientation = bconfig.get('app', 'orientation', 'landscape')
638         if orientation == 'all':
639             orientation = 'sensor'
640
641         cmd = ['./build.py'
642                '--dir', root_dir,
643                '--name', bconfig.get('app', 'title'),
644                '--package', app.id,
645                '--version', bconfig.get('app', 'version'),
646                '--orientation', orientation
647                ]
648
649         perms = bconfig.get('app', 'permissions')
650         for perm in perms.split(','):
651             cmd.extend(['--permission', perm])
652
653         if config.get('app', 'fullscreen') == 0:
654             cmd.append('--window')
655
656         icon = bconfig.get('app', 'icon.filename')
657         if icon:
658             cmd.extend(['--icon', os.path.join(root_dir, icon)])
659
660         cmd.append('release')
661         p = FDroidPopen(cmd, cwd=distdir)
662
663     elif bmethod == 'buildozer':
664         logging.info("Building Kivy project using buildozer...")
665
666         # parse buildozer.spez
667         spec = os.path.join(root_dir, 'buildozer.spec')
668         if not os.path.exists(spec):
669             raise BuildException("Expected to find buildozer-compatible spec at {0}"
670                                  .format(spec))
671         defaults = {'orientation': 'landscape', 'icon': '',
672                     'permissions': '', 'android.api': "19"}
673         bconfig = ConfigParser(defaults, allow_no_value=True)
674         bconfig.read(spec)
675
676         # update spec with sdk and ndk locations to prevent buildozer from
677         # downloading.
678         loc_ndk = common.env['ANDROID_NDK']
679         loc_sdk = common.env['ANDROID_SDK']
680         if loc_ndk == '$ANDROID_NDK':
681             loc_ndk = loc_sdk + '/ndk-bundle'
682
683         bc_ndk = None
684         bc_sdk = None
685         try:
686             bc_ndk = bconfig.get('app', 'android.sdk_path')
687         except Exception:
688             pass
689         try:
690             bc_sdk = bconfig.get('app', 'android.ndk_path')
691         except Exception:
692             pass
693
694         if bc_sdk is None:
695             bconfig.set('app', 'android.sdk_path', loc_sdk)
696         if bc_ndk is None:
697             bconfig.set('app', 'android.ndk_path', loc_ndk)
698
699         fspec = open(spec, 'w')
700         bconfig.write(fspec)
701         fspec.close()
702
703         logging.info("sdk_path = %s" % loc_sdk)
704         logging.info("ndk_path = %s" % loc_ndk)
705
706         p = None
707         # execute buildozer
708         cmd = ['buildozer', 'android', 'release']
709         try:
710             p = FDroidPopen(cmd, cwd=root_dir)
711         except Exception:
712             pass
713
714         # buidozer not installed ? clone repo and run
715         if (p is None or p.returncode != 0):
716             cmd = ['git', 'clone', 'https://github.com/kivy/buildozer.git']
717             p = subprocess.Popen(cmd, cwd=root_dir, shell=False)
718             p.wait()
719             if p.returncode != 0:
720                 raise BuildException("Distribute build failed")
721
722             cmd = ['python', 'buildozer/buildozer/scripts/client.py', 'android', 'release']
723             p = FDroidPopen(cmd, cwd=root_dir)
724
725         # expected to fail.
726         # Signing will fail if not set by environnment vars (cf. p4a docs).
727         # But the unsigned apk will be ok.
728         p.returncode = 0
729
730     elif bmethod == 'gradle':
731         logging.info("Building Gradle project...")
732
733         cmd = [config['gradle']]
734         if build.gradleprops:
735             cmd += ['-P' + kv for kv in build.gradleprops]
736
737         cmd += gradletasks
738
739         p = FDroidPopen(cmd, cwd=root_dir)
740
741     elif bmethod == 'ant':
742         logging.info("Building Ant project...")
743         cmd = ['ant']
744         if build.antcommands:
745             cmd += build.antcommands
746         else:
747             cmd += ['release']
748         p = FDroidPopen(cmd, cwd=root_dir)
749
750         bindir = os.path.join(root_dir, 'bin')
751
752     if p is not None and p.returncode != 0:
753         raise BuildException("Build failed for %s:%s" % (app.id, build.versionName), p.output)
754     logging.info("Successfully built version " + build.versionName + ' of ' + app.id)
755
756     omethod = build.output_method()
757     if omethod == 'maven':
758         stdout_apk = '\n'.join([
759             line for line in p.output.splitlines() if any(
760                 a in line for a in ('.apk', '.ap_', '.jar'))])
761         m = re.match(r".*^\[INFO\] .*apkbuilder.*/([^/]*)\.apk",
762                      stdout_apk, re.S | re.M)
763         if not m:
764             m = re.match(r".*^\[INFO\] Creating additional unsigned apk file .*/([^/]+)\.apk[^l]",
765                          stdout_apk, re.S | re.M)
766         if not m:
767             m = re.match(r'.*^\[INFO\] [^$]*aapt \[package,[^$]*' + bindir + r'/([^/]+)\.ap[_k][,\]]',
768                          stdout_apk, re.S | re.M)
769
770         if not m:
771             m = re.match(r".*^\[INFO\] Building jar: .*/" + bindir + r"/(.+)\.jar",
772                          stdout_apk, re.S | re.M)
773         if not m:
774             raise BuildException('Failed to find output')
775         src = m.group(1)
776         src = os.path.join(bindir, src) + '.apk'
777     elif omethod == 'kivy':
778         src = os.path.join('python-for-android', 'dist', 'default', 'bin',
779                            '{0}-{1}-release.apk'.format(
780                                bconfig.get('app', 'title'),
781                                bconfig.get('app', 'version')))
782
783     elif omethod == 'buildozer':
784         src = None
785         for apks_dir in [
786                 os.path.join(root_dir, '.buildozer', 'android', 'platform', 'build', 'dists', bconfig.get('app', 'title'), 'bin'),
787                 ]:
788             for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']:
789                 apks = glob.glob(os.path.join(apks_dir, apkglob))
790
791                 if len(apks) > 1:
792                     raise BuildException('More than one resulting apks found in %s' % apks_dir,
793                                          '\n'.join(apks))
794                 if len(apks) == 1:
795                     src = apks[0]
796                     break
797             if src is not None:
798                 break
799
800         if src is None:
801             raise BuildException('Failed to find any output apks')
802
803     elif omethod == 'gradle':
804         src = None
805         apk_dirs = [
806             os.path.join(root_dir, 'build', 'outputs', 'apk', 'release'),
807             os.path.join(root_dir, 'build', 'outputs', 'apk'),
808             os.path.join(root_dir, 'build', 'apk'),
809             ]
810         if flavours_cmd:
811             apk_dirs.append(os.path.join(root_dir, 'build', 'outputs', 'apk', transform_first_char(flavours_cmd, str.lower), 'release'))
812         for apks_dir in apk_dirs:
813             for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']:
814                 apks = glob.glob(os.path.join(apks_dir, apkglob))
815
816                 if len(apks) > 1:
817                     raise BuildException('More than one resulting apks found in %s' % apks_dir,
818                                          '\n'.join(apks))
819                 if len(apks) == 1:
820                     src = apks[0]
821                     break
822             if src is not None:
823                 break
824
825         if src is None:
826             raise BuildException('Failed to find any output apks')
827
828     elif omethod == 'ant':
829         stdout_apk = '\n'.join([
830             line for line in p.output.splitlines() if '.apk' in line])
831         src = re.match(r".*^.*Creating (.+) for release.*$.*", stdout_apk,
832                        re.S | re.M).group(1)
833         src = os.path.join(bindir, src)
834     elif omethod == 'raw':
835         output_path = common.replace_build_vars(build.output, build)
836         globpath = os.path.join(root_dir, output_path)
837         apks = glob.glob(globpath)
838         if len(apks) > 1:
839             raise BuildException('Multiple apks match %s' % globpath, '\n'.join(apks))
840         if len(apks) < 1:
841             raise BuildException('No apks match %s' % globpath)
842         src = os.path.normpath(apks[0])
843
844     # Make sure it's not debuggable...
845     if common.isApkAndDebuggable(src):
846         raise BuildException("APK is debuggable")
847
848     # By way of a sanity check, make sure the version and version
849     # code in our new apk match what we expect...
850     logging.debug("Checking " + src)
851     if not os.path.exists(src):
852         raise BuildException("Unsigned apk is not at expected location of " + src)
853
854     if common.get_file_extension(src) == 'apk':
855         vercode, version = get_metadata_from_apk(app, build, src)
856         if (version != build.versionName or vercode != build.versionCode):
857             raise BuildException(("Unexpected version/version code in output;"
858                                   " APK: '%s' / '%s', "
859                                   " Expected: '%s' / '%s'")
860                                  % (version, str(vercode), build.versionName,
861                                     str(build.versionCode)))
862     else:
863         vercode = build.versionCode
864         version = build.versionName
865
866     # Add information for 'fdroid verify' to be able to reproduce the build
867     # environment.
868     if onserver:
869         metadir = os.path.join(tmp_dir, 'META-INF')
870         if not os.path.exists(metadir):
871             os.mkdir(metadir)
872         homedir = os.path.expanduser('~')
873         for fn in ['buildserverid', 'fdroidserverid']:
874             shutil.copyfile(os.path.join(homedir, fn),
875                             os.path.join(metadir, fn))
876             subprocess.call(['jar', 'uf', os.path.abspath(src),
877                              'META-INF/' + fn], cwd=tmp_dir)
878
879     # Copy the unsigned apk to our destination directory for further
880     # processing (by publish.py)...
881     dest = os.path.join(output_dir, common.get_release_filename(app, build))
882     shutil.copyfile(src, dest)
883
884     # Move the source tarball into the output directory...
885     if output_dir != tmp_dir and not options.notarball:
886         shutil.move(os.path.join(tmp_dir, tarname),
887                     os.path.join(output_dir, tarname))
888
889
890 def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir,
891              srclib_dir, extlib_dir, tmp_dir, repo_dir, vcs, test,
892              server, force, onserver, refresh):
893     """
894     Build a particular version of an application, if it needs building.
895
896     :param output_dir: The directory where the build output will go. Usually
897        this is the 'unsigned' directory.
898     :param repo_dir: The repo directory - used for checking if the build is
899        necessary.
900     :param also_check_dir: An additional location for checking if the build
901        is necessary (usually the archive repo)
902     :param test: True if building in test mode, in which case the build will
903        always happen, even if the output already exists. In test mode, the
904        output directory should be a temporary location, not any of the real
905        ones.
906
907     :returns: True if the build was done, False if it wasn't necessary.
908     """
909
910     dest_file = common.get_release_filename(app, build)
911
912     dest = os.path.join(output_dir, dest_file)
913     dest_repo = os.path.join(repo_dir, dest_file)
914
915     if not test:
916         if os.path.exists(dest) or os.path.exists(dest_repo):
917             return False
918
919         if also_check_dir:
920             dest_also = os.path.join(also_check_dir, dest_file)
921             if os.path.exists(dest_also):
922                 return False
923
924     if build.disable and not options.force:
925         return False
926
927     logging.info("Building version %s (%s) of %s" % (
928         build.versionName, build.versionCode, app.id))
929
930     if server:
931         # When using server mode, still keep a local cache of the repo, by
932         # grabbing the source now.
933         vcs.gotorevision(build.commit)
934
935         build_server(app, build, vcs, build_dir, output_dir, log_dir, force)
936     else:
937         build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, extlib_dir, tmp_dir, force, onserver, refresh)
938     return True
939
940
941 def get_android_tools_versions(ndk_path=None):
942     '''get a list of the versions of all installed Android SDK/NDK components'''
943
944     global config
945     sdk_path = config['sdk_path']
946     if sdk_path[-1] != '/':
947         sdk_path += '/'
948     components = []
949     if ndk_path:
950         ndk_release_txt = os.path.join(ndk_path, 'RELEASE.TXT')
951         if os.path.isfile(ndk_release_txt):
952             with open(ndk_release_txt, 'r') as fp:
953                 components.append((os.path.basename(ndk_path), fp.read()[:-1]))
954
955     pattern = re.compile('^Pkg.Revision=(.+)', re.MULTILINE)
956     for root, dirs, files in os.walk(sdk_path):
957         if 'source.properties' in files:
958             source_properties = os.path.join(root, 'source.properties')
959             with open(source_properties, 'r') as fp:
960                 m = pattern.search(fp.read())
961                 if m:
962                     components.append((root[len(sdk_path):], m.group(1)))
963
964     return components
965
966
967 def get_android_tools_version_log(ndk_path):
968     '''get a list of the versions of all installed Android SDK/NDK components'''
969     log = '== Installed Android Tools ==\n\n'
970     components = get_android_tools_versions(ndk_path)
971     for name, version in sorted(components):
972         log += '* ' + name + ' (' + version + ')\n'
973
974     return log
975
976
977 def parse_commandline():
978     """Parse the command line. Returns options, parser."""
979
980     parser = ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
981     common.setup_global_opts(parser)
982     parser.add_argument("appid", nargs='*', help="app-id with optional versionCode in the form APPID[:VERCODE]")
983     parser.add_argument("-l", "--latest", action="store_true", default=False,
984                         help="Build only the latest version of each package")
985     parser.add_argument("-s", "--stop", action="store_true", default=False,
986                         help="Make the build stop on exceptions")
987     parser.add_argument("-t", "--test", action="store_true", default=False,
988                         help="Test mode - put output in the tmp directory only, and always build, even if the output already exists.")
989     parser.add_argument("--server", action="store_true", default=False,
990                         help="Use build server")
991     parser.add_argument("--resetserver", action="store_true", default=False,
992                         help="Reset and create a brand new build server, even if the existing one appears to be ok.")
993     parser.add_argument("--on-server", dest="onserver", action="store_true", default=False,
994                         help="Specify that we're running on the build server")
995     parser.add_argument("--skip-scan", dest="skipscan", action="store_true", default=False,
996                         help="Skip scanning the source code for binaries and other problems")
997     parser.add_argument("--dscanner", action="store_true", default=False,
998                         help="Setup an emulator, install the apk on it and perform a drozer scan")
999     parser.add_argument("--no-tarball", dest="notarball", action="store_true", default=False,
1000                         help="Don't create a source tarball, useful when testing a build")
1001     parser.add_argument("--no-refresh", dest="refresh", action="store_false", default=True,
1002                         help="Don't refresh the repository, useful when testing a build with no internet connection")
1003     parser.add_argument("-f", "--force", action="store_true", default=False,
1004                         help="Force build of disabled apps, and carries on regardless of scan problems. Only allowed in test mode.")
1005     parser.add_argument("-a", "--all", action="store_true", default=False,
1006                         help="Build all applications available")
1007     parser.add_argument("-w", "--wiki", default=False, action="store_true",
1008                         help="Update the wiki")
1009     metadata.add_metadata_arguments(parser)
1010     options = parser.parse_args()
1011     metadata.warnings_action = options.W
1012
1013     # Force --stop with --on-server to get correct exit code
1014     if options.onserver:
1015         options.stop = True
1016
1017     if options.force and not options.test:
1018         parser.error("option %s: Force is only allowed in test mode" % "force")
1019
1020     return options, parser
1021
1022
1023 options = None
1024 config = None
1025 buildserverid = None
1026
1027
1028 def main():
1029
1030     global options, config, buildserverid
1031
1032     options, parser = parse_commandline()
1033
1034     # The defaults for .fdroid.* metadata that is included in a git repo are
1035     # different than for the standard metadata/ layout because expectations
1036     # are different.  In this case, the most common user will be the app
1037     # developer working on the latest update of the app on their own machine.
1038     local_metadata_files = common.get_local_metadata_files()
1039     if len(local_metadata_files) == 1:  # there is local metadata in an app's source
1040         config = dict(common.default_config)
1041         # `fdroid build` should build only the latest version by default since
1042         # most of the time the user will be building the most recent update
1043         if not options.all:
1044             options.latest = True
1045     elif len(local_metadata_files) > 1:
1046         raise FDroidException("Only one local metadata file allowed! Found: "
1047                               + " ".join(local_metadata_files))
1048     else:
1049         if not os.path.isdir('metadata') and len(local_metadata_files) == 0:
1050             raise FDroidException("No app metadata found, nothing to process!")
1051         if not options.appid and not options.all:
1052             parser.error("option %s: If you really want to build all the apps, use --all" % "all")
1053
1054     config = common.read_config(options)
1055
1056     if config['build_server_always']:
1057         options.server = True
1058     if options.resetserver and not options.server:
1059         parser.error("option %s: Using --resetserver without --server makes no sense" % "resetserver")
1060
1061     log_dir = 'logs'
1062     if not os.path.isdir(log_dir):
1063         logging.info("Creating log directory")
1064         os.makedirs(log_dir)
1065
1066     tmp_dir = 'tmp'
1067     if not os.path.isdir(tmp_dir):
1068         logging.info("Creating temporary directory")
1069         os.makedirs(tmp_dir)
1070
1071     if options.test:
1072         output_dir = tmp_dir
1073     else:
1074         output_dir = 'unsigned'
1075         if not os.path.isdir(output_dir):
1076             logging.info("Creating output directory")
1077             os.makedirs(output_dir)
1078
1079     if config['archive_older'] != 0:
1080         also_check_dir = 'archive'
1081     else:
1082         also_check_dir = None
1083
1084     repo_dir = 'repo'
1085
1086     build_dir = 'build'
1087     if not os.path.isdir(build_dir):
1088         logging.info("Creating build directory")
1089         os.makedirs(build_dir)
1090     srclib_dir = os.path.join(build_dir, 'srclib')
1091     extlib_dir = os.path.join(build_dir, 'extlib')
1092
1093     # Read all app and srclib metadata
1094     pkgs = common.read_pkg_args(options.appid, True)
1095     allapps = metadata.read_metadata(not options.onserver, pkgs)
1096     apps = common.read_app_args(options.appid, allapps, True)
1097
1098     for appid, app in list(apps.items()):
1099         if (app.Disabled and not options.force) or not app.RepoType or not app.builds:
1100             del apps[appid]
1101
1102     if not apps:
1103         raise FDroidException("No apps to process.")
1104
1105     if options.latest:
1106         for app in apps.values():
1107             for build in reversed(app.builds):
1108                 if build.disable and not options.force:
1109                     continue
1110                 app.builds = [build]
1111                 break
1112
1113     if options.wiki:
1114         import mwclient
1115         site = mwclient.Site((config['wiki_protocol'], config['wiki_server']),
1116                              path=config['wiki_path'])
1117         site.login(config['wiki_user'], config['wiki_password'])
1118
1119     # Build applications...
1120     failed_apps = {}
1121     build_succeeded = []
1122     for appid, app in apps.items():
1123
1124         first = True
1125
1126         for build in app.builds:
1127             wikilog = None
1128             tools_version_log = ''
1129             if not options.onserver:
1130                 tools_version_log = get_android_tools_version_log(build.ndk_path())
1131             try:
1132
1133                 # For the first build of a particular app, we need to set up
1134                 # the source repo. We can reuse it on subsequent builds, if
1135                 # there are any.
1136                 if first:
1137                     vcs, build_dir = common.setup_vcs(app)
1138                     first = False
1139
1140                 logging.debug("Checking " + build.versionName)
1141                 if trybuild(app, build, build_dir, output_dir, log_dir,
1142                             also_check_dir, srclib_dir, extlib_dir,
1143                             tmp_dir, repo_dir, vcs, options.test,
1144                             options.server, options.force,
1145                             options.onserver, options.refresh):
1146                     toolslog = os.path.join(log_dir,
1147                                             common.get_toolsversion_logname(app, build))
1148                     if not options.onserver and os.path.exists(toolslog):
1149                         with open(toolslog, 'r') as f:
1150                             tools_version_log = ''.join(f.readlines())
1151                         os.remove(toolslog)
1152
1153                     if app.Binaries is not None:
1154                         # This is an app where we build from source, and
1155                         # verify the apk contents against a developer's
1156                         # binary. We get that binary now, and save it
1157                         # alongside our built one in the 'unsigend'
1158                         # directory.
1159                         url = app.Binaries
1160                         url = url.replace('%v', build.versionName)
1161                         url = url.replace('%c', str(build.versionCode))
1162                         logging.info("...retrieving " + url)
1163                         of = re.sub(r'.apk$', '.binary.apk', common.get_release_filename(app, build))
1164                         of = os.path.join(output_dir, of)
1165                         try:
1166                             net.download_file(url, local_filename=of)
1167                         except requests.exceptions.HTTPError as e:
1168                             raise FDroidException(
1169                                 'Downloading Binaries from %s failed. %s' % (url, e))
1170
1171                         # Now we check weather the build can be verified to
1172                         # match the supplied binary or not. Should the
1173                         # comparison fail, we mark this build as a failure
1174                         # and remove everything from the unsigend folder.
1175                         with tempfile.TemporaryDirectory() as tmpdir:
1176                             unsigned_apk = \
1177                                 common.get_release_filename(app, build)
1178                             unsigned_apk = \
1179                                 os.path.join(output_dir, unsigned_apk)
1180                             compare_result = \
1181                                 common.verify_apks(of, unsigned_apk, tmpdir)
1182                             if compare_result:
1183                                 logging.debug('removing %s', unsigned_apk)
1184                                 os.remove(unsigned_apk)
1185                                 logging.debug('removing %s', of)
1186                                 os.remove(of)
1187                                 compare_result = compare_result.split('\n')
1188                                 line_count = len(compare_result)
1189                                 compare_result = compare_result[:299]
1190                                 if line_count > len(compare_result):
1191                                     line_difference = \
1192                                         line_count - len(compare_result)
1193                                     compare_result.append('%d more lines ...' %
1194                                                           line_difference)
1195                                 compare_result = '\n'.join(compare_result)
1196                                 raise FDroidException('compared built binary '
1197                                                       'to supplied reference '
1198                                                       'binary but failed',
1199                                                       compare_result)
1200                             else:
1201                                 logging.info('compared built binary to '
1202                                              'supplied reference binary '
1203                                              'successfully')
1204
1205                     build_succeeded.append(app)
1206                     wikilog = "Build succeeded"
1207
1208             except VCSException as vcse:
1209                 reason = str(vcse).split('\n', 1)[0] if options.verbose else str(vcse)
1210                 logging.error("VCS error while building app %s: %s" % (
1211                     appid, reason))
1212                 if options.stop:
1213                     sys.exit(1)
1214                 failed_apps[appid] = vcse
1215                 wikilog = str(vcse)
1216             except FDroidException as e:
1217                 with open(os.path.join(log_dir, appid + '.log'), 'a+') as f:
1218                     f.write('\n\n============================================================\n')
1219                     f.write('versionCode: %s\nversionName: %s\ncommit: %s\n' %
1220                             (build.versionCode, build.versionName, build.commit))
1221                     f.write('Build completed at '
1222                             + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n')
1223                     f.write('\n' + tools_version_log + '\n')
1224                     f.write(str(e))
1225                 logging.error("Could not build app %s: %s" % (appid, e))
1226                 if options.stop:
1227                     sys.exit(1)
1228                 failed_apps[appid] = e
1229                 wikilog = e.get_wikitext()
1230             except Exception as e:
1231                 logging.error("Could not build app %s due to unknown error: %s" % (
1232                     appid, traceback.format_exc()))
1233                 if options.stop:
1234                     sys.exit(1)
1235                 failed_apps[appid] = e
1236                 wikilog = str(e)
1237
1238             if options.wiki and wikilog:
1239                 try:
1240                     # Write a page with the last build log for this version code
1241                     lastbuildpage = appid + '/lastbuild_' + build.versionCode
1242                     newpage = site.Pages[lastbuildpage]
1243                     with open(os.path.join('tmp', 'fdroidserverid')) as fp:
1244                         fdroidserverid = fp.read().rstrip()
1245                     txt = "* build completed at " + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n' \
1246                           + '* fdroidserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \
1247                           + fdroidserverid + ' ' + fdroidserverid + ']\n\n'
1248                     if options.onserver:
1249                         txt += '* buildserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \
1250                                + buildserverid + ' ' + buildserverid + ']\n\n'
1251                     txt += tools_version_log + '\n\n'
1252                     txt += '== Build Log ==\n\n' + wikilog
1253                     newpage.save(txt, summary='Build log')
1254                     # Redirect from /lastbuild to the most recent build log
1255                     newpage = site.Pages[appid + '/lastbuild']
1256                     newpage.save('#REDIRECT [[' + lastbuildpage + ']]', summary='Update redirect')
1257                 except Exception as e:
1258                     logging.error("Error while attempting to publish build log: %s" % e)
1259
1260     for app in build_succeeded:
1261         logging.info("success: %s" % (app.id))
1262
1263     if not options.verbose:
1264         for fa in failed_apps:
1265             logging.info("Build for app %s failed:\n%s" % (fa, failed_apps[fa]))
1266
1267     # perform a drozer scan of all successful builds
1268     if options.dscanner and build_succeeded:
1269         from .dscanner import DockerDriver
1270
1271         docker = DockerDriver()
1272
1273         try:
1274             for app in build_succeeded:
1275
1276                 logging.info("Need to sign the app before we can install it.")
1277                 subprocess.call("fdroid publish {0}".format(app.id), shell=True)
1278
1279                 apk_path = None
1280
1281                 for f in os.listdir(repo_dir):
1282                     if f.endswith('.apk') and f.startswith(app.id):
1283                         apk_path = os.path.join(repo_dir, f)
1284                         break
1285
1286                 if not apk_path:
1287                     raise Exception("No signed APK found at path: {0}".format(apk_path))
1288
1289                 if not os.path.isdir(repo_dir):
1290                     exit(1)
1291
1292                 logging.info("Performing Drozer scan on {0}.".format(app))
1293                 docker.perform_drozer_scan(apk_path, app.id, repo_dir)
1294         except Exception as e:
1295             logging.error(str(e))
1296             logging.error("An exception happened. Making sure to clean up")
1297         else:
1298             logging.info("Scan succeeded.")
1299
1300         logging.info("Cleaning up after ourselves.")
1301         docker.clean()
1302
1303     logging.info("Finished.")
1304     if len(build_succeeded) > 0:
1305         logging.info(str(len(build_succeeded)) + ' builds succeeded')
1306     if len(failed_apps) > 0:
1307         logging.info(str(len(failed_apps)) + ' builds failed')
1308
1309     sys.exit(0)
1310
1311
1312 if __name__ == "__main__":
1313     main()