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 # In the order in which they are laid out on files
57 app_defaults = OrderedDict([
61 ('Categories', ['None']),
62 ('License', 'Unknown'),
65 ('Issue Tracker', ''),
76 ('Requires Root', False),
80 ('Maintainer Notes', []),
81 ('Archive Policy', None),
82 ('Auto Update Mode', 'None'),
83 ('Update Check Mode', 'None'),
84 ('Update Check Ignore', None),
85 ('Vercode Operation', None),
86 ('Update Check Name', None),
87 ('Update Check Data', None),
88 ('Current Version', ''),
89 ('Current Version Code', '0'),
90 ('No Source Since', ''),
94 # In the order in which they are laid out on files
95 # Sorted by their action and their place in the build timeline
96 # These variables can have varying datatypes. For example, anything with
97 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
98 flag_defaults = OrderedDict([
102 ('submodules', False),
110 ('oldsdkloc', False),
112 ('forceversion', False),
113 ('forcevercode', False),
117 ('update', ['auto']),
123 ('ndk', 'r10e'), # defaults to latest
126 ('antcommands', None),
131 # Designates a metadata field type and checks that it matches
133 # 'name' - The long name of the field type
134 # 'matching' - List of possible values or regex expression
135 # 'sep' - Separator to use if value may be a list
136 # 'fields' - Metadata fields (Field:Value) of this type
137 # 'attrs' - Build attributes (attr=value) of this type
139 class FieldValidator():
141 def __init__(self, name, matching, sep, fields, attrs):
143 self.matching = matching
144 if type(matching) is str:
145 self.compiled = re.compile(matching)
150 def _assert_regex(self, values, appid):
152 if not self.compiled.match(v):
153 raise MetaDataException("'%s' is not a valid %s in %s. "
154 % (v, self.name, appid) +
155 "Regex pattern: %s" % (self.matching))
157 def _assert_list(self, values, appid):
159 if v not in self.matching:
160 raise MetaDataException("'%s' is not a valid %s in %s. "
161 % (v, self.name, appid) +
162 "Possible values: %s" % (", ".join(self.matching)))
164 def check(self, value, appid):
165 if type(value) is not str or not value:
167 if self.sep is not None:
168 values = value.split(self.sep)
171 if type(self.matching) is list:
172 self._assert_list(values, appid)
174 self._assert_regex(values, appid)
177 # Generic value types
179 FieldValidator("Integer",
180 r'^[1-9][0-9]*$', None,
184 FieldValidator("Hexadecimal",
185 r'^[0-9a-f]+$', None,
189 FieldValidator("HTTP link",
190 r'^http[s]?://', None,
191 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
193 FieldValidator("Bitcoin address",
194 r'^[a-zA-Z0-9]{27,34}$', None,
198 FieldValidator("Litecoin address",
199 r'^L[a-zA-Z0-9]{33}$', None,
203 FieldValidator("Dogecoin address",
204 r'^D[a-zA-Z0-9]{33}$', None,
208 FieldValidator("bool",
209 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
211 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
214 FieldValidator("Repo Type",
215 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
219 FieldValidator("Binaries",
220 r'^http[s]?://', None,
224 FieldValidator("Archive Policy",
225 r'^[0-9]+ versions$', None,
229 FieldValidator("Anti-Feature",
230 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
234 FieldValidator("Auto Update Mode",
235 r"^(Version .+|None)$", None,
236 ["Auto Update Mode"],
239 FieldValidator("Update Check Mode",
240 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
241 ["Update Check Mode"],
246 # Check an app's metadata information for integrity errors
247 def check_metadata(info):
249 for field in v.fields:
250 v.check(info[field], info['id'])
251 for build in info['builds']:
253 v.check(build[attr], info['id'])
256 # Formatter for descriptions. Create an instance, and call parseline() with
257 # each line of the description source from the metadata. At the end, call
258 # end() and then text_wiki and text_html will contain the result.
259 class DescriptionFormatter:
273 def __init__(self, linkres):
274 self.linkResolver = linkres
276 def endcur(self, notstates=None):
277 if notstates and self.state in notstates:
279 if self.state == self.stPARA:
281 elif self.state == self.stUL:
283 elif self.state == self.stOL:
287 self.text_html += '</p>'
288 self.state = self.stNONE
289 whole_para = ' '.join(self.para_lines)
290 self.addtext(whole_para)
291 self.text_txt += textwrap.fill(whole_para, 80) + '\n\n'
292 del self.para_lines[:]
295 self.text_html += '</ul>'
296 self.text_txt += '\n'
297 self.state = self.stNONE
300 self.text_html += '</ol>'
301 self.text_txt += '\n'
302 self.state = self.stNONE
304 def formatted(self, txt, html):
307 txt = cgi.escape(txt)
309 index = txt.find("''")
311 return formatted + txt
312 formatted += txt[:index]
314 if txt.startswith("'''"):
320 self.bold = not self.bold
328 self.ital = not self.ital
331 def linkify(self, txt):
335 index = txt.find("[")
337 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
338 linkified_plain += self.formatted(txt[:index], False)
339 linkified_html += self.formatted(txt[:index], True)
341 if txt.startswith("[["):
342 index = txt.find("]]")
344 raise MetaDataException("Unterminated ]]")
346 if self.linkResolver:
347 url, urltext = self.linkResolver(url)
350 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
351 linkified_plain += urltext
352 txt = txt[index + 2:]
354 index = txt.find("]")
356 raise MetaDataException("Unterminated ]")
358 index2 = url.find(' ')
362 urltxt = url[index2 + 1:]
365 raise MetaDataException("Url title is just the URL - use [url]")
366 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
367 linkified_plain += urltxt
369 linkified_plain += ' (' + url + ')'
370 txt = txt[index + 1:]
372 def addtext(self, txt):
373 p, h = self.linkify(txt)
376 def parseline(self, line):
377 self.text_wiki += "%s\n" % line
380 elif line.startswith('* '):
381 self.endcur([self.stUL])
382 self.text_txt += "%s\n" % line
383 if self.state != self.stUL:
384 self.text_html += '<ul>'
385 self.state = self.stUL
386 self.text_html += '<li>'
387 self.addtext(line[1:])
388 self.text_html += '</li>'
389 elif line.startswith('# '):
390 self.endcur([self.stOL])
391 self.text_txt += "%s\n" % line
392 if self.state != self.stOL:
393 self.text_html += '<ol>'
394 self.state = self.stOL
395 self.text_html += '<li>'
396 self.addtext(line[1:])
397 self.text_html += '</li>'
399 self.para_lines.append(line)
400 self.endcur([self.stPARA])
401 if self.state == self.stNONE:
402 self.text_html += '<p>'
403 self.state = self.stPARA
404 elif self.state == self.stPARA:
405 self.text_html += ' '
409 self.text_txt = self.text_txt.strip()
412 # Parse multiple lines of description as written in a metadata file, returning
413 # a single string in text format and wrapped to 80 columns.
414 def description_txt(lines):
415 ps = DescriptionFormatter(None)
422 # Parse multiple lines of description as written in a metadata file, returning
423 # a single string in wiki format. Used for the Maintainer Notes field as well,
424 # because it's the same format.
425 def description_wiki(lines):
426 ps = DescriptionFormatter(None)
433 # Parse multiple lines of description as written in a metadata file, returning
434 # a single string in HTML format.
435 def description_html(lines, linkres):
436 ps = DescriptionFormatter(linkres)
443 def parse_srclib(metadatapath):
447 # Defaults for fields that come from metadata
448 thisinfo['Repo Type'] = ''
449 thisinfo['Repo'] = ''
450 thisinfo['Subdir'] = None
451 thisinfo['Prepare'] = None
453 if not os.path.exists(metadatapath):
456 metafile = open(metadatapath, "r")
459 for line in metafile:
461 line = line.rstrip('\r\n')
462 if not line or line.startswith("#"):
466 field, value = line.split(':', 1)
468 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
470 if field == "Subdir":
471 thisinfo[field] = value.split(',')
473 thisinfo[field] = value
479 """Read all srclib metadata.
481 The information read will be accessible as metadata.srclibs, which is a
482 dictionary, keyed on srclib name, with the values each being a dictionary
483 in the same format as that returned by the parse_srclib function.
485 A MetaDataException is raised if there are any problems with the srclib
490 # They were already loaded
491 if srclibs is not None:
497 if not os.path.exists(srcdir):
500 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
501 srclibname = os.path.basename(metadatapath[:-4])
502 srclibs[srclibname] = parse_srclib(metadatapath)
505 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
506 # returned by the parse_txt_metadata function.
507 def read_metadata(xref=True):
509 # Always read the srclibs before the apps, since they can use a srlib as
510 # their source repository.
515 for basedir in ('metadata', 'tmp'):
516 if not os.path.exists(basedir):
519 # If there are multiple metadata files for a single appid, then the first
520 # file that is parsed wins over all the others, and the rest throw an
521 # exception. So the original .txt format is parsed first, at least until
522 # newer formats stabilize.
524 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
525 + glob.glob(os.path.join('metadata', '*.json'))
526 + glob.glob(os.path.join('metadata', '*.xml'))
527 + glob.glob(os.path.join('metadata', '*.yaml'))):
528 appid, appinfo = parse_metadata(apps, metadatapath)
529 check_metadata(appinfo)
530 apps[appid] = appinfo
533 # Parse all descriptions at load time, just to ensure cross-referencing
534 # errors are caught early rather than when they hit the build server.
537 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
538 raise MetaDataException("Cannot resolve app id " + appid)
540 for appid, app in apps.iteritems():
542 description_html(app['Description'], linkres)
543 except MetaDataException, e:
544 raise MetaDataException("Problem with description of " + appid +
550 # Get the type expected for a given metadata field.
551 def metafieldtype(name):
552 if name in ['Description', 'Maintainer Notes']:
554 if name in ['Categories', 'AntiFeatures']:
556 if name == 'Build Version':
560 if name == 'Use Built':
562 if name not in app_defaults:
568 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
569 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
572 if name in ['init', 'prebuild', 'build']:
574 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
580 def fill_build_defaults(build):
582 def get_build_type():
583 for t in ['maven', 'gradle', 'kivy']:
590 for flag, value in flag_defaults.iteritems():
594 build['type'] = get_build_type()
595 build['ndk_path'] = common.get_ndk_path(build['ndk'])
598 def split_list_values(s):
599 # Port legacy ';' separators
600 l = [v.strip() for v in s.replace(';', ',').split(',')]
601 return [v for v in l if v]
604 def get_default_app_info_list(apps, metadatapath=None):
605 if metadatapath is None:
608 appid = os.path.splitext(os.path.basename(metadatapath))[0]
610 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
611 % (metadatapath, appid, apps[appid]['metadatapath']))
615 thisinfo.update(app_defaults)
616 thisinfo['metadatapath'] = metadatapath
617 if appid is not None:
618 thisinfo['id'] = appid
620 # General defaults...
621 thisinfo['builds'] = []
622 thisinfo['comments'] = []
624 return appid, thisinfo
627 def sorted_builds(builds):
628 return sorted(builds, key=lambda build: int(build['vercode']))
631 def post_metadata_parse(thisinfo):
633 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
634 for k, v in thisinfo.iteritems():
635 if k not in supported_metadata:
636 raise MetaDataException("Unrecognised metadata: {0}: {1}"
638 if type(v) in (float, int):
641 # convert to the odd internal format
642 for k in ('Description', 'Maintainer Notes'):
643 if isinstance(thisinfo[k], basestring):
644 text = thisinfo[k].rstrip().lstrip()
645 thisinfo[k] = text.split('\n')
647 supported_flags = (flag_defaults.keys()
648 + ['vercode', 'version', 'versionCode', 'versionName'])
649 esc_newlines = re.compile('\\\\( |\\n)')
651 for build in thisinfo['builds']:
652 for k, v in build.items():
653 if k not in supported_flags:
654 raise MetaDataException("Unrecognised build flag: {0}={1}"
657 if k == 'versionCode':
658 build['vercode'] = str(v)
659 del build['versionCode']
660 elif k == 'versionName':
661 build['version'] = str(v)
662 del build['versionName']
663 elif type(v) in (float, int):
666 keyflagtype = flagtype(k)
667 if keyflagtype == 'list':
668 # these can be bools, strings or lists, but ultimately are lists
669 if isinstance(v, basestring):
671 elif isinstance(v, bool):
676 elif keyflagtype == 'script':
677 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
678 elif keyflagtype == 'bool':
679 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
680 if isinstance(v, basestring):
686 if not thisinfo['Description']:
687 thisinfo['Description'].append('No description available')
689 for build in thisinfo['builds']:
690 fill_build_defaults(build)
692 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
695 # Parse metadata for a single application.
697 # 'metadatapath' - the filename to read. The package id for the application comes
698 # from this filename. Pass None to get a blank entry.
700 # Returns a dictionary containing all the details of the application. There are
701 # two major kinds of information in the dictionary. Keys beginning with capital
702 # letters correspond directory to identically named keys in the metadata file.
703 # Keys beginning with lower case letters are generated in one way or another,
704 # and are not found verbatim in the metadata.
706 # Known keys not originating from the metadata are:
708 # 'builds' - a list of dictionaries containing build information
709 # for each defined build
710 # 'comments' - a list of comments from the metadata file. Each is
711 # a list of the form [field, comment] where field is
712 # the name of the field it preceded in the metadata
713 # file. Where field is None, the comment goes at the
714 # end of the file. Alternatively, 'build:version' is
715 # for a comment before a particular build version.
716 # 'descriptionlines' - original lines of description as formatted in the
721 def _decode_list(data):
722 '''convert items in a list from unicode to basestring'''
725 if isinstance(item, unicode):
726 item = item.encode('utf-8')
727 elif isinstance(item, list):
728 item = _decode_list(item)
729 elif isinstance(item, dict):
730 item = _decode_dict(item)
735 def _decode_dict(data):
736 '''convert items in a dict from unicode to basestring'''
738 for key, value in data.iteritems():
739 if isinstance(key, unicode):
740 key = key.encode('utf-8')
741 if isinstance(value, unicode):
742 value = value.encode('utf-8')
743 elif isinstance(value, list):
744 value = _decode_list(value)
745 elif isinstance(value, dict):
746 value = _decode_dict(value)
751 def parse_metadata(apps, metadatapath):
752 root, ext = os.path.splitext(metadatapath)
753 metadataformat = ext[1:]
754 accepted = common.config['accepted_formats']
755 if metadataformat not in accepted:
756 logging.critical('"' + metadatapath
757 + '" is not in an accepted format, '
758 + 'convert to: ' + ', '.join(accepted))
761 if metadataformat == 'txt':
762 return parse_txt_metadata(apps, metadatapath)
763 elif metadataformat == 'json':
764 return parse_json_metadata(apps, metadatapath)
765 elif metadataformat == 'xml':
766 return parse_xml_metadata(apps, metadatapath)
767 elif metadataformat == 'yaml':
768 return parse_yaml_metadata(apps, metadatapath)
770 logging.critical('Unknown metadata format: ' + metadatapath)
774 def parse_json_metadata(apps, metadatapath):
776 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
778 # fdroid metadata is only strings and booleans, no floats or ints. And
779 # json returns unicode, and fdroidserver still uses plain python strings
780 # TODO create schema using https://pypi.python.org/pypi/jsonschema
781 jsoninfo = json.load(open(metadatapath, 'r'),
782 object_hook=_decode_dict,
783 parse_int=lambda s: s,
784 parse_float=lambda s: s)
785 thisinfo.update(jsoninfo)
786 post_metadata_parse(thisinfo)
788 return (appid, thisinfo)
791 def parse_xml_metadata(apps, metadatapath):
793 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
795 tree = ElementTree.ElementTree(file=metadatapath)
796 root = tree.getroot()
798 if root.tag != 'resources':
799 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
802 supported_metadata = app_defaults.keys()
804 if child.tag != 'builds':
805 # builds does not have name="" attrib
806 name = child.attrib['name']
807 if name not in supported_metadata:
808 raise MetaDataException("Unrecognised metadata: <"
809 + child.tag + ' name="' + name + '">'
811 + "</" + child.tag + '>')
813 if child.tag == 'string':
814 thisinfo[name] = child.text
815 elif child.tag == 'string-array':
818 items.append(item.text)
819 thisinfo[name] = items
820 elif child.tag == 'builds':
825 builddict[key.tag] = key.text
826 builds.append(builddict)
827 thisinfo['builds'] = builds
829 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
830 if not isinstance(thisinfo['Requires Root'], bool):
831 if thisinfo['Requires Root'] == 'true':
832 thisinfo['Requires Root'] = True
834 thisinfo['Requires Root'] = False
836 post_metadata_parse(thisinfo)
838 return (appid, thisinfo)
841 def parse_yaml_metadata(apps, metadatapath):
843 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
845 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
846 thisinfo.update(yamlinfo)
847 post_metadata_parse(thisinfo)
849 return (appid, thisinfo)
852 def parse_txt_metadata(apps, metadatapath):
856 def add_buildflag(p, thisbuild):
858 raise MetaDataException("Empty build flag at {1}"
859 .format(buildlines[0], linedesc))
862 raise MetaDataException("Invalid build flag at {0} in {1}"
863 .format(buildlines[0], linedesc))
866 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
867 .format(pk, thisbuild['version'], linedesc))
870 if pk not in flag_defaults:
871 raise MetaDataException("Unrecognised build flag at {0} in {1}"
872 .format(p, linedesc))
875 pv = split_list_values(pv)
877 if len(pv) == 1 and pv[0] in ['main', 'yes']:
880 elif t == 'string' or t == 'script':
887 logging.debug("...ignoring bool flag %s" % p)
890 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
893 def parse_buildline(lines):
894 value = "".join(lines)
895 parts = [p.replace("\\,", ",")
896 for p in re.split(r"(?<!\\),", value)]
898 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
900 thisbuild['origlines'] = lines
901 thisbuild['version'] = parts[0]
902 thisbuild['vercode'] = parts[1]
903 if parts[2].startswith('!'):
904 # For backwards compatibility, handle old-style disabling,
905 # including attempting to extract the commit from the message
906 thisbuild['disable'] = parts[2][1:]
907 commit = 'unknown - see disabled'
908 index = parts[2].rfind('at ')
910 commit = parts[2][index + 3:]
911 if commit.endswith(')'):
913 thisbuild['commit'] = commit
915 thisbuild['commit'] = parts[2]
917 add_buildflag(p, thisbuild)
921 def add_comments(key):
924 for comment in curcomments:
925 thisinfo['comments'].append([key, comment])
928 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
929 metafile = open(metadatapath, "r")
938 for line in metafile:
940 linedesc = "%s:%d" % (metafile.name, c)
941 line = line.rstrip('\r\n')
943 if not any(line.startswith(s) for s in (' ', '\t')):
944 commit = curbuild['commit'] if 'commit' in curbuild else None
945 if not commit and 'disable' not in curbuild:
946 raise MetaDataException("No commit specified for {0} in {1}"
947 .format(curbuild['version'], linedesc))
949 thisinfo['builds'].append(curbuild)
950 add_comments('build:' + curbuild['vercode'])
953 if line.endswith('\\'):
954 buildlines.append(line[:-1].lstrip())
956 buildlines.append(line.lstrip())
957 bl = ''.join(buildlines)
958 add_buildflag(bl, curbuild)
964 if line.startswith("#"):
965 curcomments.append(line)
968 field, value = line.split(':', 1)
970 raise MetaDataException("Invalid metadata in " + linedesc)
971 if field != field.strip() or value != value.strip():
972 raise MetaDataException("Extra spacing found in " + linedesc)
974 # Translate obsolete fields...
975 if field == 'Market Version':
976 field = 'Current Version'
977 if field == 'Market Version Code':
978 field = 'Current Version Code'
980 fieldtype = metafieldtype(field)
981 if fieldtype not in ['build', 'buildv2']:
983 if fieldtype == 'multiline':
987 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
988 elif fieldtype == 'string':
989 thisinfo[field] = value
990 elif fieldtype == 'list':
991 thisinfo[field] = split_list_values(value)
992 elif fieldtype == 'build':
993 if value.endswith("\\"):
995 buildlines = [value[:-1]]
997 curbuild = parse_buildline([value])
998 thisinfo['builds'].append(curbuild)
999 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1000 elif fieldtype == 'buildv2':
1002 vv = value.split(',')
1004 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1005 .format(value, linedesc))
1006 curbuild['version'] = vv[0]
1007 curbuild['vercode'] = vv[1]
1008 if curbuild['vercode'] in vc_seen:
1009 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1010 curbuild['vercode'], linedesc))
1011 vc_seen[curbuild['vercode']] = True
1014 elif fieldtype == 'obsolete':
1015 pass # Just throw it away!
1017 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1018 elif mode == 1: # Multiline field
1022 thisinfo[field].append(line)
1023 elif mode == 2: # Line continuation mode in Build Version
1024 if line.endswith("\\"):
1025 buildlines.append(line[:-1])
1027 buildlines.append(line)
1028 curbuild = parse_buildline(buildlines)
1029 thisinfo['builds'].append(curbuild)
1030 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1034 # Mode at end of file should always be 0...
1036 raise MetaDataException(field + " not terminated in " + metafile.name)
1038 raise MetaDataException("Unterminated continuation in " + metafile.name)
1040 raise MetaDataException("Unterminated build in " + metafile.name)
1042 post_metadata_parse(thisinfo)
1044 return (appid, thisinfo)
1047 # Write a metadata file.
1049 # 'dest' - The path to the output file
1050 # 'app' - The app data
1051 def write_metadata(dest, app):
1053 def writecomments(key):
1055 for pf, comment in app['comments']:
1057 mf.write("%s\n" % comment)
1060 logging.debug("...writing comments for " + (key or 'EOF'))
1062 def writefield(field, value=None):
1063 writecomments(field)
1066 t = metafieldtype(field)
1068 value = ','.join(value)
1069 elif t == 'multiline':
1070 if type(value) == list:
1071 value = '\n' + '\n'.join(value) + '\n.'
1073 value = '\n' + value + '\n.'
1074 mf.write("%s:%s\n" % (field, value))
1076 def writefield_nonempty(field, value=None):
1080 writefield(field, value)
1082 mf = open(dest, 'w')
1083 writefield_nonempty('Disabled')
1084 if app['AntiFeatures']:
1085 writefield('AntiFeatures')
1086 writefield_nonempty('Provides')
1087 writefield('Categories')
1088 writefield('License')
1089 writefield('Web Site')
1090 writefield('Source Code')
1091 writefield('Issue Tracker')
1092 writefield_nonempty('Changelog')
1093 writefield_nonempty('Donate')
1094 writefield_nonempty('FlattrID')
1095 writefield_nonempty('Bitcoin')
1096 writefield_nonempty('Litecoin')
1097 writefield_nonempty('Dogecoin')
1099 writefield_nonempty('Name')
1100 writefield_nonempty('Auto Name')
1101 writefield('Summary')
1102 writefield('Description', description_txt(app['Description']))
1104 if app['Requires Root']:
1105 writefield('Requires Root', 'yes')
1107 if app['Repo Type']:
1108 writefield('Repo Type')
1111 writefield('Binaries')
1113 for build in sorted_builds(app['builds']):
1115 if build['version'] == "Ignore":
1118 writecomments('build:' + build['vercode'])
1119 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1121 def write_builditem(key, value):
1123 if key in ['version', 'vercode']:
1126 if value == flag_defaults[key]:
1131 logging.debug("...writing {0} : {1}".format(key, value))
1132 outline = ' %s=' % key
1139 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1141 outline += ','.join(value) if type(value) == list else value
1146 for flag in flag_defaults:
1149 write_builditem(flag, value)
1152 if app['Maintainer Notes']:
1153 writefield('Maintainer Notes', app['Maintainer Notes'])
1156 writefield_nonempty('Archive Policy')
1157 writefield('Auto Update Mode')
1158 writefield('Update Check Mode')
1159 writefield_nonempty('Update Check Ignore')
1160 writefield_nonempty('Vercode Operation')
1161 writefield_nonempty('Update Check Name')
1162 writefield_nonempty('Update Check Data')
1163 if app['Current Version']:
1164 writefield('Current Version')
1165 writefield('Current Version Code')
1167 if app['No Source Since']:
1168 writefield('No Source Since')