1
0
mirror of https://gitlab.com/fdroid/fdroidserver.git synced 2024-11-14 11:00:10 +01:00

Merge branch 'metadata-checks'

This commit is contained in:
Daniel Martí 2013-11-20 18:54:40 +01:00
commit 47ec7bff2d
10 changed files with 758 additions and 697 deletions

View File

@ -29,7 +29,7 @@ import time
import json import json
from optparse import OptionParser from optparse import OptionParser
import common import common, metadata
from common import BuildException, VCSException, FDroidPopen from common import BuildException, VCSException, FDroidPopen
def get_builder_vm_id(): def get_builder_vm_id():
@ -816,7 +816,7 @@ def main():
sys.exit(1) sys.exit(1)
# Get all apps... # Get all apps...
apps = common.read_metadata(xref=not options.onserver) apps = metadata.read_metadata(xref=not options.onserver)
log_dir = 'logs' log_dir = 'logs'
if not os.path.isdir(log_dir): if not os.path.isdir(log_dir):

View File

@ -28,7 +28,7 @@ from optparse import OptionParser
import traceback import traceback
import HTMLParser import HTMLParser
from distutils.version import LooseVersion from distutils.version import LooseVersion
import common import common, metadata
from common import BuildException from common import BuildException
from common import VCSException from common import VCSException
@ -295,7 +295,7 @@ def main():
config = common.read_config(options) config = common.read_config(options)
# Get all apps... # Get all apps...
apps = common.read_metadata(options.verbose) apps = metadata.read_metadata(options.verbose)
# Filter apps according to command-line options # Filter apps according to command-line options
if options.package: if options.package:
@ -453,7 +453,7 @@ def main():
if writeit: if writeit:
metafile = os.path.join('metadata', app['id'] + '.txt') metafile = os.path.join('metadata', app['id'] + '.txt')
common.write_metadata(metafile, app) metadata.write_metadata(metafile, app)
if options.commit and logmsg: if options.commit and logmsg:
print "Commiting update for " + metafile print "Commiting update for " + metafile
gitcmd = ["git", "commit", "-m", gitcmd = ["git", "commit", "-m",

View File

@ -23,20 +23,15 @@ import stat
import subprocess import subprocess
import time import time
import operator import operator
import cgi
import Queue import Queue
import threading import threading
import magic import magic
import metadata
config = None config = None
options = None options = None
# These can only contain 'yes' or 'no'
bool_keys = (
'submodules', 'oldsdkloc',
'forceversion', 'forcevercode',
'fixtrans', 'fixapos', 'novcheck')
def read_config(opts, config_file='config.py'): def read_config(opts, config_file='config.py'):
"""Read the repository config """Read the repository config
@ -52,7 +47,7 @@ def read_config(opts, config_file='config.py'):
sys.exit(2) sys.exit(2)
st = os.stat(config_file) st = os.stat(config_file)
if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO: if st.st_mode & stat.S_IRWXG or st.st_mode & stat.S_IRWXO:
print("WARNING: unsafe permissions on config.py (should be 0600)!") print "WARNING: unsafe permissions on config.py (should be 0600)!"
options = opts options = opts
if not hasattr(options, 'verbose'): if not hasattr(options, 'verbose'):
@ -73,6 +68,11 @@ def read_config(opts, config_file='config.py'):
execfile(config_file, config) execfile(config_file, config)
return config return config
def getapkname(app, build):
return "%s_%s.apk" % (app['id'], build['vercode'])
def getsrcname(app, build):
return "%s_%s_src.tar.gz" % (app['id'], build['vercode'])
def getvcs(vcstype, remote, local): def getvcs(vcstype, remote, local):
if vcstype == 'git': if vcstype == 'git':
@ -95,7 +95,7 @@ def getsrclibvcs(name):
srclib_path = os.path.join('srclibs', name + ".txt") srclib_path = os.path.join('srclibs', name + ".txt")
if not os.path.exists(srclib_path): if not os.path.exists(srclib_path):
raise VCSException("Missing srclib " + name) raise VCSException("Missing srclib " + name)
return parse_srclib(srclib_path)['Repo Type'] return metadata.parse_srclib(srclib_path)['Repo Type']
class vcs: class vcs:
def __init__(self, remote, local): def __init__(self, remote, local):
@ -457,625 +457,6 @@ class vcs_bzr(vcs):
return [tag.split(' ')[0].strip() for tag in return [tag.split(' ')[0].strip() for tag in
p.communicate()[0].splitlines()] p.communicate()[0].splitlines()]
# Get the type expected for a given metadata field.
def metafieldtype(name):
if name in ['Description', 'Maintainer Notes']:
return 'multiline'
if name == 'Requires Root':
return 'flag'
if name == 'Build Version':
return 'build'
if name == 'Build':
return 'buildv2'
if name == 'Use Built':
return 'obsolete'
return 'string'
# Parse metadata for a single application.
#
# 'metafile' - the filename to read. The package id for the application comes
# from this filename. Pass None to get a blank entry.
#
# Returns a dictionary containing all the details of the application. There are
# two major kinds of information in the dictionary. Keys beginning with capital
# letters correspond directory to identically named keys in the metadata file.
# Keys beginning with lower case letters are generated in one way or another,
# and are not found verbatim in the metadata.
#
# 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
# a tuple of the form (field, comment) where field is
# the name of the field it preceded in the metadata
# file. Where field is None, the comment goes at the
# end of the file. Alternatively, 'build:version' is
# for a comment before a particular build version.
# 'descriptionlines' - original lines of description as formatted in the
# metadata file.
#
def parse_metadata(metafile):
def parse_buildline(lines):
value = "".join(lines)
parts = [p.replace("\\,", ",")
for p in re.split(r"(?<!\\),", value)]
if len(parts) < 3:
raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
thisbuild = {}
thisbuild['origlines'] = lines
thisbuild['version'] = parts[0]
thisbuild['vercode'] = parts[1]
try:
int(thisbuild['vercode'])
except:
raise MetaDataException("Invalid version code for build in " + metafile.name)
if parts[2].startswith('!'):
# For backwards compatibility, handle old-style disabling,
# including attempting to extract the commit from the message
thisbuild['disable'] = parts[2][1:]
commit = 'unknown - see disabled'
index = parts[2].rfind('at ')
if index != -1:
commit = parts[2][index+3:]
if commit.endswith(')'):
commit = commit[:-1]
thisbuild['commit'] = commit
else:
thisbuild['commit'] = parts[2]
for p in parts[3:]:
pk, pv = p.split('=', 1)
thisbuild[pk.strip()] = pv
return thisbuild
def add_comments(key):
if not curcomments:
return
for comment in curcomments:
thisinfo['comments'].append((key, comment))
del curcomments[:]
thisinfo = {}
if metafile:
if not isinstance(metafile, file):
metafile = open(metafile, "r")
thisinfo['id'] = metafile.name[9:-4]
else:
thisinfo['id'] = None
# Defaults for fields that come from metadata...
thisinfo['Name'] = None
thisinfo['Auto Name'] = ''
thisinfo['Categories'] = 'None'
thisinfo['Description'] = []
thisinfo['Summary'] = ''
thisinfo['License'] = 'Unknown'
thisinfo['Web Site'] = ''
thisinfo['Source Code'] = ''
thisinfo['Issue Tracker'] = ''
thisinfo['Donate'] = None
thisinfo['FlattrID'] = None
thisinfo['Bitcoin'] = None
thisinfo['Litecoin'] = None
thisinfo['Disabled'] = None
thisinfo['AntiFeatures'] = None
thisinfo['Archive Policy'] = None
thisinfo['Update Check Mode'] = 'None'
thisinfo['Vercode Operation'] = None
thisinfo['Auto Update Mode'] = 'None'
thisinfo['Current Version'] = ''
thisinfo['Current Version Code'] = '0'
thisinfo['Repo Type'] = ''
thisinfo['Repo'] = ''
thisinfo['Requires Root'] = False
thisinfo['No Source Since'] = ''
# General defaults...
thisinfo['builds'] = []
thisinfo['comments'] = []
if metafile is None:
return thisinfo
mode = 0
buildlines = []
curcomments = []
curbuild = None
for line in 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:
raise MetaDataException("No commit specified for {0} in {1}".format(
curbuild['version'], metafile.name))
thisinfo['builds'].append(curbuild)
add_comments('build:' + curbuild['version'])
mode = 0
else:
if line.endswith('\\'):
buildlines.append(line[:-1].lstrip())
else:
buildlines.append(line.lstrip())
bl = ''.join(buildlines)
bv = bl.split('=', 1)
if len(bv) != 2:
raise MetaDataException("Invalid build flag at {0} in {1}".
format(buildlines[0], metafile.name))
name, val = bv
if name in curbuild:
raise MetaDataException("Duplicate definition on {0} in version {1} of {2}".
format(name, curbuild['version'], metafile.name))
curbuild[name] = val.lstrip()
buildlines = []
if mode == 0:
if not line:
continue
if line.startswith("#"):
curcomments.append(line)
continue
index = line.find(':')
if index == -1:
raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
field = line[:index]
value = line[index+1:]
# Translate obsolete fields...
if field == 'Market Version':
field = 'Current Version'
if field == 'Market Version Code':
field = 'Current Version Code'
fieldtype = metafieldtype(field)
if fieldtype not in ['build', 'buildv2']:
add_comments(field)
if fieldtype == 'multiline':
mode = 1
thisinfo[field] = []
if value:
raise MetaDataException("Unexpected text on same line as " + field + " in " + metafile.name)
elif fieldtype == 'string':
if field == 'Category' and thisinfo['Categories'] == 'None':
thisinfo['Categories'] = value.replace(';',',')
thisinfo[field] = value
elif fieldtype == 'flag':
if value == 'Yes':
thisinfo[field] = True
elif value == 'No':
thisinfo[field] = False
else:
raise MetaDataException("Expected Yes or No for " + field + " in " + metafile.name)
elif fieldtype == 'build':
if value.endswith("\\"):
mode = 2
buildlines = [value[:-1]]
else:
thisinfo['builds'].append(parse_buildline([value]))
add_comments('build:' + thisinfo['builds'][-1]['version'])
elif fieldtype == 'buildv2':
curbuild = {}
vv = value.split(',')
if len(vv) != 2:
raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'.
format(value, metafile.name))
curbuild['version'] = vv[0]
curbuild['vercode'] = vv[1]
try:
int(curbuild['vercode'])
except:
raise MetaDataException("Invalid version code for build in " + metafile.name)
buildlines = []
mode = 3
elif fieldtype == 'obsolete':
pass # Just throw it away!
else:
raise MetaDataException("Unrecognised field type for " + field + " in " + metafile.name)
elif mode == 1: # Multiline field
if line == '.':
mode = 0
else:
thisinfo[field].append(line)
elif mode == 2: # Line continuation mode in Build Version
if line.endswith("\\"):
buildlines.append(line[:-1])
else:
buildlines.append(line)
thisinfo['builds'].append(
parse_buildline(buildlines))
add_comments('build:' + thisinfo['builds'][-1]['version'])
mode = 0
add_comments(None)
for key in bool_keys:
for build in thisinfo['builds']:
if key not in build:
build[key] = False
continue
if build[key] == 'yes':
build[key] = True
elif build[key] == 'no':
build[key] = False
else:
raise MetaDataException("Invalid value %s assigned to boolean build flag %s"
% (build[key], key))
# Mode at end of file should always be 0...
if mode == 1:
raise MetaDataException(field + " not terminated in " + metafile.name)
elif mode == 2:
raise MetaDataException("Unterminated continuation in " + metafile.name)
elif mode == 3:
raise MetaDataException("Unterminated build in " + metafile.name)
if not thisinfo['Description']:
thisinfo['Description'].append('No description available')
# Validate archive policy...
if thisinfo['Archive Policy']:
if not thisinfo['Archive Policy'].endswith(' versions'):
raise MetaDataException("Invalid archive policy")
try:
versions = int(thisinfo['Archive Policy'][:-9])
if versions < 1 or versions > 20:
raise MetaDataException("Silly number of versions for archive policy")
except:
raise MetaDataException("Incomprehensible number of versions for archive policy")
# Ensure all AntiFeatures are recognised...
if thisinfo['AntiFeatures']:
parts = thisinfo['AntiFeatures'].split(",")
for part in parts:
if (part != "Ads" and
part != "Tracking" and
part != "NonFreeNet" and
part != "NonFreeDep" and
part != "NonFreeAdd"):
raise MetaDataException("Unrecognised antifeature '" + part + "' in " \
+ metafile.name)
return thisinfo
def getvercode(build):
return "%s" % (build['vercode'])
def getapkname(app, build):
return "%s_%s.apk" % (app['id'], getvercode(build))
def getsrcname(app, build):
return "%s_%s_src.tar.gz" % (app['id'], getvercode(build))
# Write a metadata file.
#
# 'dest' - The path to the output file
# 'app' - The app data
def write_metadata(dest, app):
def writecomments(key):
written = 0
for pf, comment in app['comments']:
if pf == key:
mf.write(comment + '\n')
written += 1
if options.verbose and written > 0:
print "...writing comments for " + (key if key else 'EOF')
def writefield(field, value=None):
writecomments(field)
if value is None:
value = app[field]
mf.write(field + ':' + value + '\n')
mf = open(dest, 'w')
if app['Disabled']:
writefield('Disabled')
if app['AntiFeatures']:
writefield('AntiFeatures')
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')
mf.write('\n')
if app['Name']:
writefield('Name')
if app['Auto Name']:
writefield('Auto Name')
writefield('Summary')
writefield('Description', '')
for line in app['Description']:
mf.write(line + '\n')
mf.write('.\n')
mf.write('\n')
if app['Requires Root']:
writefield('Requires Root', 'Yes')
mf.write('\n')
if app['Repo Type']:
writefield('Repo Type')
writefield('Repo')
mf.write('\n')
for build in app['builds']:
writecomments('build:' + build['version'])
mf.write('Build:')
mf.write("%s,%s\n" % (
build['version'],
getvercode(build)))
# This defines the preferred order for the build items - as in the
# manual, they're roughly in order of application.
keyorder = ['disable', 'commit', 'subdir', 'submodules', 'init',
'gradle', 'maven', 'oldsdkloc', 'target', 'compilesdk',
'update', 'encoding', 'forceversion', 'forcevercode', 'rm',
'fixtrans', 'fixapos', 'extlibs', 'srclibs', 'patch',
'prebuild', 'scanignore', 'scandelete', 'build', 'buildjni',
'preassemble', 'bindir', 'antcommand', 'novcheck']
def write_builditem(key, value):
if key not in ['version', 'vercode', 'origlines']:
if key in bool_keys:
if not value:
return
value = 'yes'
if options.verbose:
print "...writing {0} : {1}".format(key, value)
outline = ' %s=' % key
outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
outline += '\n'
mf.write(outline)
for key in keyorder:
if key in build:
write_builditem(key, build[key])
for key, value in build.iteritems():
if not key in keyorder:
write_builditem(key, value)
mf.write('\n')
if 'Maintainer Notes' in app:
writefield('Maintainer Notes', '')
for line in app['Maintainer Notes']:
mf.write(line + '\n')
mf.write('.\n')
mf.write('\n')
if app['Archive Policy']:
writefield('Archive Policy')
writefield('Auto Update Mode')
writefield('Update Check Mode')
if app['Vercode Operation']:
writefield('Vercode Operation')
if 'Update Check Data' in app:
writefield('Update Check Data')
if app['Current Version']:
writefield('Current Version')
writefield('Current Version Code')
mf.write('\n')
if app['No Source Since']:
writefield('No Source Since')
mf.write('\n')
writecomments(None)
mf.close()
# Read all metadata. Returns a list of 'app' objects (which are dictionaries as
# returned by the parse_metadata function.
def read_metadata(xref=True, package=None):
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'))):
if package is None or metafile == os.path.join('metadata', package + '.txt'):
try:
appinfo = parse_metadata(metafile)
except Exception, e:
raise MetaDataException("Problem reading metadata file %s: - %s" % (metafile, str(e)))
apps.append(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:
try:
description_html(app['Description'], linkres)
except Exception, e:
raise MetaDataException("Problem with description of " + app['id'] +
" - " + str(e))
return apps
# Formatter for descriptions. Create an instance, and call parseline() with
# each line of the description source from the metadata. At the end, call
# end() and then text_plain, text_wiki and text_html will contain the result.
class DescriptionFormatter:
stNONE = 0
stPARA = 1
stUL = 2
stOL = 3
bold = False
ital = False
state = stNONE
text_plain = ''
text_wiki = ''
text_html = ''
linkResolver = None
def __init__(self, linkres):
self.linkResolver = linkres
def endcur(self, notstates=None):
if notstates and self.state in notstates:
return
if self.state == self.stPARA:
self.endpara()
elif self.state == self.stUL:
self.endul()
elif self.state == self.stOL:
self.endol()
def endpara(self):
self.text_plain += '\n'
self.text_html += '</p>'
self.state = self.stNONE
def endul(self):
self.text_html += '</ul>'
self.state = self.stNONE
def endol(self):
self.text_html += '</ol>'
self.state = self.stNONE
def formatted(self, txt, html):
formatted = ''
if html:
txt = cgi.escape(txt)
while True:
index = txt.find("''")
if index == -1:
return formatted + txt
formatted += txt[:index]
txt = txt[index:]
if txt.startswith("'''"):
if html:
if self.bold:
formatted += '</b>'
else:
formatted += '<b>'
self.bold = not self.bold
txt = txt[3:]
else:
if html:
if self.ital:
formatted += '</i>'
else:
formatted += '<i>'
self.ital = not self.ital
txt = txt[2:]
def linkify(self, txt):
linkified_plain = ''
linkified_html = ''
while True:
index = txt.find("[")
if index == -1:
return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
linkified_plain += self.formatted(txt[:index], False)
linkified_html += self.formatted(txt[:index], True)
txt = txt[index:]
if txt.startswith("[["):
index = txt.find("]]")
if index == -1:
raise MetaDataException("Unterminated ]]")
url = txt[2:index]
if self.linkResolver:
url, urltext = self.linkResolver(url)
else:
urltext = url
linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
linkified_plain += urltext
txt = txt[index+2:]
else:
index = txt.find("]")
if index == -1:
raise MetaDataException("Unterminated ]")
url = txt[1:index]
index2 = url.find(' ')
if index2 == -1:
urltxt = url
else:
urltxt = url[index2 + 1:]
url = url[:index2]
linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
linkified_plain += urltxt
if urltxt != url:
linkified_plain += ' (' + url + ')'
txt = txt[index+1:]
def addtext(self, txt):
p, h = self.linkify(txt)
self.text_plain += p
self.text_html += h
def parseline(self, line):
self.text_wiki += line + '\n'
if not line:
self.endcur()
elif line.startswith('*'):
self.endcur([self.stUL])
if self.state != self.stUL:
self.text_html += '<ul>'
self.state = self.stUL
self.text_html += '<li>'
self.text_plain += '*'
self.addtext(line[1:])
self.text_html += '</li>'
elif line.startswith('#'):
self.endcur([self.stOL])
if self.state != self.stOL:
self.text_html += '<ol>'
self.state = self.stOL
self.text_html += '<li>'
self.text_plain += '*' #TODO: lazy - put the numbers in!
self.addtext(line[1:])
self.text_html += '</li>'
else:
self.endcur([self.stPARA])
if self.state == self.stNONE:
self.text_html += '<p>'
self.state = self.stPARA
elif self.state == self.stPARA:
self.text_html += ' '
self.text_plain += ' '
self.addtext(line)
def end(self):
self.endcur()
# Parse multiple lines of description as written in a metadata file, returning
# a single string in plain text format.
def description_plain(lines, linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_plain
# Parse multiple lines of description as written in a metadata file, returning
# a single string in wiki format. Used for the Maintainer Notes field as well,
# because it's the same format.
def description_wiki(lines):
ps = DescriptionFormatter(None)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_wiki
# Parse multiple lines of description as written in a metadata file, returning
# a single string in HTML format.
def description_html(lines,linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_html
def retrieve_string(xml_dir, string): def retrieve_string(xml_dir, string):
if not string.startswith('@string/'): if not string.startswith('@string/'):
return string.replace("\\'","'") return string.replace("\\'","'")
@ -1248,48 +629,6 @@ class VCSException(Exception):
def __str__(self): def __str__(self):
return repr(self.value) return repr(self.value)
class MetaDataException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
def parse_srclib(metafile, **kw):
thisinfo = {}
if metafile and not isinstance(metafile, file):
metafile = open(metafile, "r")
# Defaults for fields that come from metadata
thisinfo['Repo Type'] = ''
thisinfo['Repo'] = ''
thisinfo['Subdir'] = None
thisinfo['Prepare'] = None
thisinfo['Srclibs'] = None
thisinfo['Update Project'] = None
if metafile is None:
return thisinfo
for line in metafile:
line = line.rstrip('\r\n')
if not line or line.startswith("#"):
continue
index = line.find(':')
if index == -1:
raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
field = line[:index]
value = line[index+1:]
if field == "Subdir":
thisinfo[field] = value.split(',')
else:
thisinfo[field] = value
return thisinfo
# Get the specified source library. # Get the specified source library.
# Returns the path to it. Normally this is the path to be used when referencing # Returns the path to it. Normally this is the path to be used when referencing
# it, which may be a subdirectory of the actual project. If you want the base # it, which may be a subdirectory of the actual project. If you want the base
@ -1314,7 +653,7 @@ def getsrclib(spec, srclib_dir, srclibpaths=[], subdir=None, basepath=False,
if not os.path.exists(srclib_path): if not os.path.exists(srclib_path):
raise BuildException('srclib ' + name + ' not found.') raise BuildException('srclib ' + name + ' not found.')
srclib = parse_srclib(srclib_path) srclib = metadata.parse_srclib(srclib_path)
sdir = os.path.join(srclib_dir, name) sdir = os.path.join(srclib_dir, name)

View File

@ -22,7 +22,7 @@ import os
import shutil import shutil
import urllib import urllib
from optparse import OptionParser from optparse import OptionParser
import common import common, metadata
# Get the repo type and address from the given web page. The page is scanned # Get the repo type and address from the given web page. The page is scanned
# in a rather naive manner for 'git clone xxxx', 'hg clone xxxx', etc, and # in a rather naive manner for 'git clone xxxx', 'hg clone xxxx', etc, and
@ -114,7 +114,7 @@ def main():
os.makedirs(tmp_dir) os.makedirs(tmp_dir)
# Get all apps... # Get all apps...
apps = common.read_metadata() apps = metadata.read_metadata()
# Figure out what kind of project it is... # Figure out what kind of project it is...
projecttype = None projecttype = None
@ -249,7 +249,7 @@ def main():
sys.exit(1) sys.exit(1)
# Construct the metadata... # Construct the metadata...
app = common.parse_metadata(None) app = metadata.parse_metadata(None)
app['id'] = package app['id'] = package
app['Web Site'] = website app['Web Site'] = website
app['Source Code'] = sourcecode app['Source Code'] = sourcecode
@ -281,7 +281,7 @@ def main():
f.write(repotype + ' ' + repo) f.write(repotype + ' ' + repo)
metafile = os.path.join('metadata', package + '.txt') metafile = os.path.join('metadata', package + '.txt')
common.write_metadata(metafile, app) metadata.write_metadata(metafile, app)
print "Wrote " + metafile print "Wrote " + metafile

722
fdroidserver/metadata.py Normal file
View File

@ -0,0 +1,722 @@
# -*- coding: utf-8 -*-
#
# common.py - part of the FDroid server tools
# Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com
# Copyright (C) 2013 Daniel Martí <mvdan@mvdan.cc>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os, re, glob
import cgi
class MetaDataException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
# Designates a metadata field type and checks that it matches
#
# 'name' - The long name of the field type
# 'matching' - List of possible values or regex expression
# 'sep' - Separator to use if value may be a list
# 'fields' - Metadata fields (Field:Value) of this type
# 'attrs' - Build attributes (attr=value) of this type
#
class FieldType():
def __init__(self, name, matching, sep, fields, attrs):
self.name = name
if type(matching) is str:
self.matching = re.compile(matching)
elif type(matching) is list:
self.matching = matching
self.sep = sep
self.fields = fields
self.attrs = attrs
def _assert_regex(self, values, appid):
for v in values:
if not self.matching.match(v):
raise MetaDataException("'%s' is not a valid %s in %s"
% (v, self.name, appid))
def _assert_list(self, values, appid):
for v in values:
if v not in self.matching:
raise MetaDataException("'%s' is not a valid %s in %s"
% (v, self.name, appid))
def check(self, value, appid):
if type(value) is not str or not value:
return
if self.sep is not None:
values = value.split(self.sep)
else:
values = [value]
if type(self.matching) is list:
self._assert_list(values, appid)
else:
self._assert_regex(values, appid)
# Generic value types
valuetypes = {
'int' : FieldType("Integer",
r'^[0-9]+$', None,
[ 'FlattrID' ],
[ 'vercode' ]),
'http' : FieldType("HTTP link",
r'^http[s]?://', None,
[ "Web Site", "Source Code", "Issue Tracker", "Donate" ], []),
'bitcoin' : FieldType("Bitcoin address",
r'^[a-zA-Z0-9]{27,34}$', None,
[ "Bitcoin" ],
[ ]),
'litecoin' : FieldType("Litecoin address",
r'^[a-zA-Z0-9]{27,34}$', None,
[ "Bitcoin" ],
[ ]),
'archive' : FieldType("Archive Policy",
r'^[0-9]+ versions$', None,
[ "Archive Policy" ],
[ ]),
'bool' : FieldType("Boolean",
['yes', 'no'], None,
[ ],
[ 'submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
'fixtrans', 'fixapos', 'novcheck' ]),
'Bool' : FieldType("Boolean",
['Yes', 'No'], None,
[ "Requires Root" ],
[ ]),
'antifeatures' : FieldType("Anti-Feature",
[ "Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd" ], ',',
[ "AntiFeatures" ],
[ ])
}
# Check an app's metadata information for integrity errors
def check_metadata(info):
for k, t in valuetypes.iteritems():
for field in t.fields:
if field in info:
t.check(info[field], info['id'])
if k == 'Bool':
info[field] = info[field] == "Yes"
for build in info['builds']:
for attr in t.attrs:
if attr in build:
t.check(build[attr], info['id'])
if k == 'bool':
build[attr] = build[attr] == "yes"
elif k == 'bool':
build[attr] = False
# Formatter for descriptions. Create an instance, and call parseline() with
# each line of the description source from the metadata. At the end, call
# end() and then text_plain, text_wiki and text_html will contain the result.
class DescriptionFormatter:
stNONE = 0
stPARA = 1
stUL = 2
stOL = 3
bold = False
ital = False
state = stNONE
text_plain = ''
text_wiki = ''
text_html = ''
linkResolver = None
def __init__(self, linkres):
self.linkResolver = linkres
def endcur(self, notstates=None):
if notstates and self.state in notstates:
return
if self.state == self.stPARA:
self.endpara()
elif self.state == self.stUL:
self.endul()
elif self.state == self.stOL:
self.endol()
def endpara(self):
self.text_plain += '\n'
self.text_html += '</p>'
self.state = self.stNONE
def endul(self):
self.text_html += '</ul>'
self.state = self.stNONE
def endol(self):
self.text_html += '</ol>'
self.state = self.stNONE
def formatted(self, txt, html):
formatted = ''
if html:
txt = cgi.escape(txt)
while True:
index = txt.find("''")
if index == -1:
return formatted + txt
formatted += txt[:index]
txt = txt[index:]
if txt.startswith("'''"):
if html:
if self.bold:
formatted += '</b>'
else:
formatted += '<b>'
self.bold = not self.bold
txt = txt[3:]
else:
if html:
if self.ital:
formatted += '</i>'
else:
formatted += '<i>'
self.ital = not self.ital
txt = txt[2:]
def linkify(self, txt):
linkified_plain = ''
linkified_html = ''
while True:
index = txt.find("[")
if index == -1:
return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
linkified_plain += self.formatted(txt[:index], False)
linkified_html += self.formatted(txt[:index], True)
txt = txt[index:]
if txt.startswith("[["):
index = txt.find("]]")
if index == -1:
raise MetaDataException("Unterminated ]]")
url = txt[2:index]
if self.linkResolver:
url, urltext = self.linkResolver(url)
else:
urltext = url
linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
linkified_plain += urltext
txt = txt[index+2:]
else:
index = txt.find("]")
if index == -1:
raise MetaDataException("Unterminated ]")
url = txt[1:index]
index2 = url.find(' ')
if index2 == -1:
urltxt = url
else:
urltxt = url[index2 + 1:]
url = url[:index2]
linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
linkified_plain += urltxt
if urltxt != url:
linkified_plain += ' (' + url + ')'
txt = txt[index+1:]
def addtext(self, txt):
p, h = self.linkify(txt)
self.text_plain += p
self.text_html += h
def parseline(self, line):
self.text_wiki += "%s\n" % line
if not line:
self.endcur()
elif line.startswith('*'):
self.endcur([self.stUL])
if self.state != self.stUL:
self.text_html += '<ul>'
self.state = self.stUL
self.text_html += '<li>'
self.text_plain += '*'
self.addtext(line[1:])
self.text_html += '</li>'
elif line.startswith('#'):
self.endcur([self.stOL])
if self.state != self.stOL:
self.text_html += '<ol>'
self.state = self.stOL
self.text_html += '<li>'
self.text_plain += '*' #TODO: lazy - put the numbers in!
self.addtext(line[1:])
self.text_html += '</li>'
else:
self.endcur([self.stPARA])
if self.state == self.stNONE:
self.text_html += '<p>'
self.state = self.stPARA
elif self.state == self.stPARA:
self.text_html += ' '
self.text_plain += ' '
self.addtext(line)
def end(self):
self.endcur()
# Parse multiple lines of description as written in a metadata file, returning
# a single string in plain text format.
def description_plain(lines, linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_plain
# Parse multiple lines of description as written in a metadata file, returning
# a single string in wiki format. Used for the Maintainer Notes field as well,
# because it's the same format.
def description_wiki(lines):
ps = DescriptionFormatter(None)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_wiki
# Parse multiple lines of description as written in a metadata file, returning
# a single string in HTML format.
def description_html(lines,linkres):
ps = DescriptionFormatter(linkres)
for line in lines:
ps.parseline(line)
ps.end()
return ps.text_html
def parse_srclib(metafile, **kw):
thisinfo = {}
if metafile and not isinstance(metafile, file):
metafile = open(metafile, "r")
# Defaults for fields that come from metadata
thisinfo['Repo Type'] = ''
thisinfo['Repo'] = ''
thisinfo['Subdir'] = None
thisinfo['Prepare'] = None
thisinfo['Srclibs'] = None
thisinfo['Update Project'] = None
if metafile is None:
return thisinfo
for line in metafile:
line = line.rstrip('\r\n')
if not line or line.startswith("#"):
continue
index = line.find(':')
if index == -1:
raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
field = line[:index]
value = line[index+1:]
if field == "Subdir":
thisinfo[field] = value.split(',')
else:
thisinfo[field] = value
return thisinfo
# Read all metadata. Returns a list of 'app' objects (which are dictionaries as
# returned by the parse_metadata function.
def read_metadata(xref=True, package=None):
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'))):
if package is None or metafile == os.path.join('metadata', package + '.txt'):
try:
appinfo = parse_metadata(metafile)
except Exception, e:
raise MetaDataException("Problem reading metadata file %s: - %s" % (metafile, str(e)))
check_metadata(appinfo)
apps.append(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:
try:
description_html(app['Description'], linkres)
except Exception, e:
raise MetaDataException("Problem with description of " + app['id'] +
" - " + str(e))
return apps
# Get the type expected for a given metadata field.
def metafieldtype(name):
if name in ['Description', 'Maintainer Notes']:
return 'multiline'
if name == 'Build Version':
return 'build'
if name == 'Build':
return 'buildv2'
if name == 'Use Built':
return 'obsolete'
return 'string'
# Parse metadata for a single application.
#
# 'metafile' - the filename to read. The package id for the application comes
# from this filename. Pass None to get a blank entry.
#
# Returns a dictionary containing all the details of the application. There are
# two major kinds of information in the dictionary. Keys beginning with capital
# letters correspond directory to identically named keys in the metadata file.
# Keys beginning with lower case letters are generated in one way or another,
# and are not found verbatim in the metadata.
#
# 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
# a tuple of the form (field, comment) where field is
# the name of the field it preceded in the metadata
# file. Where field is None, the comment goes at the
# end of the file. Alternatively, 'build:version' is
# for a comment before a particular build version.
# 'descriptionlines' - original lines of description as formatted in the
# metadata file.
#
def parse_metadata(metafile):
def parse_buildline(lines):
value = "".join(lines)
parts = [p.replace("\\,", ",")
for p in re.split(r"(?<!\\),", value)]
if len(parts) < 3:
raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
thisbuild = {}
thisbuild['origlines'] = lines
thisbuild['version'] = parts[0]
thisbuild['vercode'] = parts[1]
if parts[2].startswith('!'):
# For backwards compatibility, handle old-style disabling,
# including attempting to extract the commit from the message
thisbuild['disable'] = parts[2][1:]
commit = 'unknown - see disabled'
index = parts[2].rfind('at ')
if index != -1:
commit = parts[2][index+3:]
if commit.endswith(')'):
commit = commit[:-1]
thisbuild['commit'] = commit
else:
thisbuild['commit'] = parts[2]
for p in parts[3:]:
pk, pv = p.split('=', 1)
thisbuild[pk.strip()] = pv
return thisbuild
def add_comments(key):
if not curcomments:
return
for comment in curcomments:
thisinfo['comments'].append((key, comment))
del curcomments[:]
thisinfo = {}
if metafile:
if not isinstance(metafile, file):
metafile = open(metafile, "r")
thisinfo['id'] = metafile.name[9:-4]
else:
thisinfo['id'] = None
# Defaults for fields that come from metadata...
thisinfo['Name'] = None
thisinfo['Auto Name'] = ''
thisinfo['Categories'] = 'None'
thisinfo['Description'] = []
thisinfo['Summary'] = ''
thisinfo['License'] = 'Unknown'
thisinfo['Web Site'] = ''
thisinfo['Source Code'] = ''
thisinfo['Issue Tracker'] = ''
thisinfo['Donate'] = None
thisinfo['FlattrID'] = None
thisinfo['Bitcoin'] = None
thisinfo['Litecoin'] = None
thisinfo['Disabled'] = None
thisinfo['AntiFeatures'] = None
thisinfo['Archive Policy'] = None
thisinfo['Update Check Mode'] = 'None'
thisinfo['Vercode Operation'] = None
thisinfo['Auto Update Mode'] = 'None'
thisinfo['Current Version'] = ''
thisinfo['Current Version Code'] = '0'
thisinfo['Repo Type'] = ''
thisinfo['Repo'] = ''
thisinfo['Requires Root'] = False
thisinfo['No Source Since'] = ''
# General defaults...
thisinfo['builds'] = []
thisinfo['comments'] = []
if metafile is None:
return thisinfo
mode = 0
buildlines = []
curcomments = []
curbuild = None
for line in 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:
raise MetaDataException("No commit specified for {0} in {1}".format(
curbuild['version'], metafile.name))
thisinfo['builds'].append(curbuild)
add_comments('build:' + curbuild['version'])
mode = 0
else:
if line.endswith('\\'):
buildlines.append(line[:-1].lstrip())
else:
buildlines.append(line.lstrip())
bl = ''.join(buildlines)
bv = bl.split('=', 1)
if len(bv) != 2:
raise MetaDataException("Invalid build flag at {0} in {1}".
format(buildlines[0], metafile.name))
name, val = bv
if name in curbuild:
raise MetaDataException("Duplicate definition on {0} in version {1} of {2}".
format(name, curbuild['version'], metafile.name))
curbuild[name] = val.lstrip()
buildlines = []
if mode == 0:
if not line:
continue
if line.startswith("#"):
curcomments.append(line)
continue
index = line.find(':')
if index == -1:
raise MetaDataException("Invalid metadata in " + metafile.name + " at: " + line)
field = line[:index]
value = line[index+1:]
# Translate obsolete fields...
if field == 'Market Version':
field = 'Current Version'
if field == 'Market Version Code':
field = 'Current Version Code'
fieldtype = metafieldtype(field)
if fieldtype not in ['build', 'buildv2']:
add_comments(field)
if fieldtype == 'multiline':
mode = 1
thisinfo[field] = []
if value:
raise MetaDataException("Unexpected text on same line as " + field + " in " + metafile.name)
elif fieldtype == 'string':
if field == 'Category' and thisinfo['Categories'] == 'None':
thisinfo['Categories'] = value.replace(';',',')
thisinfo[field] = value
elif fieldtype == 'build':
if value.endswith("\\"):
mode = 2
buildlines = [value[:-1]]
else:
thisinfo['builds'].append(parse_buildline([value]))
add_comments('build:' + thisinfo['builds'][-1]['version'])
elif fieldtype == 'buildv2':
curbuild = {}
vv = value.split(',')
if len(vv) != 2:
raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'.
format(value, metafile.name))
curbuild['version'] = vv[0]
curbuild['vercode'] = vv[1]
buildlines = []
mode = 3
elif fieldtype == 'obsolete':
pass # Just throw it away!
else:
raise MetaDataException("Unrecognised field type for " + field + " in " + metafile.name)
elif mode == 1: # Multiline field
if line == '.':
mode = 0
else:
thisinfo[field].append(line)
elif mode == 2: # Line continuation mode in Build Version
if line.endswith("\\"):
buildlines.append(line[:-1])
else:
buildlines.append(line)
thisinfo['builds'].append(
parse_buildline(buildlines))
add_comments('build:' + thisinfo['builds'][-1]['version'])
mode = 0
add_comments(None)
# Mode at end of file should always be 0...
if mode == 1:
raise MetaDataException(field + " not terminated in " + metafile.name)
elif mode == 2:
raise MetaDataException("Unterminated continuation in " + metafile.name)
elif mode == 3:
raise MetaDataException("Unterminated build in " + metafile.name)
if not thisinfo['Description']:
thisinfo['Description'].append('No description available')
return thisinfo
# Write a metadata file.
#
# 'dest' - The path to the output file
# 'app' - The app data
def write_metadata(dest, app):
def writecomments(key):
written = 0
for pf, comment in app['comments']:
if pf == key:
mf.write("%s\n" % comment)
written += 1
#if options.verbose and written > 0:
#print "...writing comments for " + (key if key else 'EOF')
def writefield(field, value=None):
writecomments(field)
if value is None:
value = app[field]
mf.write("%s:%s\n" % (field, value))
mf = open(dest, 'w')
if app['Disabled']:
writefield('Disabled')
if app['AntiFeatures']:
writefield('AntiFeatures')
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')
mf.write('\n')
if app['Name']:
writefield('Name')
if app['Auto Name']:
writefield('Auto Name')
writefield('Summary')
writefield('Description', '')
for line in app['Description']:
mf.write("%s\n" % line)
mf.write('.\n')
mf.write('\n')
if app['Requires Root']:
writefield('Requires Root', 'Yes')
mf.write('\n')
if app['Repo Type']:
writefield('Repo Type')
writefield('Repo')
mf.write('\n')
for build in app['builds']:
writecomments('build:' + build['version'])
mf.write("Build:%s,%s\n" % ( build['version'], build['vercode']))
# This defines the preferred order for the build items - as in the
# manual, they're roughly in order of application.
keyorder = ['disable', 'commit', 'subdir', 'submodules', 'init',
'gradle', 'maven', 'oldsdkloc', 'target', 'compilesdk',
'update', 'encoding', 'forceversion', 'forcevercode', 'rm',
'fixtrans', 'fixapos', 'extlibs', 'srclibs', 'patch',
'prebuild', 'scanignore', 'scandelete', 'build', 'buildjni',
'preassemble', 'bindir', 'antcommand', 'novcheck']
def write_builditem(key, value):
if key in ['version', 'vercode', 'origlines']:
return
if key in valuetypes['bool'].attrs:
if not value:
return
value = 'yes'
#if options.verbose:
#print "...writing {0} : {1}".format(key, value)
outline = ' %s=' % key
outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
outline += '\n'
mf.write(outline)
for key in keyorder:
if key in build:
write_builditem(key, build[key])
for key, value in build.iteritems():
if not key in keyorder:
write_builditem(key, value)
mf.write('\n')
if 'Maintainer Notes' in app:
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('Auto Update Mode')
writefield('Update Check Mode')
if app['Vercode Operation']:
writefield('Vercode Operation')
if 'Update Check Data' in app:
writefield('Update Check Data')
if app['Current Version']:
writefield('Current Version')
writefield('Current Version Code')
mf.write('\n')
if app['No Source Since']:
writefield('No Source Since')
mf.write('\n')
writecomments(None)
mf.close()

View File

@ -26,7 +26,7 @@ import md5
import glob import glob
from optparse import OptionParser from optparse import OptionParser
import common import common, metadata
from common import BuildException from common import BuildException
config = None config = None
@ -74,7 +74,7 @@ def main():
# and b) a sane-looking ID that would make its way into the repo. # and b) a sane-looking ID that would make its way into the repo.
# Nonetheless, to be sure, before publishing we check that there are no # Nonetheless, to be sure, before publishing we check that there are no
# collisions, and refuse to do any publishing if that's the case... # collisions, and refuse to do any publishing if that's the case...
apps = common.read_metadata() apps = metadata.read_metadata()
allaliases = [] allaliases = []
for app in apps: for app in apps:
m = md5.new() m = md5.new()

View File

@ -20,7 +20,7 @@
import sys import sys
import os import os
from optparse import OptionParser from optparse import OptionParser
import common import common, metadata
config = None config = None
options = None options = None
@ -40,7 +40,7 @@ def main():
config = common.read_config(options) config = common.read_config(options)
# Get all apps... # Get all apps...
apps = common.read_metadata(package=options.package, xref=False) apps = metadata.read_metadata(package=options.package, xref=False)
if len(apps) == 0 and options.package: if len(apps) == 0 and options.package:
print "No such package" print "No such package"
@ -48,7 +48,7 @@ def main():
for app in apps: for app in apps:
print "Writing " + app['id'] print "Writing " + app['id']
common.write_metadata(os.path.join('metadata', app['id']) + '.txt', app) metadata.write_metadata(os.path.join('metadata', app['id']) + '.txt', app)
print "Finished." print "Finished."

View File

@ -21,7 +21,7 @@ import sys
import os import os
import traceback import traceback
from optparse import OptionParser from optparse import OptionParser
import common import common, metadata
from common import BuildException from common import BuildException
from common import VCSException from common import VCSException
@ -45,7 +45,7 @@ def main():
config = common.read_config(options) config = common.read_config(options)
# Get all apps... # Get all apps...
apps = common.read_metadata() apps = metadata.read_metadata()
# Filter apps according to command-line options # Filter apps according to command-line options
if options.package: if options.package:

View File

@ -25,7 +25,7 @@ import traceback
import glob import glob
from optparse import OptionParser from optparse import OptionParser
import paramiko import paramiko
import common import common, metadata
import socket import socket
import subprocess import subprocess
@ -58,7 +58,7 @@ def main():
sys.exit(1) sys.exit(1)
# Get all metadata-defined apps... # Get all metadata-defined apps...
metaapps = common.read_metadata(options.verbose) metaapps = metadata.read_metadata(options.verbose)
statsdir = 'stats' statsdir = 'stats'
logsdir = os.path.join(statsdir, 'logs') logsdir = os.path.join(statsdir, 'logs')

View File

@ -30,7 +30,7 @@ import pickle
from xml.dom.minidom import Document from xml.dom.minidom import Document
from optparse import OptionParser from optparse import OptionParser
import time import time
import common import common, metadata
from common import MetaDataException from common import MetaDataException
from PIL import Image from PIL import Image
@ -75,11 +75,11 @@ def update_wiki(apps, apks):
wikidata += " - [http://f-droid.org/repository/browse/?fdid=" + app['id'] + " view in repository]\n\n" wikidata += " - [http://f-droid.org/repository/browse/?fdid=" + app['id'] + " view in repository]\n\n"
wikidata += "=Description=\n" wikidata += "=Description=\n"
wikidata += common.description_wiki(app['Description']) + "\n" wikidata += metadata.description_wiki(app['Description']) + "\n"
wikidata += "=Maintainer Notes=\n" wikidata += "=Maintainer Notes=\n"
if 'Maintainer Notes' in app: if 'Maintainer Notes' in app:
wikidata += common.description_wiki(app['Maintainer Notes']) + "\n" wikidata += metadata.description_wiki(app['Maintainer Notes']) + "\n"
wikidata += "\nMetadata: [https://gitorious.org/f-droid/fdroiddata/source/master:metadata/{0}.txt current] [https://gitorious.org/f-droid/fdroiddata/history/metadata/{0}.txt history]\n".format(app['id']) wikidata += "\nMetadata: [https://gitorious.org/f-droid/fdroiddata/source/master:metadata/{0}.txt current] [https://gitorious.org/f-droid/fdroiddata/history/metadata/{0}.txt history]\n".format(app['id'])
# Get a list of all packages for this application... # Get a list of all packages for this application...
@ -232,7 +232,7 @@ def update_wiki(apps, apks):
def delete_disabled_builds(apps, apkcache, repodirs): def delete_disabled_builds(apps, apkcache, repodirs):
"""Delete disabled build outputs. """Delete disabled build outputs.
:param apps: list of all applications, as per common.read_metadata :param apps: list of all applications, as per metadata.read_metadata
:param apkcache: current apk cache information :param apkcache: current apk cache information
:param repodirs: the repo directories to process :param repodirs: the repo directories to process
""" """
@ -268,7 +268,7 @@ def resize_icon(iconpath):
def resize_all_icons(repodirs): def resize_all_icons(repodirs):
"""Resize all icons that exceed the max size """Resize all icons that exceed the max size
:param apps: list of all applications, as per common.read_metadata :param apps: list of all applications, as per metadata.read_metadata
:param repodirs: the repo directories to process :param repodirs: the repo directories to process
""" """
for repodir in repodirs: for repodir in repodirs:
@ -280,7 +280,7 @@ def scan_apks(apps, apkcache, repodir, knownapks):
This also extracts the icons. This also extracts the icons.
:param apps: list of all applications, as per common.read_metadata :param apps: list of all applications, as per metadata.read_metadata
:param apkcache: current apk cache information :param apkcache: current apk cache information
:param repodir: repo directory to scan :param repodir: repo directory to scan
:param knownapks: known apks info :param knownapks: known apks info
@ -538,7 +538,7 @@ def make_index(apps, apks, repodir, archive, categories):
return ("fdroid.app:" + link, app['Name']) return ("fdroid.app:" + link, app['Name'])
raise MetaDataException("Cannot resolve app id " + link) raise MetaDataException("Cannot resolve app id " + link)
addElement('desc', addElement('desc',
common.description_html(app['Description'], linkres), doc, apel) metadata.description_html(app['Description'], linkres), doc, apel)
addElement('license', app['License'], doc, apel) addElement('license', app['License'], doc, apel)
if 'Categories' in app: if 'Categories' in app:
appcategories = [c.strip() for c in app['Categories'].split(',')] appcategories = [c.strip() for c in app['Categories'].split(',')]
@ -739,7 +739,7 @@ def main():
sys.exit(0) sys.exit(0)
# Get all apps... # Get all apps...
apps = common.read_metadata() apps = metadata.read_metadata()
# Generate a list of categories... # Generate a list of categories...
categories = [] categories = []