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/>.
29 # use libyaml if it is available
31 from yaml import CLoader
34 from yaml import Loader
37 # use the C implementation when available
38 import xml.etree.cElementTree as ElementTree
40 from collections import OrderedDict
47 class MetaDataException(Exception):
49 def __init__(self, value):
55 # In the order in which they are laid out on files
56 app_defaults = OrderedDict([
60 ('Categories', ['None']),
61 ('License', 'Unknown'),
64 ('Issue Tracker', ''),
75 ('Requires Root', False),
79 ('Maintainer Notes', []),
80 ('Archive Policy', None),
81 ('Auto Update Mode', 'None'),
82 ('Update Check Mode', 'None'),
83 ('Update Check Ignore', None),
84 ('Vercode Operation', None),
85 ('Update Check Name', None),
86 ('Update Check Data', None),
87 ('Current Version', ''),
88 ('Current Version Code', '0'),
89 ('No Source Since', ''),
93 # In the order in which they are laid out on files
94 # Sorted by their action and their place in the build timeline
95 # These variables can have varying datatypes. For example, anything with
96 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
97 flag_defaults = OrderedDict([
101 ('submodules', False),
109 ('oldsdkloc', False),
111 ('forceversion', False),
112 ('forcevercode', False),
116 ('update', ['auto']),
122 ('ndk', 'r10e'), # defaults to latest
125 ('antcommands', None),
130 # Designates a metadata field type and checks that it matches
132 # 'name' - The long name of the field type
133 # 'matching' - List of possible values or regex expression
134 # 'sep' - Separator to use if value may be a list
135 # 'fields' - Metadata fields (Field:Value) of this type
136 # 'attrs' - Build attributes (attr=value) of this type
138 class FieldValidator():
140 def __init__(self, name, matching, sep, fields, attrs):
142 self.matching = matching
143 if type(matching) is str:
144 self.compiled = re.compile(matching)
149 def _assert_regex(self, values, appid):
151 if not self.compiled.match(v):
152 raise MetaDataException("'%s' is not a valid %s in %s. "
153 % (v, self.name, appid) +
154 "Regex pattern: %s" % (self.matching))
156 def _assert_list(self, values, appid):
158 if v not in self.matching:
159 raise MetaDataException("'%s' is not a valid %s in %s. "
160 % (v, self.name, appid) +
161 "Possible values: %s" % (", ".join(self.matching)))
163 def check(self, value, appid):
164 if type(value) is not str or not value:
166 if self.sep is not None:
167 values = value.split(self.sep)
170 if type(self.matching) is list:
171 self._assert_list(values, appid)
173 self._assert_regex(values, appid)
176 # Generic value types
178 FieldValidator("Integer",
179 r'^[1-9][0-9]*$', None,
183 FieldValidator("Hexadecimal",
184 r'^[0-9a-f]+$', None,
188 FieldValidator("HTTP link",
189 r'^http[s]?://', None,
190 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
192 FieldValidator("Bitcoin address",
193 r'^[a-zA-Z0-9]{27,34}$', None,
197 FieldValidator("Litecoin address",
198 r'^L[a-zA-Z0-9]{33}$', None,
202 FieldValidator("Dogecoin address",
203 r'^D[a-zA-Z0-9]{33}$', None,
207 FieldValidator("bool",
208 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
210 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
213 FieldValidator("Repo Type",
214 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
218 FieldValidator("Binaries",
219 r'^http[s]?://', None,
223 FieldValidator("Archive Policy",
224 r'^[0-9]+ versions$', None,
228 FieldValidator("Anti-Feature",
229 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
233 FieldValidator("Auto Update Mode",
234 r"^(Version .+|None)$", None,
235 ["Auto Update Mode"],
238 FieldValidator("Update Check Mode",
239 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
240 ["Update Check Mode"],
245 # Check an app's metadata information for integrity errors
246 def check_metadata(info):
248 for field in v.fields:
249 v.check(info[field], info['id'])
250 for build in info['builds']:
252 v.check(build[attr], info['id'])
255 # Formatter for descriptions. Create an instance, and call parseline() with
256 # each line of the description source from the metadata. At the end, call
257 # end() and then text_wiki and text_html will contain the result.
258 class DescriptionFormatter:
271 def __init__(self, linkres):
272 self.linkResolver = linkres
274 def endcur(self, notstates=None):
275 if notstates and self.state in notstates:
277 if self.state == self.stPARA:
279 elif self.state == self.stUL:
281 elif self.state == self.stOL:
285 self.text_html += '</p>'
286 self.state = self.stNONE
289 self.text_html += '</ul>'
290 self.state = self.stNONE
293 self.text_html += '</ol>'
294 self.state = self.stNONE
296 def formatted(self, txt, html):
299 txt = cgi.escape(txt)
301 index = txt.find("''")
303 return formatted + txt
304 formatted += txt[:index]
306 if txt.startswith("'''"):
312 self.bold = not self.bold
320 self.ital = not self.ital
323 def linkify(self, txt):
327 index = txt.find("[")
329 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
330 linkified_plain += self.formatted(txt[:index], False)
331 linkified_html += self.formatted(txt[:index], True)
333 if txt.startswith("[["):
334 index = txt.find("]]")
336 raise MetaDataException("Unterminated ]]")
338 if self.linkResolver:
339 url, urltext = self.linkResolver(url)
342 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
343 linkified_plain += urltext
344 txt = txt[index + 2:]
346 index = txt.find("]")
348 raise MetaDataException("Unterminated ]")
350 index2 = url.find(' ')
354 urltxt = url[index2 + 1:]
357 raise MetaDataException("Url title is just the URL - use [url]")
358 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
359 linkified_plain += urltxt
361 linkified_plain += ' (' + url + ')'
362 txt = txt[index + 1:]
364 def addtext(self, txt):
365 p, h = self.linkify(txt)
368 def parseline(self, line):
369 self.text_wiki += "%s\n" % line
370 self.text_txt += "%s\n" % line
373 elif line.startswith('* '):
374 self.endcur([self.stUL])
375 if self.state != self.stUL:
376 self.text_html += '<ul>'
377 self.state = self.stUL
378 self.text_html += '<li>'
379 self.addtext(line[1:])
380 self.text_html += '</li>'
381 elif line.startswith('# '):
382 self.endcur([self.stOL])
383 if self.state != self.stOL:
384 self.text_html += '<ol>'
385 self.state = self.stOL
386 self.text_html += '<li>'
387 self.addtext(line[1:])
388 self.text_html += '</li>'
390 self.endcur([self.stPARA])
391 if self.state == self.stNONE:
392 self.text_html += '<p>'
393 self.state = self.stPARA
394 elif self.state == self.stPARA:
395 self.text_html += ' '
402 # Parse multiple lines of description as written in a metadata file, returning
403 # a single string in text format and wrapped to 80 columns.
404 def description_txt(lines):
405 ps = DescriptionFormatter(None)
412 # Parse multiple lines of description as written in a metadata file, returning
413 # a single string in wiki format. Used for the Maintainer Notes field as well,
414 # because it's the same format.
415 def description_wiki(lines):
416 ps = DescriptionFormatter(None)
423 # Parse multiple lines of description as written in a metadata file, returning
424 # a single string in HTML format.
425 def description_html(lines, linkres):
426 ps = DescriptionFormatter(linkres)
433 def parse_srclib(metadatapath):
437 # Defaults for fields that come from metadata
438 thisinfo['Repo Type'] = ''
439 thisinfo['Repo'] = ''
440 thisinfo['Subdir'] = None
441 thisinfo['Prepare'] = None
443 if not os.path.exists(metadatapath):
446 metafile = open(metadatapath, "r")
449 for line in metafile:
451 line = line.rstrip('\r\n')
452 if not line or line.startswith("#"):
456 field, value = line.split(':', 1)
458 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
460 if field == "Subdir":
461 thisinfo[field] = value.split(',')
463 thisinfo[field] = value
469 """Read all srclib metadata.
471 The information read will be accessible as metadata.srclibs, which is a
472 dictionary, keyed on srclib name, with the values each being a dictionary
473 in the same format as that returned by the parse_srclib function.
475 A MetaDataException is raised if there are any problems with the srclib
480 # They were already loaded
481 if srclibs is not None:
487 if not os.path.exists(srcdir):
490 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
491 srclibname = os.path.basename(metadatapath[:-4])
492 srclibs[srclibname] = parse_srclib(metadatapath)
495 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
496 # returned by the parse_txt_metadata function.
497 def read_metadata(xref=True):
499 # Always read the srclibs before the apps, since they can use a srlib as
500 # their source repository.
505 for basedir in ('metadata', 'tmp'):
506 if not os.path.exists(basedir):
509 # If there are multiple metadata files for a single appid, then the first
510 # file that is parsed wins over all the others, and the rest throw an
511 # exception. So the original .txt format is parsed first, at least until
512 # newer formats stabilize.
514 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
515 + glob.glob(os.path.join('metadata', '*.json'))
516 + glob.glob(os.path.join('metadata', '*.xml'))
517 + glob.glob(os.path.join('metadata', '*.yaml'))):
518 appid, appinfo = parse_metadata(apps, metadatapath)
519 check_metadata(appinfo)
520 apps[appid] = appinfo
523 # Parse all descriptions at load time, just to ensure cross-referencing
524 # errors are caught early rather than when they hit the build server.
527 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
528 raise MetaDataException("Cannot resolve app id " + appid)
530 for appid, app in apps.iteritems():
532 description_html(app['Description'], linkres)
533 except MetaDataException, e:
534 raise MetaDataException("Problem with description of " + appid +
540 # Get the type expected for a given metadata field.
541 def metafieldtype(name):
542 if name in ['Description', 'Maintainer Notes']:
544 if name in ['Categories', 'AntiFeatures']:
546 if name == 'Build Version':
550 if name == 'Use Built':
552 if name not in app_defaults:
558 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
559 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
562 if name in ['init', 'prebuild', 'build']:
564 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
570 def fill_build_defaults(build):
572 def get_build_type():
573 for t in ['maven', 'gradle', 'kivy']:
580 for flag, value in flag_defaults.iteritems():
584 build['type'] = get_build_type()
585 build['ndk_path'] = common.get_ndk_path(build['ndk'])
588 def split_list_values(s):
589 # Port legacy ';' separators
590 l = [v.strip() for v in s.replace(';', ',').split(',')]
591 return [v for v in l if v]
594 def get_default_app_info_list(apps, metadatapath=None):
595 if metadatapath is None:
598 appid = os.path.splitext(os.path.basename(metadatapath))[0]
600 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
601 % (metadatapath, appid, apps[appid]['metadatapath']))
605 thisinfo.update(app_defaults)
606 thisinfo['metadatapath'] = metadatapath
607 if appid is not None:
608 thisinfo['id'] = appid
610 # General defaults...
611 thisinfo['builds'] = []
612 thisinfo['comments'] = []
614 return appid, thisinfo
617 def sorted_builds(builds):
618 return sorted(builds, key=lambda build: int(build['vercode']))
621 def post_metadata_parse(thisinfo):
623 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
624 for k, v in thisinfo.iteritems():
625 if k not in supported_metadata:
626 raise MetaDataException("Unrecognised metadata: {0}: {1}"
628 if type(v) in (float, int):
631 # convert to the odd internal format
632 for k in ('Description', 'Maintainer Notes'):
633 if isinstance(thisinfo[k], basestring):
634 text = thisinfo[k].rstrip().lstrip()
635 thisinfo[k] = text.split('\n')
637 supported_flags = (flag_defaults.keys()
638 + ['vercode', 'version', 'versionCode', 'versionName'])
639 esc_newlines = re.compile('\\\\( |\\n)')
641 for build in thisinfo['builds']:
642 for k, v in build.items():
643 if k not in supported_flags:
644 raise MetaDataException("Unrecognised build flag: {0}={1}"
647 if k == 'versionCode':
648 build['vercode'] = str(v)
649 del build['versionCode']
650 elif k == 'versionName':
651 build['version'] = str(v)
652 del build['versionName']
653 elif type(v) in (float, int):
656 keyflagtype = flagtype(k)
657 if keyflagtype == 'list':
658 # these can be bools, strings or lists, but ultimately are lists
659 if isinstance(v, basestring):
661 elif isinstance(v, bool):
666 elif keyflagtype == 'script':
667 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
668 elif keyflagtype == 'bool':
669 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
670 if isinstance(v, basestring):
676 if not thisinfo['Description']:
677 thisinfo['Description'].append('No description available')
679 for build in thisinfo['builds']:
680 fill_build_defaults(build)
682 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
685 # Parse metadata for a single application.
687 # 'metadatapath' - the filename to read. The package id for the application comes
688 # from this filename. Pass None to get a blank entry.
690 # Returns a dictionary containing all the details of the application. There are
691 # two major kinds of information in the dictionary. Keys beginning with capital
692 # letters correspond directory to identically named keys in the metadata file.
693 # Keys beginning with lower case letters are generated in one way or another,
694 # and are not found verbatim in the metadata.
696 # Known keys not originating from the metadata are:
698 # 'builds' - a list of dictionaries containing build information
699 # for each defined build
700 # 'comments' - a list of comments from the metadata file. Each is
701 # a list of the form [field, comment] where field is
702 # the name of the field it preceded in the metadata
703 # file. Where field is None, the comment goes at the
704 # end of the file. Alternatively, 'build:version' is
705 # for a comment before a particular build version.
706 # 'descriptionlines' - original lines of description as formatted in the
711 def _decode_list(data):
712 '''convert items in a list from unicode to basestring'''
715 if isinstance(item, unicode):
716 item = item.encode('utf-8')
717 elif isinstance(item, list):
718 item = _decode_list(item)
719 elif isinstance(item, dict):
720 item = _decode_dict(item)
725 def _decode_dict(data):
726 '''convert items in a dict from unicode to basestring'''
728 for key, value in data.iteritems():
729 if isinstance(key, unicode):
730 key = key.encode('utf-8')
731 if isinstance(value, unicode):
732 value = value.encode('utf-8')
733 elif isinstance(value, list):
734 value = _decode_list(value)
735 elif isinstance(value, dict):
736 value = _decode_dict(value)
741 def parse_metadata(apps, metadatapath):
742 root, ext = os.path.splitext(metadatapath)
743 metadataformat = ext[1:]
744 accepted = common.config['accepted_formats']
745 if metadataformat not in accepted:
746 logging.critical('"' + metadatapath
747 + '" is not in an accepted format, '
748 + 'convert to: ' + ', '.join(accepted))
751 if metadataformat == 'txt':
752 return parse_txt_metadata(apps, metadatapath)
753 elif metadataformat == 'json':
754 return parse_json_metadata(apps, metadatapath)
755 elif metadataformat == 'xml':
756 return parse_xml_metadata(apps, metadatapath)
757 elif metadataformat == 'yaml':
758 return parse_yaml_metadata(apps, metadatapath)
760 logging.critical('Unknown metadata format: ' + metadatapath)
764 def parse_json_metadata(apps, metadatapath):
766 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
768 # fdroid metadata is only strings and booleans, no floats or ints. And
769 # json returns unicode, and fdroidserver still uses plain python strings
770 # TODO create schema using https://pypi.python.org/pypi/jsonschema
771 jsoninfo = json.load(open(metadatapath, 'r'),
772 object_hook=_decode_dict,
773 parse_int=lambda s: s,
774 parse_float=lambda s: s)
775 thisinfo.update(jsoninfo)
776 post_metadata_parse(thisinfo)
778 return (appid, thisinfo)
781 def parse_xml_metadata(apps, metadatapath):
783 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
785 tree = ElementTree.ElementTree(file=metadatapath)
786 root = tree.getroot()
788 if root.tag != 'resources':
789 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
792 supported_metadata = app_defaults.keys()
794 if child.tag != 'builds':
795 # builds does not have name="" attrib
796 name = child.attrib['name']
797 if name not in supported_metadata:
798 raise MetaDataException("Unrecognised metadata: <"
799 + child.tag + ' name="' + name + '">'
801 + "</" + child.tag + '>')
803 if child.tag == 'string':
804 thisinfo[name] = child.text
805 elif child.tag == 'string-array':
808 items.append(item.text)
809 thisinfo[name] = items
810 elif child.tag == 'builds':
815 builddict[key.tag] = key.text
816 builds.append(builddict)
817 thisinfo['builds'] = builds
819 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
820 if not isinstance(thisinfo['Requires Root'], bool):
821 if thisinfo['Requires Root'] == 'true':
822 thisinfo['Requires Root'] = True
824 thisinfo['Requires Root'] = False
826 post_metadata_parse(thisinfo)
828 return (appid, thisinfo)
831 def parse_yaml_metadata(apps, metadatapath):
833 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
835 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
836 thisinfo.update(yamlinfo)
837 post_metadata_parse(thisinfo)
839 return (appid, thisinfo)
842 def parse_txt_metadata(apps, metadatapath):
846 def add_buildflag(p, thisbuild):
848 raise MetaDataException("Empty build flag at {1}"
849 .format(buildlines[0], linedesc))
852 raise MetaDataException("Invalid build flag at {0} in {1}"
853 .format(buildlines[0], linedesc))
856 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
857 .format(pk, thisbuild['version'], linedesc))
860 if pk not in flag_defaults:
861 raise MetaDataException("Unrecognised build flag at {0} in {1}"
862 .format(p, linedesc))
865 pv = split_list_values(pv)
867 if len(pv) == 1 and pv[0] in ['main', 'yes']:
870 elif t == 'string' or t == 'script':
877 logging.debug("...ignoring bool flag %s" % p)
880 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
883 def parse_buildline(lines):
884 value = "".join(lines)
885 parts = [p.replace("\\,", ",")
886 for p in re.split(r"(?<!\\),", value)]
888 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
890 thisbuild['origlines'] = lines
891 thisbuild['version'] = parts[0]
892 thisbuild['vercode'] = parts[1]
893 if parts[2].startswith('!'):
894 # For backwards compatibility, handle old-style disabling,
895 # including attempting to extract the commit from the message
896 thisbuild['disable'] = parts[2][1:]
897 commit = 'unknown - see disabled'
898 index = parts[2].rfind('at ')
900 commit = parts[2][index + 3:]
901 if commit.endswith(')'):
903 thisbuild['commit'] = commit
905 thisbuild['commit'] = parts[2]
907 add_buildflag(p, thisbuild)
911 def add_comments(key):
914 for comment in curcomments:
915 thisinfo['comments'].append([key, comment])
918 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
919 metafile = open(metadatapath, "r")
928 for line in metafile:
930 linedesc = "%s:%d" % (metafile.name, c)
931 line = line.rstrip('\r\n')
933 if not any(line.startswith(s) for s in (' ', '\t')):
934 commit = curbuild['commit'] if 'commit' in curbuild else None
935 if not commit and 'disable' not in curbuild:
936 raise MetaDataException("No commit specified for {0} in {1}"
937 .format(curbuild['version'], linedesc))
939 thisinfo['builds'].append(curbuild)
940 add_comments('build:' + curbuild['vercode'])
943 if line.endswith('\\'):
944 buildlines.append(line[:-1].lstrip())
946 buildlines.append(line.lstrip())
947 bl = ''.join(buildlines)
948 add_buildflag(bl, curbuild)
954 if line.startswith("#"):
955 curcomments.append(line)
958 field, value = line.split(':', 1)
960 raise MetaDataException("Invalid metadata in " + linedesc)
961 if field != field.strip() or value != value.strip():
962 raise MetaDataException("Extra spacing found in " + linedesc)
964 # Translate obsolete fields...
965 if field == 'Market Version':
966 field = 'Current Version'
967 if field == 'Market Version Code':
968 field = 'Current Version Code'
970 fieldtype = metafieldtype(field)
971 if fieldtype not in ['build', 'buildv2']:
973 if fieldtype == 'multiline':
977 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
978 elif fieldtype == 'string':
979 thisinfo[field] = value
980 elif fieldtype == 'list':
981 thisinfo[field] = split_list_values(value)
982 elif fieldtype == 'build':
983 if value.endswith("\\"):
985 buildlines = [value[:-1]]
987 curbuild = parse_buildline([value])
988 thisinfo['builds'].append(curbuild)
989 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
990 elif fieldtype == 'buildv2':
992 vv = value.split(',')
994 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
995 .format(value, linedesc))
996 curbuild['version'] = vv[0]
997 curbuild['vercode'] = vv[1]
998 if curbuild['vercode'] in vc_seen:
999 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1000 curbuild['vercode'], linedesc))
1001 vc_seen[curbuild['vercode']] = True
1004 elif fieldtype == 'obsolete':
1005 pass # Just throw it away!
1007 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
1008 elif mode == 1: # Multiline field
1012 thisinfo[field].append(line)
1013 elif mode == 2: # Line continuation mode in Build Version
1014 if line.endswith("\\"):
1015 buildlines.append(line[:-1])
1017 buildlines.append(line)
1018 curbuild = parse_buildline(buildlines)
1019 thisinfo['builds'].append(curbuild)
1020 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1024 # Mode at end of file should always be 0...
1026 raise MetaDataException(field + " not terminated in " + metafile.name)
1028 raise MetaDataException("Unterminated continuation in " + metafile.name)
1030 raise MetaDataException("Unterminated build in " + metafile.name)
1032 post_metadata_parse(thisinfo)
1034 return (appid, thisinfo)
1037 # Write a metadata file.
1039 # 'dest' - The path to the output file
1040 # 'app' - The app data
1041 def write_metadata(dest, app):
1043 def writecomments(key):
1045 for pf, comment in app['comments']:
1047 mf.write("%s\n" % comment)
1050 logging.debug("...writing comments for " + (key or 'EOF'))
1052 def writefield(field, value=None):
1053 writecomments(field)
1056 t = metafieldtype(field)
1058 value = ','.join(value)
1059 elif t == 'multiline':
1060 if type(value) == list:
1061 value = '\n' + '\n'.join(value) + '\n.'
1063 value = '\n' + value + '.'
1064 mf.write("%s:%s\n" % (field, value))
1066 def writefield_nonempty(field, value=None):
1070 writefield(field, value)
1072 mf = open(dest, 'w')
1073 writefield_nonempty('Disabled')
1074 if app['AntiFeatures']:
1075 writefield('AntiFeatures')
1076 writefield_nonempty('Provides')
1077 writefield('Categories')
1078 writefield('License')
1079 writefield('Web Site')
1080 writefield('Source Code')
1081 writefield('Issue Tracker')
1082 writefield_nonempty('Changelog')
1083 writefield_nonempty('Donate')
1084 writefield_nonempty('FlattrID')
1085 writefield_nonempty('Bitcoin')
1086 writefield_nonempty('Litecoin')
1087 writefield_nonempty('Dogecoin')
1089 writefield_nonempty('Name')
1090 writefield_nonempty('Auto Name')
1091 writefield('Summary')
1092 writefield('Description', description_txt(app['Description']))
1094 if app['Requires Root']:
1095 writefield('Requires Root', 'yes')
1097 if app['Repo Type']:
1098 writefield('Repo Type')
1101 writefield('Binaries')
1103 for build in sorted_builds(app['builds']):
1105 if build['version'] == "Ignore":
1108 writecomments('build:' + build['vercode'])
1109 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1111 def write_builditem(key, value):
1113 if key in ['version', 'vercode']:
1116 if value == flag_defaults[key]:
1121 logging.debug("...writing {0} : {1}".format(key, value))
1122 outline = ' %s=' % key
1129 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1131 outline += ','.join(value) if type(value) == list else value
1136 for flag in flag_defaults:
1139 write_builditem(flag, value)
1142 if app['Maintainer Notes']:
1143 writefield('Maintainer Notes', app['Maintainer Notes'])
1146 writefield_nonempty('Archive Policy')
1147 writefield('Auto Update Mode')
1148 writefield('Update Check Mode')
1149 writefield_nonempty('Update Check Ignore')
1150 writefield_nonempty('Vercode Operation')
1151 writefield_nonempty('Update Check Name')
1152 writefield_nonempty('Update Check Data')
1153 if app['Current Version']:
1154 writefield('Current Version')
1155 writefield('Current Version Code')
1157 if app['No Source Since']:
1158 writefield('No Source Since')