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):
82 'Update Check Ignore',
87 'Current Version Code',
90 'comments', # For formats that don't do inline comments
91 'builds', # For formats that do builds as a list
99 self.AntiFeatures = []
101 self.Categories = ['None']
102 self.License = 'Unknown'
105 self.IssueTracker = ''
114 self.Description = []
115 self.RequiresRoot = False
119 self.MaintainerNotes = []
120 self.ArchivePolicy = None
121 self.AutoUpdateMode = 'None'
122 self.UpdateCheckMode = 'None'
123 self.UpdateCheckIgnore = None
124 self.VercodeOperation = None
125 self.UpdateCheckName = None
126 self.UpdateCheckData = None
127 self.CurrentVersion = ''
128 self.CurrentVersionCode = '0'
129 self.NoSourceSince = ''
132 self.metadatapath = None
136 self.lastupdated = None
139 def field_to_attr(cls, f):
140 return f.replace(' ', '')
143 def attr_to_field(cls, k):
146 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
149 def field_dict(self):
150 return {App.attr_to_field(k): v for k, v in self.__dict__.iteritems()}
152 def get_field(self, f):
153 if f not in app_fields:
154 raise MetaDataException('Unrecognised app field: ' + f)
155 k = App.field_to_attr(f)
156 return getattr(self, k)
158 def set_field(self, f, v):
159 if f not in app_fields:
160 raise MetaDataException('Unrecognised app field: ' + f)
161 k = App.field_to_attr(f)
164 def append_field(self, f, v):
165 if f not in app_fields:
166 raise MetaDataException('Unrecognised app field: ' + f)
167 k = App.field_to_attr(f)
168 if k not in self.__dict__:
169 self.__dict__[k] = [v]
171 self.__dict__[k].append(v)
173 def update_fields(self, d):
174 for f, v in d.iteritems():
178 # In the order in which they are laid out on files
179 # Sorted by their action and their place in the build timeline
180 # These variables can have varying datatypes. For example, anything with
181 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
182 flag_defaults = OrderedDict([
186 ('submodules', False),
194 ('oldsdkloc', False),
196 ('forceversion', False),
197 ('forcevercode', False),
201 ('update', ['auto']),
207 ('ndk', 'r10e'), # defaults to latest
210 ('antcommands', None),
215 # Designates a metadata field type and checks that it matches
217 # 'name' - The long name of the field type
218 # 'matching' - List of possible values or regex expression
219 # 'sep' - Separator to use if value may be a list
220 # 'fields' - Metadata fields (Field:Value) of this type
221 # 'attrs' - Build attributes (attr=value) of this type
223 class FieldValidator():
225 def __init__(self, name, matching, sep, fields, attrs):
227 self.matching = matching
228 if type(matching) is str:
229 self.compiled = re.compile(matching)
234 def _assert_regex(self, values, appid):
236 if not self.compiled.match(v):
237 raise MetaDataException("'%s' is not a valid %s in %s. "
238 % (v, self.name, appid) +
239 "Regex pattern: %s" % (self.matching))
241 def _assert_list(self, values, appid):
243 if v not in self.matching:
244 raise MetaDataException("'%s' is not a valid %s in %s. "
245 % (v, self.name, appid) +
246 "Possible values: %s" % (", ".join(self.matching)))
248 def check(self, value, appid):
249 if type(value) is not str or not value:
251 if self.sep is not None:
252 values = value.split(self.sep)
255 if type(self.matching) is list:
256 self._assert_list(values, appid)
258 self._assert_regex(values, appid)
261 # Generic value types
263 FieldValidator("Integer",
264 r'^[1-9][0-9]*$', None,
268 FieldValidator("Hexadecimal",
269 r'^[0-9a-f]+$', None,
273 FieldValidator("HTTP link",
274 r'^http[s]?://', None,
275 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
277 FieldValidator("Bitcoin address",
278 r'^[a-zA-Z0-9]{27,34}$', None,
282 FieldValidator("Litecoin address",
283 r'^L[a-zA-Z0-9]{33}$', None,
287 FieldValidator("bool",
288 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
290 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
293 FieldValidator("Repo Type",
294 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
298 FieldValidator("Binaries",
299 r'^http[s]?://', None,
303 FieldValidator("Archive Policy",
304 r'^[0-9]+ versions$', None,
308 FieldValidator("Anti-Feature",
309 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
313 FieldValidator("Auto Update Mode",
314 r"^(Version .+|None)$", None,
315 ["Auto Update Mode"],
318 FieldValidator("Update Check Mode",
319 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
320 ["Update Check Mode"],
325 # Check an app's metadata information for integrity errors
326 def check_metadata(app):
328 for field in v.fields:
329 v.check(app.get_field(field), app.id)
330 for build in app.builds:
332 v.check(build[attr], app.id)
335 # Formatter for descriptions. Create an instance, and call parseline() with
336 # each line of the description source from the metadata. At the end, call
337 # end() and then text_wiki and text_html will contain the result.
338 class DescriptionFormatter:
352 def __init__(self, linkres):
353 self.linkResolver = linkres
355 def endcur(self, notstates=None):
356 if notstates and self.state in notstates:
358 if self.state == self.stPARA:
360 elif self.state == self.stUL:
362 elif self.state == self.stOL:
366 self.state = self.stNONE
367 whole_para = ' '.join(self.para_lines)
368 self.addtext(whole_para)
369 self.text_txt += textwrap.fill(whole_para, 80,
370 break_long_words=False,
371 break_on_hyphens=False) + '\n\n'
372 self.text_html += '</p>'
373 del self.para_lines[:]
376 self.text_html += '</ul>'
377 self.text_txt += '\n'
378 self.state = self.stNONE
381 self.text_html += '</ol>'
382 self.text_txt += '\n'
383 self.state = self.stNONE
385 def formatted(self, txt, html):
388 txt = cgi.escape(txt)
390 index = txt.find("''")
392 return formatted + txt
393 formatted += txt[:index]
395 if txt.startswith("'''"):
401 self.bold = not self.bold
409 self.ital = not self.ital
412 def linkify(self, txt):
416 index = txt.find("[")
418 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
419 linkified_plain += self.formatted(txt[:index], False)
420 linkified_html += self.formatted(txt[:index], True)
422 if txt.startswith("[["):
423 index = txt.find("]]")
425 raise MetaDataException("Unterminated ]]")
427 if self.linkResolver:
428 url, urltext = self.linkResolver(url)
431 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
432 linkified_plain += urltext
433 txt = txt[index + 2:]
435 index = txt.find("]")
437 raise MetaDataException("Unterminated ]")
439 index2 = url.find(' ')
443 urltxt = url[index2 + 1:]
446 raise MetaDataException("Url title is just the URL - use [url]")
447 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
448 linkified_plain += urltxt
450 linkified_plain += ' (' + url + ')'
451 txt = txt[index + 1:]
453 def addtext(self, txt):
454 p, h = self.linkify(txt)
457 def parseline(self, line):
458 self.text_wiki += "%s\n" % line
461 elif line.startswith('* '):
462 self.endcur([self.stUL])
463 self.text_txt += "%s\n" % line
464 if self.state != self.stUL:
465 self.text_html += '<ul>'
466 self.state = self.stUL
467 self.text_html += '<li>'
468 self.addtext(line[1:])
469 self.text_html += '</li>'
470 elif line.startswith('# '):
471 self.endcur([self.stOL])
472 self.text_txt += "%s\n" % line
473 if self.state != self.stOL:
474 self.text_html += '<ol>'
475 self.state = self.stOL
476 self.text_html += '<li>'
477 self.addtext(line[1:])
478 self.text_html += '</li>'
480 self.para_lines.append(line)
481 self.endcur([self.stPARA])
482 if self.state == self.stNONE:
483 self.text_html += '<p>'
484 self.state = self.stPARA
488 self.text_txt = self.text_txt.strip()
491 # Parse multiple lines of description as written in a metadata file, returning
492 # a single string in text format and wrapped to 80 columns.
493 def description_txt(lines):
494 ps = DescriptionFormatter(None)
501 # Parse multiple lines of description as written in a metadata file, returning
502 # a single string in wiki format. Used for the Maintainer Notes field as well,
503 # because it's the same format.
504 def description_wiki(lines):
505 ps = DescriptionFormatter(None)
512 # Parse multiple lines of description as written in a metadata file, returning
513 # a single string in HTML format.
514 def description_html(lines, linkres):
515 ps = DescriptionFormatter(linkres)
522 def parse_srclib(metadatapath):
526 # Defaults for fields that come from metadata
527 thisinfo['Repo Type'] = ''
528 thisinfo['Repo'] = ''
529 thisinfo['Subdir'] = None
530 thisinfo['Prepare'] = None
532 if not os.path.exists(metadatapath):
535 metafile = open(metadatapath, "r")
538 for line in metafile:
540 line = line.rstrip('\r\n')
541 if not line or line.startswith("#"):
545 field, value = line.split(':', 1)
547 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
549 if field == "Subdir":
550 thisinfo[field] = value.split(',')
552 thisinfo[field] = value
558 """Read all srclib metadata.
560 The information read will be accessible as metadata.srclibs, which is a
561 dictionary, keyed on srclib name, with the values each being a dictionary
562 in the same format as that returned by the parse_srclib function.
564 A MetaDataException is raised if there are any problems with the srclib
569 # They were already loaded
570 if srclibs is not None:
576 if not os.path.exists(srcdir):
579 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
580 srclibname = os.path.basename(metadatapath[:-4])
581 srclibs[srclibname] = parse_srclib(metadatapath)
584 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
585 # returned by the parse_txt_metadata function.
586 def read_metadata(xref=True):
588 # Always read the srclibs before the apps, since they can use a srlib as
589 # their source repository.
594 for basedir in ('metadata', 'tmp'):
595 if not os.path.exists(basedir):
598 # If there are multiple metadata files for a single appid, then the first
599 # file that is parsed wins over all the others, and the rest throw an
600 # exception. So the original .txt format is parsed first, at least until
601 # newer formats stabilize.
603 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
604 + glob.glob(os.path.join('metadata', '*.json'))
605 + glob.glob(os.path.join('metadata', '*.xml'))
606 + glob.glob(os.path.join('metadata', '*.yaml'))):
607 app = parse_metadata(metadatapath)
609 raise MetaDataException("Found multiple metadata files for " + app.id)
614 # Parse all descriptions at load time, just to ensure cross-referencing
615 # errors are caught early rather than when they hit the build server.
618 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
619 raise MetaDataException("Cannot resolve app id " + appid)
621 for appid, app in apps.iteritems():
623 description_html(app.Description, linkres)
624 except MetaDataException, e:
625 raise MetaDataException("Problem with description of " + appid +
631 # Get the type expected for a given metadata field.
632 def metafieldtype(name):
633 if name in ['Description', 'Maintainer Notes']:
635 if name in ['Categories', 'AntiFeatures']:
637 if name == 'Build Version':
641 if name == 'Use Built':
643 if name not in app_fields:
649 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
650 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
653 if name in ['init', 'prebuild', 'build']:
655 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
661 def fill_build_defaults(build):
663 def get_build_type():
664 for t in ['maven', 'gradle', 'kivy']:
671 for flag, value in flag_defaults.iteritems():
675 build['type'] = get_build_type()
676 build['ndk_path'] = common.get_ndk_path(build['ndk'])
679 def split_list_values(s):
680 # Port legacy ';' separators
681 l = [v.strip() for v in s.replace(';', ',').split(',')]
682 return [v for v in l if v]
685 def get_default_app_info(metadatapath=None):
686 if metadatapath is None:
689 appid, _ = common.get_extension(os.path.basename(metadatapath))
692 app.metadatapath = metadatapath
693 if appid is not None:
699 def sorted_builds(builds):
700 return sorted(builds, key=lambda build: int(build['vercode']))
703 def post_metadata_parse(app):
707 if type(v) in (float, int):
708 app.set_field(f, str(v))
710 # convert to the odd internal format
711 for f in ('Description', 'Maintainer Notes'):
713 if isinstance(v, basestring):
714 text = v.rstrip().lstrip()
715 app.set_field(f, text.split('\n'))
717 supported_flags = (flag_defaults.keys()
718 + ['vercode', 'version', 'versionCode', 'versionName',
720 esc_newlines = re.compile('\\\\( |\\n)')
722 for build in app.builds:
723 for k, v in build.items():
724 if k not in supported_flags:
725 raise MetaDataException("Unrecognised build flag: {0}={1}"
728 if k == 'versionCode':
729 build['vercode'] = str(v)
730 del build['versionCode']
731 elif k == 'versionName':
732 build['version'] = str(v)
733 del build['versionName']
734 elif type(v) in (float, int):
737 keyflagtype = flagtype(k)
738 if keyflagtype == 'list':
739 # these can be bools, strings or lists, but ultimately are lists
740 if isinstance(v, basestring):
742 elif isinstance(v, bool):
743 build[k] = ['yes' if v else 'no']
744 elif isinstance(v, list):
747 if isinstance(e, bool):
748 build[k].append('yes' if v else 'no')
752 elif keyflagtype == 'script':
753 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
754 elif keyflagtype == 'bool':
755 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
756 if isinstance(v, basestring):
761 elif keyflagtype == 'string':
762 if isinstance(v, bool):
763 build[k] = 'yes' if v else 'no'
765 if not app.Description:
766 app.Description = ['No description available']
768 for build in app.builds:
769 fill_build_defaults(build)
771 app.builds = sorted_builds(app.builds)
774 # Parse metadata for a single application.
776 # 'metadatapath' - the filename to read. The package id for the application comes
777 # from this filename. Pass None to get a blank entry.
779 # Returns a dictionary containing all the details of the application. There are
780 # two major kinds of information in the dictionary. Keys beginning with capital
781 # letters correspond directory to identically named keys in the metadata file.
782 # Keys beginning with lower case letters are generated in one way or another,
783 # and are not found verbatim in the metadata.
785 # Known keys not originating from the metadata are:
787 # 'builds' - a list of dictionaries containing build information
788 # for each defined build
789 # 'comments' - a list of comments from the metadata file. Each is
790 # a list of the form [field, comment] where field is
791 # the name of the field it preceded in the metadata
792 # file. Where field is None, the comment goes at the
793 # end of the file. Alternatively, 'build:version' is
794 # for a comment before a particular build version.
795 # 'descriptionlines' - original lines of description as formatted in the
800 def _decode_list(data):
801 '''convert items in a list from unicode to basestring'''
804 if isinstance(item, unicode):
805 item = item.encode('utf-8')
806 elif isinstance(item, list):
807 item = _decode_list(item)
808 elif isinstance(item, dict):
809 item = _decode_dict(item)
814 def _decode_dict(data):
815 '''convert items in a dict from unicode to basestring'''
817 for key, value in data.iteritems():
818 if isinstance(key, unicode):
819 key = key.encode('utf-8')
820 if isinstance(value, unicode):
821 value = value.encode('utf-8')
822 elif isinstance(value, list):
823 value = _decode_list(value)
824 elif isinstance(value, dict):
825 value = _decode_dict(value)
830 def parse_metadata(metadatapath):
831 _, ext = common.get_extension(metadatapath)
832 accepted = common.config['accepted_formats']
833 if ext not in accepted:
834 logging.critical('"' + metadatapath
835 + '" is not in an accepted format, '
836 + 'convert to: ' + ', '.join(accepted))
840 return parse_txt_metadata(metadatapath)
842 return parse_json_metadata(metadatapath)
844 return parse_xml_metadata(metadatapath)
846 return parse_yaml_metadata(metadatapath)
848 logging.critical('Unknown metadata format: ' + metadatapath)
852 def parse_json_metadata(metadatapath):
854 app = get_default_app_info(metadatapath)
856 # fdroid metadata is only strings and booleans, no floats or ints. And
857 # json returns unicode, and fdroidserver still uses plain python strings
858 # TODO create schema using https://pypi.python.org/pypi/jsonschema
859 jsoninfo = json.load(open(metadatapath, 'r'),
860 object_hook=_decode_dict,
861 parse_int=lambda s: s,
862 parse_float=lambda s: s)
863 app.update_fields(jsoninfo)
864 post_metadata_parse(app)
869 def parse_xml_metadata(metadatapath):
871 app = get_default_app_info(metadatapath)
873 tree = ElementTree.ElementTree(file=metadatapath)
874 root = tree.getroot()
876 if root.tag != 'resources':
877 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
881 if child.tag != 'builds':
882 # builds does not have name="" attrib
883 name = child.attrib['name']
885 if child.tag == 'string':
886 app.set_field(name, child.text)
887 elif child.tag == 'string-array':
890 items.append(item.text)
891 app.set_field(name, items)
892 elif child.tag == 'builds':
896 builddict[key.tag] = key.text
897 app.builds.append(builddict)
899 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
900 if not isinstance(app.RequiresRoot, bool):
901 if app.RequiresRoot == 'true':
902 app.RequiresRoot = True
904 app.RequiresRoot = False
906 post_metadata_parse(app)
911 def parse_yaml_metadata(metadatapath):
913 app = get_default_app_info(metadatapath)
915 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
916 app.update_fields(yamlinfo)
917 post_metadata_parse(app)
922 def parse_txt_metadata(metadatapath):
926 def add_buildflag(p, thisbuild):
928 raise MetaDataException("Empty build flag at {1}"
929 .format(buildlines[0], linedesc))
932 raise MetaDataException("Invalid build flag at {0} in {1}"
933 .format(buildlines[0], linedesc))
936 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
937 .format(pk, thisbuild['version'], linedesc))
940 if pk not in flag_defaults:
941 raise MetaDataException("Unrecognised build flag at {0} in {1}"
942 .format(p, linedesc))
945 pv = split_list_values(pv)
947 if len(pv) == 1 and pv[0] in ['main', 'yes']:
950 elif t == 'string' or t == 'script':
958 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
961 def parse_buildline(lines):
962 value = "".join(lines)
963 parts = [p.replace("\\,", ",")
964 for p in re.split(r"(?<!\\),", value)]
966 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
968 thisbuild['origlines'] = lines
969 thisbuild['version'] = parts[0]
970 thisbuild['vercode'] = parts[1]
971 if parts[2].startswith('!'):
972 # For backwards compatibility, handle old-style disabling,
973 # including attempting to extract the commit from the message
974 thisbuild['disable'] = parts[2][1:]
975 commit = 'unknown - see disabled'
976 index = parts[2].rfind('at ')
978 commit = parts[2][index + 3:]
979 if commit.endswith(')'):
981 thisbuild['commit'] = commit
983 thisbuild['commit'] = parts[2]
985 add_buildflag(p, thisbuild)
989 def add_comments(key):
992 app.comments[key] = list(curcomments)
995 app = get_default_app_info(metadatapath)
996 metafile = open(metadatapath, "r")
1005 for line in metafile:
1007 linedesc = "%s:%d" % (metafile.name, c)
1008 line = line.rstrip('\r\n')
1010 if not any(line.startswith(s) for s in (' ', '\t')):
1011 commit = curbuild['commit'] if 'commit' in curbuild else None
1012 if not commit and 'disable' not in curbuild:
1013 raise MetaDataException("No commit specified for {0} in {1}"
1014 .format(curbuild['version'], linedesc))
1016 app.builds.append(curbuild)
1017 add_comments('build:' + curbuild['vercode'])
1020 if line.endswith('\\'):
1021 buildlines.append(line[:-1].lstrip())
1023 buildlines.append(line.lstrip())
1024 bl = ''.join(buildlines)
1025 add_buildflag(bl, curbuild)
1031 if line.startswith("#"):
1032 curcomments.append(line[1:].strip())
1035 field, value = line.split(':', 1)
1037 raise MetaDataException("Invalid metadata in " + linedesc)
1038 if field != field.strip() or value != value.strip():
1039 raise MetaDataException("Extra spacing found in " + linedesc)
1041 # Translate obsolete fields...
1042 if field == 'Market Version':
1043 field = 'Current Version'
1044 if field == 'Market Version Code':
1045 field = 'Current Version Code'
1047 fieldtype = metafieldtype(field)
1048 if fieldtype not in ['build', 'buildv2']:
1050 if fieldtype == 'multiline':
1053 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
1054 elif fieldtype == 'string':
1055 app.set_field(field, value)
1056 elif fieldtype == 'list':
1057 app.set_field(field, split_list_values(value))
1058 elif fieldtype == 'build':
1059 if value.endswith("\\"):
1061 buildlines = [value[:-1]]
1063 curbuild = parse_buildline([value])
1064 app.builds.append(curbuild)
1065 add_comments('build:' + app.builds[-1]['vercode'])
1066 elif fieldtype == 'buildv2':
1068 vv = value.split(',')
1070 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1071 .format(value, linedesc))
1072 curbuild['version'] = vv[0]
1073 curbuild['vercode'] = vv[1]
1074 if curbuild['vercode'] in vc_seen:
1075 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1076 curbuild['vercode'], linedesc))
1077 vc_seen[curbuild['vercode']] = True
1080 elif fieldtype == 'obsolete':
1081 pass # Just throw it away!
1083 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1084 elif mode == 1: # Multiline field
1088 app.append_field(field, line)
1089 elif mode == 2: # Line continuation mode in Build Version
1090 if line.endswith("\\"):
1091 buildlines.append(line[:-1])
1093 buildlines.append(line)
1094 curbuild = parse_buildline(buildlines)
1095 app.builds.append(curbuild)
1096 add_comments('build:' + app.builds[-1]['vercode'])
1100 # Mode at end of file should always be 0...
1102 raise MetaDataException(field + " not terminated in " + metafile.name)
1104 raise MetaDataException("Unterminated continuation in " + metafile.name)
1106 raise MetaDataException("Unterminated build in " + metafile.name)
1108 post_metadata_parse(app)
1113 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1115 def w_comments(key):
1116 if key not in app.comments:
1118 for line in app.comments[key]:
1121 def w_field_always(field, value=None):
1123 value = app.get_field(field)
1125 w_field(field, value)
1127 def w_field_nonempty(field, value=None):
1129 value = app.get_field(field)
1132 w_field(field, value)
1134 w_field_nonempty('Disabled')
1135 if app.AntiFeatures:
1136 w_field_always('AntiFeatures')
1137 w_field_nonempty('Provides')
1138 w_field_always('Categories')
1139 w_field_always('License')
1140 w_field_always('Web Site')
1141 w_field_always('Source Code')
1142 w_field_always('Issue Tracker')
1143 w_field_nonempty('Changelog')
1144 w_field_nonempty('Donate')
1145 w_field_nonempty('FlattrID')
1146 w_field_nonempty('Bitcoin')
1147 w_field_nonempty('Litecoin')
1149 w_field_nonempty('Name')
1150 w_field_nonempty('Auto Name')
1151 w_field_always('Summary')
1152 w_field_always('Description', description_txt(app.Description))
1154 if app.RequiresRoot:
1155 w_field_always('Requires Root', 'yes')
1158 w_field_always('Repo Type')
1159 w_field_always('Repo')
1161 w_field_always('Binaries')
1164 for build in sorted_builds(app.builds):
1166 if build['version'] == "Ignore":
1169 w_comments('build:' + build['vercode'])
1173 if app.MaintainerNotes:
1174 w_field_always('Maintainer Notes', app.MaintainerNotes)
1177 w_field_nonempty('Archive Policy')
1178 w_field_always('Auto Update Mode')
1179 w_field_always('Update Check Mode')
1180 w_field_nonempty('Update Check Ignore')
1181 w_field_nonempty('Vercode Operation')
1182 w_field_nonempty('Update Check Name')
1183 w_field_nonempty('Update Check Data')
1184 if app.CurrentVersion:
1185 w_field_always('Current Version')
1186 w_field_always('Current Version Code')
1187 if app.NoSourceSince:
1189 w_field_always('No Source Since')
1193 # Write a metadata file in txt format.
1195 # 'mf' - Writer interface (file, StringIO, ...)
1196 # 'app' - The app data
1197 def write_txt_metadata(mf, app):
1199 def w_comment(line):
1200 mf.write("# %s\n" % line)
1202 def w_field(field, value):
1203 t = metafieldtype(field)
1205 value = ','.join(value)
1206 elif t == 'multiline':
1207 if type(value) == list:
1208 value = '\n' + '\n'.join(value) + '\n.'
1210 value = '\n' + value + '\n.'
1211 mf.write("%s:%s\n" % (field, value))
1214 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1216 for key in flag_defaults:
1220 if value == flag_defaults[key]:
1230 v += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1232 v += ','.join(value) if type(value) == list else value
1237 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1240 def write_yaml_metadata(mf, app):
1242 def w_comment(line):
1243 mf.write("# %s\n" % line)
1248 if any(c in value for c in [': ', '%', '@', '*']):
1249 return "'" + value.replace("'", "''") + "'"
1252 def w_field(field, value, prefix='', t=None):
1254 t = metafieldtype(field)
1259 v += prefix + ' - ' + escape(e) + '\n'
1260 elif t == 'multiline':
1263 if type(value) == str:
1264 lines = value.splitlines()
1267 v += prefix + ' ' + l + '\n'
1273 cmds = [s + '&& \\' for s in value.split('&& ')]
1275 cmds[-1] = cmds[-1][:-len('&& \\')]
1276 w_field(field, cmds, prefix, 'multiline')
1279 v = ' ' + escape(value) + '\n'
1292 mf.write("builds:\n")
1295 w_field('versionName', build['version'], ' - ', 'string')
1296 w_field('versionCode', build['vercode'], ' ', 'strsng')
1297 for key in flag_defaults:
1301 if value == flag_defaults[key]:
1304 w_field(key, value, ' ', flagtype(key))
1306 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1309 def write_metadata(fmt, mf, app):
1311 return write_txt_metadata(mf, app)
1313 return write_yaml_metadata(mf, app)
1314 raise MetaDataException("Unknown metadata format given")