import os
import re
import glob
-import cgi
+import html
import logging
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
warnings_action = None
-class MetaDataException(Exception):
-
- def __init__(self, value):
- self.value = value
-
- def __str__(self):
- return self.value
-
-
def warn_or_exception(value):
'''output warning or Exception depending on -W'''
if warnings_action == 'ignore':
elif warnings_action == 'error':
raise MetaDataException(value)
else:
- logging.warn(value)
+ logging.warning(value)
# To filter which ones should be written to the metadata files if
'License',
'Author Name',
'Author Email',
+ 'Author Web Site',
'Web Site',
'Source Code',
'Issue Tracker',
'Changelog',
'Donate',
'FlattrID',
+ 'LiberapayID',
'Bitcoin',
'Litecoin',
'Name',
self.License = 'Unknown'
self.AuthorName = None
self.AuthorEmail = None
+ self.AuthorWebSite = None
self.WebSite = ''
self.SourceCode = ''
self.IssueTracker = ''
self.Changelog = ''
self.Donate = None
self.FlattrID = None
+ self.LiberapayID = None
self.Bitcoin = None
self.Litecoin = None
self.Name = None
TYPE_MULTILINE = 6
TYPE_BUILD = 7
TYPE_BUILD_V2 = 8
+TYPE_INT = 9
fieldtypes = {
'Description': TYPE_MULTILINE,
'commit',
'subdir',
'submodules',
+ 'sudo',
'init',
'patch',
'gradle',
'maven',
'kivy',
+ 'buildozer',
'output',
'srclibs',
'oldsdkloc',
'gradleprops',
'antcommands',
'novcheck',
+ 'antifeatures',
]
-
-build_flags = set(build_flags_order + ['versionName', 'versionCode'])
+# old .txt format has version name/code inline in the 'Build:' line
+# but YAML and JSON have a explicit key for them
+build_flags = ['versionName', 'versionCode'] + build_flags_order
class Build(dict):
self.commit = None
self.subdir = None
self.submodules = False
+ self.sudo = ''
self.init = ''
self.patch = []
self.gradle = []
self.maven = False
self.kivy = False
+ self.buildozer = False
self.output = None
self.srclibs = []
self.oldsdkloc = False
self.gradleprops = []
self.antcommands = []
self.novcheck = False
+ self.antifeatures = []
if copydict:
super().__init__(copydict)
return
raise AttributeError("No such attribute: " + name)
def build_method(self):
- for f in ['maven', 'gradle', 'kivy']:
+ for f in ['maven', 'gradle', 'kivy', 'buildozer']:
if self.get(f):
return f
if self.output:
def output_method(self):
if self.output:
return 'raw'
- for f in ['maven', 'gradle', 'kivy']:
+ for f in ['maven', 'gradle', 'kivy', 'buildozer']:
if self.get(f):
return f
return 'ant'
flagtypes = {
+ 'versionCode': TYPE_INT,
'extlibs': TYPE_LIST,
'srclibs': TYPE_LIST,
'patch': TYPE_LIST,
'gradle': TYPE_LIST,
'antcommands': TYPE_LIST,
'gradleprops': TYPE_LIST,
+ 'sudo': TYPE_SCRIPT,
'init': TYPE_SCRIPT,
'prebuild': TYPE_SCRIPT,
'build': TYPE_SCRIPT,
'forceversion': TYPE_BOOL,
'forcevercode': TYPE_BOOL,
'novcheck': TYPE_BOOL,
+ 'antifeatures': TYPE_LIST,
}
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"]),
["ArchivePolicy"]),
FieldValidator("Anti-Feature",
- r'^(Ads|Tracking|NonFreeNet|NonFreeDep|NonFreeAdd|UpstreamNonFree|NonFreeAssets|KnownVuln)$',
+ r'^(Ads|Tracking|NonFreeNet|NonFreeDep|NonFreeAdd|UpstreamNonFree|NonFreeAssets|KnownVuln|ApplicationDebuggable)$',
["AntiFeatures"]),
FieldValidator("Auto Update Mode",
self.laststate = self.state
self.state = self.stNONE
- def formatted(self, txt, html):
+ def formatted(self, txt, htmlbody):
res = ''
- if html:
- txt = cgi.escape(txt)
+ if htmlbody:
+ txt = html.escape(txt, quote=False)
while True:
index = txt.find("''")
if index == -1:
res += txt[:index]
txt = txt[index:]
if txt.startswith("'''"):
- if html:
+ if htmlbody:
if self.bold:
res += '</b>'
else:
self.bold = not self.bold
txt = txt[3:]
else:
- if html:
+ if htmlbody:
if self.ital:
res += '</i>'
else:
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:
urltext = url
- res_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
+ res_html += '<a href="' + url + '">' + html.escape(urltext, quote=False) + '</a>'
res_plain += urltext
txt = txt[index + 2:]
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]")
- res_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
+ 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:
res_plain += ' (' + 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()
if type(v) in (float, int):
app[k] = str(v)
+ if 'Builds' in app:
+ app['builds'] = app.pop('Builds')
+
+ if 'flavours' in app and app['flavours'] == [True]:
+ app['flavours'] = 'yes'
+
+ if isinstance(app.Categories, str):
+ app.Categories = [app.Categories]
+ elif app.Categories is None:
+ app.Categories = ['None']
+ else:
+ app.Categories = [str(i) for i in app.Categories]
+
+ def _yaml_bool_unmapable(v):
+ return v in (True, False, [True], [False])
+
+ def _yaml_bool_unmap(v):
+ if v is True:
+ return 'yes'
+ elif v is False:
+ return 'no'
+ elif v == [True]:
+ return ['yes']
+ elif v == [False]:
+ return ['no']
+
+ _bool_allowed = ('disable', 'kivy', 'maven', 'buildozer')
+
builds = []
if 'builds' in app:
for build in app['builds']:
if not isinstance(build, Build):
build = Build(build)
for k, v in build.items():
- if flagtype(k) == TYPE_LIST:
- if isinstance(v, str):
- build[k] = [v]
- elif isinstance(v, bool):
- if v:
- build[k] = ['yes']
+ if not (v is None):
+ if flagtype(k) == TYPE_LIST:
+ if _yaml_bool_unmapable(v):
+ build[k] = _yaml_bool_unmap(v)
+
+ if isinstance(v, str):
+ build[k] = [v]
+ elif isinstance(v, bool):
+ if v:
+ build[k] = ['yes']
+ else:
+ build[k] = []
+ elif flagtype(k) is TYPE_INT:
+ build[k] = str(v)
+ elif flagtype(k) is TYPE_STRING:
+ if isinstance(v, bool) and k in _bool_allowed:
+ build[k] = v
else:
- build[k] = []
- elif flagtype(k) == TYPE_STRING and type(v) in (float, int):
- build[k] = str(v)
+ if _yaml_bool_unmapable(v):
+ build[k] = _yaml_bool_unmap(v)
+ else:
+ build[k] = str(v)
builds.append(build)
app.builds = sorted_builds(builds)
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):
-
- yamlinfo = yaml.load(mf, Loader=YamlLoader)
- app.update(yamlinfo)
+ yamldata = yaml.load(mf, Loader=YamlLoader)
+ if yamldata:
+ app.update(yamldata)
return app
+def write_yaml(mf, app):
+
+ # import rumael.yaml and check version
+ try:
+ import ruamel.yaml
+ except ImportError as e:
+ raise FDroidException('ruamel.yaml not instlled, can not write metadata.') from e
+ if not ruamel.yaml.__version__:
+ raise FDroidException('ruamel.yaml.__version__ not accessible. Please make sure a ruamel.yaml >= 0.13 is installed..')
+ m = re.match('(?P<major>[0-9]+)\.(?P<minor>[0-9]+)\.(?P<patch>[0-9]+)(-.+)?',
+ ruamel.yaml.__version__)
+ if not m:
+ raise FDroidException('ruamel.yaml version malfored, please install an upstream version of ruamel.yaml')
+ if int(m.group('major')) < 0 or int(m.group('minor')) < 13:
+ raise FDroidException('currently installed version of ruamel.yaml ({}) is too old, >= 1.13 required.'.format(ruamel.yaml.__version__))
+ # suiteable version ruamel.yaml imported successfully
+
+ _yaml_bools_true = ('y', 'Y', 'yes', 'Yes', 'YES',
+ 'true', 'True', 'TRUE',
+ 'on', 'On', 'ON')
+ _yaml_bools_false = ('n', 'N', 'no', 'No', 'NO',
+ 'false', 'False', 'FALSE',
+ 'off', 'Off', 'OFF')
+ _yaml_bools_plus_lists = []
+ _yaml_bools_plus_lists.extend(_yaml_bools_true)
+ _yaml_bools_plus_lists.extend([[x] for x in _yaml_bools_true])
+ _yaml_bools_plus_lists.extend(_yaml_bools_false)
+ _yaml_bools_plus_lists.extend([[x] for x in _yaml_bools_false])
+
+ def _class_as_dict_representer(dumper, data):
+ '''Creates a YAML representation of a App/Build instance'''
+ return dumper.represent_dict(data)
+
+ def _field_to_yaml(typ, value):
+ if typ is TYPE_STRING:
+ if value in _yaml_bools_plus_lists:
+ return ruamel.yaml.scalarstring.SingleQuotedScalarString(str(value))
+ return str(value)
+ elif typ is TYPE_INT:
+ return int(value)
+ elif typ is TYPE_MULTILINE:
+ if '\n' in value:
+ return ruamel.yaml.scalarstring.preserve_literal(str(value))
+ else:
+ return str(value)
+ elif typ is TYPE_SCRIPT:
+ if len(value) > 50:
+ return ruamel.yaml.scalarstring.preserve_literal(value)
+ else:
+ return value
+ else:
+ return value
+
+ def _app_to_yaml(app):
+ cm = ruamel.yaml.comments.CommentedMap()
+ insert_newline = False
+ for field in yaml_app_field_order:
+ if field is '\n':
+ # next iteration will need to insert a newline
+ insert_newline = True
+ else:
+ if app.get(field) or field is 'Builds':
+ # .txt calls it 'builds' internally, everywhere else its 'Builds'
+ if field is 'Builds':
+ if app.get('builds'):
+ cm.update({field: _builds_to_yaml(app)})
+ elif field is 'CurrentVersionCode':
+ cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))})
+ else:
+ cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))})
+
+ if insert_newline:
+ # we need to prepend a newline in front of this field
+ insert_newline = False
+ # inserting empty lines is not supported so we add a
+ # bogus comment and over-write its value
+ cm.yaml_set_comment_before_after_key(field, 'bogus')
+ cm.ca.items[field][1][-1].value = '\n'
+ return cm
+
+ def _builds_to_yaml(app):
+ fields = ['versionName', 'versionCode']
+ fields.extend(build_flags_order)
+ builds = ruamel.yaml.comments.CommentedSeq()
+ for build in app.builds:
+ b = ruamel.yaml.comments.CommentedMap()
+ for field in fields:
+ if hasattr(build, field) and getattr(build, field):
+ value = getattr(build, field)
+ if field == 'gradle' and value == ['off']:
+ value = [ruamel.yaml.scalarstring.SingleQuotedScalarString('off')]
+ if field in ('disable', 'kivy', 'maven', 'buildozer'):
+ if value == 'no':
+ continue
+ elif value == 'yes':
+ value = 'yes'
+ b.update({field: _field_to_yaml(flagtype(field), value)})
+ builds.append(b)
+
+ # insert extra empty lines between build entries
+ for i in range(1, len(builds)):
+ builds.yaml_set_comment_before_after_key(i, 'bogus')
+ builds.ca.items[i][1][-1].value = '\n'
+
+ return builds
+
+ yaml_app_field_order = [
+ 'Disabled',
+ 'AntiFeatures',
+ 'Provides',
+ 'Categories',
+ 'License',
+ 'AuthorName',
+ 'AuthorEmail',
+ 'AuthorWebSite',
+ 'WebSite',
+ 'SourceCode',
+ 'IssueTracker',
+ 'Changelog',
+ 'Donate',
+ 'FlattrID',
+ 'LiberapayID',
+ 'Bitcoin',
+ 'Litecoin',
+ '\n',
+ 'Name',
+ 'AutoName',
+ 'Summary',
+ 'Description',
+ '\n',
+ 'RequiresRoot',
+ '\n',
+ 'RepoType',
+ 'Repo',
+ 'Binaries',
+ '\n',
+ 'Builds',
+ '\n',
+ 'MaintainerNotes',
+ '\n',
+ 'ArchivePolicy',
+ 'AutoUpdateMode',
+ 'UpdateCheckMode',
+ 'UpdateCheckIgnore',
+ 'VercodeOperation',
+ 'UpdateCheckName',
+ 'UpdateCheckData',
+ 'CurrentVersion',
+ 'CurrentVersionCode',
+ '\n',
+ 'NoSourceSince',
+ ]
+
+ yaml_app = _app_to_yaml(app)
+ ruamel.yaml.round_trip_dump(yaml_app, mf, indent=4, block_seq_indent=2)
+
+
build_line_sep = re.compile(r'(?<!\\),')
build_cont = re.compile(r'^[ \t]')
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_always('License')
w_field_nonempty('Author Name')
w_field_nonempty('Author Email')
+ w_field_nonempty('Author Web Site')
w_field_always('Web Site')
w_field_always('Source Code')
w_field_always('Issue Tracker')
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')
w_field_nonempty('Name')
w_field_nonempty('Auto Name')
- w_field_always('Summary')
- w_field_always('Description', description_txt(app.Description))
+ w_field_nonempty('Summary')
+ w_field_nonempty('Description', description_txt(app.Description))
mf.write('\n')
if app.RequiresRoot:
w_field_always('Requires Root', 'yes')
first = False
else:
mf.write(' && \\\n ')
- mf.write(s)
+ mf.write(s.strip())
elif t == TYPE_LIST:
mf.write(','.join(v))
write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
-def write_yaml(mf, app):
-
- def w_comment(line):
- mf.write("# %s\n" % line)
-
- def escape(v):
- if not v:
- return ''
- if any(c in v for c in [': ', '%', '@', '*']):
- return "'" + v.replace("'", "''") + "'"
- return v
-
- def w_field(f, v, prefix='', t=None):
- if t is None:
- t = fieldtype(f)
- v = ''
- if t == TYPE_LIST:
- v = '\n'
- for e in v:
- v += prefix + ' - ' + escape(e) + '\n'
- elif t == TYPE_MULTILINE:
- v = ' |\n'
- for l in v.splitlines():
- if l:
- v += prefix + ' ' + l + '\n'
- else:
- v += '\n'
- elif t == TYPE_BOOL:
- v = ' yes\n'
- elif t == TYPE_SCRIPT:
- cmds = [s + '&& \\' for s in v.split('&& ')]
- if len(cmds) > 0:
- cmds[-1] = cmds[-1][:-len('&& \\')]
- w_field(f, cmds, prefix, 'multiline')
- return
- else:
- v = ' ' + escape(v) + '\n'
-
- mf.write(prefix)
- mf.write(f)
- mf.write(":")
- mf.write(v)
-
- global first_build
- first_build = True
-
- def w_build(build):
- global first_build
- if first_build:
- mf.write("builds:\n")
- first_build = False
-
- w_field('versionName', build.versionName, ' - ', TYPE_STRING)
- w_field('versionCode', build.versionCode, ' ', TYPE_STRING)
- for f in build_flags_order:
- v = build.get(f)
- if not v:
- continue
-
- w_field(f, v, ' ', flagtype(f))
-
- write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
-
-
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)))
- with open(metadatapath, 'w', encoding='utf8') as mf:
- if ext == 'txt':
- return write_txt(mf, app)
- elif ext == 'yml':
- return write_yaml(mf, app)
- warn_or_exception('Unknown metadata format: %s' % metadatapath)
+ try:
+ with open(metadatapath, 'w', encoding='utf8') as mf:
+ if ext == 'txt':
+ return write_txt(mf, app)
+ elif ext == 'yml':
+ return write_yaml(mf, app)
+ except FDroidException as e:
+ os.remove(metadatapath)
+ raise e
+
+ 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."))