chiark / gitweb /
build: log the start time of the current build session
[fdroidserver.git] / fdroidserver / build.py
index d5092a6d8ae0917e4aedea778b68fefe5fe2d7ff..8bbe8aab75d68bb82e97877c94e4e9b0a47bbd36 100644 (file)
@@ -23,6 +23,7 @@ import shutil
 import glob
 import subprocess
 import re
+import resource
 import tarfile
 import traceback
 import time
@@ -33,6 +34,7 @@ from argparse import ArgumentParser
 import logging
 from gettext import ngettext
 
+from . import _
 from . import common
 from . import net
 from . import metadata
@@ -97,22 +99,22 @@ def build_server(app, build, vcs, build_dir, output_dir, log_dir, force):
 
         # 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__))
@@ -163,7 +165,7 @@ def build_server(app, build, vcs, build_dir, output_dir, log_dir, force):
                         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 = []
@@ -286,6 +288,10 @@ def force_gradle_build_tools(build_dir, build_tools):
                                path)
 
 
+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:
@@ -412,6 +418,16 @@ def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, ext
                 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:
@@ -497,7 +513,7 @@ def build_local(app, build, vcs, build_dir, output_dir, log_dir, srclib_dir, ext
                 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
@@ -950,7 +966,7 @@ def trybuild(app, build, build_dir, output_dir, log_dir, also_check_dir,
     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:
@@ -999,33 +1015,33 @@ def parse_commandline():
 
     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
@@ -1043,6 +1059,7 @@ def parse_commandline():
 options = None
 config = None
 buildserverid = None
+starttime = _get_build_timestamp()
 
 
 def main():
@@ -1112,7 +1129,7 @@ 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()):
@@ -1122,6 +1139,19 @@ def main():
     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):
@@ -1139,12 +1169,17 @@ def main():
     # 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())
@@ -1157,6 +1192,7 @@ def main():
                     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,
@@ -1230,6 +1266,7 @@ def main():
                 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)
@@ -1239,11 +1276,12 @@ def main():
                     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()
@@ -1251,6 +1289,7 @@ def main():
                 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)
@@ -1262,7 +1301,9 @@ def main():
                     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:
@@ -1304,10 +1345,11 @@ def main():
                         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)
@@ -1320,7 +1362,7 @@ def main():
         logging.info("Cleaning up after ourselves.")
         docker.clean()
 
-    logging.info("Finished.")
+    logging.info(_("Finished"))
     if len(build_succeeded) > 0:
         logging.info(ngettext("{} build succeeded",
                               "{} builds succeeded", len(build_succeeded)).format(len(build_succeeded)))
@@ -1328,7 +1370,10 @@ def main():
         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__":