chiark / gitweb /
add force_build_tools config option
[fdroidserver.git] / fdroidserver / build.py
index 12cc61c144d57894fda53d8be57ad9e2f62d5ea1..bad025a6d9a37258aedad1bada262e1a82301f6e 100644 (file)
@@ -1,5 +1,4 @@
-#!/usr/bin/env python2
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
 #
 # build.py - part of the FDroid server tools
 # Copyright (C) 2010-2014, Ciaran Gultnieks, ciaran@ciarang.com
@@ -28,15 +27,15 @@ import tarfile
 import traceback
 import time
 import json
-from ConfigParser import ConfigParser
+from configparser import ConfigParser
 from argparse import ArgumentParser
 import logging
 
-import common
-import net
-import metadata
-import scanner
-from common import FDroidException, BuildException, VCSException, FDroidPopen, SdkToolsPopen
+from . import common
+from . import net
+from . import metadata
+from . import scanner
+from .common import FDroidException, BuildException, VCSException, FDroidPopen, SdkToolsPopen
 
 try:
     import paramiko
@@ -177,6 +176,7 @@ def get_clean_vm(reset=False):
         os.mkdir('builder')
 
         p = subprocess.Popen(['vagrant', '--version'],
+                             universal_newlines=True,
                              stdout=subprocess.PIPE)
         vver = p.communicate()[0].strip().split(' ')[1]
         if vver.split('.')[0] != '1' or int(vver.split('.')[1]) < 4:
@@ -388,8 +388,9 @@ def build_server(app, build, vcs, build_dir, output_dir, force):
         if options.verbose:
             cmdline += ' --verbose'
         cmdline += " %s:%s" % (app.id, build.vercode)
-        chan.exec_command('bash -c ". ~/.bsenv && ' + cmdline + '"')
-        output = ''
+        cmdline = '. /etc/profile && ' + cmdline
+        chan.exec_command('bash -c "' + cmdline + '"')
+        output = bytes()
         while not chan.exit_status_ready():
             while chan.recv_ready():
                 output += chan.recv(1024)
@@ -404,7 +405,7 @@ def build_server(app, build, vcs, build_dir, output_dir, force):
         if returncode != 0:
             raise BuildException(
                 "Build.py failed on server for {0}:{1}".format(
-                    app.id, build.version), output)
+                    app.id, build.version), str(output, 'utf-8'))
 
         # Retrieve the built files...
         logging.info("Retrieving build output...")
@@ -430,8 +431,7 @@ def build_server(app, build, vcs, build_dir, output_dir, force):
         release_vm()
 
 
-def adapt_gradle(build_dir):
-    filename = 'build.gradle'
+def force_gradle_build_tools(build_dir, build_tools):
     for root, dirs, files in os.walk(build_dir):
         for filename in files:
             if not filename.endswith('.gradle'):
@@ -439,9 +439,9 @@ def adapt_gradle(build_dir):
             path = os.path.join(root, filename)
             if not os.path.isfile(path):
                 continue
-            logging.debug("Adapting %s at %s" % (filename, path))
+            logging.debug("Forcing build-tools %s in %s" % (build_tools, path))
             common.regsub_file(r"""(\s*)buildToolsVersion([\s=]+).*""",
-                               r"""\1buildToolsVersion\2'%s'""" % config['build_tools'],
+                               r"""\1buildToolsVersion\2'%s'""" % build_tools,
                                path)
 
 
@@ -463,7 +463,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
         if not ndk_path:
             logging.critical("Android NDK version '%s' could not be found!" % build.ndk or 'r10e')
             logging.critical("Configured versions:")
-            for k, v in config['ndk_paths'].iteritems():
+            for k, v in config['ndk_paths'].items():
                 if k.endswith("_orig"):
                     continue
                 logging.critical("  %s: %s" % (k, v))
@@ -472,13 +472,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
             logging.critical("Android NDK '%s' is not a directory!" % ndk_path)
             sys.exit(3)
 
-    # Set up environment vars that depend on each build
-    for n in ['ANDROID_NDK', 'NDK', 'ANDROID_NDK_HOME']:
-        common.env[n] = ndk_path
-
-    common.reset_env_path()
-    # Set up the current NDK to the PATH
-    common.add_to_env_path(ndk_path)
+    common.set_FDroidPopen_env(build)
 
     # Prepare the source code...
     root_dir, srclibpaths = common.prepare_source(vcs, app, build,
@@ -489,8 +483,8 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
     # different from the default ones
     p = None
     gradletasks = []
-    method = build.method()
-    if method == 'maven':
+    bmethod = build.build_method()
+    if bmethod == 'maven':
         logging.info("Cleaning Maven project...")
         cmd = [config['mvn3'], 'clean', '-Dandroid.sdk.path=' + config['sdk_path']]
 
@@ -502,7 +496,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
 
         p = FDroidPopen(cmd, cwd=maven_dir)
 
-    elif method == 'gradle':
+    elif bmethod == 'gradle':
 
         logging.info("Cleaning Gradle project...")
 
@@ -517,22 +511,23 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
 
         gradletasks += ['assemble' + flavours_cmd + 'Release']
 
-        adapt_gradle(build_dir)
-        for name, number, libpath in srclibpaths:
-            adapt_gradle(libpath)
+        if config['force_build_tools']:
+            force_gradle_build_tools(build_dir, config['build_tools'])
+            for name, number, libpath in srclibpaths:
+                force_gradle_build_tools(libpath, config['build_tools'])
 
         cmd = [config['gradle']]
         if build.gradleprops:
-            cmd += ['-P'+kv for kv in build.gradleprops]
+            cmd += ['-P' + kv for kv in build.gradleprops]
 
         cmd += ['clean']
 
         p = FDroidPopen(cmd, cwd=root_dir)
 
-    elif method == 'kivy':
+    elif bmethod == 'kivy':
         pass
 
-    elif method == 'ant':
+    elif bmethod == 'ant':
         logging.info("Cleaning Ant project...")
         p = FDroidPopen(['ant', 'clean'], cwd=root_dir)
 
@@ -557,7 +552,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
             # .gradle/ as binary files. To avoid overcomplicating the scanner,
             # manually delete them, just like `gradle clean` should have removed
             # the build/ dirs.
-            del_dirs(['build', '.gradle', 'gradle'])
+            del_dirs(['build', '.gradle'])
             del_files(['gradlew', 'gradlew.bat'])
 
         if 'pom.xml' in files:
@@ -639,7 +634,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
 
     p = None
     # Build the release...
-    if method == 'maven':
+    if bmethod == 'maven':
         logging.info("Building Maven project...")
 
         if '@' in build.maven:
@@ -665,7 +660,7 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
 
         bindir = os.path.join(root_dir, 'target')
 
-    elif method == 'kivy':
+    elif bmethod == 'kivy':
         logging.info("Building Kivy project...")
 
         spec = os.path.join(root_dir, 'buildozer.spec')
@@ -726,18 +721,18 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
         cmd.append('release')
         p = FDroidPopen(cmd, cwd=distdir)
 
-    elif method == 'gradle':
+    elif bmethod == 'gradle':
         logging.info("Building Gradle project...")
 
         cmd = [config['gradle']]
         if build.gradleprops:
-            cmd += ['-P'+kv for kv in build.gradleprops]
+            cmd += ['-P' + kv for kv in build.gradleprops]
 
         cmd += gradletasks
 
         p = FDroidPopen(cmd, cwd=root_dir)
 
-    elif method == 'ant':
+    elif bmethod == 'ant':
         logging.info("Building Ant project...")
         cmd = ['ant']
         if build.antcommands:
@@ -752,7 +747,8 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
         raise BuildException("Build failed for %s:%s" % (app.id, build.version), p.output)
     logging.info("Successfully built version " + build.version + ' of ' + app.id)
 
-    if method == 'maven':
+    omethod = build.output_method()
+    if omethod == 'maven':
         stdout_apk = '\n'.join([
             line for line in p.output.splitlines() if any(
                 a in line for a in ('.apk', '.ap_', '.jar'))])
@@ -772,12 +768,12 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
             raise BuildException('Failed to find output')
         src = m.group(1)
         src = os.path.join(bindir, src) + '.apk'
-    elif method == 'kivy':
+    elif omethod == 'kivy':
         src = os.path.join('python-for-android', 'dist', 'default', 'bin',
                            '{0}-{1}-release.apk'.format(
                                bconfig.get('app', 'title'),
                                bconfig.get('app', 'version')))
-    elif method == 'gradle':
+    elif omethod == 'gradle':
         src = None
         for apks_dir in [
                 os.path.join(root_dir, 'build', 'outputs', 'apk'),
@@ -798,15 +794,20 @@ def build_local(app, build, vcs, build_dir, output_dir, srclib_dir, extlib_dir,
         if src is None:
             raise BuildException('Failed to find any output apks')
 
-    elif method == 'ant':
+    elif omethod == 'ant':
         stdout_apk = '\n'.join([
             line for line in p.output.splitlines() if '.apk' in line])
         src = re.match(r".*^.*Creating (.+) for release.*$.*", stdout_apk,
                        re.S | re.M).group(1)
         src = os.path.join(bindir, src)
-    elif method == 'raw':
-        src = os.path.join(root_dir, build.output)
-        src = os.path.normpath(src)
+    elif omethod == 'raw':
+        globpath = os.path.join(root_dir, build.output)
+        apks = glob.glob(globpath)
+        if len(apks) > 1:
+            raise BuildException('Multiple apks match %s' % globpath, '\n'.join(apks))
+        if len(apks) < 1:
+            raise BuildException('No apks match %s' % globpath)
+        src = os.path.normpath(apks[0])
 
     # Make sure it's not debuggable...
     if common.isApkDebuggable(src, config):
@@ -1002,16 +1003,25 @@ def main():
 
     options, parser = parse_commandline()
 
-    metadata_files = glob.glob('.fdroid.*[a-z]')  # ignore files ending in ~
-    if len(metadata_files) > 1:
+    # The defaults for .fdroid.* metadata that is included in a git repo are
+    # different than for the standard metadata/ layout because expectations
+    # are different.  In this case, the most common user will be the app
+    # developer working on the latest update of the app on their own machine.
+    local_metadata_files = common.get_local_metadata_files()
+    if len(local_metadata_files) == 1:  # there is local metadata in an app's source
+        config = dict(common.default_config)
+        # `fdroid build` should build only the latest version by default since
+        # most of the time the user will be building the most recent update
+        if not options.all:
+            options.latest = True
+    elif len(local_metadata_files) > 1:
         raise FDroidException("Only one local metadata file allowed! Found: "
-                              + " ".join(metadata_files))
-
-    if not os.path.isdir('metadata') and len(metadata_files) == 0:
-        raise FDroidException("No app metadata found, nothing to process!")
-
-    if not options.appid and not options.all:
-        parser.error("option %s: If you really want to build all the apps, use --all" % "all")
+                              + " ".join(local_metadata_files))
+    else:
+        if not os.path.isdir('metadata') and len(local_metadata_files) == 0:
+            raise FDroidException("No app metadata found, nothing to process!")
+        if not options.appid and not options.all:
+            parser.error("option %s: If you really want to build all the apps, use --all" % "all")
 
     config = common.read_config(options)
 
@@ -1056,7 +1066,7 @@ def main():
     allapps = metadata.read_metadata(xref=not options.onserver)
 
     apps = common.read_app_args(options.appid, allapps, True)
-    for appid, app in apps.items():
+    for appid, app in list(apps.items()):
         if (app.Disabled and not options.force) or not app.RepoType or not app.builds:
             del apps[appid]
 
@@ -1064,7 +1074,7 @@ def main():
         raise FDroidException("No apps to process.")
 
     if options.latest:
-        for app in apps.itervalues():
+        for app in apps.values():
             for build in reversed(app.builds):
                 if build.disable and not options.force:
                     continue
@@ -1080,7 +1090,7 @@ def main():
     # Build applications...
     failed_apps = {}
     build_succeeded = []
-    for appid, app in apps.iteritems():
+    for appid, app in apps.items():
 
         first = True