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(apps, metadatapath)
523 check_metadata(appinfo)
524 apps[appid] = appinfo
527 # Parse all descriptions at load time, just to ensure cross-referencing
528 # errors are caught early rather than when they hit the build server.
531 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
532 raise MetaDataException("Cannot resolve app id " + appid)
534 for appid, app in apps.iteritems():
536 description_html(app['Description'], linkres)
537 except MetaDataException, e:
538 raise MetaDataException("Problem with description of " + appid +
544 # Get the type expected for a given metadata field.
545 def metafieldtype(name):
546 if name in ['Description', 'Maintainer Notes']:
548 if name in ['Categories', 'AntiFeatures']:
550 if name == 'Build Version':
554 if name == 'Use Built':
556 if name not in app_defaults:
562 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
563 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
566 if name in ['init', 'prebuild', 'build']:
568 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
574 def fill_build_defaults(build):
576 def get_build_type():
577 for t in ['maven', 'gradle', 'kivy']:
584 for flag, value in flag_defaults.iteritems():
588 build['type'] = get_build_type()
589 build['ndk_path'] = common.get_ndk_path(build['ndk'])
592 def split_list_values(s):
593 # Port legacy ';' separators
594 l = [v.strip() for v in s.replace(';', ',').split(',')]
595 return [v for v in l if v]
598 def get_default_app_info_list(apps, metadatapath=None):
599 if metadatapath is None:
602 appid, _ = common.get_extension(os.path.basename(metadatapath))
604 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
605 % (metadatapath, appid, apps[appid]['metadatapath']))
609 thisinfo.update(app_defaults)
610 thisinfo['metadatapath'] = metadatapath
611 if appid is not None:
612 thisinfo['id'] = appid
614 # General defaults...
615 thisinfo['builds'] = []
616 thisinfo['comments'] = dict()
618 return appid, thisinfo
621 def sorted_builds(builds):
622 return sorted(builds, key=lambda build: int(build['vercode']))
625 def post_metadata_parse(thisinfo):
627 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
628 for k, v in thisinfo.iteritems():
629 if k not in supported_metadata:
630 raise MetaDataException("Unrecognised metadata: {0}: {1}"
632 if type(v) in (float, int):
635 # convert to the odd internal format
636 for k in ('Description', 'Maintainer Notes'):
637 if isinstance(thisinfo[k], basestring):
638 text = thisinfo[k].rstrip().lstrip()
639 thisinfo[k] = text.split('\n')
641 supported_flags = (flag_defaults.keys()
642 + ['vercode', 'version', 'versionCode', 'versionName'])
643 esc_newlines = re.compile('\\\\( |\\n)')
645 for build in thisinfo['builds']:
646 for k, v in build.items():
647 if k not in supported_flags:
648 raise MetaDataException("Unrecognised build flag: {0}={1}"
651 if k == 'versionCode':
652 build['vercode'] = str(v)
653 del build['versionCode']
654 elif k == 'versionName':
655 build['version'] = str(v)
656 del build['versionName']
657 elif type(v) in (float, int):
660 keyflagtype = flagtype(k)
661 if keyflagtype == 'list':
662 # these can be bools, strings or lists, but ultimately are lists
663 if isinstance(v, basestring):
665 elif isinstance(v, bool):
666 build[k] = ['yes' if v else 'no']
667 elif isinstance(v, list):
670 if isinstance(e, bool):
671 build[k].append('yes' if v else 'no')
675 elif keyflagtype == 'script':
676 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
677 elif keyflagtype == 'bool':
678 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
679 if isinstance(v, basestring):
684 elif keyflagtype == 'string':
685 if isinstance(v, bool):
686 build[k] = 'yes' if v else 'no'
688 if not thisinfo['Description']:
689 thisinfo['Description'].append('No description available')
691 for build in thisinfo['builds']:
692 fill_build_defaults(build)
694 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
697 # Parse metadata for a single application.
699 # 'metadatapath' - the filename to read. The package id for the application comes
700 # from this filename. Pass None to get a blank entry.
702 # Returns a dictionary containing all the details of the application. There are
703 # two major kinds of information in the dictionary. Keys beginning with capital
704 # letters correspond directory to identically named keys in the metadata file.
705 # Keys beginning with lower case letters are generated in one way or another,
706 # and are not found verbatim in the metadata.
708 # Known keys not originating from the metadata are:
710 # 'builds' - a list of dictionaries containing build information
711 # for each defined build
712 # 'comments' - a list of comments from the metadata file. Each is
713 # a list of the form [field, comment] where field is
714 # the name of the field it preceded in the metadata
715 # file. Where field is None, the comment goes at the
716 # end of the file. Alternatively, 'build:version' is
717 # for a comment before a particular build version.
718 # 'descriptionlines' - original lines of description as formatted in the
723 def _decode_list(data):
724 '''convert items in a list from unicode to basestring'''
727 if isinstance(item, unicode):
728 item = item.encode('utf-8')
729 elif isinstance(item, list):
730 item = _decode_list(item)
731 elif isinstance(item, dict):
732 item = _decode_dict(item)
737 def _decode_dict(data):
738 '''convert items in a dict from unicode to basestring'''
740 for key, value in data.iteritems():
741 if isinstance(key, unicode):
742 key = key.encode('utf-8')
743 if isinstance(value, unicode):
744 value = value.encode('utf-8')
745 elif isinstance(value, list):
746 value = _decode_list(value)
747 elif isinstance(value, dict):
748 value = _decode_dict(value)
753 def parse_metadata(apps, metadatapath):
754 _, ext = common.get_extension(metadatapath)
755 accepted = common.config['accepted_formats']
756 if ext not in accepted:
757 logging.critical('"' + metadatapath
758 + '" is not in an accepted format, '
759 + 'convert to: ' + ', '.join(accepted))
763 return parse_txt_metadata(apps, metadatapath)
765 return parse_json_metadata(apps, metadatapath)
767 return parse_xml_metadata(apps, metadatapath)
769 return parse_yaml_metadata(apps, metadatapath)
771 logging.critical('Unknown metadata format: ' + metadatapath)
775 def parse_json_metadata(apps, metadatapath):
777 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
779 # fdroid metadata is only strings and booleans, no floats or ints. And
780 # json returns unicode, and fdroidserver still uses plain python strings
781 # TODO create schema using https://pypi.python.org/pypi/jsonschema
782 jsoninfo = json.load(open(metadatapath, 'r'),
783 object_hook=_decode_dict,
784 parse_int=lambda s: s,
785 parse_float=lambda s: s)
786 thisinfo.update(jsoninfo)
787 post_metadata_parse(thisinfo)
789 return (appid, thisinfo)
792 def parse_xml_metadata(apps, metadatapath):
794 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
796 tree = ElementTree.ElementTree(file=metadatapath)
797 root = tree.getroot()
799 if root.tag != 'resources':
800 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
803 supported_metadata = app_defaults.keys()
805 if child.tag != 'builds':
806 # builds does not have name="" attrib
807 name = child.attrib['name']
808 if name not in supported_metadata:
809 raise MetaDataException("Unrecognised metadata: <"
810 + child.tag + ' name="' + name + '">'
812 + "</" + child.tag + '>')
814 if child.tag == 'string':
815 thisinfo[name] = child.text
816 elif child.tag == 'string-array':
819 items.append(item.text)
820 thisinfo[name] = items
821 elif child.tag == 'builds':
826 builddict[key.tag] = key.text
827 builds.append(builddict)
828 thisinfo['builds'] = builds
830 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
831 if not isinstance(thisinfo['Requires Root'], bool):
832 if thisinfo['Requires Root'] == 'true':
833 thisinfo['Requires Root'] = True
835 thisinfo['Requires Root'] = False
837 post_metadata_parse(thisinfo)
839 return (appid, thisinfo)
842 def parse_yaml_metadata(apps, metadatapath):
844 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
846 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
847 thisinfo.update(yamlinfo)
848 post_metadata_parse(thisinfo)
850 return (appid, thisinfo)
853 def parse_txt_metadata(apps, metadatapath):
857 def add_buildflag(p, thisbuild):
859 raise MetaDataException("Empty build flag at {1}"
860 .format(buildlines[0], linedesc))
863 raise MetaDataException("Invalid build flag at {0} in {1}"
864 .format(buildlines[0], linedesc))
867 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
868 .format(pk, thisbuild['version'], linedesc))
871 if pk not in flag_defaults:
872 raise MetaDataException("Unrecognised build flag at {0} in {1}"
873 .format(p, linedesc))
876 pv = split_list_values(pv)
878 if len(pv) == 1 and pv[0] in ['main', 'yes']:
881 elif t == 'string' or t == 'script':
889 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
892 def parse_buildline(lines):
893 value = "".join(lines)
894 parts = [p.replace("\\,", ",")
895 for p in re.split(r"(?<!\\),", value)]
897 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
899 thisbuild['origlines'] = lines
900 thisbuild['version'] = parts[0]
901 thisbuild['vercode'] = parts[1]
902 if parts[2].startswith('!'):
903 # For backwards compatibility, handle old-style disabling,
904 # including attempting to extract the commit from the message
905 thisbuild['disable'] = parts[2][1:]
906 commit = 'unknown - see disabled'
907 index = parts[2].rfind('at ')
909 commit = parts[2][index + 3:]
910 if commit.endswith(')'):
912 thisbuild['commit'] = commit
914 thisbuild['commit'] = parts[2]
916 add_buildflag(p, thisbuild)
920 def add_comments(key):
923 thisinfo['comments'][key] = list(curcomments)
926 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
927 metafile = open(metadatapath, "r")
936 for line in metafile:
938 linedesc = "%s:%d" % (metafile.name, c)
939 line = line.rstrip('\r\n')
941 if not any(line.startswith(s) for s in (' ', '\t')):
942 commit = curbuild['commit'] if 'commit' in curbuild else None
943 if not commit and 'disable' not in curbuild:
944 raise MetaDataException("No commit specified for {0} in {1}"
945 .format(curbuild['version'], linedesc))
947 thisinfo['builds'].append(curbuild)
948 add_comments('build:' + curbuild['vercode'])
951 if line.endswith('\\'):
952 buildlines.append(line[:-1].lstrip())
954 buildlines.append(line.lstrip())
955 bl = ''.join(buildlines)
956 add_buildflag(bl, curbuild)
962 if line.startswith("#"):
963 curcomments.append(line[1:].strip())
966 field, value = line.split(':', 1)
968 raise MetaDataException("Invalid metadata in " + linedesc)
969 if field != field.strip() or value != value.strip():
970 raise MetaDataException("Extra spacing found in " + linedesc)
972 # Translate obsolete fields...
973 if field == 'Market Version':
974 field = 'Current Version'
975 if field == 'Market Version Code':
976 field = 'Current Version Code'
978 fieldtype = metafieldtype(field)
979 if fieldtype not in ['build', 'buildv2']:
981 if fieldtype == 'multiline':
985 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
986 elif fieldtype == 'string':
987 thisinfo[field] = value
988 elif fieldtype == 'list':
989 thisinfo[field] = split_list_values(value)
990 elif fieldtype == 'build':
991 if value.endswith("\\"):
993 buildlines = [value[:-1]]
995 curbuild = parse_buildline([value])
996 thisinfo['builds'].append(curbuild)
997 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
998 elif fieldtype == 'buildv2':
1000 vv = value.split(',')
1002 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1003 .format(value, linedesc))
1004 curbuild['version'] = vv[0]
1005 curbuild['vercode'] = vv[1]
1006 if curbuild['vercode'] in vc_seen:
1007 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1008 curbuild['vercode'], linedesc))
1009 vc_seen[curbuild['vercode']] = True
1012 elif fieldtype == 'obsolete':
1013 pass # Just throw it away!
1015 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1016 elif mode == 1: # Multiline field
1020 thisinfo[field].append(line)
1021 elif mode == 2: # Line continuation mode in Build Version
1022 if line.endswith("\\"):
1023 buildlines.append(line[:-1])
1025 buildlines.append(line)
1026 curbuild = parse_buildline(buildlines)
1027 thisinfo['builds'].append(curbuild)
1028 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1032 # Mode at end of file should always be 0...
1034 raise MetaDataException(field + " not terminated in " + metafile.name)
1036 raise MetaDataException("Unterminated continuation in " + metafile.name)
1038 raise MetaDataException("Unterminated build in " + metafile.name)
1040 post_metadata_parse(thisinfo)
1042 return (appid, thisinfo)
1045 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1047 def w_comments(key):
1048 if key not in app['comments']:
1050 for line in app['comments'][key]:
1053 def w_field_always(field, value=None):
1057 w_field(field, value)
1059 def w_field_nonempty(field, value=None):
1064 w_field(field, value)
1066 w_field_nonempty('Disabled')
1067 if app['AntiFeatures']:
1068 w_field_always('AntiFeatures')
1069 w_field_nonempty('Provides')
1070 w_field_always('Categories')
1071 w_field_always('License')
1072 w_field_always('Web Site')
1073 w_field_always('Source Code')
1074 w_field_always('Issue Tracker')
1075 w_field_nonempty('Changelog')
1076 w_field_nonempty('Donate')
1077 w_field_nonempty('FlattrID')
1078 w_field_nonempty('Bitcoin')
1079 w_field_nonempty('Litecoin')
1081 w_field_nonempty('Name')
1082 w_field_nonempty('Auto Name')
1083 w_field_always('Summary')
1084 w_field_always('Description', description_txt(app['Description']))
1086 if app['Requires Root']:
1087 w_field_always('Requires Root', 'yes')
1089 if app['Repo Type']:
1090 w_field_always('Repo Type')
1091 w_field_always('Repo')
1093 w_field_always('Binaries')
1096 for build in sorted_builds(app['builds']):
1098 if build['version'] == "Ignore":
1101 w_comments('build:' + build['vercode'])
1105 if app['Maintainer Notes']:
1106 w_field_always('Maintainer Notes', app['Maintainer Notes'])
1109 w_field_nonempty('Archive Policy')
1110 w_field_always('Auto Update Mode')
1111 w_field_always('Update Check Mode')
1112 w_field_nonempty('Update Check Ignore')
1113 w_field_nonempty('Vercode Operation')
1114 w_field_nonempty('Update Check Name')
1115 w_field_nonempty('Update Check Data')
1116 if app['Current Version']:
1117 w_field_always('Current Version')
1118 w_field_always('Current Version Code')
1119 if app['No Source Since']:
1121 w_field_always('No Source Since')
1125 # Write a metadata file in txt format.
1127 # 'mf' - Writer interface (file, StringIO, ...)
1128 # 'app' - The app data
1129 def write_txt_metadata(mf, app):
1131 def w_comment(line):
1132 mf.write("# %s\n" % line)
1134 def w_field(field, value):
1135 t = metafieldtype(field)
1137 value = ','.join(value)
1138 elif t == 'multiline':
1139 if type(value) == list:
1140 value = '\n' + '\n'.join(value) + '\n.'
1142 value = '\n' + value + '\n.'
1143 mf.write("%s:%s\n" % (field, value))
1146 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1148 for key in flag_defaults:
1152 if value == flag_defaults[key]:
1162 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1164 v += ','.join(value) if type(value) == list else value
1169 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1172 def write_yaml_metadata(mf, app):
1174 def w_comment(line):
1175 mf.write("# %s\n" % line)
1180 if any(c in value for c in [': ', '%', '@', '*']):
1181 return "'" + value.replace("'", "''") + "'"
1184 def w_field(field, value, prefix='', t=None):
1186 t = metafieldtype(field)
1191 v += prefix + ' - ' + escape(e) + '\n'
1192 elif t == 'multiline':
1195 if type(value) == str:
1196 lines = value.splitlines()
1199 v += prefix + ' ' + l + '\n'
1205 cmds = [s + '&& \\' for s in value.split('&& ')]
1207 cmds[-1] = cmds[-1][:-len('&& \\')]
1208 w_field(field, cmds, prefix, 'multiline')
1211 v = ' ' + escape(value) + '\n'
1224 mf.write("builds:\n")
1227 w_field('versionName', build['version'], ' - ', 'string')
1228 w_field('versionCode', build['vercode'], ' ', 'strsng')
1229 for key in flag_defaults:
1233 if value == flag_defaults[key]:
1236 w_field(key, value, ' ', flagtype(key))
1238 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1241 def write_metadata(fmt, mf, app):
1243 return write_txt_metadata(mf, app)
1245 return write_yaml_metadata(mf, app)
1246 raise MetaDataException("Unknown metadata format given")