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/>.
28 from cStringIO import StringIO
30 from StringIO import StringIO
33 # use libyaml if it is available
35 from yaml import CLoader
38 from yaml import Loader
41 # use the C implementation when available
42 import xml.etree.cElementTree as ElementTree
49 class MetaDataException(Exception):
51 def __init__(self, value):
57 # To filter which ones should be written to the metadata files if
85 'Update Check Ignore',
90 'Current Version Code',
93 'comments', # For formats that don't do inline comments
94 'builds', # For formats that do builds as a list
102 self.AntiFeatures = []
104 self.Categories = ['None']
105 self.License = 'Unknown'
108 self.IssueTracker = ''
117 self.Description = ''
118 self.RequiresRoot = False
122 self.MaintainerNotes = ''
123 self.ArchivePolicy = None
124 self.AutoUpdateMode = 'None'
125 self.UpdateCheckMode = 'None'
126 self.UpdateCheckIgnore = None
127 self.VercodeOperation = None
128 self.UpdateCheckName = None
129 self.UpdateCheckData = None
130 self.CurrentVersion = ''
131 self.CurrentVersionCode = '0'
132 self.NoSourceSince = ''
135 self.metadatapath = None
139 self.lastupdated = None
141 # Translates human-readable field names to attribute names, e.g.
142 # 'Auto Name' to 'AutoName'
144 def field_to_attr(cls, f):
145 return f.replace(' ', '')
147 # Translates attribute names to human-readable field names, e.g.
148 # 'AutoName' to 'Auto Name'
150 def attr_to_field(cls, k):
153 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
156 # Constructs an old-fashioned dict with the human-readable field
157 # names. Should only be used for tests.
158 def field_dict(self):
160 for k, v in self.__dict__.iteritems():
164 d['builds'].append(build.__dict__)
166 k = App.attr_to_field(k)
170 # Gets the value associated to a field name, e.g. 'Auto Name'
171 def get_field(self, f):
172 if f not in app_fields:
173 raise MetaDataException('Unrecognised app field: ' + f)
174 k = App.field_to_attr(f)
175 return getattr(self, k)
177 # Sets the value associated to a field name, e.g. 'Auto Name'
178 def set_field(self, f, v):
179 if f not in app_fields:
180 raise MetaDataException('Unrecognised app field: ' + f)
181 k = App.field_to_attr(f)
184 # Appends to the value associated to a field name, e.g. 'Auto Name'
185 def append_field(self, f, v):
186 if f not in app_fields:
187 raise MetaDataException('Unrecognised app field: ' + f)
188 k = App.field_to_attr(f)
189 if k not in self.__dict__:
190 self.__dict__[k] = [v]
192 self.__dict__[k].append(v)
194 # Like dict.update(), but using human-readable field names
195 def update_fields(self, d):
196 for f, v in d.iteritems():
200 build.update_flags(b)
201 self.builds.append(build)
216 def metafieldtype(name):
217 if name in ['Description', 'Maintainer Notes']:
218 return TYPE_MULTILINE
219 if name in ['Categories', 'AntiFeatures']:
221 if name == 'Build Version':
225 if name == 'Use Built':
227 if name in app_fields:
232 # In the order in which they are laid out on files
233 build_flags_order = [
266 build_flags = set(build_flags_order + ['version', 'vercode'])
275 self.submodules = False
283 self.oldsdkloc = False
285 self.forceversion = False
286 self.forcevercode = False
297 self.preassemble = []
298 self.gradleprops = []
299 self.antcommands = None
300 self.novcheck = False
302 def get_flag(self, f):
303 if f not in build_flags:
304 raise MetaDataException('Unrecognised build flag: ' + f)
305 return getattr(self, f)
307 def set_flag(self, f, v):
308 if f == 'versionName':
310 if f == 'versionCode':
312 if f not in build_flags:
313 raise MetaDataException('Unrecognised build flag: ' + f)
316 def append_flag(self, f, v):
317 if f not in build_flags:
318 raise MetaDataException('Unrecognised build flag: ' + f)
319 if f not in self.__dict__:
320 self.__dict__[f] = [v]
322 self.__dict__[f].append(v)
325 for f in ['maven', 'gradle', 'kivy']:
335 version = 'r10e' # falls back to latest
336 paths = common.config['ndk_paths']
337 if version not in paths:
339 return paths[version]
341 def update_flags(self, d):
342 for f, v in d.iteritems():
345 list_flags = set(['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
346 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
348 script_flags = set(['init', 'prebuild', 'build'])
349 bool_flags = set(['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
354 if name in list_flags:
356 if name in script_flags:
358 if name in bool_flags:
363 # Designates a metadata field type and checks that it matches
365 # 'name' - The long name of the field type
366 # 'matching' - List of possible values or regex expression
367 # 'sep' - Separator to use if value may be a list
368 # 'fields' - Metadata fields (Field:Value) of this type
369 # 'flags' - Build flags (flag=value) of this type
371 class FieldValidator():
373 def __init__(self, name, matching, sep, fields, flags):
375 self.matching = matching
376 if type(matching) is str:
377 self.compiled = re.compile(matching)
382 def _assert_regex(self, values, appid):
384 if not self.compiled.match(v):
385 raise MetaDataException("'%s' is not a valid %s in %s. "
386 % (v, self.name, appid) +
387 "Regex pattern: %s" % (self.matching))
389 def _assert_list(self, values, appid):
391 if v not in self.matching:
392 raise MetaDataException("'%s' is not a valid %s in %s. "
393 % (v, self.name, appid) +
394 "Possible values: %s" % (", ".join(self.matching)))
396 def check(self, v, appid):
397 if type(v) is not str or not v:
399 if self.sep is not None:
400 values = v.split(self.sep)
403 if type(self.matching) is list:
404 self._assert_list(values, appid)
406 self._assert_regex(values, appid)
409 # Generic value types
411 FieldValidator("Integer",
412 r'^[1-9][0-9]*$', None,
416 FieldValidator("Hexadecimal",
417 r'^[0-9a-f]+$', None,
421 FieldValidator("HTTP link",
422 r'^http[s]?://', None,
423 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
425 FieldValidator("Bitcoin address",
426 r'^[a-zA-Z0-9]{27,34}$', None,
430 FieldValidator("Litecoin address",
431 r'^L[a-zA-Z0-9]{33}$', None,
435 FieldValidator("bool",
436 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
438 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
441 FieldValidator("Repo Type",
442 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
446 FieldValidator("Binaries",
447 r'^http[s]?://', None,
451 FieldValidator("Archive Policy",
452 r'^[0-9]+ versions$', None,
456 FieldValidator("Anti-Feature",
457 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
461 FieldValidator("Auto Update Mode",
462 r"^(Version .+|None)$", None,
463 ["Auto Update Mode"],
466 FieldValidator("Update Check Mode",
467 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
468 ["Update Check Mode"],
473 # Check an app's metadata information for integrity errors
474 def check_metadata(app):
477 v.check(app.get_field(f), app.id)
478 for build in app.builds:
480 v.check(build.get_flag(f), app.id)
483 # Formatter for descriptions. Create an instance, and call parseline() with
484 # each line of the description source from the metadata. At the end, call
485 # end() and then text_txt and text_html will contain the result.
486 class DescriptionFormatter:
493 def __init__(self, linkres):
496 self.state = self.stNONE
499 self.html = StringIO()
500 self.text = StringIO()
502 self.linkResolver = None
503 self.linkResolver = linkres
505 def endcur(self, notstates=None):
506 if notstates and self.state in notstates:
508 if self.state == self.stPARA:
510 elif self.state == self.stUL:
512 elif self.state == self.stOL:
516 self.state = self.stNONE
517 whole_para = ' '.join(self.para_lines)
518 self.addtext(whole_para)
519 self.text.write(textwrap.fill(whole_para, 80,
520 break_long_words=False,
521 break_on_hyphens=False))
522 self.text.write('\n\n')
523 self.html.write('</p>')
524 del self.para_lines[:]
527 self.html.write('</ul>')
528 self.text.write('\n')
529 self.state = self.stNONE
532 self.html.write('</ol>')
533 self.text.write('\n')
534 self.state = self.stNONE
536 def formatted(self, txt, html):
539 txt = cgi.escape(txt)
541 index = txt.find("''")
546 if txt.startswith("'''"):
552 self.bold = not self.bold
560 self.ital = not self.ital
563 def linkify(self, txt):
567 index = txt.find("[")
569 return (res_plain + self.formatted(txt, False), res_html + self.formatted(txt, True))
570 res_plain += self.formatted(txt[:index], False)
571 res_html += self.formatted(txt[:index], True)
573 if txt.startswith("[["):
574 index = txt.find("]]")
576 raise MetaDataException("Unterminated ]]")
578 if self.linkResolver:
579 url, urltext = self.linkResolver(url)
582 res_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
584 txt = txt[index + 2:]
586 index = txt.find("]")
588 raise MetaDataException("Unterminated ]")
590 index2 = url.find(' ')
594 urltxt = url[index2 + 1:]
597 raise MetaDataException("Url title is just the URL - use [url]")
598 res_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
601 res_plain += ' (' + url + ')'
602 txt = txt[index + 1:]
604 def addtext(self, txt):
605 p, h = self.linkify(txt)
608 def parseline(self, line):
611 elif line.startswith('* '):
612 self.endcur([self.stUL])
613 self.text.write(line)
614 self.text.write('\n')
615 if self.state != self.stUL:
616 self.html.write('<ul>')
617 self.state = self.stUL
618 self.html.write('<li>')
619 self.addtext(line[1:])
620 self.html.write('</li>')
621 elif line.startswith('# '):
622 self.endcur([self.stOL])
623 self.text.write(line)
624 self.text.write('\n')
625 if self.state != self.stOL:
626 self.html.write('<ol>')
627 self.state = self.stOL
628 self.html.write('<li>')
629 self.addtext(line[1:])
630 self.html.write('</li>')
632 self.para_lines.append(line)
633 self.endcur([self.stPARA])
634 if self.state == self.stNONE:
635 self.html.write('<p>')
636 self.state = self.stPARA
640 self.text_txt = self.text.getvalue().rstrip()
641 self.text_html = self.html.getvalue()
646 # Parse multiple lines of description as written in a metadata file, returning
647 # a single string in text format and wrapped to 80 columns.
648 def description_txt(s):
649 ps = DescriptionFormatter(None)
650 for line in s.splitlines():
656 # Parse multiple lines of description as written in a metadata file, returning
657 # a single string in wiki format. Used for the Maintainer Notes field as well,
658 # because it's the same format.
659 def description_wiki(s):
663 # Parse multiple lines of description as written in a metadata file, returning
664 # a single string in HTML format.
665 def description_html(s, linkres):
666 ps = DescriptionFormatter(linkres)
667 for line in s.splitlines():
673 def parse_srclib(metadatapath):
677 # Defaults for fields that come from metadata
678 thisinfo['Repo Type'] = ''
679 thisinfo['Repo'] = ''
680 thisinfo['Subdir'] = None
681 thisinfo['Prepare'] = None
683 if not os.path.exists(metadatapath):
686 metafile = open(metadatapath, "r")
689 for line in metafile:
691 line = line.rstrip('\r\n')
692 if not line or line.startswith("#"):
696 f, v = line.split(':', 1)
698 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
701 thisinfo[f] = v.split(',')
711 """Read all srclib metadata.
713 The information read will be accessible as metadata.srclibs, which is a
714 dictionary, keyed on srclib name, with the values each being a dictionary
715 in the same format as that returned by the parse_srclib function.
717 A MetaDataException is raised if there are any problems with the srclib
722 # They were already loaded
723 if srclibs is not None:
729 if not os.path.exists(srcdir):
732 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
733 srclibname = os.path.basename(metadatapath[:-4])
734 srclibs[srclibname] = parse_srclib(metadatapath)
737 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
738 # returned by the parse_txt_metadata function.
739 def read_metadata(xref=True):
741 # Always read the srclibs before the apps, since they can use a srlib as
742 # their source repository.
747 for basedir in ('metadata', 'tmp'):
748 if not os.path.exists(basedir):
751 # If there are multiple metadata files for a single appid, then the first
752 # file that is parsed wins over all the others, and the rest throw an
753 # exception. So the original .txt format is parsed first, at least until
754 # newer formats stabilize.
756 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
757 + glob.glob(os.path.join('metadata', '*.json'))
758 + glob.glob(os.path.join('metadata', '*.xml'))
759 + glob.glob(os.path.join('metadata', '*.yaml'))):
760 app = parse_metadata(metadatapath)
762 raise MetaDataException("Found multiple metadata files for " + app.id)
767 # Parse all descriptions at load time, just to ensure cross-referencing
768 # errors are caught early rather than when they hit the build server.
771 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
772 raise MetaDataException("Cannot resolve app id " + appid)
774 for appid, app in apps.iteritems():
776 description_html(app.Description, linkres)
777 except MetaDataException, e:
778 raise MetaDataException("Problem with description of " + appid +
783 # Port legacy ';' separators
784 list_sep = re.compile(r'[,;]')
787 def split_list_values(s):
789 for v in re.split(list_sep, s):
799 def get_default_app_info(metadatapath=None):
800 if metadatapath is None:
803 appid, _ = common.get_extension(os.path.basename(metadatapath))
806 app.metadatapath = metadatapath
807 if appid is not None:
813 def sorted_builds(builds):
814 return sorted(builds, key=lambda build: int(build.vercode))
817 esc_newlines = re.compile(r'\\( |\n)')
820 # This function uses __dict__ to be faster
821 def post_metadata_parse(app):
823 for k, v in app.__dict__.iteritems():
824 if type(v) in (float, int):
825 app.__dict__[k] = str(v)
827 for build in app.builds:
828 for k, v in build.__dict__.iteritems():
830 if type(v) in (float, int):
831 build.__dict__[k] = str(v)
835 if ftype == TYPE_SCRIPT:
836 build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
837 elif ftype == TYPE_BOOL:
838 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
839 if isinstance(v, basestring) and v == 'true':
840 build.__dict__[k] = True
841 elif ftype == TYPE_BOOL:
842 if isinstance(v, bool) and v:
843 build.__dict__[k] = 'yes'
845 if not app.Description:
846 app.Description = 'No description available'
848 app.builds = sorted_builds(app.builds)
851 # Parse metadata for a single application.
853 # 'metadatapath' - the filename to read. The package id for the application comes
854 # from this filename. Pass None to get a blank entry.
856 # Returns a dictionary containing all the details of the application. There are
857 # two major kinds of information in the dictionary. Keys beginning with capital
858 # letters correspond directory to identically named keys in the metadata file.
859 # Keys beginning with lower case letters are generated in one way or another,
860 # and are not found verbatim in the metadata.
862 # Known keys not originating from the metadata are:
864 # 'builds' - a list of dictionaries containing build information
865 # for each defined build
866 # 'comments' - a list of comments from the metadata file. Each is
867 # a list of the form [field, comment] where field is
868 # the name of the field it preceded in the metadata
869 # file. Where field is None, the comment goes at the
870 # end of the file. Alternatively, 'build:version' is
871 # for a comment before a particular build version.
872 # 'descriptionlines' - original lines of description as formatted in the
877 def _decode_list(data):
878 '''convert items in a list from unicode to basestring'''
881 if isinstance(item, unicode):
882 item = item.encode('utf-8')
883 elif isinstance(item, list):
884 item = _decode_list(item)
885 elif isinstance(item, dict):
886 item = _decode_dict(item)
891 def _decode_dict(data):
892 '''convert items in a dict from unicode to basestring'''
894 for k, v in data.iteritems():
895 if isinstance(k, unicode):
896 k = k.encode('utf-8')
897 if isinstance(v, unicode):
898 v = v.encode('utf-8')
899 elif isinstance(v, list):
901 elif isinstance(v, dict):
907 def parse_metadata(metadatapath):
908 _, ext = common.get_extension(metadatapath)
909 accepted = common.config['accepted_formats']
910 if ext not in accepted:
911 raise MetaDataException('"%s" is not an accepted format, convert to: %s' % (
912 metadatapath, ', '.join(accepted)))
916 app = parse_txt_metadata(metadatapath)
918 app = parse_json_metadata(metadatapath)
920 app = parse_xml_metadata(metadatapath)
922 app = parse_yaml_metadata(metadatapath)
924 raise MetaDataException('Unknown metadata format: %s' % metadatapath)
926 post_metadata_parse(app)
930 def parse_json_metadata(metadatapath):
932 app = get_default_app_info(metadatapath)
934 # fdroid metadata is only strings and booleans, no floats or ints. And
935 # json returns unicode, and fdroidserver still uses plain python strings
936 # TODO create schema using https://pypi.python.org/pypi/jsonschema
938 with open(metadatapath, 'r') as f:
939 jsoninfo = json.load(f, object_hook=_decode_dict,
940 parse_int=lambda s: s,
941 parse_float=lambda s: s)
942 app.update_fields(jsoninfo)
943 for f in ['Description', 'Maintainer Notes']:
945 app.set_field(f, '\n'.join(v))
949 def parse_xml_metadata(metadatapath):
951 app = get_default_app_info(metadatapath)
953 tree = ElementTree.ElementTree(file=metadatapath)
954 root = tree.getroot()
956 if root.tag != 'resources':
957 raise MetaDataException('%s does not have root as <resources></resources>!' % metadatapath)
960 if child.tag != 'builds':
961 # builds does not have name="" attrib
962 name = child.attrib['name']
964 if child.tag == 'string':
965 app.set_field(name, child.text)
966 elif child.tag == 'string-array':
968 app.append_field(name, item.text)
969 elif child.tag == 'builds':
973 build.set_flag(key.tag, key.text)
974 app.builds.append(build)
976 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
977 if not isinstance(app.RequiresRoot, bool):
978 app.RequiresRoot = app.RequiresRoot == 'true'
983 def parse_yaml_metadata(metadatapath):
985 app = get_default_app_info(metadatapath)
988 with open(metadatapath, 'r') as f:
989 yamlinfo = yaml.load(f, Loader=YamlLoader)
990 app.update_fields(yamlinfo)
994 build_line_sep = re.compile(r'(?<!\\),')
995 build_cont = re.compile(r'^[ \t]')
998 def parse_txt_metadata(metadatapath):
1002 def add_buildflag(p, build):
1004 raise MetaDataException("Empty build flag at {1}"
1005 .format(buildlines[0], linedesc))
1006 bv = p.split('=', 1)
1008 raise MetaDataException("Invalid build flag at {0} in {1}"
1009 .format(buildlines[0], linedesc))
1015 pv = split_list_values(pv)
1016 build.set_flag(pk, pv)
1017 elif t == TYPE_STRING or t == TYPE_SCRIPT:
1018 build.set_flag(pk, pv)
1019 elif t == TYPE_BOOL:
1021 build.set_flag(pk, True)
1023 def parse_buildline(lines):
1025 parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
1027 raise MetaDataException("Invalid build format: " + v + " in " + metafile.name)
1029 build.origlines = lines
1030 build.version = parts[0]
1031 build.vercode = parts[1]
1032 if parts[2].startswith('!'):
1033 # For backwards compatibility, handle old-style disabling,
1034 # including attempting to extract the commit from the message
1035 build.disable = parts[2][1:]
1036 commit = 'unknown - see disabled'
1037 index = parts[2].rfind('at ')
1039 commit = parts[2][index + 3:]
1040 if commit.endswith(')'):
1041 commit = commit[:-1]
1042 build.commit = commit
1044 build.commit = parts[2]
1046 add_buildflag(p, build)
1050 def add_comments(key):
1053 app.comments[key] = list(curcomments)
1056 app = get_default_app_info(metadatapath)
1057 metafile = open(metadatapath, "r")
1061 multiline_lines = []
1067 for line in metafile:
1069 linedesc = "%s:%d" % (metafile.name, c)
1070 line = line.rstrip('\r\n')
1072 if build_cont.match(line):
1073 if line.endswith('\\'):
1074 buildlines.append(line[:-1].lstrip())
1076 buildlines.append(line.lstrip())
1077 bl = ''.join(buildlines)
1078 add_buildflag(bl, build)
1081 if not build.commit and not build.disable:
1082 raise MetaDataException("No commit specified for {0} in {1}"
1083 .format(build.version, linedesc))
1085 app.builds.append(build)
1086 add_comments('build:' + build.vercode)
1092 if line.startswith("#"):
1093 curcomments.append(line[1:].strip())
1096 f, v = line.split(':', 1)
1098 raise MetaDataException("Invalid metadata in " + linedesc)
1100 # Translate obsolete fields...
1101 if f == 'Market Version':
1102 f = 'Current Version'
1103 if f == 'Market Version Code':
1104 f = 'Current Version Code'
1106 ftype = metafieldtype(f)
1107 if ftype not in [TYPE_BUILD, TYPE_BUILD_V2]:
1109 if ftype == TYPE_MULTILINE:
1112 raise MetaDataException("Unexpected text on same line as " + f + " in " + linedesc)
1113 elif ftype == TYPE_STRING:
1115 elif ftype == TYPE_LIST:
1116 app.set_field(f, split_list_values(v))
1117 elif ftype == TYPE_BUILD:
1118 if v.endswith("\\"):
1121 buildlines.append(v[:-1])
1123 build = parse_buildline([v])
1124 app.builds.append(build)
1125 add_comments('build:' + app.builds[-1].vercode)
1126 elif ftype == TYPE_BUILD_V2:
1129 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1130 .format(v, linedesc))
1132 build.version = vv[0]
1133 build.vercode = vv[1]
1134 if build.vercode in vc_seen:
1135 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1136 build.vercode, linedesc))
1137 vc_seen.add(build.vercode)
1140 elif ftype == TYPE_OBSOLETE:
1141 pass # Just throw it away!
1143 raise MetaDataException("Unrecognised field '" + f + "' in " + linedesc)
1144 elif mode == 1: # Multiline field
1147 app.set_field(f, '\n'.join(multiline_lines))
1148 del multiline_lines[:]
1150 multiline_lines.append(line)
1151 elif mode == 2: # Line continuation mode in Build Version
1152 if line.endswith("\\"):
1153 buildlines.append(line[:-1])
1155 buildlines.append(line)
1156 build = parse_buildline(buildlines)
1157 app.builds.append(build)
1158 add_comments('build:' + app.builds[-1].vercode)
1163 # Mode at end of file should always be 0
1165 raise MetaDataException(f + " not terminated in " + metafile.name)
1167 raise MetaDataException("Unterminated continuation in " + metafile.name)
1169 raise MetaDataException("Unterminated build in " + metafile.name)
1174 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1176 def w_comments(key):
1177 if key not in app.comments:
1179 for line in app.comments[key]:
1182 def w_field_always(f, v=None):
1184 v = app.get_field(f)
1188 def w_field_nonempty(f, v=None):
1190 v = app.get_field(f)
1195 w_field_nonempty('Disabled')
1196 if app.AntiFeatures:
1197 w_field_always('AntiFeatures')
1198 w_field_nonempty('Provides')
1199 w_field_always('Categories')
1200 w_field_always('License')
1201 w_field_always('Web Site')
1202 w_field_always('Source Code')
1203 w_field_always('Issue Tracker')
1204 w_field_nonempty('Changelog')
1205 w_field_nonempty('Donate')
1206 w_field_nonempty('FlattrID')
1207 w_field_nonempty('Bitcoin')
1208 w_field_nonempty('Litecoin')
1210 w_field_nonempty('Name')
1211 w_field_nonempty('Auto Name')
1212 w_field_always('Summary')
1213 w_field_always('Description', description_txt(app.Description))
1215 if app.RequiresRoot:
1216 w_field_always('Requires Root', 'yes')
1219 w_field_always('Repo Type')
1220 w_field_always('Repo')
1222 w_field_always('Binaries')
1225 for build in sorted_builds(app.builds):
1227 if build.version == "Ignore":
1230 w_comments('build:' + build.vercode)
1234 if app.MaintainerNotes:
1235 w_field_always('Maintainer Notes', app.MaintainerNotes)
1238 w_field_nonempty('Archive Policy')
1239 w_field_always('Auto Update Mode')
1240 w_field_always('Update Check Mode')
1241 w_field_nonempty('Update Check Ignore')
1242 w_field_nonempty('Vercode Operation')
1243 w_field_nonempty('Update Check Name')
1244 w_field_nonempty('Update Check Data')
1245 if app.CurrentVersion:
1246 w_field_always('Current Version')
1247 w_field_always('Current Version Code')
1248 if app.NoSourceSince:
1250 w_field_always('No Source Since')
1254 # Write a metadata file in txt format.
1256 # 'mf' - Writer interface (file, StringIO, ...)
1257 # 'app' - The app data
1258 def write_txt_metadata(mf, app):
1260 def w_comment(line):
1261 mf.write("# %s\n" % line)
1264 t = metafieldtype(f)
1267 elif t == TYPE_MULTILINE:
1268 v = '\n' + v + '\n.'
1269 mf.write("%s:%s\n" % (f, v))
1272 mf.write("Build:%s,%s\n" % (build.version, build.vercode))
1274 for f in build_flags_order:
1275 v = build.get_flag(f)
1281 if t == TYPE_STRING:
1283 elif t == TYPE_BOOL:
1285 elif t == TYPE_SCRIPT:
1286 out += '&& \\\n '.join([s.lstrip() for s in v.split('&& ')])
1287 elif t == TYPE_LIST:
1288 out += ','.join(v) if type(v) == list else v
1293 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1296 def write_yaml_metadata(mf, app):
1298 def w_comment(line):
1299 mf.write("# %s\n" % line)
1304 if any(c in v for c in [': ', '%', '@', '*']):
1305 return "'" + v.replace("'", "''") + "'"
1308 def w_field(f, v, prefix='', t=None):
1310 t = metafieldtype(f)
1315 v += prefix + ' - ' + escape(e) + '\n'
1316 elif t == TYPE_MULTILINE:
1318 for l in v.splitlines():
1320 v += prefix + ' ' + l + '\n'
1323 elif t == TYPE_BOOL:
1325 elif t == TYPE_SCRIPT:
1326 cmds = [s + '&& \\' for s in v.split('&& ')]
1328 cmds[-1] = cmds[-1][:-len('&& \\')]
1329 w_field(f, cmds, prefix, 'multiline')
1332 v = ' ' + escape(v) + '\n'
1345 mf.write("builds:\n")
1348 w_field('versionName', build.version, ' - ', TYPE_STRING)
1349 w_field('versionCode', build.vercode, ' ', TYPE_STRING)
1350 for f in build_flags_order:
1351 v = build.get_flag(f)
1355 w_field(f, v, ' ', flagtype(f))
1357 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1360 def write_metadata(fmt, mf, app):
1362 return write_txt_metadata(mf, app)
1364 return write_yaml_metadata(mf, app)
1365 raise MetaDataException("Unknown metadata format given")