import json
from optparse import OptionParser
-import common
+import common, metadata
from common import BuildException, VCSException, FDroidPopen
def get_builder_vm_id():
sys.exit(1)
# Get all apps...
- apps = common.read_metadata(xref=not options.onserver)
+ apps = metadata.read_metadata(xref=not options.onserver)
log_dir = 'logs'
if not os.path.isdir(log_dir):
import traceback
import HTMLParser
from distutils.version import LooseVersion
-import common
+import common, metadata
from common import BuildException
from common import VCSException
config = common.read_config(options)
# Get all apps...
- apps = common.read_metadata(options.verbose)
+ apps = metadata.read_metadata(options.verbose)
# Filter apps according to command-line options
if options.package:
if writeit:
metafile = os.path.join('metadata', app['id'] + '.txt')
- common.write_metadata(metafile, app)
+ metadata.write_metadata(metafile, app)
if options.commit and logmsg:
print "Commiting update for " + metafile
gitcmd = ["git", "commit", "-m",
import subprocess
import time
import operator
-import cgi
import Queue
import threading
import magic
+import metadata
+
config = 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'):
"""Read the repository config
sys.exit(2)
st = os.stat(config_file)
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
if not hasattr(options, 'verbose'):
execfile(config_file, 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):
if vcstype == 'git':
srclib_path = os.path.join('srclibs', name + ".txt")
if not os.path.exists(srclib_path):
raise VCSException("Missing srclib " + name)
- return parse_srclib(srclib_path)['Repo Type']
+ return metadata.parse_srclib(srclib_path)['Repo Type']
class vcs:
def __init__(self, remote, local):
return [tag.split(' ')[0].strip() for tag in
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):
if not string.startswith('@string/'):
return string.replace("\\'","'")
def __str__(self):
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.
# 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
if not os.path.exists(srclib_path):
raise BuildException('srclib ' + name + ' not found.')
- srclib = parse_srclib(srclib_path)
+ srclib = metadata.parse_srclib(srclib_path)
sdir = os.path.join(srclib_dir, name)
import shutil
import urllib
from optparse import OptionParser
-import common
+import common, metadata
# 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
os.makedirs(tmp_dir)
# Get all apps...
- apps = common.read_metadata()
+ apps = metadata.read_metadata()
# Figure out what kind of project it is...
projecttype = None
sys.exit(1)
# Construct the metadata...
- app = common.parse_metadata(None)
+ app = metadata.parse_metadata(None)
app['id'] = package
app['Web Site'] = website
app['Source Code'] = sourcecode
f.write(repotype + ' ' + repo)
metafile = os.path.join('metadata', package + '.txt')
- common.write_metadata(metafile, app)
+ metadata.write_metadata(metafile, app)
print "Wrote " + metafile
--- /dev/null
+# -*- 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)
+
+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)
+
+
+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" ],
+ [ ]),
+
+ '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" ],
+ [ ]),
+}
+
+def check_metadata(info):
+
+ # Generic fields and attributes
+ for k, t in valuetypes.iteritems():
+ for field in [f for f in t.fields if f in info]:
+ t.check(info[field], info['id'])
+ if k == 'Bool':
+ info[field] = info[field] == "Yes"
+ for build in info['builds']:
+ for attr in [a for a in t.attrs if a in build]:
+ t.check(build[attr], info['id'])
+ if k == 'bool':
+ info[field] = info[field] == "yes"
+
+ # Special fields
+ if info['Archive Policy']:
+ if not re.match(r'^[0-9]+ versions$', info['Archive Policy']):
+ raise MetaDataException("Invalid archive policy '%s' in %s"
+ % (info['Archive Policy'], info["id"]))
+ versions = int(info['Archive Policy'][:-9])
+ if versions < 1 or versions > 20:
+ raise MetaDataException("Silly number of versions '%s' for archive policy in %s"
+ % (versions, info["id"]))
+
+# 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 not in ['version', 'vercode', 'origlines']:
+ 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()
+
+
import glob
from optparse import OptionParser
-import common
+import common, metadata
from common import BuildException
config = None
# 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
# collisions, and refuse to do any publishing if that's the case...
- apps = common.read_metadata()
+ apps = metadata.read_metadata()
allaliases = []
for app in apps:
m = md5.new()
import sys
import os
from optparse import OptionParser
-import common
+import common, metadata
config = None
options = None
config = common.read_config(options)
# 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:
print "No such package"
for app in apps:
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."
import os
import traceback
from optparse import OptionParser
-import common
+import common, metadata
from common import BuildException
from common import VCSException
config = common.read_config(options)
# Get all apps...
- apps = common.read_metadata()
+ apps = metadata.read_metadata()
# Filter apps according to command-line options
if options.package:
import glob
from optparse import OptionParser
import paramiko
-import common
+import common, metadata
import socket
import subprocess
sys.exit(1)
# Get all metadata-defined apps...
- metaapps = common.read_metadata(options.verbose)
+ metaapps = metadata.read_metadata(options.verbose)
statsdir = 'stats'
logsdir = os.path.join(statsdir, 'logs')
from xml.dom.minidom import Document
from optparse import OptionParser
import time
-import common
+import common, metadata
from common import MetaDataException
from PIL import Image
wikidata += " - [http://f-droid.org/repository/browse/?fdid=" + app['id'] + " view in repository]\n\n"
wikidata += "=Description=\n"
- wikidata += common.description_wiki(app['Description']) + "\n"
+ wikidata += metadata.description_wiki(app['Description']) + "\n"
wikidata += "=Maintainer Notes=\n"
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'])
# Get a list of all packages for this application...
def delete_disabled_builds(apps, apkcache, repodirs):
"""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 repodirs: the repo directories to process
"""
def resize_all_icons(repodirs):
"""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
"""
for repodir in repodirs:
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 repodir: repo directory to scan
:param knownapks: known apks info
return ("fdroid.app:" + link, app['Name'])
raise MetaDataException("Cannot resolve app id " + link)
addElement('desc',
- common.description_html(app['Description'], linkres), doc, apel)
+ metadata.description_html(app['Description'], linkres), doc, apel)
addElement('license', app['License'], doc, apel)
if 'Categories' in app:
appcategories = [c.strip() for c in app['Categories'].split(',')]
sys.exit(0)
# Get all apps...
- apps = common.read_metadata()
+ apps = metadata.read_metadata()
# Generate a list of categories...
categories = []