-#!/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
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
os.mkdir('builder')
p = subprocess.Popen(['vagrant', '--version'],
+ universal_newlines=True,
stdout=subprocess.PIPE)
- vver = p.communicate()[0]
+ vver = p.communicate()[0].strip().split(' ')[1]
+ if vver.split('.')[0] != '1' or int(vver.split('.')[1]) < 4:
+ raise BuildException("Unsupported vagrant version {0}".format(vver))
+
with open(os.path.join('builder', 'Vagrantfile'), 'w') as vf:
- if vver.startswith('Vagrant version 1.2'):
- vf.write('Vagrant.configure("2") do |config|\n')
- vf.write('config.vm.box = "buildserver"\n')
- vf.write('end\n')
- else:
- vf.write('Vagrant::Config.run do |config|\n')
- vf.write('config.vm.box = "buildserver"\n')
- vf.write('end\n')
+ vf.write('Vagrant.configure("2") do |config|\n')
+ vf.write('config.vm.box = "buildserver"\n')
+ vf.write('config.vm.synced_folder ".", "/vagrant", disabled: true\n')
+ vf.write('end\n')
logging.info("Starting new build server")
retcode, _ = vagrant(['up'], cwd='builder')
# Get an SFTP connection...
ftp = sshs.open_sftp()
- ftp.get_channel().settimeout(15)
+ ftp.get_channel().settimeout(60)
# Put all the necessary files in place...
ftp.chdir(homedir)
if options.verbose:
cmdline += ' --verbose'
cmdline += " %s:%s" % (app.id, build.vercode)
- chan.exec_command('bash -c ". ~/.bsenv && ' + cmdline + '"')
- output = ''
+ chan.exec_command('bash --login -c "' + cmdline + '"')
+ output = bytes()
while not chan.exit_status_ready():
while chan.recv_ready():
output += chan.recv(1024)
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...")
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'):
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)
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))
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,
# 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']]
p = FDroidPopen(cmd, cwd=maven_dir)
- elif method == 'gradle':
+ elif bmethod == 'gradle':
logging.info("Cleaning Gradle project...")
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]
-
- for task in gradletasks:
- parts = task.split(':')
- parts[-1] = 'clean' + capitalize_intact(parts[-1])
- cmd += [':'.join(parts)]
+ 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)
# .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:
p = None
# Build the release...
- if method == 'maven':
+ if bmethod == 'maven':
logging.info("Building Maven project...")
if '@' in build.maven:
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')
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:
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'))])
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'),
os.path.join(root_dir, 'build', 'apk'),
]:
- apks = glob.glob(os.path.join(apks_dir, '*-release-unsigned.apk'))
-
- if len(apks) > 1:
- raise BuildException('More than one resulting apks found in %s' % apks_dir,
- '\n'.join(apks))
- if len(apks) == 1:
- src = apks[0]
+ for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']:
+ apks = glob.glob(os.path.join(apks_dir, apkglob))
+
+ if len(apks) > 1:
+ raise BuildException('More than one resulting apks found in %s' % apks_dir,
+ '\n'.join(apks))
+ if len(apks) == 1:
+ src = apks[0]
+ break
+ if src is not None:
break
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):
global options, config
options, parser = parse_commandline()
- if not options.appid and not options.all:
- parser.error("option %s: If you really want to build all the apps, use --all" % "all")
+
+ # 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(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)
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]
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
# Build applications...
failed_apps = {}
build_succeeded = []
- for appid, app in apps.iteritems():
+ for appid, app in apps.items():
first = True