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
46 class MetaDataException(Exception):
48 def __init__(self, value):
54 # To filter which ones should be written to the metadata files if
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
138 # Translates human-readable field names to attribute names, e.g.
139 # 'Auto Name' to 'AutoName'
141 def field_to_attr(cls, f):
142 return f.replace(' ', '')
144 # Translates attribute names to human-readable field names, e.g.
145 # 'AutoName' to 'Auto Name'
147 def attr_to_field(cls, k):
150 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
153 # Constructs an old-fashioned dict with the human-readable field
154 # names. Should only be used for tests.
155 def field_dict(self):
157 for k, v in self.__dict__.iteritems():
161 d['builds'].append(build.__dict__)
163 k = App.attr_to_field(k)
167 # Gets the value associated to a field name, e.g. 'Auto Name'
168 def get_field(self, f):
169 if f not in app_fields:
170 raise MetaDataException('Unrecognised app field: ' + f)
171 k = App.field_to_attr(f)
172 return getattr(self, k)
174 # Sets the value associated to a field name, e.g. 'Auto Name'
175 def set_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)
181 # Appends to the value associated to a field name, e.g. 'Auto Name'
182 def append_field(self, f, v):
183 if f not in app_fields:
184 raise MetaDataException('Unrecognised app field: ' + f)
185 k = App.field_to_attr(f)
186 if k not in self.__dict__:
187 self.__dict__[k] = [v]
189 self.__dict__[k].append(v)
191 # Like dict.update(), but using human-readable field names
192 def update_fields(self, d):
193 for f, v in d.iteritems():
197 build.update_flags(b)
198 self.builds.append(build)
203 def metafieldtype(name):
204 if name in ['Description', 'Maintainer Notes']:
206 if name in ['Categories', 'AntiFeatures']:
208 if name == 'Build Version':
212 if name == 'Use Built':
214 if name not in app_fields:
219 # In the order in which they are laid out on files
220 build_flags_order = [
253 build_flags = set(build_flags_order + ['version', 'vercode'])
262 self.submodules = False
270 self.oldsdkloc = False
272 self.forceversion = False
273 self.forcevercode = False
284 self.preassemble = []
285 self.gradleprops = []
286 self.antcommands = None
287 self.novcheck = False
289 def get_flag(self, f):
290 if f not in build_flags:
291 raise MetaDataException('Unrecognised build flag: ' + f)
292 return getattr(self, f)
294 def set_flag(self, f, v):
295 if f == 'versionName':
297 if f == 'versionCode':
299 if f not in build_flags:
300 raise MetaDataException('Unrecognised build flag: ' + f)
303 def append_flag(self, f, v):
304 if f not in build_flags:
305 raise MetaDataException('Unrecognised build flag: ' + f)
306 if f not in self.__dict__:
307 self.__dict__[f] = [v]
309 self.__dict__[f].append(v)
312 for f in ['maven', 'gradle', 'kivy']:
322 version = 'r10e' # falls back to latest
323 paths = common.config['ndk_paths']
324 if version not in paths:
326 return paths[version]
328 def update_flags(self, d):
329 for f, v in d.iteritems():
332 list_flags = set(['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
333 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
335 script_flags = set(['init', 'prebuild', 'build'])
336 bool_flags = set(['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
341 if name in list_flags:
343 if name in script_flags:
345 if name in bool_flags:
350 # Designates a metadata field type and checks that it matches
352 # 'name' - The long name of the field type
353 # 'matching' - List of possible values or regex expression
354 # 'sep' - Separator to use if value may be a list
355 # 'fields' - Metadata fields (Field:Value) of this type
356 # 'flags' - Build flags (flag=value) of this type
358 class FieldValidator():
360 def __init__(self, name, matching, sep, fields, flags):
362 self.matching = matching
363 if type(matching) is str:
364 self.compiled = re.compile(matching)
369 def _assert_regex(self, values, appid):
371 if not self.compiled.match(v):
372 raise MetaDataException("'%s' is not a valid %s in %s. "
373 % (v, self.name, appid) +
374 "Regex pattern: %s" % (self.matching))
376 def _assert_list(self, values, appid):
378 if v not in self.matching:
379 raise MetaDataException("'%s' is not a valid %s in %s. "
380 % (v, self.name, appid) +
381 "Possible values: %s" % (", ".join(self.matching)))
383 def check(self, v, appid):
384 if type(v) is not str or not v:
386 if self.sep is not None:
387 values = v.split(self.sep)
390 if type(self.matching) is list:
391 self._assert_list(values, appid)
393 self._assert_regex(values, appid)
396 # Generic value types
398 FieldValidator("Integer",
399 r'^[1-9][0-9]*$', None,
403 FieldValidator("Hexadecimal",
404 r'^[0-9a-f]+$', None,
408 FieldValidator("HTTP link",
409 r'^http[s]?://', None,
410 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
412 FieldValidator("Bitcoin address",
413 r'^[a-zA-Z0-9]{27,34}$', None,
417 FieldValidator("Litecoin address",
418 r'^L[a-zA-Z0-9]{33}$', None,
422 FieldValidator("bool",
423 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
425 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
428 FieldValidator("Repo Type",
429 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
433 FieldValidator("Binaries",
434 r'^http[s]?://', None,
438 FieldValidator("Archive Policy",
439 r'^[0-9]+ versions$', None,
443 FieldValidator("Anti-Feature",
444 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
448 FieldValidator("Auto Update Mode",
449 r"^(Version .+|None)$", None,
450 ["Auto Update Mode"],
453 FieldValidator("Update Check Mode",
454 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
455 ["Update Check Mode"],
460 # Check an app's metadata information for integrity errors
461 def check_metadata(app):
464 v.check(app.get_field(f), app.id)
465 for build in app.builds:
467 v.check(build.get_flag(f), app.id)
470 # Formatter for descriptions. Create an instance, and call parseline() with
471 # each line of the description source from the metadata. At the end, call
472 # end() and then text_wiki and text_html will contain the result.
473 class DescriptionFormatter:
487 def __init__(self, linkres):
488 self.linkResolver = linkres
490 def endcur(self, notstates=None):
491 if notstates and self.state in notstates:
493 if self.state == self.stPARA:
495 elif self.state == self.stUL:
497 elif self.state == self.stOL:
501 self.state = self.stNONE
502 whole_para = ' '.join(self.para_lines)
503 self.addtext(whole_para)
504 self.text_txt += textwrap.fill(whole_para, 80,
505 break_long_words=False,
506 break_on_hyphens=False) + '\n\n'
507 self.text_html += '</p>'
508 del self.para_lines[:]
511 self.text_html += '</ul>'
512 self.text_txt += '\n'
513 self.state = self.stNONE
516 self.text_html += '</ol>'
517 self.text_txt += '\n'
518 self.state = self.stNONE
520 def formatted(self, txt, html):
523 txt = cgi.escape(txt)
525 index = txt.find("''")
527 return formatted + txt
528 formatted += txt[:index]
530 if txt.startswith("'''"):
536 self.bold = not self.bold
544 self.ital = not self.ital
547 def linkify(self, txt):
551 index = txt.find("[")
553 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
554 linkified_plain += self.formatted(txt[:index], False)
555 linkified_html += self.formatted(txt[:index], True)
557 if txt.startswith("[["):
558 index = txt.find("]]")
560 raise MetaDataException("Unterminated ]]")
562 if self.linkResolver:
563 url, urltext = self.linkResolver(url)
566 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
567 linkified_plain += urltext
568 txt = txt[index + 2:]
570 index = txt.find("]")
572 raise MetaDataException("Unterminated ]")
574 index2 = url.find(' ')
578 urltxt = url[index2 + 1:]
581 raise MetaDataException("Url title is just the URL - use [url]")
582 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
583 linkified_plain += urltxt
585 linkified_plain += ' (' + url + ')'
586 txt = txt[index + 1:]
588 def addtext(self, txt):
589 p, h = self.linkify(txt)
592 def parseline(self, line):
593 self.text_wiki += "%s\n" % line
596 elif line.startswith('* '):
597 self.endcur([self.stUL])
598 self.text_txt += "%s\n" % line
599 if self.state != self.stUL:
600 self.text_html += '<ul>'
601 self.state = self.stUL
602 self.text_html += '<li>'
603 self.addtext(line[1:])
604 self.text_html += '</li>'
605 elif line.startswith('# '):
606 self.endcur([self.stOL])
607 self.text_txt += "%s\n" % line
608 if self.state != self.stOL:
609 self.text_html += '<ol>'
610 self.state = self.stOL
611 self.text_html += '<li>'
612 self.addtext(line[1:])
613 self.text_html += '</li>'
615 self.para_lines.append(line)
616 self.endcur([self.stPARA])
617 if self.state == self.stNONE:
618 self.text_html += '<p>'
619 self.state = self.stPARA
623 self.text_txt = self.text_txt.strip()
626 # Parse multiple lines of description as written in a metadata file, returning
627 # a single string in text format and wrapped to 80 columns.
628 def description_txt(lines):
629 ps = DescriptionFormatter(None)
636 # Parse multiple lines of description as written in a metadata file, returning
637 # a single string in wiki format. Used for the Maintainer Notes field as well,
638 # because it's the same format.
639 def description_wiki(lines):
640 ps = DescriptionFormatter(None)
647 # Parse multiple lines of description as written in a metadata file, returning
648 # a single string in HTML format.
649 def description_html(lines, linkres):
650 ps = DescriptionFormatter(linkres)
657 def parse_srclib(metadatapath):
661 # Defaults for fields that come from metadata
662 thisinfo['Repo Type'] = ''
663 thisinfo['Repo'] = ''
664 thisinfo['Subdir'] = None
665 thisinfo['Prepare'] = None
667 if not os.path.exists(metadatapath):
670 metafile = open(metadatapath, "r")
673 for line in metafile:
675 line = line.rstrip('\r\n')
676 if not line or line.startswith("#"):
680 f, v = line.split(':', 1)
682 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
685 thisinfo[f] = v.split(',')
693 """Read all srclib metadata.
695 The information read will be accessible as metadata.srclibs, which is a
696 dictionary, keyed on srclib name, with the values each being a dictionary
697 in the same format as that returned by the parse_srclib function.
699 A MetaDataException is raised if there are any problems with the srclib
704 # They were already loaded
705 if srclibs is not None:
711 if not os.path.exists(srcdir):
714 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
715 srclibname = os.path.basename(metadatapath[:-4])
716 srclibs[srclibname] = parse_srclib(metadatapath)
719 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
720 # returned by the parse_txt_metadata function.
721 def read_metadata(xref=True):
723 # Always read the srclibs before the apps, since they can use a srlib as
724 # their source repository.
729 for basedir in ('metadata', 'tmp'):
730 if not os.path.exists(basedir):
733 # If there are multiple metadata files for a single appid, then the first
734 # file that is parsed wins over all the others, and the rest throw an
735 # exception. So the original .txt format is parsed first, at least until
736 # newer formats stabilize.
738 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
739 + glob.glob(os.path.join('metadata', '*.json'))
740 + glob.glob(os.path.join('metadata', '*.xml'))
741 + glob.glob(os.path.join('metadata', '*.yaml'))):
742 app = parse_metadata(metadatapath)
744 raise MetaDataException("Found multiple metadata files for " + app.id)
749 # Parse all descriptions at load time, just to ensure cross-referencing
750 # errors are caught early rather than when they hit the build server.
753 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
754 raise MetaDataException("Cannot resolve app id " + appid)
756 for appid, app in apps.iteritems():
758 description_html(app.Description, linkres)
759 except MetaDataException, e:
760 raise MetaDataException("Problem with description of " + appid +
766 def split_list_values(s):
767 # Port legacy ';' separators
768 l = [v.strip() for v in s.replace(';', ',').split(',')]
769 return [v for v in l if v]
772 def get_default_app_info(metadatapath=None):
773 if metadatapath is None:
776 appid, _ = common.get_extension(os.path.basename(metadatapath))
779 app.metadatapath = metadatapath
780 if appid is not None:
786 def sorted_builds(builds):
787 return sorted(builds, key=lambda build: int(build.vercode))
790 esc_newlines = re.compile('\\\\( |\\n)')
793 # This function uses __dict__ to be faster
794 def post_metadata_parse(app):
796 for k, v in app.__dict__.iteritems():
797 if type(v) in (float, int):
798 app.__dict__[f] = str(v)
800 for build in app.builds:
801 for k, v in app.__dict__.iteritems():
803 if type(v) in (float, int):
804 build.__dict__[k] = str(v)
809 if ftype == 'script':
810 build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
811 elif ftype == 'bool':
812 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
813 if isinstance(v, basestring) and v == 'true':
814 build.__dict__[k] = True
815 elif ftype == 'string':
816 if isinstance(v, bool) and v:
817 build.__dict__[k] = 'yes'
819 # convert to the odd internal format
820 for f in ('Description', 'Maintainer Notes'):
822 if isinstance(v, basestring):
823 text = v.rstrip().lstrip()
824 app.set_field(f, text.split('\n'))
826 if not app.Description:
827 app.Description = ['No description available']
829 app.builds = sorted_builds(app.builds)
832 # Parse metadata for a single application.
834 # 'metadatapath' - the filename to read. The package id for the application comes
835 # from this filename. Pass None to get a blank entry.
837 # Returns a dictionary containing all the details of the application. There are
838 # two major kinds of information in the dictionary. Keys beginning with capital
839 # letters correspond directory to identically named keys in the metadata file.
840 # Keys beginning with lower case letters are generated in one way or another,
841 # and are not found verbatim in the metadata.
843 # Known keys not originating from the metadata are:
845 # 'builds' - a list of dictionaries containing build information
846 # for each defined build
847 # 'comments' - a list of comments from the metadata file. Each is
848 # a list of the form [field, comment] where field is
849 # the name of the field it preceded in the metadata
850 # file. Where field is None, the comment goes at the
851 # end of the file. Alternatively, 'build:version' is
852 # for a comment before a particular build version.
853 # 'descriptionlines' - original lines of description as formatted in the
858 def _decode_list(data):
859 '''convert items in a list from unicode to basestring'''
862 if isinstance(item, unicode):
863 item = item.encode('utf-8')
864 elif isinstance(item, list):
865 item = _decode_list(item)
866 elif isinstance(item, dict):
867 item = _decode_dict(item)
872 def _decode_dict(data):
873 '''convert items in a dict from unicode to basestring'''
875 for k, v in data.iteritems():
876 if isinstance(k, unicode):
877 k = k.encode('utf-8')
878 if isinstance(v, unicode):
879 v = v.encode('utf-8')
880 elif isinstance(v, list):
882 elif isinstance(v, dict):
888 def parse_metadata(metadatapath):
889 _, ext = common.get_extension(metadatapath)
890 accepted = common.config['accepted_formats']
891 if ext not in accepted:
892 logging.critical('"' + metadatapath
893 + '" is not in an accepted format, '
894 + 'convert to: ' + ', '.join(accepted))
899 app = parse_txt_metadata(metadatapath)
901 app = parse_json_metadata(metadatapath)
903 app = parse_xml_metadata(metadatapath)
905 app = parse_yaml_metadata(metadatapath)
907 logging.critical('Unknown metadata format: ' + metadatapath)
910 post_metadata_parse(app)
914 def parse_json_metadata(metadatapath):
916 app = get_default_app_info(metadatapath)
918 # fdroid metadata is only strings and booleans, no floats or ints. And
919 # json returns unicode, and fdroidserver still uses plain python strings
920 # TODO create schema using https://pypi.python.org/pypi/jsonschema
921 jsoninfo = json.load(open(metadatapath, 'r'),
922 object_hook=_decode_dict,
923 parse_int=lambda s: s,
924 parse_float=lambda s: s)
925 app.update_fields(jsoninfo)
929 def parse_xml_metadata(metadatapath):
931 app = get_default_app_info(metadatapath)
933 tree = ElementTree.ElementTree(file=metadatapath)
934 root = tree.getroot()
936 if root.tag != 'resources':
937 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
941 if child.tag != 'builds':
942 # builds does not have name="" attrib
943 name = child.attrib['name']
945 if child.tag == 'string':
946 app.set_field(name, child.text)
947 elif child.tag == 'string-array':
949 app.append_field(name, item.text)
950 elif child.tag == 'builds':
954 build.set_flag(key.tag, key.text)
955 app.builds.append(build)
957 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
958 if not isinstance(app.RequiresRoot, bool):
959 if app.RequiresRoot == 'true':
960 app.RequiresRoot = True
962 app.RequiresRoot = False
967 def parse_yaml_metadata(metadatapath):
969 app = get_default_app_info(metadatapath)
971 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
972 app.update_fields(yamlinfo)
976 build_line_sep = re.compile(r"(?<!\\),")
979 def parse_txt_metadata(metadatapath):
983 def add_buildflag(p, build):
985 raise MetaDataException("Empty build flag at {1}"
986 .format(buildlines[0], linedesc))
989 raise MetaDataException("Invalid build flag at {0} in {1}"
990 .format(buildlines[0], linedesc))
994 if pk not in build_flags:
995 raise MetaDataException("Unrecognised build flag at {0} in {1}"
996 .format(p, linedesc))
999 pv = split_list_values(pv)
1001 if len(pv) == 1 and pv[0] in ['main', 'yes']:
1003 build.set_flag(pk, pv)
1004 elif t == 'string' or t == 'script':
1005 build.set_flag(pk, pv)
1009 build.set_flag(pk, True)
1012 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
1015 def parse_buildline(lines):
1017 parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
1019 raise MetaDataException("Invalid build format: " + v + " in " + metafile.name)
1021 build.origlines = lines
1022 build.version = parts[0]
1023 build.vercode = parts[1]
1024 if parts[2].startswith('!'):
1025 # For backwards compatibility, handle old-style disabling,
1026 # including attempting to extract the commit from the message
1027 build.disable = parts[2][1:]
1028 commit = 'unknown - see disabled'
1029 index = parts[2].rfind('at ')
1031 commit = parts[2][index + 3:]
1032 if commit.endswith(')'):
1033 commit = commit[:-1]
1034 build.commit = commit
1036 build.commit = parts[2]
1038 add_buildflag(p, build)
1042 def add_comments(key):
1045 app.comments[key] = list(curcomments)
1048 app = get_default_app_info(metadatapath)
1049 metafile = open(metadatapath, "r")
1058 for line in metafile:
1060 linedesc = "%s:%d" % (metafile.name, c)
1061 line = line.rstrip('\r\n')
1063 if not any(line.startswith(s) for s in (' ', '\t')):
1064 if not build.commit and not build.disable:
1065 raise MetaDataException("No commit specified for {0} in {1}"
1066 .format(build.version, linedesc))
1068 app.builds.append(build)
1069 add_comments('build:' + build.vercode)
1072 if line.endswith('\\'):
1073 buildlines.append(line[:-1].lstrip())
1075 buildlines.append(line.lstrip())
1076 bl = ''.join(buildlines)
1077 add_buildflag(bl, build)
1083 if line.startswith("#"):
1084 curcomments.append(line[1:].strip())
1087 f, v = line.split(':', 1)
1089 raise MetaDataException("Invalid metadata in " + linedesc)
1090 if f != f.strip() or v != v.strip():
1091 raise MetaDataException("Extra spacing found in " + linedesc)
1093 # Translate obsolete fields...
1094 if f == 'Market Version':
1095 f = 'Current Version'
1096 if f == 'Market Version Code':
1097 f = 'Current Version Code'
1099 fieldtype = metafieldtype(f)
1100 if fieldtype not in ['build', 'buildv2']:
1102 if fieldtype == 'multiline':
1105 raise MetaDataException("Unexpected text on same line as " + f + " in " + linedesc)
1106 elif fieldtype == 'string':
1108 elif fieldtype == 'list':
1109 app.set_field(f, split_list_values(v))
1110 elif fieldtype == 'build':
1111 if v.endswith("\\"):
1113 buildlines = [v[:-1]]
1115 build = parse_buildline([v])
1116 app.builds.append(build)
1117 add_comments('build:' + app.builds[-1].vercode)
1118 elif fieldtype == 'buildv2':
1122 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1123 .format(v, linedesc))
1124 build.version = vv[0]
1125 build.vercode = vv[1]
1126 if build.vercode in vc_seen:
1127 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1128 build.vercode, linedesc))
1129 vc_seen[build.vercode] = True
1132 elif fieldtype == 'obsolete':
1133 pass # Just throw it away!
1135 raise MetaDataException("Unrecognised field type for " + f + " in " + linedesc)
1136 elif mode == 1: # Multiline field
1140 app.append_field(f, line)
1141 elif mode == 2: # Line continuation mode in Build Version
1142 if line.endswith("\\"):
1143 buildlines.append(line[:-1])
1145 buildlines.append(line)
1146 build = parse_buildline(buildlines)
1147 app.builds.append(build)
1148 add_comments('build:' + app.builds[-1].vercode)
1152 # Mode at end of file should always be 0...
1154 raise MetaDataException(f + " not terminated in " + metafile.name)
1156 raise MetaDataException("Unterminated continuation in " + metafile.name)
1158 raise MetaDataException("Unterminated build in " + metafile.name)
1163 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1165 def w_comments(key):
1166 if key not in app.comments:
1168 for line in app.comments[key]:
1171 def w_field_always(f, v=None):
1173 v = app.get_field(f)
1177 def w_field_nonempty(f, v=None):
1179 v = app.get_field(f)
1184 w_field_nonempty('Disabled')
1185 if app.AntiFeatures:
1186 w_field_always('AntiFeatures')
1187 w_field_nonempty('Provides')
1188 w_field_always('Categories')
1189 w_field_always('License')
1190 w_field_always('Web Site')
1191 w_field_always('Source Code')
1192 w_field_always('Issue Tracker')
1193 w_field_nonempty('Changelog')
1194 w_field_nonempty('Donate')
1195 w_field_nonempty('FlattrID')
1196 w_field_nonempty('Bitcoin')
1197 w_field_nonempty('Litecoin')
1199 w_field_nonempty('Name')
1200 w_field_nonempty('Auto Name')
1201 w_field_always('Summary')
1202 w_field_always('Description', description_txt(app.Description))
1204 if app.RequiresRoot:
1205 w_field_always('Requires Root', 'yes')
1208 w_field_always('Repo Type')
1209 w_field_always('Repo')
1211 w_field_always('Binaries')
1214 for build in sorted_builds(app.builds):
1216 if build.version == "Ignore":
1219 w_comments('build:' + build.vercode)
1223 if app.MaintainerNotes:
1224 w_field_always('Maintainer Notes', app.MaintainerNotes)
1227 w_field_nonempty('Archive Policy')
1228 w_field_always('Auto Update Mode')
1229 w_field_always('Update Check Mode')
1230 w_field_nonempty('Update Check Ignore')
1231 w_field_nonempty('Vercode Operation')
1232 w_field_nonempty('Update Check Name')
1233 w_field_nonempty('Update Check Data')
1234 if app.CurrentVersion:
1235 w_field_always('Current Version')
1236 w_field_always('Current Version Code')
1237 if app.NoSourceSince:
1239 w_field_always('No Source Since')
1243 # Write a metadata file in txt format.
1245 # 'mf' - Writer interface (file, StringIO, ...)
1246 # 'app' - The app data
1247 def write_txt_metadata(mf, app):
1249 def w_comment(line):
1250 mf.write("# %s\n" % line)
1253 t = metafieldtype(f)
1256 elif t == 'multiline':
1258 v = '\n' + '\n'.join(v) + '\n.'
1260 v = '\n' + v + '\n.'
1261 mf.write("%s:%s\n" % (f, v))
1264 mf.write("Build:%s,%s\n" % (build.version, build.vercode))
1266 for f in build_flags_order:
1267 v = build.get_flag(f)
1278 out += '&& \\\n '.join([s.lstrip() for s in v.split('&& ')])
1280 out += ','.join(v) if type(v) == list else v
1285 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1288 def write_yaml_metadata(mf, app):
1290 def w_comment(line):
1291 mf.write("# %s\n" % line)
1296 if any(c in v for c in [': ', '%', '@', '*']):
1297 return "'" + v.replace("'", "''") + "'"
1300 def w_field(f, v, prefix='', t=None):
1302 t = metafieldtype(f)
1307 v += prefix + ' - ' + escape(e) + '\n'
1308 elif t == 'multiline':
1312 lines = v.splitlines()
1315 v += prefix + ' ' + l + '\n'
1321 cmds = [s + '&& \\' for s in v.split('&& ')]
1323 cmds[-1] = cmds[-1][:-len('&& \\')]
1324 w_field(f, cmds, prefix, 'multiline')
1327 v = ' ' + escape(v) + '\n'
1340 mf.write("builds:\n")
1343 w_field('versionName', build.version, ' - ', 'string')
1344 w_field('versionCode', build.vercode, ' ', 'strsng')
1345 for f in build_flags_order:
1346 v = build.get_flag(f)
1350 w_field(f, v, ' ', flagtype(f))
1352 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1355 def write_metadata(fmt, mf, app):
1357 return write_txt_metadata(mf, app)
1359 return write_yaml_metadata(mf, app)
1360 raise MetaDataException("Unknown metadata format given")