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 # To filter which ones should be written to the metadata files if
84 'Update Check Ignore',
89 'Current Version Code',
92 'comments', # For formats that don't do inline comments
93 'builds', # For formats that do builds as a list
101 self.AntiFeatures = []
103 self.Categories = ['None']
104 self.License = 'Unknown'
107 self.IssueTracker = ''
116 self.Description = []
117 self.RequiresRoot = False
121 self.MaintainerNotes = []
122 self.ArchivePolicy = None
123 self.AutoUpdateMode = 'None'
124 self.UpdateCheckMode = 'None'
125 self.UpdateCheckIgnore = None
126 self.VercodeOperation = None
127 self.UpdateCheckName = None
128 self.UpdateCheckData = None
129 self.CurrentVersion = ''
130 self.CurrentVersionCode = '0'
131 self.NoSourceSince = ''
134 self.metadatapath = None
138 self.lastupdated = None
140 # Translates human-readable field names to attribute names, e.g.
141 # 'Auto Name' to 'AutoName'
143 def field_to_attr(cls, f):
144 return f.replace(' ', '')
146 # Translates attribute names to human-readable field names, e.g.
147 # 'AutoName' to 'Auto Name'
149 def attr_to_field(cls, k):
152 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
155 # Constructs an old-fashioned dict with the human-readable field
156 # names. Should only be used for tests.
157 def field_dict(self):
158 return {App.attr_to_field(k): v for k, v in self.__dict__.iteritems()}
160 # Gets the value associated to a field name, e.g. 'Auto Name'
161 def get_field(self, f):
162 if f not in app_fields:
163 raise MetaDataException('Unrecognised app field: ' + f)
164 k = App.field_to_attr(f)
165 return getattr(self, k)
167 # Sets the value associated to a field name, e.g. 'Auto Name'
168 def set_field(self, f, v):
169 if f not in app_fields:
170 raise MetaDataException('Unrecognised app field: ' + f)
171 k = App.field_to_attr(f)
174 # Appends to the value associated to a field name, e.g. 'Auto Name'
175 def append_field(self, f, v):
176 if f not in app_fields:
177 raise MetaDataException('Unrecognised app field: ' + f)
178 k = App.field_to_attr(f)
179 if k not in self.__dict__:
180 self.__dict__[k] = [v]
182 self.__dict__[k].append(v)
184 # Like dict.update(), but using human-readable field names
185 def update_fields(self, d):
186 for f, v in d.iteritems():
190 # In the order in which they are laid out on files
191 # Sorted by their action and their place in the build timeline
192 # These variables can have varying datatypes. For example, anything with
193 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
194 flag_defaults = OrderedDict([
198 ('submodules', False),
206 ('oldsdkloc', False),
208 ('forceversion', False),
209 ('forcevercode', False),
213 ('update', ['auto']),
219 ('ndk', 'r10e'), # defaults to latest
222 ('antcommands', None),
227 # Designates a metadata field type and checks that it matches
229 # 'name' - The long name of the field type
230 # 'matching' - List of possible values or regex expression
231 # 'sep' - Separator to use if value may be a list
232 # 'fields' - Metadata fields (Field:Value) of this type
233 # 'attrs' - Build attributes (attr=value) of this type
235 class FieldValidator():
237 def __init__(self, name, matching, sep, fields, attrs):
239 self.matching = matching
240 if type(matching) is str:
241 self.compiled = re.compile(matching)
246 def _assert_regex(self, values, appid):
248 if not self.compiled.match(v):
249 raise MetaDataException("'%s' is not a valid %s in %s. "
250 % (v, self.name, appid) +
251 "Regex pattern: %s" % (self.matching))
253 def _assert_list(self, values, appid):
255 if v not in self.matching:
256 raise MetaDataException("'%s' is not a valid %s in %s. "
257 % (v, self.name, appid) +
258 "Possible values: %s" % (", ".join(self.matching)))
260 def check(self, value, appid):
261 if type(value) is not str or not value:
263 if self.sep is not None:
264 values = value.split(self.sep)
267 if type(self.matching) is list:
268 self._assert_list(values, appid)
270 self._assert_regex(values, appid)
273 # Generic value types
275 FieldValidator("Integer",
276 r'^[1-9][0-9]*$', None,
280 FieldValidator("Hexadecimal",
281 r'^[0-9a-f]+$', None,
285 FieldValidator("HTTP link",
286 r'^http[s]?://', None,
287 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
289 FieldValidator("Bitcoin address",
290 r'^[a-zA-Z0-9]{27,34}$', None,
294 FieldValidator("Litecoin address",
295 r'^L[a-zA-Z0-9]{33}$', None,
299 FieldValidator("bool",
300 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
302 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
305 FieldValidator("Repo Type",
306 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
310 FieldValidator("Binaries",
311 r'^http[s]?://', None,
315 FieldValidator("Archive Policy",
316 r'^[0-9]+ versions$', None,
320 FieldValidator("Anti-Feature",
321 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
325 FieldValidator("Auto Update Mode",
326 r"^(Version .+|None)$", None,
327 ["Auto Update Mode"],
330 FieldValidator("Update Check Mode",
331 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
332 ["Update Check Mode"],
337 # Check an app's metadata information for integrity errors
338 def check_metadata(app):
340 for field in v.fields:
341 v.check(app.get_field(field), app.id)
342 for build in app.builds:
344 v.check(build[attr], app.id)
347 # Formatter for descriptions. Create an instance, and call parseline() with
348 # each line of the description source from the metadata. At the end, call
349 # end() and then text_wiki and text_html will contain the result.
350 class DescriptionFormatter:
364 def __init__(self, linkres):
365 self.linkResolver = linkres
367 def endcur(self, notstates=None):
368 if notstates and self.state in notstates:
370 if self.state == self.stPARA:
372 elif self.state == self.stUL:
374 elif self.state == self.stOL:
378 self.state = self.stNONE
379 whole_para = ' '.join(self.para_lines)
380 self.addtext(whole_para)
381 self.text_txt += textwrap.fill(whole_para, 80,
382 break_long_words=False,
383 break_on_hyphens=False) + '\n\n'
384 self.text_html += '</p>'
385 del self.para_lines[:]
388 self.text_html += '</ul>'
389 self.text_txt += '\n'
390 self.state = self.stNONE
393 self.text_html += '</ol>'
394 self.text_txt += '\n'
395 self.state = self.stNONE
397 def formatted(self, txt, html):
400 txt = cgi.escape(txt)
402 index = txt.find("''")
404 return formatted + txt
405 formatted += txt[:index]
407 if txt.startswith("'''"):
413 self.bold = not self.bold
421 self.ital = not self.ital
424 def linkify(self, txt):
428 index = txt.find("[")
430 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
431 linkified_plain += self.formatted(txt[:index], False)
432 linkified_html += self.formatted(txt[:index], True)
434 if txt.startswith("[["):
435 index = txt.find("]]")
437 raise MetaDataException("Unterminated ]]")
439 if self.linkResolver:
440 url, urltext = self.linkResolver(url)
443 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
444 linkified_plain += urltext
445 txt = txt[index + 2:]
447 index = txt.find("]")
449 raise MetaDataException("Unterminated ]")
451 index2 = url.find(' ')
455 urltxt = url[index2 + 1:]
458 raise MetaDataException("Url title is just the URL - use [url]")
459 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
460 linkified_plain += urltxt
462 linkified_plain += ' (' + url + ')'
463 txt = txt[index + 1:]
465 def addtext(self, txt):
466 p, h = self.linkify(txt)
469 def parseline(self, line):
470 self.text_wiki += "%s\n" % line
473 elif line.startswith('* '):
474 self.endcur([self.stUL])
475 self.text_txt += "%s\n" % line
476 if self.state != self.stUL:
477 self.text_html += '<ul>'
478 self.state = self.stUL
479 self.text_html += '<li>'
480 self.addtext(line[1:])
481 self.text_html += '</li>'
482 elif line.startswith('# '):
483 self.endcur([self.stOL])
484 self.text_txt += "%s\n" % line
485 if self.state != self.stOL:
486 self.text_html += '<ol>'
487 self.state = self.stOL
488 self.text_html += '<li>'
489 self.addtext(line[1:])
490 self.text_html += '</li>'
492 self.para_lines.append(line)
493 self.endcur([self.stPARA])
494 if self.state == self.stNONE:
495 self.text_html += '<p>'
496 self.state = self.stPARA
500 self.text_txt = self.text_txt.strip()
503 # Parse multiple lines of description as written in a metadata file, returning
504 # a single string in text format and wrapped to 80 columns.
505 def description_txt(lines):
506 ps = DescriptionFormatter(None)
513 # Parse multiple lines of description as written in a metadata file, returning
514 # a single string in wiki format. Used for the Maintainer Notes field as well,
515 # because it's the same format.
516 def description_wiki(lines):
517 ps = DescriptionFormatter(None)
524 # Parse multiple lines of description as written in a metadata file, returning
525 # a single string in HTML format.
526 def description_html(lines, linkres):
527 ps = DescriptionFormatter(linkres)
534 def parse_srclib(metadatapath):
538 # Defaults for fields that come from metadata
539 thisinfo['Repo Type'] = ''
540 thisinfo['Repo'] = ''
541 thisinfo['Subdir'] = None
542 thisinfo['Prepare'] = None
544 if not os.path.exists(metadatapath):
547 metafile = open(metadatapath, "r")
550 for line in metafile:
552 line = line.rstrip('\r\n')
553 if not line or line.startswith("#"):
557 field, value = line.split(':', 1)
559 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
561 if field == "Subdir":
562 thisinfo[field] = value.split(',')
564 thisinfo[field] = value
570 """Read all srclib metadata.
572 The information read will be accessible as metadata.srclibs, which is a
573 dictionary, keyed on srclib name, with the values each being a dictionary
574 in the same format as that returned by the parse_srclib function.
576 A MetaDataException is raised if there are any problems with the srclib
581 # They were already loaded
582 if srclibs is not None:
588 if not os.path.exists(srcdir):
591 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
592 srclibname = os.path.basename(metadatapath[:-4])
593 srclibs[srclibname] = parse_srclib(metadatapath)
596 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
597 # returned by the parse_txt_metadata function.
598 def read_metadata(xref=True):
600 # Always read the srclibs before the apps, since they can use a srlib as
601 # their source repository.
606 for basedir in ('metadata', 'tmp'):
607 if not os.path.exists(basedir):
610 # If there are multiple metadata files for a single appid, then the first
611 # file that is parsed wins over all the others, and the rest throw an
612 # exception. So the original .txt format is parsed first, at least until
613 # newer formats stabilize.
615 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
616 + glob.glob(os.path.join('metadata', '*.json'))
617 + glob.glob(os.path.join('metadata', '*.xml'))
618 + glob.glob(os.path.join('metadata', '*.yaml'))):
619 app = parse_metadata(metadatapath)
621 raise MetaDataException("Found multiple metadata files for " + app.id)
626 # Parse all descriptions at load time, just to ensure cross-referencing
627 # errors are caught early rather than when they hit the build server.
630 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
631 raise MetaDataException("Cannot resolve app id " + appid)
633 for appid, app in apps.iteritems():
635 description_html(app.Description, linkres)
636 except MetaDataException, e:
637 raise MetaDataException("Problem with description of " + appid +
643 # Get the type expected for a given metadata field.
644 def metafieldtype(name):
645 if name in ['Description', 'Maintainer Notes']:
647 if name in ['Categories', 'AntiFeatures']:
649 if name == 'Build Version':
653 if name == 'Use Built':
655 if name not in app_fields:
661 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
662 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
665 if name in ['init', 'prebuild', 'build']:
667 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
673 def fill_build_defaults(build):
675 def get_build_type():
676 for t in ['maven', 'gradle', 'kivy']:
683 for flag, value in flag_defaults.iteritems():
687 build['type'] = get_build_type()
688 build['ndk_path'] = common.get_ndk_path(build['ndk'])
691 def split_list_values(s):
692 # Port legacy ';' separators
693 l = [v.strip() for v in s.replace(';', ',').split(',')]
694 return [v for v in l if v]
697 def get_default_app_info(metadatapath=None):
698 if metadatapath is None:
701 appid, _ = common.get_extension(os.path.basename(metadatapath))
704 app.metadatapath = metadatapath
705 if appid is not None:
711 def sorted_builds(builds):
712 return sorted(builds, key=lambda build: int(build['vercode']))
715 def post_metadata_parse(app):
719 if type(v) in (float, int):
720 app.set_field(f, str(v))
722 # convert to the odd internal format
723 for f in ('Description', 'Maintainer Notes'):
725 if isinstance(v, basestring):
726 text = v.rstrip().lstrip()
727 app.set_field(f, text.split('\n'))
729 supported_flags = (flag_defaults.keys()
730 + ['vercode', 'version', 'versionCode', 'versionName',
732 esc_newlines = re.compile('\\\\( |\\n)')
734 for build in app.builds:
735 for k, v in build.items():
736 if k not in supported_flags:
737 raise MetaDataException("Unrecognised build flag: {0}={1}"
740 if k == 'versionCode':
741 build['vercode'] = str(v)
742 del build['versionCode']
743 elif k == 'versionName':
744 build['version'] = str(v)
745 del build['versionName']
746 elif type(v) in (float, int):
749 keyflagtype = flagtype(k)
750 if keyflagtype == 'list':
751 # these can be bools, strings or lists, but ultimately are lists
752 if isinstance(v, basestring):
754 elif isinstance(v, bool):
755 build[k] = ['yes' if v else 'no']
756 elif isinstance(v, list):
759 if isinstance(e, bool):
760 build[k].append('yes' if v else 'no')
764 elif keyflagtype == 'script':
765 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
766 elif keyflagtype == 'bool':
767 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
768 if isinstance(v, basestring):
773 elif keyflagtype == 'string':
774 if isinstance(v, bool):
775 build[k] = 'yes' if v else 'no'
777 if not app.Description:
778 app.Description = ['No description available']
780 for build in app.builds:
781 fill_build_defaults(build)
783 app.builds = sorted_builds(app.builds)
786 # Parse metadata for a single application.
788 # 'metadatapath' - the filename to read. The package id for the application comes
789 # from this filename. Pass None to get a blank entry.
791 # Returns a dictionary containing all the details of the application. There are
792 # two major kinds of information in the dictionary. Keys beginning with capital
793 # letters correspond directory to identically named keys in the metadata file.
794 # Keys beginning with lower case letters are generated in one way or another,
795 # and are not found verbatim in the metadata.
797 # Known keys not originating from the metadata are:
799 # 'builds' - a list of dictionaries containing build information
800 # for each defined build
801 # 'comments' - a list of comments from the metadata file. Each is
802 # a list of the form [field, comment] where field is
803 # the name of the field it preceded in the metadata
804 # file. Where field is None, the comment goes at the
805 # end of the file. Alternatively, 'build:version' is
806 # for a comment before a particular build version.
807 # 'descriptionlines' - original lines of description as formatted in the
812 def _decode_list(data):
813 '''convert items in a list from unicode to basestring'''
816 if isinstance(item, unicode):
817 item = item.encode('utf-8')
818 elif isinstance(item, list):
819 item = _decode_list(item)
820 elif isinstance(item, dict):
821 item = _decode_dict(item)
826 def _decode_dict(data):
827 '''convert items in a dict from unicode to basestring'''
829 for key, value in data.iteritems():
830 if isinstance(key, unicode):
831 key = key.encode('utf-8')
832 if isinstance(value, unicode):
833 value = value.encode('utf-8')
834 elif isinstance(value, list):
835 value = _decode_list(value)
836 elif isinstance(value, dict):
837 value = _decode_dict(value)
842 def parse_metadata(metadatapath):
843 _, ext = common.get_extension(metadatapath)
844 accepted = common.config['accepted_formats']
845 if ext not in accepted:
846 logging.critical('"' + metadatapath
847 + '" is not in an accepted format, '
848 + 'convert to: ' + ', '.join(accepted))
852 return parse_txt_metadata(metadatapath)
854 return parse_json_metadata(metadatapath)
856 return parse_xml_metadata(metadatapath)
858 return parse_yaml_metadata(metadatapath)
860 logging.critical('Unknown metadata format: ' + metadatapath)
864 def parse_json_metadata(metadatapath):
866 app = get_default_app_info(metadatapath)
868 # fdroid metadata is only strings and booleans, no floats or ints. And
869 # json returns unicode, and fdroidserver still uses plain python strings
870 # TODO create schema using https://pypi.python.org/pypi/jsonschema
871 jsoninfo = json.load(open(metadatapath, 'r'),
872 object_hook=_decode_dict,
873 parse_int=lambda s: s,
874 parse_float=lambda s: s)
875 app.update_fields(jsoninfo)
876 post_metadata_parse(app)
881 def parse_xml_metadata(metadatapath):
883 app = get_default_app_info(metadatapath)
885 tree = ElementTree.ElementTree(file=metadatapath)
886 root = tree.getroot()
888 if root.tag != 'resources':
889 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
893 if child.tag != 'builds':
894 # builds does not have name="" attrib
895 name = child.attrib['name']
897 if child.tag == 'string':
898 app.set_field(name, child.text)
899 elif child.tag == 'string-array':
902 items.append(item.text)
903 app.set_field(name, items)
904 elif child.tag == 'builds':
908 builddict[key.tag] = key.text
909 app.builds.append(builddict)
911 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
912 if not isinstance(app.RequiresRoot, bool):
913 if app.RequiresRoot == 'true':
914 app.RequiresRoot = True
916 app.RequiresRoot = False
918 post_metadata_parse(app)
923 def parse_yaml_metadata(metadatapath):
925 app = get_default_app_info(metadatapath)
927 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
928 app.update_fields(yamlinfo)
929 post_metadata_parse(app)
934 def parse_txt_metadata(metadatapath):
938 def add_buildflag(p, thisbuild):
940 raise MetaDataException("Empty build flag at {1}"
941 .format(buildlines[0], linedesc))
944 raise MetaDataException("Invalid build flag at {0} in {1}"
945 .format(buildlines[0], linedesc))
948 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
949 .format(pk, thisbuild['version'], linedesc))
952 if pk not in flag_defaults:
953 raise MetaDataException("Unrecognised build flag at {0} in {1}"
954 .format(p, linedesc))
957 pv = split_list_values(pv)
959 if len(pv) == 1 and pv[0] in ['main', 'yes']:
962 elif t == 'string' or t == 'script':
970 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
973 def parse_buildline(lines):
974 value = "".join(lines)
975 parts = [p.replace("\\,", ",")
976 for p in re.split(r"(?<!\\),", value)]
978 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
980 thisbuild['origlines'] = lines
981 thisbuild['version'] = parts[0]
982 thisbuild['vercode'] = parts[1]
983 if parts[2].startswith('!'):
984 # For backwards compatibility, handle old-style disabling,
985 # including attempting to extract the commit from the message
986 thisbuild['disable'] = parts[2][1:]
987 commit = 'unknown - see disabled'
988 index = parts[2].rfind('at ')
990 commit = parts[2][index + 3:]
991 if commit.endswith(')'):
993 thisbuild['commit'] = commit
995 thisbuild['commit'] = parts[2]
997 add_buildflag(p, thisbuild)
1001 def add_comments(key):
1004 app.comments[key] = list(curcomments)
1007 app = get_default_app_info(metadatapath)
1008 metafile = open(metadatapath, "r")
1017 for line in metafile:
1019 linedesc = "%s:%d" % (metafile.name, c)
1020 line = line.rstrip('\r\n')
1022 if not any(line.startswith(s) for s in (' ', '\t')):
1023 commit = curbuild['commit'] if 'commit' in curbuild else None
1024 if not commit and 'disable' not in curbuild:
1025 raise MetaDataException("No commit specified for {0} in {1}"
1026 .format(curbuild['version'], linedesc))
1028 app.builds.append(curbuild)
1029 add_comments('build:' + curbuild['vercode'])
1032 if line.endswith('\\'):
1033 buildlines.append(line[:-1].lstrip())
1035 buildlines.append(line.lstrip())
1036 bl = ''.join(buildlines)
1037 add_buildflag(bl, curbuild)
1043 if line.startswith("#"):
1044 curcomments.append(line[1:].strip())
1047 field, value = line.split(':', 1)
1049 raise MetaDataException("Invalid metadata in " + linedesc)
1050 if field != field.strip() or value != value.strip():
1051 raise MetaDataException("Extra spacing found in " + linedesc)
1053 # Translate obsolete fields...
1054 if field == 'Market Version':
1055 field = 'Current Version'
1056 if field == 'Market Version Code':
1057 field = 'Current Version Code'
1059 fieldtype = metafieldtype(field)
1060 if fieldtype not in ['build', 'buildv2']:
1062 if fieldtype == 'multiline':
1065 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
1066 elif fieldtype == 'string':
1067 app.set_field(field, value)
1068 elif fieldtype == 'list':
1069 app.set_field(field, split_list_values(value))
1070 elif fieldtype == 'build':
1071 if value.endswith("\\"):
1073 buildlines = [value[:-1]]
1075 curbuild = parse_buildline([value])
1076 app.builds.append(curbuild)
1077 add_comments('build:' + app.builds[-1]['vercode'])
1078 elif fieldtype == 'buildv2':
1080 vv = value.split(',')
1082 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1083 .format(value, linedesc))
1084 curbuild['version'] = vv[0]
1085 curbuild['vercode'] = vv[1]
1086 if curbuild['vercode'] in vc_seen:
1087 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1088 curbuild['vercode'], linedesc))
1089 vc_seen[curbuild['vercode']] = True
1092 elif fieldtype == 'obsolete':
1093 pass # Just throw it away!
1095 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1096 elif mode == 1: # Multiline field
1100 app.append_field(field, line)
1101 elif mode == 2: # Line continuation mode in Build Version
1102 if line.endswith("\\"):
1103 buildlines.append(line[:-1])
1105 buildlines.append(line)
1106 curbuild = parse_buildline(buildlines)
1107 app.builds.append(curbuild)
1108 add_comments('build:' + app.builds[-1]['vercode'])
1112 # Mode at end of file should always be 0...
1114 raise MetaDataException(field + " not terminated in " + metafile.name)
1116 raise MetaDataException("Unterminated continuation in " + metafile.name)
1118 raise MetaDataException("Unterminated build in " + metafile.name)
1120 post_metadata_parse(app)
1125 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1127 def w_comments(key):
1128 if key not in app.comments:
1130 for line in app.comments[key]:
1133 def w_field_always(field, value=None):
1135 value = app.get_field(field)
1137 w_field(field, value)
1139 def w_field_nonempty(field, value=None):
1141 value = app.get_field(field)
1144 w_field(field, value)
1146 w_field_nonempty('Disabled')
1147 if app.AntiFeatures:
1148 w_field_always('AntiFeatures')
1149 w_field_nonempty('Provides')
1150 w_field_always('Categories')
1151 w_field_always('License')
1152 w_field_always('Web Site')
1153 w_field_always('Source Code')
1154 w_field_always('Issue Tracker')
1155 w_field_nonempty('Changelog')
1156 w_field_nonempty('Donate')
1157 w_field_nonempty('FlattrID')
1158 w_field_nonempty('Bitcoin')
1159 w_field_nonempty('Litecoin')
1161 w_field_nonempty('Name')
1162 w_field_nonempty('Auto Name')
1163 w_field_always('Summary')
1164 w_field_always('Description', description_txt(app.Description))
1166 if app.RequiresRoot:
1167 w_field_always('Requires Root', 'yes')
1170 w_field_always('Repo Type')
1171 w_field_always('Repo')
1173 w_field_always('Binaries')
1176 for build in sorted_builds(app.builds):
1178 if build['version'] == "Ignore":
1181 w_comments('build:' + build['vercode'])
1185 if app.MaintainerNotes:
1186 w_field_always('Maintainer Notes', app.MaintainerNotes)
1189 w_field_nonempty('Archive Policy')
1190 w_field_always('Auto Update Mode')
1191 w_field_always('Update Check Mode')
1192 w_field_nonempty('Update Check Ignore')
1193 w_field_nonempty('Vercode Operation')
1194 w_field_nonempty('Update Check Name')
1195 w_field_nonempty('Update Check Data')
1196 if app.CurrentVersion:
1197 w_field_always('Current Version')
1198 w_field_always('Current Version Code')
1199 if app.NoSourceSince:
1201 w_field_always('No Source Since')
1205 # Write a metadata file in txt format.
1207 # 'mf' - Writer interface (file, StringIO, ...)
1208 # 'app' - The app data
1209 def write_txt_metadata(mf, app):
1211 def w_comment(line):
1212 mf.write("# %s\n" % line)
1214 def w_field(field, value):
1215 t = metafieldtype(field)
1217 value = ','.join(value)
1218 elif t == 'multiline':
1219 if type(value) == list:
1220 value = '\n' + '\n'.join(value) + '\n.'
1222 value = '\n' + value + '\n.'
1223 mf.write("%s:%s\n" % (field, value))
1226 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1228 for key in flag_defaults:
1232 if value == flag_defaults[key]:
1242 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1244 v += ','.join(value) if type(value) == list else value
1249 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1252 def write_yaml_metadata(mf, app):
1254 def w_comment(line):
1255 mf.write("# %s\n" % line)
1260 if any(c in value for c in [': ', '%', '@', '*']):
1261 return "'" + value.replace("'", "''") + "'"
1264 def w_field(field, value, prefix='', t=None):
1266 t = metafieldtype(field)
1271 v += prefix + ' - ' + escape(e) + '\n'
1272 elif t == 'multiline':
1275 if type(value) == str:
1276 lines = value.splitlines()
1279 v += prefix + ' ' + l + '\n'
1285 cmds = [s + '&& \\' for s in value.split('&& ')]
1287 cmds[-1] = cmds[-1][:-len('&& \\')]
1288 w_field(field, cmds, prefix, 'multiline')
1291 v = ' ' + escape(value) + '\n'
1304 mf.write("builds:\n")
1307 w_field('versionName', build['version'], ' - ', 'string')
1308 w_field('versionCode', build['vercode'], ' ', 'strsng')
1309 for key in flag_defaults:
1313 if value == flag_defaults[key]:
1316 w_field(key, value, ' ', flagtype(key))
1318 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1321 def write_metadata(fmt, mf, app):
1323 return write_txt_metadata(mf, app)
1325 return write_yaml_metadata(mf, app)
1326 raise MetaDataException("Unknown metadata format given")