chiark / gitweb /
Apply some autopep8-python2 suggestions
[fdroidserver.git] / fdroidserver / metadata.py
index ee816e321128396f6fad1f02c717258cd7da5fb3..1ba5a713608f285310da8de62b7f9b5ef2256e80 100644 (file)
@@ -23,60 +23,87 @@ import glob
 import cgi
 import logging
 
-srclibs = {}
+from collections import OrderedDict
+
+srclibs = None
 
 
 class MetaDataException(Exception):
+
     def __init__(self, value):
         self.value = value
 
     def __str__(self):
         return self.value
 
-app_defaults = {
-    'Name': None,
-    'Provides': None,
-    'Auto Name': '',
-    'Categories': ['None'],
-    'Description': [],
-    'Summary': '',
-    'License': 'Unknown',
-    'Web Site': '',
-    'Source Code': '',
-    'Issue Tracker': '',
-    'Donate': None,
-    'FlattrID': None,
-    'Bitcoin': None,
-    'Litecoin': None,
-    'Dogecoin': None,
-    'Disabled': None,
-    'AntiFeatures': None,
-    'Archive Policy': None,
-    'Update Check Mode': 'None',
-    'Update Check Ignore': None,
-    'Update Check Name': None,
-    'Update Check Data': None,
-    'Vercode Operation': None,
-    'Auto Update Mode': 'None',
-    'Current Version': '',
-    'Current Version Code': '0',
-    'Repo Type': '',
-    'Repo': '',
-    'Requires Root': False,
-    'No Source Since': ''
-    }
-
-
-# This defines the preferred order for the build items - as in the
-# manual, they're roughly in order of application.
-ordered_flags = [
-    'disable', 'commit', 'subdir', 'submodules', 'init',
-    'gradle', 'maven', 'kivy', 'output', 'oldsdkloc', 'target',
-    'update', 'encoding', 'forceversion', 'forcevercode', 'rm',
-    'extlibs', 'srclibs', 'patch', 'prebuild', 'scanignore',
-    'scandelete', 'build', 'buildjni', 'preassemble', 'bindir',
-    'antcommand', 'novcheck'
-    ]
+# In the order in which they are laid out on files
+app_defaults = OrderedDict([
+    ('Disabled', None),
+    ('AntiFeatures', None),
+    ('Provides', None),
+    ('Categories', ['None']),
+    ('License', 'Unknown'),
+    ('Web Site', ''),
+    ('Source Code', ''),
+    ('Issue Tracker', ''),
+    ('Donate', None),
+    ('FlattrID', None),
+    ('Bitcoin', None),
+    ('Litecoin', None),
+    ('Dogecoin', None),
+    ('Name', None),
+    ('Auto Name', ''),
+    ('Summary', ''),
+    ('Description', []),
+    ('Requires Root', False),
+    ('Repo Type', ''),
+    ('Repo', ''),
+    ('Binaries', None),
+    ('Maintainer Notes', []),
+    ('Archive Policy', None),
+    ('Auto Update Mode', 'None'),
+    ('Update Check Mode', 'None'),
+    ('Update Check Ignore', None),
+    ('Vercode Operation', None),
+    ('Update Check Name', None),
+    ('Update Check Data', None),
+    ('Current Version', ''),
+    ('Current Version Code', '0'),
+    ('No Source Since', ''),
+])
+
+
+# In the order in which they are laid out on files
+# Sorted by their action and their place in the build timeline
+flag_defaults = OrderedDict([
+    ('disable', False),
+    ('commit', None),
+    ('subdir', None),
+    ('submodules', False),
+    ('init', ''),
+    ('patch', []),
+    ('gradle', False),
+    ('maven', False),
+    ('kivy', False),
+    ('output', None),
+    ('srclibs', []),
+    ('oldsdkloc', False),
+    ('encoding', None),
+    ('forceversion', False),
+    ('forcevercode', False),
+    ('rm', []),
+    ('extlibs', []),
+    ('prebuild', ''),
+    ('update', ['auto']),
+    ('target', None),
+    ('scanignore', []),
+    ('scandelete', []),
+    ('build', ''),
+    ('buildjni', []),
+    ('preassemble', []),
+    ('antcommands', None),
+    ('novcheck', False),
+])
 
 
 # Designates a metadata field type and checks that it matches
@@ -129,9 +156,14 @@ class FieldValidator():
 valuetypes = {
     FieldValidator("Integer",
                    r'^[1-9][0-9]*$', None,
-                   ['FlattrID'],
+                   [],
                    ['vercode']),
 
+    FieldValidator("Hexadecimal",
+                   r'^[0-9a-f]+$', None,
+                   ['FlattrID'],
+                   []),
+
     FieldValidator("HTTP link",
                    r'^http[s]?://', None,
                    ["Web Site", "Source Code", "Issue Tracker", "Donate"], []),
@@ -167,6 +199,11 @@ valuetypes = {
                    ["Repo Type"],
                    []),
 
+    FieldValidator("Binaries",
+                   r'^http[s]?://', None,
+                   ["Binaries"],
+                   []),
+
     FieldValidator("Archive Policy",
                    r'^[0-9]+ versions$', None,
                    ["Archive Policy"],
@@ -186,19 +223,17 @@ valuetypes = {
                    r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
                    ["Update Check Mode"],
                    [])
-    }
+}
 
 
 # Check an app's metadata information for integrity errors
 def check_metadata(info):
     for v in valuetypes:
         for field in v.fields:
-            if field in info:
-                v.check(info[field], info['id'])
+            v.check(info[field], info['id'])
         for build in info['builds']:
             for attr in v.attrs:
-                if attr in build:
-                    v.check(build[attr], info['id'])
+                v.check(build[attr], info['id'])
 
 
 # Formatter for descriptions. Create an instance, and call parseline() with
@@ -428,6 +463,11 @@ def read_srclibs():
     metadata.
     """
     global srclibs
+
+    # They were already loaded
+    if srclibs is not None:
+        return
+
     srclibs = {}
 
     srcdir = 'srclibs'
@@ -442,30 +482,35 @@ def read_srclibs():
 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
 # returned by the parse_metadata function.
 def read_metadata(xref=True):
-    apps = []
+
+    # Always read the srclibs before the apps, since they can use a srlib as
+    # their source repository.
+    read_srclibs()
+
+    apps = {}
 
     for basedir in ('metadata', 'tmp'):
         if not os.path.exists(basedir):
             os.makedirs(basedir)
 
     for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
-        appinfo = parse_metadata(metafile)
+        appid, appinfo = parse_metadata(metafile)
         check_metadata(appinfo)
-        apps.append(appinfo)
+        apps[appid] = appinfo
 
     if xref:
         # Parse all descriptions at load time, just to ensure cross-referencing
         # errors are caught early rather than when they hit the build server.
-        def linkres(link):
-            for app in apps:
-                if app['id'] == link:
-                    return ("fdroid.app:" + link, "Dummy name - don't know yet")
-            raise MetaDataException("Cannot resolve app id " + link)
-        for app in apps:
+        def linkres(appid):
+            if appid in apps:
+                return ("fdroid.app:" + appid, "Dummy name - don't know yet")
+            raise MetaDataException("Cannot resolve app id " + appid)
+
+        for appid, app in apps.iteritems():
             try:
                 description_html(app['Description'], linkres)
-            except Exception, e:
-                raise MetaDataException("Problem with description of " + app['id'] +
+            except MetaDataException, e:
+                raise MetaDataException("Problem with description of " + appid +
                                         " - " + str(e))
 
     return apps
@@ -489,17 +534,34 @@ def metafieldtype(name):
 
 
 def flagtype(name):
-    if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni',
-                'update', 'scanignore', 'scandelete']:
+    if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
+                'update', 'scanignore', 'scandelete', 'gradle', 'antcommands']:
         return 'list'
     if name in ['init', 'prebuild', 'build']:
         return 'script'
     if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
-            'novcheck']:
+                'novcheck']:
         return 'bool'
     return 'string'
 
 
+def fill_build_defaults(build):
+
+    def get_build_type():
+        for t in ['maven', 'gradle', 'kivy']:
+            if build[t]:
+                return t
+        if build['output']:
+            return 'raw'
+        return 'ant'
+
+    for flag, value in flag_defaults.iteritems():
+        if flag in build:
+            continue
+        build[flag] = value
+    build['type'] = get_build_type()
+
+
 # Parse metadata for a single application.
 #
 #  'metafile' - the filename to read. The package id for the application comes
@@ -513,7 +575,6 @@ def flagtype(name):
 #
 # Known keys not originating from the metadata are:
 #
-#  'id'               - the application's package ID
 #  'builds'           - a list of dictionaries containing build information
 #                       for each defined build
 #  'comments'         - a list of comments from the metadata file. Each is
@@ -527,6 +588,7 @@ def flagtype(name):
 #
 def parse_metadata(metafile):
 
+    appid = None
     linedesc = None
 
     def add_buildflag(p, thisbuild):
@@ -540,13 +602,17 @@ def parse_metadata(metafile):
                                     .format(pk, thisbuild['version'], linedesc))
 
         pk = pk.lstrip()
-        if pk not in ordered_flags:
+        if pk not in flag_defaults:
             raise MetaDataException("Unrecognised build flag at {0} in {1}"
                                     .format(p, linedesc))
         t = flagtype(pk)
         if t == 'list':
             # Port legacy ';' separators
-            thisbuild[pk] = [v.strip() for v in pv.replace(';', ',').split(',')]
+            pv = [v.strip() for v in pv.replace(';', ',').split(',')]
+            if pk == 'gradle':
+                if len(pv) == 1 and pv[0] in ['main', 'yes']:
+                    pv = ['yes']
+            thisbuild[pk] = pv
         elif t == 'string' or t == 'script':
             thisbuild[pk] = pv
         elif t == 'bool':
@@ -595,45 +661,27 @@ def parse_metadata(metafile):
             thisinfo['comments'].append((key, comment))
         del curcomments[:]
 
-    def get_build_type(build):
-        for t in ['maven', 'gradle', 'kivy']:
-            if build.get(t, 'no') != 'no':
-                return t
-        if 'output' in build:
-            return 'raw'
-        return 'ant'
-
     thisinfo = {}
     if metafile:
         if not isinstance(metafile, file):
             metafile = open(metafile, "r")
-        thisinfo['id'] = metafile.name[9:-4]
-    else:
-        thisinfo['id'] = None
+        appid = metafile.name[9:-4]
 
     thisinfo.update(app_defaults)
+    thisinfo['id'] = appid
 
     # General defaults...
     thisinfo['builds'] = []
     thisinfo['comments'] = []
 
     if metafile is None:
-        return thisinfo
+        return appid, thisinfo
 
     mode = 0
     buildlines = []
     curcomments = []
     curbuild = None
-
-    def fill_bool_defaults(build):
-        # TODO: quick fix to make bool flags default to False
-        # Should provide defaults for all flags instead of using
-        # build.get(flagname, default) each time
-        for f in ordered_flags:
-            if f in build:
-                continue
-            if flagtype(f) == 'bool':
-                build[f] = False
+    vc_seen = {}
 
     c = 0
     for line in metafile:
@@ -642,13 +690,13 @@ def parse_metadata(metafile):
         line = line.rstrip('\r\n')
         if mode == 3:
             if not any(line.startswith(s) for s in (' ', '\t')):
-                if 'commit' not in curbuild and 'disable' not in curbuild:
+                commit = curbuild['commit'] if 'commit' in curbuild else None
+                if not commit and 'disable' not in curbuild:
                     raise MetaDataException("No commit specified for {0} in {1}"
                                             .format(curbuild['version'], linedesc))
 
-                fill_bool_defaults(curbuild)
                 thisinfo['builds'].append(curbuild)
-                add_comments('build:' + curbuild['version'])
+                add_comments('build:' + curbuild['vercode'])
                 mode = 0
             else:
                 if line.endswith('\\'):
@@ -695,8 +743,9 @@ def parse_metadata(metafile):
                     mode = 2
                     buildlines = [value[:-1]]
                 else:
-                    thisinfo['builds'].append(parse_buildline([value]))
-                    add_comments('build:' + thisinfo['builds'][-1]['version'])
+                    curbuild = parse_buildline([value])
+                    thisinfo['builds'].append(curbuild)
+                    add_comments('build:' + thisinfo['builds'][-1]['vercode'])
             elif fieldtype == 'buildv2':
                 curbuild = {}
                 vv = value.split(',')
@@ -705,6 +754,10 @@ def parse_metadata(metafile):
                                             .format(value, linedesc))
                 curbuild['version'] = vv[0]
                 curbuild['vercode'] = vv[1]
+                if curbuild['vercode'] in vc_seen:
+                    raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
+                                            curbuild['vercode'], linedesc))
+                vc_seen[curbuild['vercode']] = True
                 buildlines = []
                 mode = 3
             elif fieldtype == 'obsolete':
@@ -722,9 +775,8 @@ def parse_metadata(metafile):
             else:
                 buildlines.append(line)
                 curbuild = parse_buildline(buildlines)
-                fill_bool_defaults(curbuild)
                 thisinfo['builds'].append(curbuild)
-                add_comments('build:' + thisinfo['builds'][-1]['version'])
+                add_comments('build:' + thisinfo['builds'][-1]['vercode'])
                 mode = 0
     add_comments(None)
 
@@ -740,9 +792,11 @@ def parse_metadata(metafile):
         thisinfo['Description'].append('No description available')
 
     for build in thisinfo['builds']:
-        build['type'] = get_build_type(build)
+        fill_build_defaults(build)
 
-    return thisinfo
+    thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
+
+    return (appid, thisinfo)
 
 
 # Write a metadata file.
@@ -758,7 +812,7 @@ def write_metadata(dest, app):
                 mf.write("%s\n" % comment)
                 written += 1
         if written > 0:
-            logging.debug("...writing comments for " + (key if key else 'EOF'))
+            logging.debug("...writing comments for " + (key or 'EOF'))
 
     def writefield(field, value=None):
         writecomments(field)
@@ -769,33 +823,29 @@ def write_metadata(dest, app):
             value = ','.join(value)
         mf.write("%s:%s\n" % (field, value))
 
+    def writefield_nonempty(field, value=None):
+        if value is None:
+            value = app[field]
+        if value:
+            writefield(field, value)
+
     mf = open(dest, 'w')
-    if app['Disabled']:
-        writefield('Disabled')
-    if app['AntiFeatures']:
-        writefield('AntiFeatures')
-    if app['Provides']:
-        writefield('Provides')
+    writefield_nonempty('Disabled')
+    writefield_nonempty('AntiFeatures')
+    writefield_nonempty('Provides')
     writefield('Categories')
     writefield('License')
     writefield('Web Site')
     writefield('Source Code')
     writefield('Issue Tracker')
-    if app['Donate']:
-        writefield('Donate')
-    if app['FlattrID']:
-        writefield('FlattrID')
-    if app['Bitcoin']:
-        writefield('Bitcoin')
-    if app['Litecoin']:
-        writefield('Litecoin')
-    if app['Dogecoin']:
-        writefield('Dogecoin')
+    writefield_nonempty('Donate')
+    writefield_nonempty('FlattrID')
+    writefield_nonempty('Bitcoin')
+    writefield_nonempty('Litecoin')
+    writefield_nonempty('Dogecoin')
     mf.write('\n')
-    if app['Name']:
-        writefield('Name')
-    if app['Auto Name']:
-        writefield('Auto Name')
+    writefield_nonempty('Name')
+    writefield_nonempty('Auto Name')
     writefield('Summary')
     writefield('Description', '')
     for line in app['Description']:
@@ -808,20 +858,27 @@ def write_metadata(dest, app):
     if app['Repo Type']:
         writefield('Repo Type')
         writefield('Repo')
+        if app['Binaries']:
+            writefield('Binaries')
         mf.write('\n')
     for build in app['builds']:
-        writecomments('build:' + build['version'])
+
+        if build['version'] == "Ignore":
+            continue
+
+        writecomments('build:' + build['vercode'])
         mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
 
         def write_builditem(key, value):
 
-            if key in ['version', 'vercode', 'origlines', 'type']:
+            if key in ['version', 'vercode']:
                 return
 
-            t = flagtype(key)
-            if t == 'bool' and value == False:
+            if value == flag_defaults[key]:
                 return
 
+            t = flagtype(key)
+
             logging.debug("...writing {0} : {1}".format(key, value))
             outline = '    %s=' % key
 
@@ -837,28 +894,26 @@ def write_metadata(dest, app):
             outline += '\n'
             mf.write(outline)
 
-        for key in ordered_flags:
-            if key in build:
-                write_builditem(key, build[key])
+        for flag in flag_defaults:
+            value = build[flag]
+            if value:
+                write_builditem(flag, value)
         mf.write('\n')
 
-    if 'Maintainer Notes' in app:
+    if app['Maintainer Notes']:
         writefield('Maintainer Notes', '')
         for line in app['Maintainer Notes']:
             mf.write("%s\n" % line)
         mf.write('.\n')
         mf.write('\n')
 
-    if app['Archive Policy']:
-        writefield('Archive Policy')
+    writefield_nonempty('Archive Policy')
     writefield('Auto Update Mode')
     writefield('Update Check Mode')
-    if app['Update Check Ignore']:
-        writefield('Update Check Ignore')
-    if app['Vercode Operation']:
-        writefield('Vercode Operation')
-    if app['Update Check Data']:
-        writefield('Update Check Data')
+    writefield_nonempty('Update Check Ignore')
+    writefield_nonempty('Vercode Operation')
+    writefield_nonempty('Update Check Name')
+    writefield_nonempty('Update Check Data')
     if app['Current Version']:
         writefield('Current Version')
         writefield('Current Version Code')