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 = os.path.splitext(os.path.basename(metadatapath))[0]
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):
670 elif keyflagtype == 'script':
671 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
672 elif keyflagtype == 'bool':
673 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
674 if isinstance(v, basestring):
680 if not thisinfo['Description']:
681 thisinfo['Description'].append('No description available')
683 for build in thisinfo['builds']:
684 fill_build_defaults(build)
686 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
689 # Parse metadata for a single application.
691 # 'metadatapath' - the filename to read. The package id for the application comes
692 # from this filename. Pass None to get a blank entry.
694 # Returns a dictionary containing all the details of the application. There are
695 # two major kinds of information in the dictionary. Keys beginning with capital
696 # letters correspond directory to identically named keys in the metadata file.
697 # Keys beginning with lower case letters are generated in one way or another,
698 # and are not found verbatim in the metadata.
700 # Known keys not originating from the metadata are:
702 # 'builds' - a list of dictionaries containing build information
703 # for each defined build
704 # 'comments' - a list of comments from the metadata file. Each is
705 # a list of the form [field, comment] where field is
706 # the name of the field it preceded in the metadata
707 # file. Where field is None, the comment goes at the
708 # end of the file. Alternatively, 'build:version' is
709 # for a comment before a particular build version.
710 # 'descriptionlines' - original lines of description as formatted in the
715 def _decode_list(data):
716 '''convert items in a list from unicode to basestring'''
719 if isinstance(item, unicode):
720 item = item.encode('utf-8')
721 elif isinstance(item, list):
722 item = _decode_list(item)
723 elif isinstance(item, dict):
724 item = _decode_dict(item)
729 def _decode_dict(data):
730 '''convert items in a dict from unicode to basestring'''
732 for key, value in data.iteritems():
733 if isinstance(key, unicode):
734 key = key.encode('utf-8')
735 if isinstance(value, unicode):
736 value = value.encode('utf-8')
737 elif isinstance(value, list):
738 value = _decode_list(value)
739 elif isinstance(value, dict):
740 value = _decode_dict(value)
745 def parse_metadata(apps, metadatapath):
746 root, ext = os.path.splitext(metadatapath)
747 metadataformat = ext[1:]
748 accepted = common.config['accepted_formats']
749 if metadataformat not in accepted:
750 logging.critical('"' + metadatapath
751 + '" is not in an accepted format, '
752 + 'convert to: ' + ', '.join(accepted))
755 if metadataformat == 'txt':
756 return parse_txt_metadata(apps, metadatapath)
757 if metadataformat == 'json':
758 return parse_json_metadata(apps, metadatapath)
759 if metadataformat == 'xml':
760 return parse_xml_metadata(apps, metadatapath)
761 if metadataformat == 'yaml':
762 return parse_yaml_metadata(apps, metadatapath)
764 logging.critical('Unknown metadata format: ' + metadatapath)
768 def parse_json_metadata(apps, metadatapath):
770 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
772 # fdroid metadata is only strings and booleans, no floats or ints. And
773 # json returns unicode, and fdroidserver still uses plain python strings
774 # TODO create schema using https://pypi.python.org/pypi/jsonschema
775 jsoninfo = json.load(open(metadatapath, 'r'),
776 object_hook=_decode_dict,
777 parse_int=lambda s: s,
778 parse_float=lambda s: s)
779 thisinfo.update(jsoninfo)
780 post_metadata_parse(thisinfo)
782 return (appid, thisinfo)
785 def parse_xml_metadata(apps, metadatapath):
787 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
789 tree = ElementTree.ElementTree(file=metadatapath)
790 root = tree.getroot()
792 if root.tag != 'resources':
793 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
796 supported_metadata = app_defaults.keys()
798 if child.tag != 'builds':
799 # builds does not have name="" attrib
800 name = child.attrib['name']
801 if name not in supported_metadata:
802 raise MetaDataException("Unrecognised metadata: <"
803 + child.tag + ' name="' + name + '">'
805 + "</" + child.tag + '>')
807 if child.tag == 'string':
808 thisinfo[name] = child.text
809 elif child.tag == 'string-array':
812 items.append(item.text)
813 thisinfo[name] = items
814 elif child.tag == 'builds':
819 builddict[key.tag] = key.text
820 builds.append(builddict)
821 thisinfo['builds'] = builds
823 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
824 if not isinstance(thisinfo['Requires Root'], bool):
825 if thisinfo['Requires Root'] == 'true':
826 thisinfo['Requires Root'] = True
828 thisinfo['Requires Root'] = False
830 post_metadata_parse(thisinfo)
832 return (appid, thisinfo)
835 def parse_yaml_metadata(apps, metadatapath):
837 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
839 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
840 thisinfo.update(yamlinfo)
841 post_metadata_parse(thisinfo)
843 return (appid, thisinfo)
846 def parse_txt_metadata(apps, metadatapath):
850 def add_buildflag(p, thisbuild):
852 raise MetaDataException("Empty build flag at {1}"
853 .format(buildlines[0], linedesc))
856 raise MetaDataException("Invalid build flag at {0} in {1}"
857 .format(buildlines[0], linedesc))
860 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
861 .format(pk, thisbuild['version'], linedesc))
864 if pk not in flag_defaults:
865 raise MetaDataException("Unrecognised build flag at {0} in {1}"
866 .format(p, linedesc))
869 pv = split_list_values(pv)
871 if len(pv) == 1 and pv[0] in ['main', 'yes']:
874 elif t == 'string' or t == 'script':
882 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
885 def parse_buildline(lines):
886 value = "".join(lines)
887 parts = [p.replace("\\,", ",")
888 for p in re.split(r"(?<!\\),", value)]
890 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
892 thisbuild['origlines'] = lines
893 thisbuild['version'] = parts[0]
894 thisbuild['vercode'] = parts[1]
895 if parts[2].startswith('!'):
896 # For backwards compatibility, handle old-style disabling,
897 # including attempting to extract the commit from the message
898 thisbuild['disable'] = parts[2][1:]
899 commit = 'unknown - see disabled'
900 index = parts[2].rfind('at ')
902 commit = parts[2][index + 3:]
903 if commit.endswith(')'):
905 thisbuild['commit'] = commit
907 thisbuild['commit'] = parts[2]
909 add_buildflag(p, thisbuild)
913 def add_comments(key):
916 thisinfo['comments'][key] = list(curcomments)
919 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
920 metafile = open(metadatapath, "r")
929 for line in metafile:
931 linedesc = "%s:%d" % (metafile.name, c)
932 line = line.rstrip('\r\n')
934 if not any(line.startswith(s) for s in (' ', '\t')):
935 commit = curbuild['commit'] if 'commit' in curbuild else None
936 if not commit and 'disable' not in curbuild:
937 raise MetaDataException("No commit specified for {0} in {1}"
938 .format(curbuild['version'], linedesc))
940 thisinfo['builds'].append(curbuild)
941 add_comments('build:' + curbuild['vercode'])
944 if line.endswith('\\'):
945 buildlines.append(line[:-1].lstrip())
947 buildlines.append(line.lstrip())
948 bl = ''.join(buildlines)
949 add_buildflag(bl, curbuild)
955 if line.startswith("#"):
956 curcomments.append(line[1:].strip())
959 field, value = line.split(':', 1)
961 raise MetaDataException("Invalid metadata in " + linedesc)
962 if field != field.strip() or value != value.strip():
963 raise MetaDataException("Extra spacing found in " + linedesc)
965 # Translate obsolete fields...
966 if field == 'Market Version':
967 field = 'Current Version'
968 if field == 'Market Version Code':
969 field = 'Current Version Code'
971 fieldtype = metafieldtype(field)
972 if fieldtype not in ['build', 'buildv2']:
974 if fieldtype == 'multiline':
978 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
979 elif fieldtype == 'string':
980 thisinfo[field] = value
981 elif fieldtype == 'list':
982 thisinfo[field] = split_list_values(value)
983 elif fieldtype == 'build':
984 if value.endswith("\\"):
986 buildlines = [value[:-1]]
988 curbuild = parse_buildline([value])
989 thisinfo['builds'].append(curbuild)
990 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
991 elif fieldtype == 'buildv2':
993 vv = value.split(',')
995 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
996 .format(value, linedesc))
997 curbuild['version'] = vv[0]
998 curbuild['vercode'] = vv[1]
999 if curbuild['vercode'] in vc_seen:
1000 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1001 curbuild['vercode'], linedesc))
1002 vc_seen[curbuild['vercode']] = True
1005 elif fieldtype == 'obsolete':
1006 pass # Just throw it away!
1008 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1009 elif mode == 1: # Multiline field
1013 thisinfo[field].append(line)
1014 elif mode == 2: # Line continuation mode in Build Version
1015 if line.endswith("\\"):
1016 buildlines.append(line[:-1])
1018 buildlines.append(line)
1019 curbuild = parse_buildline(buildlines)
1020 thisinfo['builds'].append(curbuild)
1021 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1025 # Mode at end of file should always be 0...
1027 raise MetaDataException(field + " not terminated in " + metafile.name)
1029 raise MetaDataException("Unterminated continuation in " + metafile.name)
1031 raise MetaDataException("Unterminated build in " + metafile.name)
1033 post_metadata_parse(thisinfo)
1035 return (appid, thisinfo)
1038 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1040 def w_comments(key):
1041 if key not in app['comments']:
1043 for line in app['comments'][key]:
1046 def w_field_always(field, value=None):
1050 w_field(field, value)
1052 def w_field_nonempty(field, value=None):
1057 w_field(field, value)
1059 w_field_nonempty('Disabled')
1060 if app['AntiFeatures']:
1061 w_field_always('AntiFeatures')
1062 w_field_nonempty('Provides')
1063 w_field_always('Categories')
1064 w_field_always('License')
1065 w_field_always('Web Site')
1066 w_field_always('Source Code')
1067 w_field_always('Issue Tracker')
1068 w_field_nonempty('Changelog')
1069 w_field_nonempty('Donate')
1070 w_field_nonempty('FlattrID')
1071 w_field_nonempty('Bitcoin')
1072 w_field_nonempty('Litecoin')
1074 w_field_nonempty('Name')
1075 w_field_nonempty('Auto Name')
1076 w_field_always('Summary')
1077 w_field_always('Description', description_txt(app['Description']))
1079 if app['Requires Root']:
1080 w_field_always('Requires Root', 'yes')
1082 if app['Repo Type']:
1083 w_field_always('Repo Type')
1084 w_field_always('Repo')
1086 w_field_always('Binaries')
1089 for build in sorted_builds(app['builds']):
1091 if build['version'] == "Ignore":
1094 w_comments('build:' + build['vercode'])
1098 if app['Maintainer Notes']:
1099 w_field_always('Maintainer Notes', app['Maintainer Notes'])
1102 w_field_nonempty('Archive Policy')
1103 w_field_always('Auto Update Mode')
1104 w_field_always('Update Check Mode')
1105 w_field_nonempty('Update Check Ignore')
1106 w_field_nonempty('Vercode Operation')
1107 w_field_nonempty('Update Check Name')
1108 w_field_nonempty('Update Check Data')
1109 if app['Current Version']:
1110 w_field_always('Current Version')
1111 w_field_always('Current Version Code')
1112 if app['No Source Since']:
1114 w_field_always('No Source Since')
1118 # Write a metadata file in txt format.
1120 # 'mf' - Writer interface (file, StringIO, ...)
1121 # 'app' - The app data
1122 def write_txt_metadata(mf, app):
1124 def w_comment(line):
1125 mf.write("# %s\n" % line)
1127 def w_field(field, value):
1128 t = metafieldtype(field)
1130 value = ','.join(value)
1131 elif t == 'multiline':
1132 if type(value) == list:
1133 value = '\n' + '\n'.join(value) + '\n.'
1135 value = '\n' + value + '\n.'
1136 mf.write("%s:%s\n" % (field, value))
1139 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1141 for key in flag_defaults:
1145 if value == flag_defaults[key]:
1155 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1157 v += ','.join(value) if type(value) == list else value
1162 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1165 def write_yaml_metadata(mf, app):
1167 def w_comment(line):
1168 mf.write("# %s\n" % line)
1170 def w_field(field, value, prefix='', t=None):
1172 t = metafieldtype(field)
1177 v += prefix + ' - ' + e + '\n'
1178 elif t == 'multiline':
1181 if type(value) == list:
1184 lines = value.splitlines()
1187 v += prefix + ' ' + l + '\n'
1193 cmds = [s + '&& \\' for s in value.split('&& ')]
1195 cmds[-1] = cmds[-1][:-len('&& \\')]
1196 w_field(field, cmds, prefix, 'multiline')
1199 v = ' ' + value + '\n'
1201 mf.write("%s%s:%s" % (prefix, field, v))
1209 mf.write("builds:\n")
1212 w_field('versionName', build['version'], ' - ', 'string')
1213 w_field('versionCode', build['vercode'], ' ', 'strsng')
1214 for key in flag_defaults:
1218 if value == flag_defaults[key]:
1221 w_field(key, value, ' ', flagtype(key))
1223 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1226 def write_metadata(fmt, mf, app):
1228 return write_txt_metadata(mf, app)
1230 return write_yaml_metadata(mf, app)
1231 raise MetaDataException("Unknown metadata format given")