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'] = []
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 for comment in curcomments:
917 thisinfo['comments'].append([key, comment])
920 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
921 metafile = open(metadatapath, "r")
930 for line in metafile:
932 linedesc = "%s:%d" % (metafile.name, c)
933 line = line.rstrip('\r\n')
935 if not any(line.startswith(s) for s in (' ', '\t')):
936 commit = curbuild['commit'] if 'commit' in curbuild else None
937 if not commit and 'disable' not in curbuild:
938 raise MetaDataException("No commit specified for {0} in {1}"
939 .format(curbuild['version'], linedesc))
941 thisinfo['builds'].append(curbuild)
942 add_comments('build:' + curbuild['vercode'])
945 if line.endswith('\\'):
946 buildlines.append(line[:-1].lstrip())
948 buildlines.append(line.lstrip())
949 bl = ''.join(buildlines)
950 add_buildflag(bl, curbuild)
956 if line.startswith("#"):
957 curcomments.append(line)
960 field, value = line.split(':', 1)
962 raise MetaDataException("Invalid metadata in " + linedesc)
963 if field != field.strip() or value != value.strip():
964 raise MetaDataException("Extra spacing found in " + linedesc)
966 # Translate obsolete fields...
967 if field == 'Market Version':
968 field = 'Current Version'
969 if field == 'Market Version Code':
970 field = 'Current Version Code'
972 fieldtype = metafieldtype(field)
973 if fieldtype not in ['build', 'buildv2']:
975 if fieldtype == 'multiline':
979 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
980 elif fieldtype == 'string':
981 thisinfo[field] = value
982 elif fieldtype == 'list':
983 thisinfo[field] = split_list_values(value)
984 elif fieldtype == 'build':
985 if value.endswith("\\"):
987 buildlines = [value[:-1]]
989 curbuild = parse_buildline([value])
990 thisinfo['builds'].append(curbuild)
991 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
992 elif fieldtype == 'buildv2':
994 vv = value.split(',')
996 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
997 .format(value, linedesc))
998 curbuild['version'] = vv[0]
999 curbuild['vercode'] = vv[1]
1000 if curbuild['vercode'] in vc_seen:
1001 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1002 curbuild['vercode'], linedesc))
1003 vc_seen[curbuild['vercode']] = True
1006 elif fieldtype == 'obsolete':
1007 pass # Just throw it away!
1009 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1010 elif mode == 1: # Multiline field
1014 thisinfo[field].append(line)
1015 elif mode == 2: # Line continuation mode in Build Version
1016 if line.endswith("\\"):
1017 buildlines.append(line[:-1])
1019 buildlines.append(line)
1020 curbuild = parse_buildline(buildlines)
1021 thisinfo['builds'].append(curbuild)
1022 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1026 # Mode at end of file should always be 0...
1028 raise MetaDataException(field + " not terminated in " + metafile.name)
1030 raise MetaDataException("Unterminated continuation in " + metafile.name)
1032 raise MetaDataException("Unterminated build in " + metafile.name)
1034 post_metadata_parse(thisinfo)
1036 return (appid, thisinfo)
1039 def write_metadata(mf, app, w_comment, w_field, w_build):
1041 def w_field_nonempty(field, value=None):
1045 w_field(field, value)
1047 w_field_nonempty('Disabled')
1048 if app['AntiFeatures']:
1049 w_field('AntiFeatures')
1050 w_field_nonempty('Provides')
1051 w_field('Categories')
1054 w_field('Source Code')
1055 w_field('Issue Tracker')
1056 w_field_nonempty('Changelog')
1057 w_field_nonempty('Donate')
1058 w_field_nonempty('FlattrID')
1059 w_field_nonempty('Bitcoin')
1060 w_field_nonempty('Litecoin')
1062 w_field_nonempty('Name')
1063 w_field_nonempty('Auto Name')
1065 w_field('Description', description_txt(app['Description']))
1067 if app['Requires Root']:
1068 w_field('Requires Root', 'yes')
1070 if app['Repo Type']:
1071 w_field('Repo Type')
1077 for build in sorted_builds(app['builds']):
1079 if build['version'] == "Ignore":
1082 w_comment('build:' + build['vercode'])
1086 if app['Maintainer Notes']:
1087 w_field('Maintainer Notes', app['Maintainer Notes'])
1090 w_field_nonempty('Archive Policy')
1091 w_field('Auto Update Mode')
1092 w_field('Update Check Mode')
1093 w_field_nonempty('Update Check Ignore')
1094 w_field_nonempty('Vercode Operation')
1095 w_field_nonempty('Update Check Name')
1096 w_field_nonempty('Update Check Data')
1097 if app['Current Version']:
1098 w_field('Current Version')
1099 w_field('Current Version Code')
1101 if app['No Source Since']:
1102 w_field('No Source Since')
1107 # Write a metadata file in txt format.
1109 # 'mf' - Writer interface (file, StringIO, ...)
1110 # 'app' - The app data
1111 def write_txt_metadata(mf, app):
1115 for pf, comment in app['comments']:
1117 mf.write("%s\n" % comment)
1120 def w_field(field, value=None):
1124 t = metafieldtype(field)
1126 value = ','.join(value)
1127 elif t == 'multiline':
1128 if type(value) == list:
1129 value = '\n' + '\n'.join(value) + '\n.'
1131 value = '\n' + value + '\n.'
1132 mf.write("%s:%s\n" % (field, value))
1135 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1137 for key in flag_defaults:
1141 if value == flag_defaults[key]:
1151 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1153 v += ','.join(value) if type(value) == list else value
1158 write_metadata(mf, app, w_comment, w_field, w_build)
1161 def write_yaml_metadata(mf, app):
1166 def w_field(field, value=None, prefix='', t=None):
1171 t = metafieldtype(field)
1176 v += prefix + ' - ' + e + '\n'
1177 elif t == 'multiline':
1180 if type(value) == list:
1183 lines = value.splitlines()
1186 v += prefix + ' ' + l + '\n'
1190 v = ' ' + value + '\n'
1192 mf.write("%s%s:%s" % (prefix, field, v))
1200 mf.write("builds:\n")
1203 w_field('versionName', build['version'], ' - ', 'string')
1204 w_field('versionCode', build['vercode'], ' ', 'strsng')
1205 for key in flag_defaults:
1209 if value == flag_defaults[key]:
1212 w_field(key, value, ' ', flagtype(key))
1214 write_metadata(mf, app, w_comment, w_field, w_build)