import glob
import subprocess
import re
+import resource
import tarfile
import traceback
import time
from configparser import ConfigParser
from argparse import ArgumentParser
import logging
+from gettext import ngettext
+from . import _
from . import common
from . import net
from . import metadata
# Helper to copy the contents of a directory to the server...
def send_dir(path):
- root = os.path.dirname(path)
- main = os.path.basename(path)
- ftp.mkdir(main)
- for r, d, f in os.walk(path):
- rr = os.path.relpath(r, root)
- ftp.chdir(rr)
- for dd in d:
- ftp.mkdir(dd)
- for ff in f:
- lfile = os.path.join(root, rr, ff)
- if not os.path.islink(lfile):
- ftp.put(lfile, ff)
- ftp.chmod(ff, os.stat(lfile).st_mode)
- for i in range(len(rr.split('/'))):
- ftp.chdir('..')
- ftp.chdir('..')
+ logging.debug("rsyncing " + path + " to " + ftp.getcwd())
+ # TODO this should move to `vagrant rsync` from >= v1.5
+ try:
+ subprocess.check_output(['rsync', '--recursive', '--perms', '--links', '--quiet', '--rsh=' +
+ 'ssh -o StrictHostKeyChecking=no' +
+ ' -o UserKnownHostsFile=/dev/null' +
+ ' -o LogLevel=FATAL' +
+ ' -o IdentitiesOnly=yes' +
+ ' -o PasswordAuthentication=no' +
+ ' -p ' + str(sshinfo['port']) +
+ ' -i ' + sshinfo['idfile'],
+ path,
+ sshinfo['user'] + "@" + sshinfo['hostname'] + ":" + ftp.getcwd()],
+ stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ raise FDroidException(str(e), e.output.decode())
logging.info("Preparing server for build...")
serverpath = os.path.abspath(os.path.dirname(__file__))
ftp.mkdir(d)
ftp.chdir(d)
ftp.put(libsrc, lp[-1])
- for _ in lp[:-1]:
+ for _ignored in lp[:-1]:
ftp.chdir('..')
# Copy any srclibs that are required...
srclibpaths = []
cmdline += " %s:%s" % (app.id, build.versionCode)
chan.exec_command('bash --login -c "' + cmdline + '"')
- output = bytes()
- output += get_android_tools_version_log(build.ndk_path()).encode()
- while not chan.exit_status_ready():
- while chan.recv_ready():
- output += chan.recv(1024)
- time.sleep(0.1)
+ # Fetch build process output ...
+ try:
+ cmd_stdout = chan.makefile('rb', 1024)
+ output = bytes()
+ output += get_android_tools_version_log(build.ndk_path()).encode()
+ while not chan.exit_status_ready():
+ line = cmd_stdout.readline()
+ if line:
+ if options.verbose:
+ logging.debug("buildserver > " + str(line, 'utf-8').rstrip())
+ output += line
+ else:
+ time.sleep(0.05)
+ for line in cmd_stdout.readlines():
+ if options.verbose:
+ logging.debug("buildserver > " + str(line, 'utf-8').rstrip())
+ output += line
+ finally:
+ cmd_stdout.close()
+
+ # Check build process exit status ...
logging.info("...getting exit status")
returncode = chan.recv_exit_status()
- while True:
- get = chan.recv(1024)
- if len(get) == 0:
- break
- output += get
if returncode != 0:
raise BuildException(
"Build.py failed on server for {0}:{1}".format(
- app.id, build.versionName), str(output, 'utf-8'))
+ app.id, build.versionName), None if options.verbose else str(output, 'utf-8'))
# Retreive logs...
toolsversion_log = common.get_toolsversion_logname(app, build)
ftp.get(tarball, os.path.join(output_dir, tarball))
except Exception:
raise BuildException(
- "Build failed for %s:%s - missing output files".format(
- app.id, build.versionName), output)
+ "Build failed for {0}:{1} - missing output files".format(
+ app.id, build.versionName), None if options.verbose else str(output, 'utf-8'))
ftp.close()
finally:
path)
-def capitalize_intact(string):
- """Like str.capitalize(), but leave the rest of the string intact without
- switching it to lowercase."""
+def _get_build_timestamp():
+ return time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime())
+
+
+def transform_first_char(string, method):
+ """Uses method() on the first character of string."""
if len(string) == 0:
return string
if len(string) == 1:
- return string.upper()
- return string[0].upper() + string[1:]
+ return method(string)
+ return method(string[0]) + string[1:]
def has_native_code(apkobj):
raise BuildException("Error running sudo command for %s:%s" %
(app.id, build.versionName), p.output)
+ p = FDroidPopen(['sudo', 'passwd', '--lock', 'root'])
+ if p.returncode != 0:
+ raise BuildException("Error locking root account for %s:%s" %
+ (app.id, build.versionName), p.output)
+
+ p = FDroidPopen(['sudo', 'SUDO_FORCE_REMOVE=yes', 'dpkg', '--purge', 'sudo'])
+ if p.returncode != 0:
+ raise BuildException("Error removing sudo for %s:%s" %
+ (app.id, build.versionName), p.output)
+
log_path = os.path.join(log_dir,
common.get_toolsversion_logname(app, build))
with open(log_path, 'w') as f:
if flavours == ['yes']:
flavours = []
- flavours_cmd = ''.join([capitalize_intact(flav) for flav in flavours])
+ flavours_cmd = ''.join([transform_first_char(flav, str.upper) for flav in flavours])
gradletasks += ['assemble' + flavours_cmd + 'Release']
if f in files:
os.remove(os.path.join(root, f))
- if 'build.gradle' in files:
+ if any(f in files for f in ['build.gradle', 'settings.gradle']):
# Even when running clean, gradle stores task/artifact caches in
# .gradle/ as binary files. To avoid overcomplicating the scanner,
# manually delete them, just like `gradle clean` should have removed
count = scanner.scan_source(build_dir, build)
if count > 0:
if force:
- logging.warn('Scanner found %d problems' % count)
+ logging.warning(ngettext('Scanner found {} problem',
+ 'Scanner found {} problems', count).format(count))
else:
- raise BuildException("Can't build due to %d errors while scanning" % count)
+ raise BuildException(ngettext(
+ "Can't build due to {} error while scanning",
+ "Can't build due to {} errors while scanning", count).format(count))
if not options.notarball:
# Build the source tarball right before we build the release...
elif omethod == 'gradle':
src = None
- for apks_dir in [
- os.path.join(root_dir, 'build', 'outputs', 'apk', 'release'),
- os.path.join(root_dir, 'build', 'outputs', 'apk'),
- os.path.join(root_dir, 'build', 'apk'),
- ]:
+ apk_dirs = [
+ # gradle plugin >= 3.0
+ os.path.join(root_dir, 'build', 'outputs', 'apk', 'release'),
+ # gradle plugin < 3.0 and >= 0.11
+ os.path.join(root_dir, 'build', 'outputs', 'apk'),
+ # really old path
+ os.path.join(root_dir, 'build', 'apk'),
+ ]
+ # If we build with gradle flavours with gradle plugin >= 3.0 the apk will be in
+ # a subdirectory corresponding to the flavour command used, but with different
+ # capitalization.
+ if flavours_cmd:
+ apk_dirs.append(os.path.join(root_dir, 'build', 'outputs', 'apk', transform_first_char(flavours_cmd, str.lower), 'release'))
+ for apks_dir in apk_dirs:
for apkglob in ['*-release-unsigned.apk', '*-unsigned.apk', '*.apk']:
apks = glob.glob(os.path.join(apks_dir, apkglob))
if server:
# When using server mode, still keep a local cache of the repo, by
# grabbing the source now.
- vcs.gotorevision(build.commit)
+ vcs.gotorevision(build.commit, refresh)
build_server(app, build, vcs, build_dir, output_dir, log_dir, force)
else:
parser = ArgumentParser(usage="%(prog)s [options] [APPID[:VERCODE] [APPID[:VERCODE] ...]]")
common.setup_global_opts(parser)
- parser.add_argument("appid", nargs='*', help="app-id with optional versionCode in the form APPID[:VERCODE]")
+ parser.add_argument("appid", nargs='*', help=_("applicationId with optional versionCode in the form APPID[:VERCODE]"))
parser.add_argument("-l", "--latest", action="store_true", default=False,
- help="Build only the latest version of each package")
+ help=_("Build only the latest version of each package"))
parser.add_argument("-s", "--stop", action="store_true", default=False,
- help="Make the build stop on exceptions")
+ help=_("Make the build stop on exceptions"))
parser.add_argument("-t", "--test", action="store_true", default=False,
- help="Test mode - put output in the tmp directory only, and always build, even if the output already exists.")
+ help=_("Test mode - put output in the tmp directory only, and always build, even if the output already exists."))
parser.add_argument("--server", action="store_true", default=False,
- help="Use build server")
+ help=_("Use build server"))
parser.add_argument("--resetserver", action="store_true", default=False,
- help="Reset and create a brand new build server, even if the existing one appears to be ok.")
+ help=_("Reset and create a brand new build server, even if the existing one appears to be ok."))
parser.add_argument("--on-server", dest="onserver", action="store_true", default=False,
- help="Specify that we're running on the build server")
+ help=_("Specify that we're running on the build server"))
parser.add_argument("--skip-scan", dest="skipscan", action="store_true", default=False,
- help="Skip scanning the source code for binaries and other problems")
+ help=_("Skip scanning the source code for binaries and other problems"))
parser.add_argument("--dscanner", action="store_true", default=False,
- help="Setup an emulator, install the apk on it and perform a drozer scan")
+ help=_("Setup an emulator, install the APK on it and perform a Drozer scan"))
parser.add_argument("--no-tarball", dest="notarball", action="store_true", default=False,
- help="Don't create a source tarball, useful when testing a build")
+ help=_("Don't create a source tarball, useful when testing a build"))
parser.add_argument("--no-refresh", dest="refresh", action="store_false", default=True,
- help="Don't refresh the repository, useful when testing a build with no internet connection")
+ help=_("Don't refresh the repository, useful when testing a build with no internet connection"))
parser.add_argument("-f", "--force", action="store_true", default=False,
- help="Force build of disabled apps, and carries on regardless of scan problems. Only allowed in test mode.")
+ help=_("Force build of disabled apps, and carries on regardless of scan problems. Only allowed in test mode."))
parser.add_argument("-a", "--all", action="store_true", default=False,
- help="Build all applications available")
+ help=_("Build all applications available"))
parser.add_argument("-w", "--wiki", default=False, action="store_true",
- help="Update the wiki")
+ help=_("Update the wiki"))
metadata.add_metadata_arguments(parser)
options = parser.parse_args()
metadata.warnings_action = options.W
options = None
config = None
buildserverid = None
+starttime = _get_build_timestamp()
def main():
# Read all app and srclib metadata
pkgs = common.read_pkg_args(options.appid, True)
- allapps = metadata.read_metadata(not options.onserver, pkgs)
+ allapps = metadata.read_metadata(not options.onserver, pkgs, options.refresh, sort_by_time=True)
apps = common.read_app_args(options.appid, allapps, True)
for appid, app in list(apps.items()):
if not apps:
raise FDroidException("No apps to process.")
+ # make sure enough open files are allowed to process everything
+ soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+ if len(apps) > soft:
+ try:
+ soft = len(apps) * 2
+ if soft > hard:
+ soft = hard
+ resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
+ logging.debug(_('Set open file limit to {integer}')
+ .format(integer=soft))
+ except (OSError, ValueError) as e:
+ logging.warning(_('Setting open file limit failed: ') + str(e))
+
if options.latest:
for app in apps.values():
for build in reversed(app.builds):
# Build applications...
failed_apps = {}
build_succeeded = []
+ max_apps_per_run = 10
for appid, app in apps.items():
+ max_apps_per_run -= 1
+ if max_apps_per_run < 1:
+ break
first = True
for build in app.builds:
wikilog = None
+ build_starttime = _get_build_timestamp()
tools_version_log = ''
if not options.onserver:
tools_version_log = get_android_tools_version_log(build.ndk_path())
vcs, build_dir = common.setup_vcs(app)
first = False
+ logging.info("Using %s" % vcs.clientversion())
logging.debug("Checking " + build.versionName)
if trybuild(app, build, build_dir, output_dir, log_dir,
also_check_dir, srclib_dir, extlib_dir,
url = url.replace('%v', build.versionName)
url = url.replace('%c', str(build.versionCode))
logging.info("...retrieving " + url)
- of = common.get_release_filename(app, build) + '.binary'
+ of = re.sub(r'.apk$', '.binary.apk', common.get_release_filename(app, build))
of = os.path.join(output_dir, of)
try:
net.download_file(url, local_filename=of)
logging.error("VCS error while building app %s: %s" % (
appid, reason))
if options.stop:
+ logging.debug("Error encoutered, stopping by user request.")
sys.exit(1)
failed_apps[appid] = vcse
wikilog = str(vcse)
f.write('versionCode: %s\nversionName: %s\ncommit: %s\n' %
(build.versionCode, build.versionName, build.commit))
f.write('Build completed at '
- + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n')
+ + _get_build_timestamp() + '\n')
f.write('\n' + tools_version_log + '\n')
f.write(str(e))
logging.error("Could not build app %s: %s" % (appid, e))
if options.stop:
+ logging.debug("Error encoutered, stopping by user request.")
sys.exit(1)
failed_apps[appid] = e
wikilog = e.get_wikitext()
logging.error("Could not build app %s due to unknown error: %s" % (
appid, traceback.format_exc()))
if options.stop:
+ logging.debug("Error encoutered, stopping by user request.")
sys.exit(1)
failed_apps[appid] = e
wikilog = str(e)
newpage = site.Pages[lastbuildpage]
with open(os.path.join('tmp', 'fdroidserverid')) as fp:
fdroidserverid = fp.read().rstrip()
- txt = "* build completed at " + time.strftime("%Y-%m-%d %H:%M:%SZ", time.gmtime()) + '\n' \
+ txt = "* build session started at " + starttime + '\n' \
+ + "* this build started at " + build_starttime + '\n' \
+ + "* this build completed at " + _get_build_timestamp() + '\n' \
+ '* fdroidserverid: [https://gitlab.com/fdroid/fdroidserver/commit/' \
+ fdroidserverid + ' ' + fdroidserverid + ']\n\n'
if options.onserver:
break
if not apk_path:
- raise Exception("No signed APK found at path: {0}".format(apk_path))
+ raise Exception("No signed APK found at path: {path}".format(path=apk_path))
if not os.path.isdir(repo_dir):
- exit(1)
+ logging.critical("directory does not exists '{path}'".format(path=repo_dir))
+ sys.exit(1)
logging.info("Performing Drozer scan on {0}.".format(app))
docker.perform_drozer_scan(apk_path, app.id, repo_dir)
logging.info("Cleaning up after ourselves.")
docker.clean()
- logging.info("Finished.")
+ logging.info(_("Finished"))
if len(build_succeeded) > 0:
- logging.info(str(len(build_succeeded)) + ' builds succeeded')
+ logging.info(ngettext("{} build succeeded",
+ "{} builds succeeded", len(build_succeeded)).format(len(build_succeeded)))
if len(failed_apps) > 0:
- logging.info(str(len(failed_apps)) + ' builds failed')
+ logging.info(ngettext("{} build failed",
+ "{} builds failed", len(failed_apps)).format(len(failed_apps)))
- sys.exit(0)
+ # hack to ensure this exits, even is some threads are still running
+ sys.stdout.flush()
+ sys.stderr.flush()
+ os._exit(0)
if __name__ == "__main__":