-# -*- coding: utf-8 -*-
+#!/usr/bin/env python3
#
# metadata.py - part of the FDroid server tools
# Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com
import re
import glob
import cgi
+import logging
import textwrap
-
-try:
- from cStringIO import StringIO
-except:
- from StringIO import StringIO
+import io
import yaml
# use libyaml if it is available
# use the C implementation when available
import xml.etree.cElementTree as ElementTree
-import common
+import fdroidserver.common
srclibs = None
'Provides',
'Categories',
'License',
+ 'Author Name',
+ 'Author Email',
'Web Site',
'Source Code',
'Issue Tracker',
self.Provides = None
self.Categories = ['None']
self.License = 'Unknown'
+ self.AuthorName = None
+ self.AuthorEmail = None
self.WebSite = ''
self.SourceCode = ''
self.IssueTracker = ''
self.UpdateCheckName = None
self.UpdateCheckData = None
self.CurrentVersion = ''
- self.CurrentVersionCode = '0'
+ self.CurrentVersionCode = None
self.NoSourceSince = ''
self.id = None
# names. Should only be used for tests.
def field_dict(self):
d = {}
- for k, v in self.__dict__.iteritems():
+ for k, v in self.__dict__.items():
if k == 'builds':
d['builds'] = []
for build in v:
- b = {k: v for k, v in build.__dict__.iteritems() if not k.startswith('_')}
+ b = {k: v for k, v in build.__dict__.items() if not k.startswith('_')}
d['builds'].append(b)
elif not k.startswith('_'):
f = App.attr_to_field(k)
# Like dict.update(), but using human-readable field names
def update_fields(self, d):
- for f, v in d.iteritems():
+ for f, v in d.items():
if f == 'builds':
for b in v:
build = Build()
self.submodules = False
self.init = ''
self.patch = []
- self.gradle = False
+ self.gradle = []
self.maven = False
self.kivy = False
self.output = None
self.rm = []
self.extlibs = []
self.prebuild = ''
- self.update = None
+ self.update = []
self.target = None
self.scanignore = []
self.scandelete = []
self.ndk = None
self.preassemble = []
self.gradleprops = []
- self.antcommands = None
+ self.antcommands = []
self.novcheck = False
self._modified = set()
else:
self.__dict__[f].append(v)
- def method(self):
+ def build_method(self):
for f in ['maven', 'gradle', 'kivy']:
if self.get_flag(f):
return f
return 'raw'
return 'ant'
+ # like build_method, but prioritize output=
+ def output_method(self):
+ if self.output:
+ return 'raw'
+ for f in ['maven', 'gradle', 'kivy']:
+ if self.get_flag(f):
+ return f
+ return 'ant'
+
def ndk_path(self):
version = self.ndk
if not version:
version = 'r10e' # falls back to latest
- paths = common.config['ndk_paths']
+ paths = fdroidserver.common.config['ndk_paths']
if version not in paths:
return ''
return paths[version]
def update_flags(self, d):
- for f, v in d.iteritems():
+ for f, v in d.items():
self.set_flag(f, v)
flagtypes = {
#
class FieldValidator():
- def __init__(self, name, matching, sep, fields, flags):
+ def __init__(self, name, matching, fields, flags):
self.name = name
self.matching = matching
- if type(matching) is str:
- self.compiled = re.compile(matching)
- else:
- self.matching = set(self.matching)
- self.sep = sep
+ self.compiled = re.compile(matching)
self.fields = fields
self.flags = flags
- def _assert_regex(self, values, appid):
- for v in values:
- if not self.compiled.match(v):
- raise MetaDataException("'%s' is not a valid %s in %s. Regex pattern: %s"
- % (v, self.name, appid, self.matching))
-
- 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. Possible values: %s"
- % (v, self.name, appid, ', '.join(self.matching)))
-
def check(self, v, appid):
if not v:
return
values = v
else:
values = [v]
- if type(self.matching) is set:
- self._assert_list(values, appid)
- else:
- self._assert_regex(values, appid)
-
+ for v in values:
+ if not self.compiled.match(v):
+ raise MetaDataException("'%s' is not a valid %s in %s. Regex pattern: %s"
+ % (v, self.name, appid, self.matching))
# Generic value types
valuetypes = {
FieldValidator("Integer",
- r'^[1-9][0-9]*$', None,
+ r'^[1-9][0-9]*$',
[],
['vercode']),
FieldValidator("Hexadecimal",
- r'^[0-9a-f]+$', None,
+ r'^[0-9a-f]+$',
['FlattrID'],
[]),
FieldValidator("HTTP link",
- r'^http[s]?://', None,
+ r'^http[s]?://',
["WebSite", "SourceCode", "IssueTracker", "Changelog", "Donate"], []),
+ FieldValidator("Email",
+ r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
+ ["AuthorEmail"], []),
+
FieldValidator("Bitcoin address",
- r'^[a-zA-Z0-9]{27,34}$', None,
+ r'^[a-zA-Z0-9]{27,34}$',
["Bitcoin"],
[]),
FieldValidator("Litecoin address",
- r'^L[a-zA-Z0-9]{33}$', None,
+ r'^L[a-zA-Z0-9]{33}$',
["Litecoin"],
[]),
FieldValidator("Repo Type",
- ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
+ r'^(git|git-svn|svn|hg|bzr|srclib)$',
["RepoType"],
[]),
FieldValidator("Binaries",
- r'^http[s]?://', None,
+ r'^http[s]?://',
["Binaries"],
[]),
FieldValidator("Archive Policy",
- r'^[0-9]+ versions$', None,
+ r'^[0-9]+ versions$',
["ArchivePolicy"],
[]),
FieldValidator("Anti-Feature",
- ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
+ r'^(Ads|Tracking|NonFreeNet|NonFreeDep|NonFreeAdd|UpstreamNonFree|NonFreeAssets)$',
["AntiFeatures"],
[]),
FieldValidator("Auto Update Mode",
- r"^(Version .+|None)$", None,
+ r"^(Version .+|None)$",
["AutoUpdateMode"],
[]),
FieldValidator("Update Check Mode",
- r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
+ r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$",
["UpdateCheckMode"],
[])
}
self.laststate = self.stNONE
self.text_html = ''
self.text_txt = ''
- self.html = StringIO()
- self.text = StringIO()
+ self.html = io.StringIO()
+ self.text = io.StringIO()
self.para_lines = []
self.linkResolver = None
self.linkResolver = linkres
self.state = self.stNONE
whole_para = ' '.join(self.para_lines)
self.addtext(whole_para)
- self.text.write(textwrap.fill(whole_para, 80,
- break_long_words=False,
- break_on_hyphens=False))
+ wrapped = textwrap.fill(whole_para, 80,
+ break_long_words=False,
+ break_on_hyphens=False)
+ self.text.write(wrapped)
self.html.write('</p>')
del self.para_lines[:]
if not os.path.exists(metadatapath):
return thisinfo
- metafile = open(metadatapath, "r")
+ metafile = open(metadatapath, "r", encoding='utf-8')
n = 0
for line in metafile:
for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
+ glob.glob(os.path.join('metadata', '*.json'))
+ glob.glob(os.path.join('metadata', '*.xml'))
- + glob.glob(os.path.join('metadata', '*.yaml'))):
+ + glob.glob(os.path.join('metadata', '*.yml'))
+ + glob.glob('.fdroid.json')
+ + glob.glob('.fdroid.xml')
+ + glob.glob('.fdroid.yml')):
+ packageName, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
+ if packageName in apps:
+ raise MetaDataException("Found multiple metadata files for " + packageName)
app = parse_metadata(metadatapath)
- if app.id in apps:
- raise MetaDataException("Found multiple metadata files for " + app.id)
check_metadata(app)
apps[app.id] = app
return ("fdroid.app:" + appid, "Dummy name - don't know yet")
raise MetaDataException("Cannot resolve app id " + appid)
- for appid, app in apps.iteritems():
+ for appid, app in apps.items():
try:
description_html(app.Description, linkres)
- except MetaDataException, e:
+ except MetaDataException as e:
raise MetaDataException("Problem with description of " + appid +
" - " + str(e))
if metadatapath is None:
appid = None
else:
- appid, _ = common.get_extension(os.path.basename(metadatapath))
+ appid, _ = 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('AndroidManifest.xml')
+ else:
+ pattern = re.compile(""".*manifest\.srcFile\s+'AndroidManifest\.xml'.*""")
+ for root, dirs, files in os.walk(os.getcwd()):
+ if 'build.gradle' in files:
+ p = os.path.join(root, 'build.gradle')
+ with open(p, 'rb') as f:
+ data = f.read()
+ m = pattern.search(data)
+ if m:
+ logging.debug('Using: ' + os.path.join(root, 'AndroidManifest.xml'))
+ manifestroot = fdroidserver.common.parse_xml(os.path.join(root, 'AndroidManifest.xml'))
+ break
+ if manifestroot is None:
+ raise MetaDataException("Cannot find a packageName for {0}!".format(metadatapath))
+ appid = manifestroot.attrib['package']
app = App()
app.metadatapath = metadatapath
# This function uses __dict__ to be faster
def post_metadata_parse(app):
- for k, v in app.__dict__.iteritems():
- if k not in app._modified:
- continue
+ for k in app._modified:
+ v = app.__dict__[k]
if type(v) in (float, int):
app.__dict__[k] = str(v)
for build in app.builds:
- for k, v in build.__dict__.iteritems():
-
- if k not in build._modified:
- continue
+ for k in build._modified:
+ v = build.__dict__[k]
if type(v) in (float, int):
build.__dict__[k] = str(v)
continue
build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
elif ftype == TYPE_BOOL:
# TODO handle this using <xsd:element type="xsd:boolean> in a schema
- if isinstance(v, basestring):
+ if isinstance(v, str):
build.__dict__[k] = _decode_bool(v)
elif ftype == TYPE_STRING:
if isinstance(v, bool) and v:
#
-def _decode_list(data):
- '''convert items in a list from unicode to basestring'''
- rv = []
- for item in data:
- if isinstance(item, unicode):
- item = item.encode('utf-8')
- elif isinstance(item, list):
- item = _decode_list(item)
- elif isinstance(item, dict):
- item = _decode_dict(item)
- rv.append(item)
- return rv
-
-
-def _decode_dict(data):
- '''convert items in a dict from unicode to basestring'''
- rv = {}
- for k, v in data.iteritems():
- if isinstance(k, unicode):
- k = k.encode('utf-8')
- if isinstance(v, unicode):
- v = v.encode('utf-8')
- elif isinstance(v, list):
- v = _decode_list(v)
- elif isinstance(v, dict):
- v = _decode_dict(v)
- rv[k] = v
- return rv
-
-
bool_true = re.compile(r'([Yy]es|[Tt]rue)')
bool_false = re.compile(r'([Nn]o|[Ff]alse)')
def parse_metadata(metadatapath):
- _, ext = common.get_extension(metadatapath)
- accepted = common.config['accepted_formats']
+ _, ext = fdroidserver.common.get_extension(metadatapath)
+ accepted = fdroidserver.common.config['accepted_formats']
if ext not in accepted:
raise MetaDataException('"%s" is not an accepted format, convert to: %s' % (
metadatapath, ', '.join(accepted)))
- app = None
- if ext == 'txt':
- app = parse_txt_metadata(metadatapath)
- elif ext == 'json':
- app = parse_json_metadata(metadatapath)
- elif ext == 'xml':
- app = parse_xml_metadata(metadatapath)
- elif ext == 'yaml':
- app = parse_yaml_metadata(metadatapath)
- else:
- raise MetaDataException('Unknown metadata format: %s' % metadatapath)
+ app = App()
+ app.metadatapath = metadatapath
+ app.id, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
+
+ with open(metadatapath, 'r', encoding='utf-8') as mf:
+ if ext == 'txt':
+ parse_txt_metadata(mf, app)
+ elif ext == 'json':
+ parse_json_metadata(mf, app)
+ elif ext == 'xml':
+ parse_xml_metadata(mf, app)
+ elif ext == 'yml':
+ parse_yaml_metadata(mf, app)
+ else:
+ raise MetaDataException('Unknown metadata format: %s' % metadatapath)
post_metadata_parse(app)
return app
-def parse_json_metadata(metadatapath):
-
- app = get_default_app_info(metadatapath)
+def parse_json_metadata(mf, app):
- # fdroid metadata is only strings and booleans, no floats or ints. And
- # json returns unicode, and fdroidserver still uses plain python strings
+ # fdroid metadata is only strings and booleans, no floats or ints.
# TODO create schema using https://pypi.python.org/pypi/jsonschema
- jsoninfo = None
- with open(metadatapath, 'r') as f:
- jsoninfo = json.load(f, object_hook=_decode_dict,
- parse_int=lambda s: s,
- parse_float=lambda s: s)
+ jsoninfo = json.load(mf, parse_int=lambda s: s,
+ parse_float=lambda s: s)
app.update_fields(jsoninfo)
for f in ['Description', 'Maintainer Notes']:
v = app.get_field(f)
return app
-def parse_xml_metadata(metadatapath):
-
- app = get_default_app_info(metadatapath)
+def parse_xml_metadata(mf, app):
- tree = ElementTree.ElementTree(file=metadatapath)
+ tree = ElementTree.ElementTree(file=mf)
root = tree.getroot()
if root.tag != 'resources':
- raise MetaDataException('%s does not have root as <resources></resources>!' % metadatapath)
+ raise MetaDataException('resources file does not have root element <resources/>')
for child in root:
if child.tag != 'builds':
return app
-def parse_yaml_metadata(metadatapath):
+def parse_yaml_metadata(mf, app):
- app = get_default_app_info(metadatapath)
-
- yamlinfo = None
- with open(metadatapath, 'r') as f:
- yamlinfo = yaml.load(f, Loader=YamlLoader)
+ yamlinfo = yaml.load(mf, Loader=YamlLoader)
app.update_fields(yamlinfo)
return app
build_cont = re.compile(r'^[ \t]')
-def parse_txt_metadata(metadatapath):
+def parse_txt_metadata(mf, app):
linedesc = None
v = "".join(lines)
parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
if len(parts) < 3:
- raise MetaDataException("Invalid build format: " + v + " in " + metafile.name)
+ raise MetaDataException("Invalid build format: " + v + " in " + mf.name)
build = Build()
- build.origlines = lines
build.version = parts[0]
build.vercode = parts[1]
if parts[2].startswith('!'):
app.comments[key] = list(curcomments)
del curcomments[:]
- app = get_default_app_info(metadatapath)
- metafile = open(metadatapath, "r")
-
mode = 0
buildlines = []
multiline_lines = []
vc_seen = set()
c = 0
- for line in metafile:
+ for line in mf:
c += 1
- linedesc = "%s:%d" % (metafile.name, c)
+ linedesc = "%s:%d" % (mf.name, c)
line = line.rstrip('\r\n')
if mode == 3:
if build_cont.match(line):
add_comments('build:' + app.builds[-1].vercode)
mode = 0
add_comments(None)
- metafile.close()
# Mode at end of file should always be 0
if mode == 1:
- raise MetaDataException(f + " not terminated in " + metafile.name)
+ raise MetaDataException(f + " not terminated in " + mf.name)
if mode == 2:
- raise MetaDataException("Unterminated continuation in " + metafile.name)
+ raise MetaDataException("Unterminated continuation in " + mf.name)
if mode == 3:
- raise MetaDataException("Unterminated build in " + metafile.name)
+ raise MetaDataException("Unterminated build in " + mf.name)
return app
w_field(f, v)
w_field_nonempty('Disabled')
- if app.AntiFeatures:
- w_field_always('AntiFeatures')
+ w_field_nonempty('AntiFeatures')
w_field_nonempty('Provides')
w_field_always('Categories')
w_field_always('License')
+ w_field_nonempty('Author Name')
+ w_field_nonempty('Author Email')
w_field_always('Web Site')
w_field_always('Source Code')
w_field_always('Issue Tracker')
w_field_always('Binaries')
mf.write('\n')
- for build in sorted_builds(app.builds):
+ for build in app.builds:
if build.version == "Ignore":
continue
#
# 'mf' - Writer interface (file, StringIO, ...)
# 'app' - The app data
-def write_txt_metadata(mf, app):
+def write_txt(mf, app):
def w_comment(line):
mf.write("# %s\n" % line)
write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
-def write_yaml_metadata(mf, app):
+def write_yaml(mf, app):
def w_comment(line):
mf.write("# %s\n" % line)
write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
-def write_metadata(fmt, mf, app):
- if fmt == 'txt':
- return write_txt_metadata(mf, app)
- if fmt == 'yaml':
- return write_yaml_metadata(mf, app)
- raise MetaDataException("Unknown metadata format given")
+def write_metadata(metadatapath, app):
+ _, ext = fdroidserver.common.get_extension(metadatapath)
+ accepted = fdroidserver.common.config['accepted_formats']
+ if ext not in accepted:
+ raise MetaDataException('Cannot write "%s", not an accepted format, use: %s' % (
+ metadatapath, ', '.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)
+ raise MetaDataException('Unknown metadata format: %s' % metadatapath)