1 # -*- coding: utf-8 -*-
3 # metadata.py - part of the FDroid server tools
4 # Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martà <mvdan@mvdan.cc>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
30 # use libyaml if it is available
32 from yaml import CLoader
35 from yaml import Loader
38 # use the C implementation when available
39 import xml.etree.cElementTree as ElementTree
41 from collections import OrderedDict
48 class MetaDataException(Exception):
50 def __init__(self, value):
56 # In the order in which they are laid out on files
57 app_defaults = OrderedDict([
61 ('Categories', ['None']),
62 ('License', 'Unknown'),
65 ('Issue Tracker', ''),
75 ('Requires Root', False),
79 ('Maintainer Notes', []),
80 ('Archive Policy', None),
81 ('Auto Update Mode', 'None'),
82 ('Update Check Mode', 'None'),
83 ('Update Check Ignore', None),
84 ('Vercode Operation', None),
85 ('Update Check Name', None),
86 ('Update Check Data', None),
87 ('Current Version', ''),
88 ('Current Version Code', '0'),
89 ('No Source Since', ''),
93 # In the order in which they are laid out on files
94 # Sorted by their action and their place in the build timeline
95 # These variables can have varying datatypes. For example, anything with
96 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
97 flag_defaults = OrderedDict([
101 ('submodules', False),
109 ('oldsdkloc', False),
111 ('forceversion', False),
112 ('forcevercode', False),
116 ('update', ['auto']),
122 ('ndk', 'r10e'), # defaults to latest
125 ('antcommands', None),
130 # Designates a metadata field type and checks that it matches
132 # 'name' - The long name of the field type
133 # 'matching' - List of possible values or regex expression
134 # 'sep' - Separator to use if value may be a list
135 # 'fields' - Metadata fields (Field:Value) of this type
136 # 'attrs' - Build attributes (attr=value) of this type
138 class FieldValidator():
140 def __init__(self, name, matching, sep, fields, attrs):
142 self.matching = matching
143 if type(matching) is str:
144 self.compiled = re.compile(matching)
149 def _assert_regex(self, values, appid):
151 if not self.compiled.match(v):
152 raise MetaDataException("'%s' is not a valid %s in %s. "
153 % (v, self.name, appid) +
154 "Regex pattern: %s" % (self.matching))
156 def _assert_list(self, values, appid):
158 if v not in self.matching:
159 raise MetaDataException("'%s' is not a valid %s in %s. "
160 % (v, self.name, appid) +
161 "Possible values: %s" % (", ".join(self.matching)))
163 def check(self, value, appid):
164 if type(value) is not str or not value:
166 if self.sep is not None:
167 values = value.split(self.sep)
170 if type(self.matching) is list:
171 self._assert_list(values, appid)
173 self._assert_regex(values, appid)
176 # Generic value types
178 FieldValidator("Integer",
179 r'^[1-9][0-9]*$', None,
183 FieldValidator("Hexadecimal",
184 r'^[0-9a-f]+$', None,
188 FieldValidator("HTTP link",
189 r'^http[s]?://', None,
190 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
192 FieldValidator("Bitcoin address",
193 r'^[a-zA-Z0-9]{27,34}$', None,
197 FieldValidator("Litecoin address",
198 r'^L[a-zA-Z0-9]{33}$', None,
202 FieldValidator("bool",
203 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
205 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
208 FieldValidator("Repo Type",
209 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
213 FieldValidator("Binaries",
214 r'^http[s]?://', None,
218 FieldValidator("Archive Policy",
219 r'^[0-9]+ versions$', None,
223 FieldValidator("Anti-Feature",
224 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
228 FieldValidator("Auto Update Mode",
229 r"^(Version .+|None)$", None,
230 ["Auto Update Mode"],
233 FieldValidator("Update Check Mode",
234 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
235 ["Update Check Mode"],
240 # Check an app's metadata information for integrity errors
241 def check_metadata(info):
243 for field in v.fields:
244 v.check(info[field], info['id'])
245 for build in info['builds']:
247 v.check(build[attr], info['id'])
250 # Formatter for descriptions. Create an instance, and call parseline() with
251 # each line of the description source from the metadata. At the end, call
252 # end() and then text_wiki and text_html will contain the result.
253 class DescriptionFormatter:
267 def __init__(self, linkres):
268 self.linkResolver = linkres
270 def endcur(self, notstates=None):
271 if notstates and self.state in notstates:
273 if self.state == self.stPARA:
275 elif self.state == self.stUL:
277 elif self.state == self.stOL:
281 self.state = self.stNONE
282 whole_para = ' '.join(self.para_lines)
283 self.addtext(whole_para)
284 self.text_txt += textwrap.fill(whole_para, 80,
285 break_long_words=False,
286 break_on_hyphens=False) + '\n\n'
287 self.text_html += '</p>'
288 del self.para_lines[:]
291 self.text_html += '</ul>'
292 self.text_txt += '\n'
293 self.state = self.stNONE
296 self.text_html += '</ol>'
297 self.text_txt += '\n'
298 self.state = self.stNONE
300 def formatted(self, txt, html):
303 txt = cgi.escape(txt)
305 index = txt.find("''")
307 return formatted + txt
308 formatted += txt[:index]
310 if txt.startswith("'''"):
316 self.bold = not self.bold
324 self.ital = not self.ital
327 def linkify(self, txt):
331 index = txt.find("[")
333 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
334 linkified_plain += self.formatted(txt[:index], False)
335 linkified_html += self.formatted(txt[:index], True)
337 if txt.startswith("[["):
338 index = txt.find("]]")
340 raise MetaDataException("Unterminated ]]")
342 if self.linkResolver:
343 url, urltext = self.linkResolver(url)
346 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
347 linkified_plain += urltext
348 txt = txt[index + 2:]
350 index = txt.find("]")
352 raise MetaDataException("Unterminated ]")
354 index2 = url.find(' ')
358 urltxt = url[index2 + 1:]
361 raise MetaDataException("Url title is just the URL - use [url]")
362 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
363 linkified_plain += urltxt
365 linkified_plain += ' (' + url + ')'
366 txt = txt[index + 1:]
368 def addtext(self, txt):
369 p, h = self.linkify(txt)
372 def parseline(self, line):
373 self.text_wiki += "%s\n" % line
376 elif line.startswith('* '):
377 self.endcur([self.stUL])
378 self.text_txt += "%s\n" % line
379 if self.state != self.stUL:
380 self.text_html += '<ul>'
381 self.state = self.stUL
382 self.text_html += '<li>'
383 self.addtext(line[1:])
384 self.text_html += '</li>'
385 elif line.startswith('# '):
386 self.endcur([self.stOL])
387 self.text_txt += "%s\n" % line
388 if self.state != self.stOL:
389 self.text_html += '<ol>'
390 self.state = self.stOL
391 self.text_html += '<li>'
392 self.addtext(line[1:])
393 self.text_html += '</li>'
395 self.para_lines.append(line)
396 self.endcur([self.stPARA])
397 if self.state == self.stNONE:
398 self.text_html += '<p>'
399 self.state = self.stPARA
403 self.text_txt = self.text_txt.strip()
406 # Parse multiple lines of description as written in a metadata file, returning
407 # a single string in text format and wrapped to 80 columns.
408 def description_txt(lines):
409 ps = DescriptionFormatter(None)
416 # Parse multiple lines of description as written in a metadata file, returning
417 # a single string in wiki format. Used for the Maintainer Notes field as well,
418 # because it's the same format.
419 def description_wiki(lines):
420 ps = DescriptionFormatter(None)
427 # Parse multiple lines of description as written in a metadata file, returning
428 # a single string in HTML format.
429 def description_html(lines, linkres):
430 ps = DescriptionFormatter(linkres)
437 def parse_srclib(metadatapath):
441 # Defaults for fields that come from metadata
442 thisinfo['Repo Type'] = ''
443 thisinfo['Repo'] = ''
444 thisinfo['Subdir'] = None
445 thisinfo['Prepare'] = None
447 if not os.path.exists(metadatapath):
450 metafile = open(metadatapath, "r")
453 for line in metafile:
455 line = line.rstrip('\r\n')
456 if not line or line.startswith("#"):
460 field, value = line.split(':', 1)
462 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
464 if field == "Subdir":
465 thisinfo[field] = value.split(',')
467 thisinfo[field] = value
473 """Read all srclib metadata.
475 The information read will be accessible as metadata.srclibs, which is a
476 dictionary, keyed on srclib name, with the values each being a dictionary
477 in the same format as that returned by the parse_srclib function.
479 A MetaDataException is raised if there are any problems with the srclib
484 # They were already loaded
485 if srclibs is not None:
491 if not os.path.exists(srcdir):
494 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
495 srclibname = os.path.basename(metadatapath[:-4])
496 srclibs[srclibname] = parse_srclib(metadatapath)
499 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
500 # returned by the parse_txt_metadata function.
501 def read_metadata(xref=True):
503 # Always read the srclibs before the apps, since they can use a srlib as
504 # their source repository.
509 for basedir in ('metadata', 'tmp'):
510 if not os.path.exists(basedir):
513 # If there are multiple metadata files for a single appid, then the first
514 # file that is parsed wins over all the others, and the rest throw an
515 # exception. So the original .txt format is parsed first, at least until
516 # newer formats stabilize.
518 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
519 + glob.glob(os.path.join('metadata', '*.json'))
520 + glob.glob(os.path.join('metadata', '*.xml'))
521 + glob.glob(os.path.join('metadata', '*.yaml'))):
522 appid, appinfo = parse_metadata(metadatapath)
524 raise MetaDataException("Found multiple metadata files for " + appid)
525 check_metadata(appinfo)
526 apps[appid] = appinfo
529 # Parse all descriptions at load time, just to ensure cross-referencing
530 # errors are caught early rather than when they hit the build server.
533 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
534 raise MetaDataException("Cannot resolve app id " + appid)
536 for appid, app in apps.iteritems():
538 description_html(app['Description'], linkres)
539 except MetaDataException, e:
540 raise MetaDataException("Problem with description of " + appid +
546 # Get the type expected for a given metadata field.
547 def metafieldtype(name):
548 if name in ['Description', 'Maintainer Notes']:
550 if name in ['Categories', 'AntiFeatures']:
552 if name == 'Build Version':
556 if name == 'Use Built':
558 if name not in app_defaults:
564 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
565 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
568 if name in ['init', 'prebuild', 'build']:
570 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
576 def fill_build_defaults(build):
578 def get_build_type():
579 for t in ['maven', 'gradle', 'kivy']:
586 for flag, value in flag_defaults.iteritems():
590 build['type'] = get_build_type()
591 build['ndk_path'] = common.get_ndk_path(build['ndk'])
594 def split_list_values(s):
595 # Port legacy ';' separators
596 l = [v.strip() for v in s.replace(';', ',').split(',')]
597 return [v for v in l if v]
600 def get_default_app_info(metadatapath=None):
601 if metadatapath is None:
604 appid, _ = common.get_extension(os.path.basename(metadatapath))
607 thisinfo.update(app_defaults)
608 thisinfo['metadatapath'] = metadatapath
609 if appid is not None:
610 thisinfo['id'] = appid
612 # General defaults...
613 thisinfo['builds'] = []
614 thisinfo['comments'] = dict()
616 return appid, thisinfo
619 def sorted_builds(builds):
620 return sorted(builds, key=lambda build: int(build['vercode']))
623 def post_metadata_parse(thisinfo):
625 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
626 for k, v in thisinfo.iteritems():
627 if k not in supported_metadata:
628 raise MetaDataException("Unrecognised metadata: {0}: {1}"
630 if type(v) in (float, int):
633 # convert to the odd internal format
634 for k in ('Description', 'Maintainer Notes'):
635 if isinstance(thisinfo[k], basestring):
636 text = thisinfo[k].rstrip().lstrip()
637 thisinfo[k] = text.split('\n')
639 supported_flags = (flag_defaults.keys()
640 + ['vercode', 'version', 'versionCode', 'versionName'])
641 esc_newlines = re.compile('\\\\( |\\n)')
643 for build in thisinfo['builds']:
644 for k, v in build.items():
645 if k not in supported_flags:
646 raise MetaDataException("Unrecognised build flag: {0}={1}"
649 if k == 'versionCode':
650 build['vercode'] = str(v)
651 del build['versionCode']
652 elif k == 'versionName':
653 build['version'] = str(v)
654 del build['versionName']
655 elif type(v) in (float, int):
658 keyflagtype = flagtype(k)
659 if keyflagtype == 'list':
660 # these can be bools, strings or lists, but ultimately are lists
661 if isinstance(v, basestring):
663 elif isinstance(v, bool):
664 build[k] = ['yes' if v else 'no']
665 elif isinstance(v, list):
668 if isinstance(e, bool):
669 build[k].append('yes' if v else 'no')
673 elif keyflagtype == 'script':
674 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
675 elif keyflagtype == 'bool':
676 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
677 if isinstance(v, basestring):
682 elif keyflagtype == 'string':
683 if isinstance(v, bool):
684 build[k] = 'yes' if v else 'no'
686 if not thisinfo['Description']:
687 thisinfo['Description'].append('No description available')
689 for build in thisinfo['builds']:
690 fill_build_defaults(build)
692 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
695 # Parse metadata for a single application.
697 # 'metadatapath' - the filename to read. The package id for the application comes
698 # from this filename. Pass None to get a blank entry.
700 # Returns a dictionary containing all the details of the application. There are
701 # two major kinds of information in the dictionary. Keys beginning with capital
702 # letters correspond directory to identically named keys in the metadata file.
703 # Keys beginning with lower case letters are generated in one way or another,
704 # and are not found verbatim in the metadata.
706 # Known keys not originating from the metadata are:
708 # 'builds' - a list of dictionaries containing build information
709 # for each defined build
710 # 'comments' - a list of comments from the metadata file. Each is
711 # a list of the form [field, comment] where field is
712 # the name of the field it preceded in the metadata
713 # file. Where field is None, the comment goes at the
714 # end of the file. Alternatively, 'build:version' is
715 # for a comment before a particular build version.
716 # 'descriptionlines' - original lines of description as formatted in the
721 def _decode_list(data):
722 '''convert items in a list from unicode to basestring'''
725 if isinstance(item, unicode):
726 item = item.encode('utf-8')
727 elif isinstance(item, list):
728 item = _decode_list(item)
729 elif isinstance(item, dict):
730 item = _decode_dict(item)
735 def _decode_dict(data):
736 '''convert items in a dict from unicode to basestring'''
738 for key, value in data.iteritems():
739 if isinstance(key, unicode):
740 key = key.encode('utf-8')
741 if isinstance(value, unicode):
742 value = value.encode('utf-8')
743 elif isinstance(value, list):
744 value = _decode_list(value)
745 elif isinstance(value, dict):
746 value = _decode_dict(value)
751 def parse_metadata(metadatapath):
752 _, ext = common.get_extension(metadatapath)
753 accepted = common.config['accepted_formats']
754 if ext not in accepted:
755 logging.critical('"' + metadatapath
756 + '" is not in an accepted format, '
757 + 'convert to: ' + ', '.join(accepted))
761 return parse_txt_metadata(metadatapath)
763 return parse_json_metadata(metadatapath)
765 return parse_xml_metadata(metadatapath)
767 return parse_yaml_metadata(metadatapath)
769 logging.critical('Unknown metadata format: ' + metadatapath)
773 def parse_json_metadata(metadatapath):
775 appid, thisinfo = get_default_app_info(metadatapath)
777 # fdroid metadata is only strings and booleans, no floats or ints. And
778 # json returns unicode, and fdroidserver still uses plain python strings
779 # TODO create schema using https://pypi.python.org/pypi/jsonschema
780 jsoninfo = json.load(open(metadatapath, 'r'),
781 object_hook=_decode_dict,
782 parse_int=lambda s: s,
783 parse_float=lambda s: s)
784 thisinfo.update(jsoninfo)
785 post_metadata_parse(thisinfo)
787 return (appid, thisinfo)
790 def parse_xml_metadata(metadatapath):
792 appid, thisinfo = get_default_app_info(metadatapath)
794 tree = ElementTree.ElementTree(file=metadatapath)
795 root = tree.getroot()
797 if root.tag != 'resources':
798 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
801 supported_metadata = app_defaults.keys()
803 if child.tag != 'builds':
804 # builds does not have name="" attrib
805 name = child.attrib['name']
806 if name not in supported_metadata:
807 raise MetaDataException("Unrecognised metadata: <"
808 + child.tag + ' name="' + name + '">'
810 + "</" + child.tag + '>')
812 if child.tag == 'string':
813 thisinfo[name] = child.text
814 elif child.tag == 'string-array':
817 items.append(item.text)
818 thisinfo[name] = items
819 elif child.tag == 'builds':
824 builddict[key.tag] = key.text
825 builds.append(builddict)
826 thisinfo['builds'] = builds
828 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
829 if not isinstance(thisinfo['Requires Root'], bool):
830 if thisinfo['Requires Root'] == 'true':
831 thisinfo['Requires Root'] = True
833 thisinfo['Requires Root'] = False
835 post_metadata_parse(thisinfo)
837 return (appid, thisinfo)
840 def parse_yaml_metadata(metadatapath):
842 appid, thisinfo = get_default_app_info(metadatapath)
844 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
845 thisinfo.update(yamlinfo)
846 post_metadata_parse(thisinfo)
848 return (appid, thisinfo)
851 def parse_txt_metadata(metadatapath):
855 def add_buildflag(p, thisbuild):
857 raise MetaDataException("Empty build flag at {1}"
858 .format(buildlines[0], linedesc))
861 raise MetaDataException("Invalid build flag at {0} in {1}"
862 .format(buildlines[0], linedesc))
865 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
866 .format(pk, thisbuild['version'], linedesc))
869 if pk not in flag_defaults:
870 raise MetaDataException("Unrecognised build flag at {0} in {1}"
871 .format(p, linedesc))
874 pv = split_list_values(pv)
876 if len(pv) == 1 and pv[0] in ['main', 'yes']:
879 elif t == 'string' or t == 'script':
887 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
890 def parse_buildline(lines):
891 value = "".join(lines)
892 parts = [p.replace("\\,", ",")
893 for p in re.split(r"(?<!\\),", value)]
895 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
897 thisbuild['origlines'] = lines
898 thisbuild['version'] = parts[0]
899 thisbuild['vercode'] = parts[1]
900 if parts[2].startswith('!'):
901 # For backwards compatibility, handle old-style disabling,
902 # including attempting to extract the commit from the message
903 thisbuild['disable'] = parts[2][1:]
904 commit = 'unknown - see disabled'
905 index = parts[2].rfind('at ')
907 commit = parts[2][index + 3:]
908 if commit.endswith(')'):
910 thisbuild['commit'] = commit
912 thisbuild['commit'] = parts[2]
914 add_buildflag(p, thisbuild)
918 def add_comments(key):
921 thisinfo['comments'][key] = list(curcomments)
924 appid, thisinfo = get_default_app_info(metadatapath)
925 metafile = open(metadatapath, "r")
934 for line in metafile:
936 linedesc = "%s:%d" % (metafile.name, c)
937 line = line.rstrip('\r\n')
939 if not any(line.startswith(s) for s in (' ', '\t')):
940 commit = curbuild['commit'] if 'commit' in curbuild else None
941 if not commit and 'disable' not in curbuild:
942 raise MetaDataException("No commit specified for {0} in {1}"
943 .format(curbuild['version'], linedesc))
945 thisinfo['builds'].append(curbuild)
946 add_comments('build:' + curbuild['vercode'])
949 if line.endswith('\\'):
950 buildlines.append(line[:-1].lstrip())
952 buildlines.append(line.lstrip())
953 bl = ''.join(buildlines)
954 add_buildflag(bl, curbuild)
960 if line.startswith("#"):
961 curcomments.append(line[1:].strip())
964 field, value = line.split(':', 1)
966 raise MetaDataException("Invalid metadata in " + linedesc)
967 if field != field.strip() or value != value.strip():
968 raise MetaDataException("Extra spacing found in " + linedesc)
970 # Translate obsolete fields...
971 if field == 'Market Version':
972 field = 'Current Version'
973 if field == 'Market Version Code':
974 field = 'Current Version Code'
976 fieldtype = metafieldtype(field)
977 if fieldtype not in ['build', 'buildv2']:
979 if fieldtype == 'multiline':
983 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
984 elif fieldtype == 'string':
985 thisinfo[field] = value
986 elif fieldtype == 'list':
987 thisinfo[field] = split_list_values(value)
988 elif fieldtype == 'build':
989 if value.endswith("\\"):
991 buildlines = [value[:-1]]
993 curbuild = parse_buildline([value])
994 thisinfo['builds'].append(curbuild)
995 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
996 elif fieldtype == 'buildv2':
998 vv = value.split(',')
1000 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1001 .format(value, linedesc))
1002 curbuild['version'] = vv[0]
1003 curbuild['vercode'] = vv[1]
1004 if curbuild['vercode'] in vc_seen:
1005 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1006 curbuild['vercode'], linedesc))
1007 vc_seen[curbuild['vercode']] = True
1010 elif fieldtype == 'obsolete':
1011 pass # Just throw it away!
1013 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1014 elif mode == 1: # Multiline field
1018 thisinfo[field].append(line)
1019 elif mode == 2: # Line continuation mode in Build Version
1020 if line.endswith("\\"):
1021 buildlines.append(line[:-1])
1023 buildlines.append(line)
1024 curbuild = parse_buildline(buildlines)
1025 thisinfo['builds'].append(curbuild)
1026 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1030 # Mode at end of file should always be 0...
1032 raise MetaDataException(field + " not terminated in " + metafile.name)
1034 raise MetaDataException("Unterminated continuation in " + metafile.name)
1036 raise MetaDataException("Unterminated build in " + metafile.name)
1038 post_metadata_parse(thisinfo)
1040 return (appid, thisinfo)
1043 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1045 def w_comments(key):
1046 if key not in app['comments']:
1048 for line in app['comments'][key]:
1051 def w_field_always(field, value=None):
1055 w_field(field, value)
1057 def w_field_nonempty(field, value=None):
1062 w_field(field, value)
1064 w_field_nonempty('Disabled')
1065 if app['AntiFeatures']:
1066 w_field_always('AntiFeatures')
1067 w_field_nonempty('Provides')
1068 w_field_always('Categories')
1069 w_field_always('License')
1070 w_field_always('Web Site')
1071 w_field_always('Source Code')
1072 w_field_always('Issue Tracker')
1073 w_field_nonempty('Changelog')
1074 w_field_nonempty('Donate')
1075 w_field_nonempty('FlattrID')
1076 w_field_nonempty('Bitcoin')
1077 w_field_nonempty('Litecoin')
1079 w_field_nonempty('Name')
1080 w_field_nonempty('Auto Name')
1081 w_field_always('Summary')
1082 w_field_always('Description', description_txt(app['Description']))
1084 if app['Requires Root']:
1085 w_field_always('Requires Root', 'yes')
1087 if app['Repo Type']:
1088 w_field_always('Repo Type')
1089 w_field_always('Repo')
1091 w_field_always('Binaries')
1094 for build in sorted_builds(app['builds']):
1096 if build['version'] == "Ignore":
1099 w_comments('build:' + build['vercode'])
1103 if app['Maintainer Notes']:
1104 w_field_always('Maintainer Notes', app['Maintainer Notes'])
1107 w_field_nonempty('Archive Policy')
1108 w_field_always('Auto Update Mode')
1109 w_field_always('Update Check Mode')
1110 w_field_nonempty('Update Check Ignore')
1111 w_field_nonempty('Vercode Operation')
1112 w_field_nonempty('Update Check Name')
1113 w_field_nonempty('Update Check Data')
1114 if app['Current Version']:
1115 w_field_always('Current Version')
1116 w_field_always('Current Version Code')
1117 if app['No Source Since']:
1119 w_field_always('No Source Since')
1123 # Write a metadata file in txt format.
1125 # 'mf' - Writer interface (file, StringIO, ...)
1126 # 'app' - The app data
1127 def write_txt_metadata(mf, app):
1129 def w_comment(line):
1130 mf.write("# %s\n" % line)
1132 def w_field(field, value):
1133 t = metafieldtype(field)
1135 value = ','.join(value)
1136 elif t == 'multiline':
1137 if type(value) == list:
1138 value = '\n' + '\n'.join(value) + '\n.'
1140 value = '\n' + value + '\n.'
1141 mf.write("%s:%s\n" % (field, value))
1144 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1146 for key in flag_defaults:
1150 if value == flag_defaults[key]:
1160 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1162 v += ','.join(value) if type(value) == list else value
1167 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1170 def write_yaml_metadata(mf, app):
1172 def w_comment(line):
1173 mf.write("# %s\n" % line)
1178 if any(c in value for c in [': ', '%', '@', '*']):
1179 return "'" + value.replace("'", "''") + "'"
1182 def w_field(field, value, prefix='', t=None):
1184 t = metafieldtype(field)
1189 v += prefix + ' - ' + escape(e) + '\n'
1190 elif t == 'multiline':
1193 if type(value) == str:
1194 lines = value.splitlines()
1197 v += prefix + ' ' + l + '\n'
1203 cmds = [s + '&& \\' for s in value.split('&& ')]
1205 cmds[-1] = cmds[-1][:-len('&& \\')]
1206 w_field(field, cmds, prefix, 'multiline')
1209 v = ' ' + escape(value) + '\n'
1222 mf.write("builds:\n")
1225 w_field('versionName', build['version'], ' - ', 'string')
1226 w_field('versionCode', build['vercode'], ' ', 'strsng')
1227 for key in flag_defaults:
1231 if value == flag_defaults[key]:
1234 w_field(key, value, ' ', flagtype(key))
1236 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1239 def write_metadata(fmt, mf, app):
1241 return write_txt_metadata(mf, app)
1243 return write_yaml_metadata(mf, app)
1244 raise MetaDataException("Unknown metadata format given")