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
87 'Update Check Ignore',
92 'Current Version Code',
95 'comments', # For formats that don't do inline comments
96 'builds', # For formats that do builds as a list
104 self.AntiFeatures = []
106 self.Categories = ['None']
107 self.License = 'Unknown'
108 self.AuthorName = None
109 self.AuthorEmail = None
112 self.IssueTracker = ''
121 self.Description = ''
122 self.RequiresRoot = False
126 self.MaintainerNotes = ''
127 self.ArchivePolicy = None
128 self.AutoUpdateMode = 'None'
129 self.UpdateCheckMode = 'None'
130 self.UpdateCheckIgnore = None
131 self.VercodeOperation = None
132 self.UpdateCheckName = None
133 self.UpdateCheckData = None
134 self.CurrentVersion = ''
135 self.CurrentVersionCode = '0'
136 self.NoSourceSince = ''
139 self.metadatapath = None
143 self.lastupdated = None
144 self._modified = set()
146 # Translates human-readable field names to attribute names, e.g.
147 # 'Auto Name' to 'AutoName'
149 def field_to_attr(cls, f):
150 return f.replace(' ', '')
152 # Translates attribute names to human-readable field names, e.g.
153 # 'AutoName' to 'Auto Name'
155 def attr_to_field(cls, k):
158 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
161 # Constructs an old-fashioned dict with the human-readable field
162 # names. Should only be used for tests.
163 def field_dict(self):
165 for k, v in self.__dict__.iteritems():
169 b = {k: v for k, v in build.__dict__.iteritems() if not k.startswith('_')}
170 d['builds'].append(b)
171 elif not k.startswith('_'):
172 f = App.attr_to_field(k)
176 # Gets the value associated to a field name, e.g. 'Auto Name'
177 def get_field(self, f):
178 if f not in app_fields:
179 raise MetaDataException('Unrecognised app field: ' + f)
180 k = App.field_to_attr(f)
181 return getattr(self, k)
183 # Sets the value associated to a field name, e.g. 'Auto Name'
184 def set_field(self, f, v):
185 if f not in app_fields:
186 raise MetaDataException('Unrecognised app field: ' + f)
187 k = App.field_to_attr(f)
189 self._modified.add(k)
191 # Appends to the value associated to a field name, e.g. 'Auto Name'
192 def append_field(self, f, v):
193 if f not in app_fields:
194 raise MetaDataException('Unrecognised app field: ' + f)
195 k = App.field_to_attr(f)
196 if k not in self.__dict__:
197 self.__dict__[k] = [v]
199 self.__dict__[k].append(v)
201 # Like dict.update(), but using human-readable field names
202 def update_fields(self, d):
203 for f, v in d.iteritems():
207 build.update_flags(b)
208 self.builds.append(build)
223 'Description': TYPE_MULTILINE,
224 'Maintainer Notes': TYPE_MULTILINE,
225 'Categories': TYPE_LIST,
226 'AntiFeatures': TYPE_LIST,
227 'Build Version': TYPE_BUILD,
228 'Build': TYPE_BUILD_V2,
229 'Use Built': TYPE_OBSOLETE,
234 if name in fieldtypes:
235 return fieldtypes[name]
239 # In the order in which they are laid out on files
240 build_flags_order = [
273 build_flags = set(build_flags_order + ['version', 'vercode'])
282 self.submodules = False
290 self.oldsdkloc = False
292 self.forceversion = False
293 self.forcevercode = False
304 self.preassemble = []
305 self.gradleprops = []
306 self.antcommands = []
307 self.novcheck = False
309 self._modified = set()
311 def get_flag(self, f):
312 if f not in build_flags:
313 raise MetaDataException('Unrecognised build flag: ' + f)
314 return getattr(self, f)
316 def set_flag(self, f, v):
317 if f == 'versionName':
319 if f == 'versionCode':
321 if f not in build_flags:
322 raise MetaDataException('Unrecognised build flag: ' + f)
324 self._modified.add(f)
326 def append_flag(self, f, v):
327 if f not in build_flags:
328 raise MetaDataException('Unrecognised build flag: ' + f)
329 if f not in self.__dict__:
330 self.__dict__[f] = [v]
332 self.__dict__[f].append(v)
335 for f in ['maven', 'gradle', 'kivy']:
345 version = 'r10e' # falls back to latest
346 paths = common.config['ndk_paths']
347 if version not in paths:
349 return paths[version]
351 def update_flags(self, d):
352 for f, v in d.iteritems():
356 'extlibs': TYPE_LIST,
357 'srclibs': TYPE_LIST,
360 'buildjni': TYPE_LIST,
361 'preassemble': TYPE_LIST,
363 'scanignore': TYPE_LIST,
364 'scandelete': TYPE_LIST,
366 'antcommands': TYPE_LIST,
367 'gradleprops': TYPE_LIST,
369 'prebuild': TYPE_SCRIPT,
370 'build': TYPE_SCRIPT,
371 'submodules': TYPE_BOOL,
372 'oldsdkloc': TYPE_BOOL,
373 'forceversion': TYPE_BOOL,
374 'forcevercode': TYPE_BOOL,
375 'novcheck': TYPE_BOOL,
380 if name in flagtypes:
381 return flagtypes[name]
385 # Designates a metadata field type and checks that it matches
387 # 'name' - The long name of the field type
388 # 'matching' - List of possible values or regex expression
389 # 'sep' - Separator to use if value may be a list
390 # 'fields' - Metadata fields (Field:Value) of this type
391 # 'flags' - Build flags (flag=value) of this type
393 class FieldValidator():
395 def __init__(self, name, matching, sep, fields, flags):
397 self.matching = matching
398 if type(matching) is str:
399 self.compiled = re.compile(matching)
401 self.matching = set(self.matching)
406 def _assert_regex(self, values, appid):
408 if not self.compiled.match(v):
409 raise MetaDataException("'%s' is not a valid %s in %s. Regex pattern: %s"
410 % (v, self.name, appid, self.matching))
412 def _assert_list(self, values, appid):
414 if v not in self.matching:
415 raise MetaDataException("'%s' is not a valid %s in %s. Possible values: %s"
416 % (v, self.name, appid, ', '.join(self.matching)))
418 def check(self, v, appid):
425 if type(self.matching) is set:
426 self._assert_list(values, appid)
428 self._assert_regex(values, appid)
431 # Generic value types
433 FieldValidator("Integer",
434 r'^[1-9][0-9]*$', None,
438 FieldValidator("Hexadecimal",
439 r'^[0-9a-f]+$', None,
443 FieldValidator("HTTP link",
444 r'^http[s]?://', None,
445 ["WebSite", "SourceCode", "IssueTracker", "Changelog", "Donate"], []),
447 FieldValidator("Email",
448 r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', None,
449 ["AuthorEmail"], []),
451 FieldValidator("Bitcoin address",
452 r'^[a-zA-Z0-9]{27,34}$', None,
456 FieldValidator("Litecoin address",
457 r'^L[a-zA-Z0-9]{33}$', None,
461 FieldValidator("Repo Type",
462 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
466 FieldValidator("Binaries",
467 r'^http[s]?://', None,
471 FieldValidator("Archive Policy",
472 r'^[0-9]+ versions$', None,
476 FieldValidator("Anti-Feature",
477 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd",
478 "UpstreamNonFree", "NonFreeAssets"], ',',
482 FieldValidator("Auto Update Mode",
483 r"^(Version .+|None)$", None,
487 FieldValidator("Update Check Mode",
488 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
494 # Check an app's metadata information for integrity errors
495 def check_metadata(app):
498 if k not in app._modified:
500 v.check(app.__dict__[k], app.id)
501 for build in app.builds:
503 if k not in build._modified:
505 v.check(build.__dict__[k], app.id)
508 # Formatter for descriptions. Create an instance, and call parseline() with
509 # each line of the description source from the metadata. At the end, call
510 # end() and then text_txt and text_html will contain the result.
511 class DescriptionFormatter:
518 def __init__(self, linkres):
521 self.state = self.stNONE
522 self.laststate = self.stNONE
525 self.html = StringIO()
526 self.text = StringIO()
528 self.linkResolver = None
529 self.linkResolver = linkres
531 def endcur(self, notstates=None):
532 if notstates and self.state in notstates:
534 if self.state == self.stPARA:
536 elif self.state == self.stUL:
538 elif self.state == self.stOL:
542 self.laststate = self.state
543 self.state = self.stNONE
544 whole_para = ' '.join(self.para_lines)
545 self.addtext(whole_para)
546 self.text.write(textwrap.fill(whole_para, 80,
547 break_long_words=False,
548 break_on_hyphens=False))
549 self.html.write('</p>')
550 del self.para_lines[:]
553 self.html.write('</ul>')
554 self.laststate = self.state
555 self.state = self.stNONE
558 self.html.write('</ol>')
559 self.laststate = self.state
560 self.state = self.stNONE
562 def formatted(self, txt, html):
565 txt = cgi.escape(txt)
567 index = txt.find("''")
572 if txt.startswith("'''"):
578 self.bold = not self.bold
586 self.ital = not self.ital
589 def linkify(self, txt):
593 index = txt.find("[")
595 return (res_plain + self.formatted(txt, False), res_html + self.formatted(txt, True))
596 res_plain += self.formatted(txt[:index], False)
597 res_html += self.formatted(txt[:index], True)
599 if txt.startswith("[["):
600 index = txt.find("]]")
602 raise MetaDataException("Unterminated ]]")
604 if self.linkResolver:
605 url, urltext = self.linkResolver(url)
608 res_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
610 txt = txt[index + 2:]
612 index = txt.find("]")
614 raise MetaDataException("Unterminated ]")
616 index2 = url.find(' ')
620 urltxt = url[index2 + 1:]
623 raise MetaDataException("Url title is just the URL - use [url]")
624 res_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
627 res_plain += ' (' + url + ')'
628 txt = txt[index + 1:]
630 def addtext(self, txt):
631 p, h = self.linkify(txt)
634 def parseline(self, line):
637 elif line.startswith('* '):
638 self.endcur([self.stUL])
639 if self.state != self.stUL:
640 self.html.write('<ul>')
641 self.state = self.stUL
642 if self.laststate != self.stNONE:
643 self.text.write('\n\n')
645 self.text.write('\n')
646 self.text.write(line)
647 self.html.write('<li>')
648 self.addtext(line[1:])
649 self.html.write('</li>')
650 elif line.startswith('# '):
651 self.endcur([self.stOL])
652 if self.state != self.stOL:
653 self.html.write('<ol>')
654 self.state = self.stOL
655 if self.laststate != self.stNONE:
656 self.text.write('\n\n')
658 self.text.write('\n')
659 self.text.write(line)
660 self.html.write('<li>')
661 self.addtext(line[1:])
662 self.html.write('</li>')
664 self.para_lines.append(line)
665 self.endcur([self.stPARA])
666 if self.state == self.stNONE:
667 self.state = self.stPARA
668 if self.laststate != self.stNONE:
669 self.text.write('\n\n')
670 self.html.write('<p>')
674 self.text_txt = self.text.getvalue()
675 self.text_html = self.html.getvalue()
680 # Parse multiple lines of description as written in a metadata file, returning
681 # a single string in text format and wrapped to 80 columns.
682 def description_txt(s):
683 ps = DescriptionFormatter(None)
684 for line in s.splitlines():
690 # Parse multiple lines of description as written in a metadata file, returning
691 # a single string in wiki format. Used for the Maintainer Notes field as well,
692 # because it's the same format.
693 def description_wiki(s):
697 # Parse multiple lines of description as written in a metadata file, returning
698 # a single string in HTML format.
699 def description_html(s, linkres):
700 ps = DescriptionFormatter(linkres)
701 for line in s.splitlines():
707 def parse_srclib(metadatapath):
711 # Defaults for fields that come from metadata
712 thisinfo['Repo Type'] = ''
713 thisinfo['Repo'] = ''
714 thisinfo['Subdir'] = None
715 thisinfo['Prepare'] = None
717 if not os.path.exists(metadatapath):
720 metafile = open(metadatapath, "r")
723 for line in metafile:
725 line = line.rstrip('\r\n')
726 if not line or line.startswith("#"):
730 f, v = line.split(':', 1)
732 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
735 thisinfo[f] = v.split(',')
745 """Read all srclib metadata.
747 The information read will be accessible as metadata.srclibs, which is a
748 dictionary, keyed on srclib name, with the values each being a dictionary
749 in the same format as that returned by the parse_srclib function.
751 A MetaDataException is raised if there are any problems with the srclib
756 # They were already loaded
757 if srclibs is not None:
763 if not os.path.exists(srcdir):
766 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
767 srclibname = os.path.basename(metadatapath[:-4])
768 srclibs[srclibname] = parse_srclib(metadatapath)
771 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
772 # returned by the parse_txt_metadata function.
773 def read_metadata(xref=True):
775 # Always read the srclibs before the apps, since they can use a srlib as
776 # their source repository.
781 for basedir in ('metadata', 'tmp'):
782 if not os.path.exists(basedir):
785 # If there are multiple metadata files for a single appid, then the first
786 # file that is parsed wins over all the others, and the rest throw an
787 # exception. So the original .txt format is parsed first, at least until
788 # newer formats stabilize.
790 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
791 + glob.glob(os.path.join('metadata', '*.json'))
792 + glob.glob(os.path.join('metadata', '*.xml'))
793 + glob.glob(os.path.join('metadata', '*.yaml'))):
794 app = parse_metadata(metadatapath)
796 raise MetaDataException("Found multiple metadata files for " + app.id)
801 # Parse all descriptions at load time, just to ensure cross-referencing
802 # errors are caught early rather than when they hit the build server.
805 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
806 raise MetaDataException("Cannot resolve app id " + appid)
808 for appid, app in apps.iteritems():
810 description_html(app.Description, linkres)
811 except MetaDataException as e:
812 raise MetaDataException("Problem with description of " + appid +
817 # Port legacy ';' separators
818 list_sep = re.compile(r'[,;]')
821 def split_list_values(s):
823 for v in re.split(list_sep, s):
833 def get_default_app_info(metadatapath=None):
834 if metadatapath is None:
837 appid, _ = common.get_extension(os.path.basename(metadatapath))
840 app.metadatapath = metadatapath
841 if appid is not None:
847 def sorted_builds(builds):
848 return sorted(builds, key=lambda build: int(build.vercode))
851 esc_newlines = re.compile(r'\\( |\n)')
854 # This function uses __dict__ to be faster
855 def post_metadata_parse(app):
857 for k, v in app.__dict__.iteritems():
858 if k not in app._modified:
860 if type(v) in (float, int):
861 app.__dict__[k] = str(v)
863 for build in app.builds:
864 for k, v in build.__dict__.iteritems():
866 if k not in build._modified:
868 if type(v) in (float, int):
869 build.__dict__[k] = str(v)
873 if ftype == TYPE_SCRIPT:
874 build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
875 elif ftype == TYPE_BOOL:
876 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
877 if isinstance(v, basestring):
878 build.__dict__[k] = _decode_bool(v)
879 elif ftype == TYPE_STRING:
880 if isinstance(v, bool) and v:
881 build.__dict__[k] = 'yes'
883 if not app.Description:
884 app.Description = 'No description available'
886 app.builds = sorted_builds(app.builds)
889 # Parse metadata for a single application.
891 # 'metadatapath' - the filename to read. The package id for the application comes
892 # from this filename. Pass None to get a blank entry.
894 # Returns a dictionary containing all the details of the application. There are
895 # two major kinds of information in the dictionary. Keys beginning with capital
896 # letters correspond directory to identically named keys in the metadata file.
897 # Keys beginning with lower case letters are generated in one way or another,
898 # and are not found verbatim in the metadata.
900 # Known keys not originating from the metadata are:
902 # 'builds' - a list of dictionaries containing build information
903 # for each defined build
904 # 'comments' - a list of comments from the metadata file. Each is
905 # a list of the form [field, comment] where field is
906 # the name of the field it preceded in the metadata
907 # file. Where field is None, the comment goes at the
908 # end of the file. Alternatively, 'build:version' is
909 # for a comment before a particular build version.
910 # 'descriptionlines' - original lines of description as formatted in the
915 def _decode_list(data):
916 '''convert items in a list from unicode to basestring'''
919 if isinstance(item, unicode):
920 item = item.encode('utf-8')
921 elif isinstance(item, list):
922 item = _decode_list(item)
923 elif isinstance(item, dict):
924 item = _decode_dict(item)
929 def _decode_dict(data):
930 '''convert items in a dict from unicode to basestring'''
932 for k, v in data.iteritems():
933 if isinstance(k, unicode):
934 k = k.encode('utf-8')
935 if isinstance(v, unicode):
936 v = v.encode('utf-8')
937 elif isinstance(v, list):
939 elif isinstance(v, dict):
945 bool_true = re.compile(r'([Yy]es|[Tt]rue)')
946 bool_false = re.compile(r'([Nn]o|[Ff]alse)')
950 if bool_true.match(s):
952 if bool_false.match(s):
954 raise MetaDataException("Invalid bool '%s'" % s)
957 def parse_metadata(metadatapath):
958 _, ext = common.get_extension(metadatapath)
959 accepted = common.config['accepted_formats']
960 if ext not in accepted:
961 raise MetaDataException('"%s" is not an accepted format, convert to: %s' % (
962 metadatapath, ', '.join(accepted)))
965 app.metadatapath = metadatapath
966 app.id, _ = common.get_extension(os.path.basename(metadatapath))
968 with open(metadatapath, 'r') as mf:
970 parse_txt_metadata(mf, app)
972 parse_json_metadata(mf, app)
974 parse_xml_metadata(mf, app)
976 parse_yaml_metadata(mf, app)
978 raise MetaDataException('Unknown metadata format: %s' % metadatapath)
980 post_metadata_parse(app)
984 def parse_json_metadata(mf, app):
986 # fdroid metadata is only strings and booleans, no floats or ints. And
987 # json returns unicode, and fdroidserver still uses plain python strings
988 # TODO create schema using https://pypi.python.org/pypi/jsonschema
989 jsoninfo = json.load(mf, object_hook=_decode_dict,
990 parse_int=lambda s: s,
991 parse_float=lambda s: s)
992 app.update_fields(jsoninfo)
993 for f in ['Description', 'Maintainer Notes']:
995 app.set_field(f, '\n'.join(v))
999 def parse_xml_metadata(mf, app):
1001 tree = ElementTree.ElementTree(file=mf)
1002 root = tree.getroot()
1004 if root.tag != 'resources':
1005 raise MetaDataException('resources file does not have root element <resources/>')
1008 if child.tag != 'builds':
1009 # builds does not have name="" attrib
1010 name = child.attrib['name']
1012 if child.tag == 'string':
1013 app.set_field(name, child.text)
1014 elif child.tag == 'string-array':
1016 app.append_field(name, item.text)
1017 elif child.tag == 'builds':
1021 build.set_flag(key.tag, key.text)
1022 app.builds.append(build)
1024 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
1025 if not isinstance(app.RequiresRoot, bool):
1026 app.RequiresRoot = app.RequiresRoot == 'true'
1031 def parse_yaml_metadata(mf, app):
1033 yamlinfo = yaml.load(mf, Loader=YamlLoader)
1034 app.update_fields(yamlinfo)
1038 build_line_sep = re.compile(r'(?<!\\),')
1039 build_cont = re.compile(r'^[ \t]')
1042 def parse_txt_metadata(mf, app):
1046 def add_buildflag(p, build):
1048 raise MetaDataException("Empty build flag at {1}"
1049 .format(buildlines[0], linedesc))
1050 bv = p.split('=', 1)
1052 raise MetaDataException("Invalid build flag at {0} in {1}"
1053 .format(buildlines[0], linedesc))
1059 pv = split_list_values(pv)
1060 build.set_flag(pk, pv)
1061 elif t == TYPE_STRING or t == TYPE_SCRIPT:
1062 build.set_flag(pk, pv)
1063 elif t == TYPE_BOOL:
1064 build.set_flag(pk, _decode_bool(pv))
1066 def parse_buildline(lines):
1068 parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
1070 raise MetaDataException("Invalid build format: " + v + " in " + mf.name)
1072 build.version = parts[0]
1073 build.vercode = parts[1]
1074 if parts[2].startswith('!'):
1075 # For backwards compatibility, handle old-style disabling,
1076 # including attempting to extract the commit from the message
1077 build.disable = parts[2][1:]
1078 commit = 'unknown - see disabled'
1079 index = parts[2].rfind('at ')
1081 commit = parts[2][index + 3:]
1082 if commit.endswith(')'):
1083 commit = commit[:-1]
1084 build.commit = commit
1086 build.commit = parts[2]
1088 add_buildflag(p, build)
1092 def add_comments(key):
1095 app.comments[key] = list(curcomments)
1100 multiline_lines = []
1108 linedesc = "%s:%d" % (mf.name, c)
1109 line = line.rstrip('\r\n')
1111 if build_cont.match(line):
1112 if line.endswith('\\'):
1113 buildlines.append(line[:-1].lstrip())
1115 buildlines.append(line.lstrip())
1116 bl = ''.join(buildlines)
1117 add_buildflag(bl, build)
1120 if not build.commit and not build.disable:
1121 raise MetaDataException("No commit specified for {0} in {1}"
1122 .format(build.version, linedesc))
1124 app.builds.append(build)
1125 add_comments('build:' + build.vercode)
1131 if line.startswith("#"):
1132 curcomments.append(line[1:].strip())
1135 f, v = line.split(':', 1)
1137 raise MetaDataException("Invalid metadata in " + linedesc)
1139 # Translate obsolete fields...
1140 if f == 'Market Version':
1141 f = 'Current Version'
1142 if f == 'Market Version Code':
1143 f = 'Current Version Code'
1145 ftype = fieldtype(f)
1146 if ftype not in [TYPE_BUILD, TYPE_BUILD_V2]:
1148 if ftype == TYPE_MULTILINE:
1151 raise MetaDataException("Unexpected text on same line as " + f + " in " + linedesc)
1152 elif ftype == TYPE_STRING:
1154 elif ftype == TYPE_LIST:
1155 app.set_field(f, split_list_values(v))
1156 elif ftype == TYPE_BUILD:
1157 if v.endswith("\\"):
1160 buildlines.append(v[:-1])
1162 build = parse_buildline([v])
1163 app.builds.append(build)
1164 add_comments('build:' + app.builds[-1].vercode)
1165 elif ftype == TYPE_BUILD_V2:
1168 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1169 .format(v, linedesc))
1171 build.version = vv[0]
1172 build.vercode = vv[1]
1173 if build.vercode in vc_seen:
1174 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1175 build.vercode, linedesc))
1176 vc_seen.add(build.vercode)
1179 elif ftype == TYPE_OBSOLETE:
1180 pass # Just throw it away!
1182 raise MetaDataException("Unrecognised field '" + f + "' in " + linedesc)
1183 elif mode == 1: # Multiline field
1186 app.set_field(f, '\n'.join(multiline_lines))
1187 del multiline_lines[:]
1189 multiline_lines.append(line)
1190 elif mode == 2: # Line continuation mode in Build Version
1191 if line.endswith("\\"):
1192 buildlines.append(line[:-1])
1194 buildlines.append(line)
1195 build = parse_buildline(buildlines)
1196 app.builds.append(build)
1197 add_comments('build:' + app.builds[-1].vercode)
1201 # Mode at end of file should always be 0
1203 raise MetaDataException(f + " not terminated in " + mf.name)
1205 raise MetaDataException("Unterminated continuation in " + mf.name)
1207 raise MetaDataException("Unterminated build in " + mf.name)
1212 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1214 def w_comments(key):
1215 if key not in app.comments:
1217 for line in app.comments[key]:
1220 def w_field_always(f, v=None):
1222 v = app.get_field(f)
1226 def w_field_nonempty(f, v=None):
1228 v = app.get_field(f)
1233 w_field_nonempty('Disabled')
1234 w_field_nonempty('AntiFeatures')
1235 w_field_nonempty('Provides')
1236 w_field_always('Categories')
1237 w_field_always('License')
1238 w_field_nonempty('Author Name')
1239 w_field_nonempty('Author Email')
1240 w_field_always('Web Site')
1241 w_field_always('Source Code')
1242 w_field_always('Issue Tracker')
1243 w_field_nonempty('Changelog')
1244 w_field_nonempty('Donate')
1245 w_field_nonempty('FlattrID')
1246 w_field_nonempty('Bitcoin')
1247 w_field_nonempty('Litecoin')
1249 w_field_nonempty('Name')
1250 w_field_nonempty('Auto Name')
1251 w_field_always('Summary')
1252 w_field_always('Description', description_txt(app.Description))
1254 if app.RequiresRoot:
1255 w_field_always('Requires Root', 'yes')
1258 w_field_always('Repo Type')
1259 w_field_always('Repo')
1261 w_field_always('Binaries')
1264 for build in sorted_builds(app.builds):
1266 if build.version == "Ignore":
1269 w_comments('build:' + build.vercode)
1273 if app.MaintainerNotes:
1274 w_field_always('Maintainer Notes', app.MaintainerNotes)
1277 w_field_nonempty('Archive Policy')
1278 w_field_always('Auto Update Mode')
1279 w_field_always('Update Check Mode')
1280 w_field_nonempty('Update Check Ignore')
1281 w_field_nonempty('Vercode Operation')
1282 w_field_nonempty('Update Check Name')
1283 w_field_nonempty('Update Check Data')
1284 if app.CurrentVersion:
1285 w_field_always('Current Version')
1286 w_field_always('Current Version Code')
1287 if app.NoSourceSince:
1289 w_field_always('No Source Since')
1293 # Write a metadata file in txt format.
1295 # 'mf' - Writer interface (file, StringIO, ...)
1296 # 'app' - The app data
1297 def write_txt_metadata(mf, app):
1299 def w_comment(line):
1300 mf.write("# %s\n" % line)
1306 elif t == TYPE_MULTILINE:
1307 v = '\n' + v + '\n.'
1308 mf.write("%s:%s\n" % (f, v))
1311 mf.write("Build:%s,%s\n" % (build.version, build.vercode))
1313 for f in build_flags_order:
1314 v = build.get_flag(f)
1319 mf.write(' %s=' % f)
1320 if t == TYPE_STRING:
1322 elif t == TYPE_BOOL:
1324 elif t == TYPE_SCRIPT:
1326 for s in v.split(' && '):
1330 mf.write(' && \\\n ')
1332 elif t == TYPE_LIST:
1333 mf.write(','.join(v))
1337 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1340 def write_yaml_metadata(mf, app):
1342 def w_comment(line):
1343 mf.write("# %s\n" % line)
1348 if any(c in v for c in [': ', '%', '@', '*']):
1349 return "'" + v.replace("'", "''") + "'"
1352 def w_field(f, v, prefix='', t=None):
1359 v += prefix + ' - ' + escape(e) + '\n'
1360 elif t == TYPE_MULTILINE:
1362 for l in v.splitlines():
1364 v += prefix + ' ' + l + '\n'
1367 elif t == TYPE_BOOL:
1369 elif t == TYPE_SCRIPT:
1370 cmds = [s + '&& \\' for s in v.split('&& ')]
1372 cmds[-1] = cmds[-1][:-len('&& \\')]
1373 w_field(f, cmds, prefix, 'multiline')
1376 v = ' ' + escape(v) + '\n'
1389 mf.write("builds:\n")
1392 w_field('versionName', build.version, ' - ', TYPE_STRING)
1393 w_field('versionCode', build.vercode, ' ', TYPE_STRING)
1394 for f in build_flags_order:
1395 v = build.get_flag(f)
1399 w_field(f, v, ' ', flagtype(f))
1401 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1404 def write_metadata(fmt, mf, app):
1406 return write_txt_metadata(mf, app)
1408 return write_yaml_metadata(mf, app)
1409 raise MetaDataException("Unknown metadata format given")