chiark / gitweb /
Handle with invalid manifests better
[fdroidserver.git] / fdroidserver / common.py
index d7260839040c3ffab16fcd625ca7e75c8fc1ee64..a20aa59b478822ced8a95353ea62bc29fe4e9d55 100644 (file)
@@ -29,13 +29,18 @@ import stat
 import subprocess
 import time
 import operator
-import Queue
 import logging
 import hashlib
 import socket
 import xml.etree.ElementTree as XMLElementTree
 
-from distutils.version import LooseVersion
+try:
+    # Python 2
+    from Queue import Queue
+except ImportError:
+    # Python 3
+    from queue import Queue
+
 from zipfile import ZipFile
 
 import metadata
@@ -221,15 +226,6 @@ def read_config(opts, config_file='config.py'):
     return config
 
 
-def get_ndk_path(version):
-    if version is None:
-        version = 'r10e'  # falls back to latest
-    paths = config['ndk_paths']
-    if version not in paths:
-        return ''
-    return paths[version] or ''
-
-
 def find_sdk_tools_cmd(cmd):
     '''find a working path to a tool from the Android SDK'''
 
@@ -305,7 +301,7 @@ def write_password_file(pwtype, password=None):
     command line argments
     '''
     filename = '.fdroid.' + pwtype + '.txt'
-    fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0600)
+    fd = os.open(filename, os.O_CREAT | os.O_TRUNC | os.O_WRONLY, 0o600)
     if password is None:
         os.write(fd, config[pwtype])
     else:
@@ -363,10 +359,10 @@ def read_app_args(args, allapps, allow_vercodes=False):
         vc = vercodes[appid]
         if not vc:
             continue
-        app['builds'] = [b for b in app['builds'] if b['vercode'] in vc]
-        if len(app['builds']) != len(vercodes[appid]):
+        app.builds = [b for b in app.builds if b.vercode in vc]
+        if len(app.builds) != len(vercodes[appid]):
             error = True
-            allvcs = [b['vercode'] for b in app['builds']]
+            allvcs = [b.vercode for b in app.builds]
             for v in vercodes[appid]:
                 if v not in allvcs:
                     logging.critical("No such vercode %s for app %s" % (v, appid))
@@ -389,7 +385,7 @@ def has_extension(filename, ext):
     return ext == f_ext
 
 
-apk_regex = None
+apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
 
 
 def clean_description(description):
@@ -406,10 +402,7 @@ def clean_description(description):
 
 
 def apknameinfo(filename):
-    global apk_regex
     filename = os.path.basename(filename)
-    if apk_regex is None:
-        apk_regex = re.compile(r"^(.+)_([0-9]+)\.apk$")
     m = apk_regex.match(filename)
     try:
         result = (m.group(1), m.group(2))
@@ -419,23 +412,23 @@ def apknameinfo(filename):
 
 
 def getapkname(app, build):
-    return "%s_%s.apk" % (app['id'], build['vercode'])
+    return "%s_%s.apk" % (app.id, build.vercode)
 
 
 def getsrcname(app, build):
-    return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
+    return "%s_%s_src.tar.gz" % (app.id, build.vercode)
 
 
 def getappname(app):
-    if app['Name']:
-        return app['Name']
-    if app['Auto Name']:
-        return app['Auto Name']
-    return app['id']
+    if app.Name:
+        return app.Name
+    if app.AutoName:
+        return app.AutoName
+    return app.id
 
 
 def getcvname(app):
-    return '%s (%s)' % (app['Current Version'], app['Current Version Code'])
+    return '%s (%s)' % (app.CurrentVersion, app.CurrentVersionCode)
 
 
 def getvcs(vcstype, remote, local):
@@ -529,7 +522,7 @@ class vcs:
 
         try:
             self.gotorevisionx(rev)
-        except FDroidException, e:
+        except FDroidException as e:
             exc = e
 
         # If necessary, write the .fdroidvcs file.
@@ -1026,7 +1019,7 @@ psearch_g = re.compile(r'.*(packageName|applicationId) *=* *["\']([^"]+)["\'].*'
 def app_matches_packagename(app, package):
     if not package:
         return False
-    appid = app['Update Check Name'] or app['id']
+    appid = app.UpdateCheckName or app.id
     if appid is None or appid == "Ignore":
         return True
     return appid == package
@@ -1037,7 +1030,7 @@ def app_matches_packagename(app, package):
 # All values returned are strings.
 def parse_androidmanifests(paths, app):
 
-    ignoreversions = app['Update Check Ignore']
+    ignoreversions = app.UpdateCheckIgnore
     ignoresearch = re.compile(ignoreversions).search if ignoreversions else None
 
     if not paths:
@@ -1079,19 +1072,22 @@ def parse_androidmanifests(paths, app):
                     if matches:
                         vercode = matches.group(1)
         else:
-            xml = parse_xml(path)
-            if "package" in xml.attrib:
-                s = xml.attrib["package"].encode('utf-8')
-                if app_matches_packagename(app, s):
-                    package = s
-            if "{http://schemas.android.com/apk/res/android}versionName" in xml.attrib:
-                version = xml.attrib["{http://schemas.android.com/apk/res/android}versionName"].encode('utf-8')
-                base_dir = os.path.dirname(path)
-                version = retrieve_string_singleline(base_dir, version)
-            if "{http://schemas.android.com/apk/res/android}versionCode" in xml.attrib:
-                a = xml.attrib["{http://schemas.android.com/apk/res/android}versionCode"].encode('utf-8')
-                if string_is_integer(a):
-                    vercode = a
+            try:
+                xml = parse_xml(path)
+                if "package" in xml.attrib:
+                    s = xml.attrib["package"].encode('utf-8')
+                    if app_matches_packagename(app, s):
+                        package = s
+                if "{http://schemas.android.com/apk/res/android}versionName" in xml.attrib:
+                    version = xml.attrib["{http://schemas.android.com/apk/res/android}versionName"].encode('utf-8')
+                    base_dir = os.path.dirname(path)
+                    version = retrieve_string_singleline(base_dir, version)
+                if "{http://schemas.android.com/apk/res/android}versionCode" in xml.attrib:
+                    a = xml.attrib["{http://schemas.android.com/apk/res/android}versionCode"].encode('utf-8')
+                    if string_is_integer(a):
+                        vercode = a
+            except Exception:
+                logging.warning("Problem with xml at {0}".format(path))
 
         # Remember package name, may be defined separately from version+vercode
         if package is None:
@@ -1250,17 +1246,17 @@ gradle_version_regex = re.compile(r"[^/]*'com\.android\.tools\.build:gradle:([^\
 def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=False, refresh=True):
 
     # Optionally, the actual app source can be in a subdirectory
-    if build['subdir']:
-        root_dir = os.path.join(build_dir, build['subdir'])
+    if build.subdir:
+        root_dir = os.path.join(build_dir, build.subdir)
     else:
         root_dir = build_dir
 
     # Get a working copy of the right revision
-    logging.info("Getting source for revision " + build['commit'])
-    vcs.gotorevision(build['commit'], refresh)
+    logging.info("Getting source for revision " + build.commit)
+    vcs.gotorevision(build.commit, refresh)
 
     # Initialise submodules if required
-    if build['submodules']:
+    if build.submodules:
         logging.info("Initialising submodules")
         vcs.initsubmodules()
 
@@ -1270,31 +1266,31 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         raise BuildException('Missing subdir ' + root_dir)
 
     # Run an init command if one is required
-    if build['init']:
-        cmd = replace_config_vars(build['init'], build)
+    if build.init:
+        cmd = replace_config_vars(build.init, build)
         logging.info("Running 'init' commands in %s" % root_dir)
 
         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
         if p.returncode != 0:
             raise BuildException("Error running init command for %s:%s" %
-                                 (app['id'], build['version']), p.output)
+                                 (app.id, build.version), p.output)
 
     # Apply patches if any
-    if build['patch']:
+    if build.patch:
         logging.info("Applying patches")
-        for patch in build['patch']:
+        for patch in build.patch:
             patch = patch.strip()
             logging.info("Applying " + patch)
-            patch_path = os.path.join('metadata', app['id'], patch)
+            patch_path = os.path.join('metadata', app.id, patch)
             p = FDroidPopen(['patch', '-p1', '-i', os.path.abspath(patch_path)], cwd=build_dir)
             if p.returncode != 0:
                 raise BuildException("Failed to apply patch %s" % patch_path)
 
     # Get required source libraries
     srclibpaths = []
-    if build['srclibs']:
+    if build.srclibs:
         logging.info("Collecting source libraries")
-        for lib in build['srclibs']:
+        for lib in build.srclibs:
             srclibpaths.append(getsrclib(lib, srclib_dir, build, preponly=onserver, refresh=refresh))
 
     for name, number, libpath in srclibpaths:
@@ -1307,8 +1303,8 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
 
     # Update the local.properties file
     localprops = [os.path.join(build_dir, 'local.properties')]
-    if build['subdir']:
-        parts = build['subdir'].split(os.sep)
+    if build.subdir:
+        parts = build.subdir.split(os.sep)
         cur = build_dir
         for d in parts:
             cur = os.path.join(cur, d)
@@ -1324,61 +1320,30 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             logging.info("Creating local.properties file at %s" % path)
         # Fix old-fashioned 'sdk-location' by copying
         # from sdk.dir, if necessary
-        if build['oldsdkloc']:
+        if build.oldsdkloc:
             sdkloc = re.match(r".*^sdk.dir=(\S+)$.*", props,
                               re.S | re.M).group(1)
             props += "sdk-location=%s\n" % sdkloc
         else:
             props += "sdk.dir=%s\n" % config['sdk_path']
             props += "sdk-location=%s\n" % config['sdk_path']
-        if build['ndk_path']:
+        ndk_path = build.ndk_path()
+        if ndk_path:
             # Add ndk location
-            props += "ndk.dir=%s\n" % build['ndk_path']
-            props += "ndk-location=%s\n" % build['ndk_path']
+            props += "ndk.dir=%s\n" % ndk_path
+            props += "ndk-location=%s\n" % ndk_path
         # Add java.encoding if necessary
-        if build['encoding']:
-            props += "java.encoding=%s\n" % build['encoding']
+        if build.encoding:
+            props += "java.encoding=%s\n" % build.encoding
         with open(path, 'w') as f:
             f.write(props)
 
     flavours = []
-    if build['type'] == 'gradle':
-        flavours = build['gradle']
-
-        gradlepluginver = None
-
-        gradle_dirs = [root_dir]
-
-        # Parent dir build.gradle
-        parent_dir = os.path.normpath(os.path.join(root_dir, '..'))
-        if parent_dir.startswith(build_dir):
-            gradle_dirs.append(parent_dir)
-
-        for dir_path in gradle_dirs:
-            if gradlepluginver:
-                break
-            if not os.path.isdir(dir_path):
-                continue
-            for filename in os.listdir(dir_path):
-                if not filename.endswith('.gradle'):
-                    continue
-                path = os.path.join(dir_path, filename)
-                if not os.path.isfile(path):
-                    continue
-                for line in file(path):
-                    match = gradle_version_regex.match(line)
-                    if match:
-                        gradlepluginver = match.group(1)
-                        break
+    if build.method() == 'gradle':
+        flavours = build.gradle
 
-        if gradlepluginver:
-            build['gradlepluginver'] = LooseVersion(gradlepluginver)
-        else:
-            logging.warn("Could not fetch the gradle plugin version, defaulting to 0.11")
-            build['gradlepluginver'] = LooseVersion('0.11')
-
-        if build['target']:
-            n = build["target"].split('-')[1]
+        if build.target:
+            n = build.target.split('-')[1]
             regsub_file(r'compileSdkVersion[ =]+[0-9]+',
                         r'compileSdkVersion %s' % n,
                         os.path.join(root_dir, 'build.gradle'))
@@ -1387,38 +1352,38 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
     remove_debuggable_flags(root_dir)
 
     # Insert version code and number into the manifest if necessary
-    if build['forceversion']:
+    if build.forceversion:
         logging.info("Changing the version name")
         for path in manifest_paths(root_dir, flavours):
             if not os.path.isfile(path):
                 continue
             if has_extension(path, 'xml'):
                 regsub_file(r'android:versionName="[^"]*"',
-                            r'android:versionName="%s"' % build['version'],
+                            r'android:versionName="%s"' % build.version,
                             path)
             elif has_extension(path, 'gradle'):
                 regsub_file(r"""(\s*)versionName[\s'"=]+.*""",
-                            r"""\1versionName '%s'""" % build['version'],
+                            r"""\1versionName '%s'""" % build.version,
                             path)
 
-    if build['forcevercode']:
+    if build.forcevercode:
         logging.info("Changing the version code")
         for path in manifest_paths(root_dir, flavours):
             if not os.path.isfile(path):
                 continue
             if has_extension(path, 'xml'):
                 regsub_file(r'android:versionCode="[^"]*"',
-                            r'android:versionCode="%s"' % build['vercode'],
+                            r'android:versionCode="%s"' % build.vercode,
                             path)
             elif has_extension(path, 'gradle'):
                 regsub_file(r'versionCode[ =]+[0-9]+',
-                            r'versionCode %s' % build['vercode'],
+                            r'versionCode %s' % build.vercode,
                             path)
 
     # Delete unwanted files
-    if build['rm']:
+    if build.rm:
         logging.info("Removing specified files")
-        for part in getpaths(build_dir, build['rm']):
+        for part in getpaths(build_dir, build.rm):
             dest = os.path.join(build_dir, part)
             logging.info("Removing {0}".format(part))
             if os.path.lexists(dest):
@@ -1432,12 +1397,12 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
     remove_signing_keys(build_dir)
 
     # Add required external libraries
-    if build['extlibs']:
+    if build.extlibs:
         logging.info("Collecting prebuilt libraries")
         libsdir = os.path.join(root_dir, 'libs')
         if not os.path.exists(libsdir):
             os.mkdir(libsdir)
-        for lib in build['extlibs']:
+        for lib in build.extlibs:
             lib = lib.strip()
             logging.info("...installing extlib {0}".format(lib))
             libf = os.path.basename(lib)
@@ -1447,10 +1412,10 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
             shutil.copyfile(libsrc, os.path.join(libsdir, libf))
 
     # Run a pre-build command if one is required
-    if build['prebuild']:
+    if build.prebuild:
         logging.info("Running 'prebuild' commands in %s" % root_dir)
 
-        cmd = replace_config_vars(build['prebuild'], build)
+        cmd = replace_config_vars(build.prebuild, build)
 
         # Substitute source library paths into prebuild commands
         for name, number, libpath in srclibpaths:
@@ -1460,20 +1425,20 @@ def prepare_source(vcs, app, build, build_dir, srclib_dir, extlib_dir, onserver=
         p = FDroidPopen(['bash', '-x', '-c', cmd], cwd=root_dir)
         if p.returncode != 0:
             raise BuildException("Error running prebuild command for %s:%s" %
-                                 (app['id'], build['version']), p.output)
+                                 (app.id, build.version), p.output)
 
     # Generate (or update) the ant build file, build.xml...
-    if build['update'] and build['update'] != ['no'] and build['type'] == 'ant':
+    if build.method() == 'ant' and build.update != ['no']:
         parms = ['android', 'update', 'lib-project']
         lparms = ['android', 'update', 'project']
 
-        if build['target']:
-            parms += ['-t', build['target']]
-            lparms += ['-t', build['target']]
-        if build['update'] == ['auto']:
-            update_dirs = ant_subprojects(root_dir) + ['.']
+        if build.target:
+            parms += ['-t', build.target]
+            lparms += ['-t', build.target]
+        if build.update:
+            update_dirs = build.update
         else:
-            update_dirs = build['update']
+            update_dirs = ant_subprojects(root_dir) + ['.']
 
         for d in update_dirs:
             subdir = os.path.join(root_dir, d)
@@ -1646,11 +1611,11 @@ def FDroidPopen(commands, cwd=None, output=True):
     try:
         p = subprocess.Popen(commands, cwd=cwd, shell=False, env=env,
                              stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
-    except OSError, e:
+    except OSError as e:
         raise BuildException("OSError while trying to execute " +
                              ' '.join(commands) + ': ' + str(e))
 
-    stdout_queue = Queue.Queue()
+    stdout_queue = Queue()
     stdout_reader = AsynchronousFileReader(p.stdout, stdout_queue)
 
     # Check the queue for output (until there is no more to get)
@@ -1674,8 +1639,6 @@ gradle_signing_configs = re.compile(r'^[\t ]*signingConfigs[ \t]*{[ \t]*$')
 gradle_line_matches = [
     re.compile(r'^[\t ]*signingConfig [^ ]*$'),
     re.compile(r'.*android\.signingConfigs\.[^{]*$'),
-    re.compile(r'.*variant\.outputFile = .*'),
-    re.compile(r'.*output\.outputFile = .*'),
     re.compile(r'.*\.readLine\(.*'),
 ]
 
@@ -1770,9 +1733,9 @@ def replace_config_vars(cmd, build):
     cmd = cmd.replace('$$NDK$$', env['ANDROID_NDK'])
     cmd = cmd.replace('$$MVN3$$', config['mvn3'])
     if build is not None:
-        cmd = cmd.replace('$$COMMIT$$', build['commit'])
-        cmd = cmd.replace('$$VERSION$$', build['version'])
-        cmd = cmd.replace('$$VERCODE$$', build['vercode'])
+        cmd = cmd.replace('$$COMMIT$$', build.commit)
+        cmd = cmd.replace('$$VERSION$$', build.version)
+        cmd = cmd.replace('$$VERCODE$$', build.vercode)
     return cmd
 
 
@@ -1993,7 +1956,7 @@ def get_per_app_repos():
     repos = []
     for root, dirs, files in os.walk(os.getcwd()):
         for d in dirs:
-            print 'checking', root, 'for', d
+            print('checking', root, 'for', d)
             if d in ('archive', 'metadata', 'repo', 'srclibs', 'tmp'):
                 # standard parts of an fdroid repo, so never packageNames
                 continue