import textwrap
import io
import yaml
+from collections import OrderedDict
# use libyaml if it is available
try:
from yaml import CLoader
YamlLoader = Loader
import fdroidserver.common
+from fdroidserver import _
from fdroidserver.exception import MetaDataException, FDroidException
srclibs = None
'Changelog',
'Donate',
'FlattrID',
+ 'LiberapayID',
'Bitcoin',
'Litecoin',
'Name',
self.Changelog = ''
self.Donate = None
self.FlattrID = None
+ self.LiberapayID = None
self.Bitcoin = None
self.Litecoin = None
self.Name = None
values = [v]
for v in values:
if not self.compiled.match(v):
- warn_or_exception("'%s' is not a valid %s in %s. Regex pattern: %s"
- % (v, self.name, appid, self.matching))
+ warn_or_exception(_("'{value}' is not a valid {field} in {appid}. Regex pattern: {pattern}")
+ .format(value=v, field=self.name, appid=appid, pattern=self.matching))
# Generic value types
valuetypes = {
- FieldValidator("Hexadecimal",
- r'^[0-9a-f]+$',
+ FieldValidator("Flattr ID",
+ r'^[0-9a-z]+$',
['FlattrID']),
+ FieldValidator("Liberapay ID",
+ r'^[0-9]+$',
+ ['LiberapayID']),
+
FieldValidator("HTTP link",
r'^http[s]?://',
["WebSite", "SourceCode", "IssueTracker", "Changelog", "Donate"]),
if txt.startswith("[["):
index = txt.find("]]")
if index == -1:
- warn_or_exception("Unterminated ]]")
+ warn_or_exception(_("Unterminated ]]"))
url = txt[2:index]
if self.linkResolver:
url, urltext = self.linkResolver(url)
else:
index = txt.find("]")
if index == -1:
- warn_or_exception("Unterminated ]")
+ warn_or_exception(_("Unterminated ]"))
url = txt[1:index]
index2 = url.find(' ')
if index2 == -1:
urltxt = url[index2 + 1:]
url = url[:index2]
if url == urltxt:
- warn_or_exception("Url title is just the URL - use [url]")
+ warn_or_exception(_("URL title is just the URL, use brackets: [URL]"))
res_html += '<a href="' + url + '">' + html.escape(urltxt, quote=False) + '</a>'
res_plain += urltxt
if urltxt != url:
try:
f, v = line.split(':', 1)
except ValueError:
- warn_or_exception("Invalid metadata in %s:%d" % (line, n))
+ warn_or_exception(_("Invalid metadata in %s:%d") % (line, n))
if f == "Subdir":
thisinfo[f] = v.split(',')
srclibs[srclibname] = parse_srclib(metadatapath)
-def read_metadata(xref=True, check_vcs=[]):
- """
- Read all metadata. Returns a list of 'app' objects (which are dictionaries as
- returned by the parse_txt_metadata function.
+def read_metadata(xref=True, check_vcs=[], sort_by_time=False):
+ """Return a list of App instances sorted newest first
+
+ This reads all of the metadata files in a 'data' repository, then
+ builds a list of App instances from those files. The list is
+ sorted based on creation time, newest first. Most of the time,
+ the newer files are the most interesting.
+
+ If there are multiple metadata files for a single appid, then the first
+ file that is parsed wins over all the others, and the rest throw an
+ exception. So the original .txt format is parsed first, at least until
+ newer formats stabilize.
check_vcs is the list of packageNames to check for .fdroid.yml in source
+
"""
# Always read the srclibs before the apps, since they can use a srlib as
# their source repository.
read_srclibs()
- apps = {}
+ apps = OrderedDict()
for basedir in ('metadata', 'tmp'):
if not os.path.exists(basedir):
os.makedirs(basedir)
- # If there are multiple metadata files for a single appid, then the first
- # file that is parsed wins over all the others, and the rest throw an
- # exception. So the original .txt format is parsed first, at least until
- # newer formats stabilize.
-
- for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
- + glob.glob(os.path.join('metadata', '*.json'))
- + glob.glob(os.path.join('metadata', '*.yml'))
- + glob.glob('.fdroid.json')
- + glob.glob('.fdroid.yml')):
- packageName, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
+ metadatafiles = (glob.glob(os.path.join('metadata', '*.txt'))
+ + glob.glob(os.path.join('metadata', '*.json'))
+ + glob.glob(os.path.join('metadata', '*.yml'))
+ + glob.glob('.fdroid.txt')
+ + glob.glob('.fdroid.json')
+ + glob.glob('.fdroid.yml'))
+
+ if sort_by_time:
+ entries = ((os.stat(path).st_mtime, path) for path in metadatafiles)
+ metadatafiles = []
+ for _ignored, path in sorted(entries, reverse=True):
+ metadatafiles.append(path)
+ else:
+ # most things want the index alpha sorted for stability
+ metadatafiles = sorted(metadatafiles)
+
+ for metadatapath in metadatafiles:
+ if metadatapath == '.fdroid.txt':
+ warn_or_exception(_('.fdroid.txt is not supported! Convert to .fdroid.yml or .fdroid.json.'))
+ packageName, _ignored = fdroidserver.common.get_extension(os.path.basename(metadatapath))
if packageName in apps:
- warn_or_exception("Found multiple metadata files for " + packageName)
+ warn_or_exception(_("Found multiple metadata files for {appid}")
+ .format(path=packageName))
app = parse_metadata(metadatapath, packageName in check_vcs)
check_metadata(app)
apps[app.id] = app
def linkres(appid):
if appid in apps:
return ("fdroid.app:" + appid, "Dummy name - don't know yet")
- warn_or_exception("Cannot resolve app id " + appid)
+ warn_or_exception(_("Cannot resolve app id {appid}").format(appid=appid))
for appid, app in apps.items():
try:
description_html(app.Description, linkres)
except MetaDataException as e:
- warn_or_exception("Problem with description of " + appid +
- " - " + str(e))
+ warn_or_exception(_("Problem with description of {appid}: {error}")
+ .format(appid=appid, error=str(e)))
return apps
if metadatapath is None:
appid = None
else:
- appid, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
+ appid, _ignored = fdroidserver.common.get_extension(os.path.basename(metadatapath))
if appid == '.fdroid': # we have local metadata in the app's source
if os.path.exists('AndroidManifest.xml'):
manifestroot = fdroidserver.common.parse_xml(os.path.join(root, 'AndroidManifest.xml'))
break
if manifestroot is None:
- warn_or_exception("Cannot find a packageName for {0}!".format(metadatapath))
+ warn_or_exception(_("Cannot find a packageName for {path}!")
+ .format(path=metadatapath))
appid = manifestroot.attrib['package']
app = App()
return True
if bool_false.match(s):
return False
- warn_or_exception("Invalid bool '%s'" % s)
+ warn_or_exception(_("Invalid boolean '%s'") % s)
def parse_metadata(metadatapath, check_vcs=False):
'''parse metadata file, optionally checking the git repo for metadata first'''
- _, ext = fdroidserver.common.get_extension(metadatapath)
+ _ignored, ext = fdroidserver.common.get_extension(metadatapath)
accepted = fdroidserver.common.config['accepted_formats']
if ext not in accepted:
- warn_or_exception('"%s" is not an accepted format, convert to: %s' % (
- metadatapath, ', '.join(accepted)))
+ warn_or_exception(_('"{path}" is not an accepted format, convert to: {formats}')
+ .format(path=metadatapath, formats=', '.join(accepted)))
app = App()
app.metadatapath = metadatapath
- name, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
+ name, _ignored = fdroidserver.common.get_extension(os.path.basename(metadatapath))
if name == '.fdroid':
check_vcs = False
else:
elif ext == 'yml':
parse_yaml_metadata(mf, app)
else:
- warn_or_exception('Unknown metadata format: %s' % metadatapath)
+ warn_or_exception(_('Unknown metadata format: {path}')
+ .format(path=metadatapath))
if check_vcs and app.Repo:
build_dir = fdroidserver.common.get_build_dir(app)
else:
root_dir = '.'
paths = fdroidserver.common.manifest_paths(root_dir, build.gradle)
- _, _, app.id = fdroidserver.common.parse_androidmanifests(paths, app)
+ _ignored, _ignored, app.id = fdroidserver.common.parse_androidmanifests(paths, app)
return app
def parse_yaml_metadata(mf, app):
yamldata = yaml.load(mf, Loader=YamlLoader)
- app.update(yamldata)
+ if yamldata:
+ app.update(yamldata)
return app
'Changelog',
'Donate',
'FlattrID',
+ 'LiberapayID',
'Bitcoin',
'Litecoin',
'\n',
def add_buildflag(p, build):
if not p.strip():
- warn_or_exception("Empty build flag at {1}"
- .format(buildlines[0], linedesc))
+ warn_or_exception(_("Empty build flag at {linedesc}")
+ .format(linedesc=linedesc))
bv = p.split('=', 1)
if len(bv) != 2:
- warn_or_exception("Invalid build flag at {0} in {1}"
- .format(buildlines[0], linedesc))
+ warn_or_exception(_("Invalid build flag at {line} in {linedesc}")
+ .format(line=buildlines[0], linedesc=linedesc))
pk, pv = bv
pk = pk.lstrip()
v = "".join(lines)
parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
if len(parts) < 3:
- warn_or_exception("Invalid build format: " + v + " in " + mf.name)
+ warn_or_exception(_("Invalid build format: {value} in {name}")
+ .format(value=v, name=mf.name))
build = Build()
build.versionName = parts[0]
build.versionCode = parts[1]
try:
int(versionCode)
except ValueError:
- warn_or_exception('Invalid versionCode: "' + versionCode + '" is not an integer!')
+ warn_or_exception(_('Invalid versionCode: "{versionCode}" is not an integer!')
+ .format(versionCode=versionCode))
def add_comments(key):
if not curcomments:
del buildlines[:]
else:
if not build.commit and not build.disable:
- warn_or_exception("No commit specified for {0} in {1}"
- .format(build.versionName, linedesc))
+ warn_or_exception(_("No commit specified for {versionName} in {linedesc}")
+ .format(versionName=build.versionName, linedesc=linedesc))
app.builds.append(build)
add_comments('build:' + build.versionCode)
try:
f, v = line.split(':', 1)
except ValueError:
- warn_or_exception("Invalid metadata in " + linedesc)
+ warn_or_exception(_("Invalid metadata in: ") + linedesc)
if f not in app_fields:
- warn_or_exception('Unrecognised app field: ' + f)
+ warn_or_exception(_('Unrecognised app field: ') + f)
# Translate obsolete fields...
if f == 'Market Version':
if ftype == TYPE_MULTILINE:
mode = 1
if v:
- warn_or_exception("Unexpected text on same line as "
- + f + " in " + linedesc)
+ warn_or_exception(_("Unexpected text on same line as {field} in {linedesc}")
+ .format(field=f, linedesc=linedesc))
elif ftype == TYPE_STRING:
app[f] = v
elif ftype == TYPE_LIST:
elif ftype == TYPE_BUILD_V2:
vv = v.split(',')
if len(vv) != 2:
- warn_or_exception('Build should have comma-separated',
- 'versionName and versionCode,',
- 'not "{0}", in {1}'.format(v, linedesc))
+ warn_or_exception(_('Build should have comma-separated '
+ 'versionName and versionCode, '
+ 'not "{value}", in {linedesc}')
+ .format(value=v, linedesc=linedesc))
build = Build()
build.versionName = vv[0]
build.versionCode = vv[1]
check_versionCode(build.versionCode)
if build.versionCode in vc_seen:
- warn_or_exception('Duplicate build recipe found for versionCode %s in %s'
- % (build.versionCode, linedesc))
+ warn_or_exception(_('Duplicate build recipe found for versionCode {versionCode} in {linedesc}')
+ .format(versionCode=build.versionCode, linedesc=linedesc))
vc_seen.add(build.versionCode)
del buildlines[:]
mode = 3
elif ftype == TYPE_OBSOLETE:
pass # Just throw it away!
else:
- warn_or_exception("Unrecognised field '" + f + "' in " + linedesc)
+ warn_or_exception(_("Unrecognised field '{field}' in {linedesc}")
+ .format(field=f, linedesc=linedesc))
elif mode == 1: # Multiline field
if line == '.':
mode = 0
# Mode at end of file should always be 0
if mode == 1:
- warn_or_exception(f + " not terminated in " + mf.name)
+ warn_or_exception(_("{field} not terminated in {name}")
+ .format(field=f, name=mf.name))
if mode == 2:
- warn_or_exception("Unterminated continuation in " + mf.name)
+ warn_or_exception(_("Unterminated continuation in {name}")
+ .format(name=mf.name))
if mode == 3:
- warn_or_exception("Unterminated build in " + mf.name)
+ warn_or_exception(_("Unterminated build in {name}")
+ .format(name=mf.name))
return app
w_field_nonempty('Changelog')
w_field_nonempty('Donate')
w_field_nonempty('FlattrID')
+ w_field_nonempty('LiberapayID')
w_field_nonempty('Bitcoin')
w_field_nonempty('Litecoin')
mf.write('\n')
def write_metadata(metadatapath, app):
- _, ext = fdroidserver.common.get_extension(metadatapath)
+ _ignored, ext = fdroidserver.common.get_extension(metadatapath)
accepted = fdroidserver.common.config['accepted_formats']
if ext not in accepted:
- warn_or_exception('Cannot write "%s", not an accepted format, use: %s'
- % (metadatapath, ', '.join(accepted)))
+ warn_or_exception(_('Cannot write "{path}", not an accepted format, use: {formats}')
+ .format(path=metadatapath, formats=', '.join(accepted)))
try:
with open(metadatapath, 'w', encoding='utf8') as mf:
os.remove(metadatapath)
raise e
- warn_or_exception('Unknown metadata format: %s' % metadatapath)
+ warn_or_exception(_('Unknown metadata format: %s') % metadatapath)
def add_metadata_arguments(parser):
'''add common command line flags related to metadata processing'''
- parser.add_argument("-W", default='error',
- help="force errors to be warnings, or ignore")
+ parser.add_argument("-W", choices=['error', 'warn', 'ignore'], default='error',
+ help=_("force metadata errors (default) to be warnings, or to be ignored."))