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:
270 def __init__(self, linkres):
271 self.linkResolver = linkres
273 def endcur(self, notstates=None):
274 if notstates and self.state in notstates:
276 if self.state == self.stPARA:
278 elif self.state == self.stUL:
280 elif self.state == self.stOL:
284 self.text_html += '</p>'
285 self.state = self.stNONE
288 self.text_html += '</ul>'
289 self.state = self.stNONE
292 self.text_html += '</ol>'
293 self.state = self.stNONE
295 def formatted(self, txt, html):
298 txt = cgi.escape(txt)
300 index = txt.find("''")
302 return formatted + txt
303 formatted += txt[:index]
305 if txt.startswith("'''"):
311 self.bold = not self.bold
319 self.ital = not self.ital
322 def linkify(self, txt):
326 index = txt.find("[")
328 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
329 linkified_plain += self.formatted(txt[:index], False)
330 linkified_html += self.formatted(txt[:index], True)
332 if txt.startswith("[["):
333 index = txt.find("]]")
335 raise MetaDataException("Unterminated ]]")
337 if self.linkResolver:
338 url, urltext = self.linkResolver(url)
341 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
342 linkified_plain += urltext
343 txt = txt[index + 2:]
345 index = txt.find("]")
347 raise MetaDataException("Unterminated ]")
349 index2 = url.find(' ')
353 urltxt = url[index2 + 1:]
356 raise MetaDataException("Url title is just the URL - use [url]")
357 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
358 linkified_plain += urltxt
360 linkified_plain += ' (' + url + ')'
361 txt = txt[index + 1:]
363 def addtext(self, txt):
364 p, h = self.linkify(txt)
367 def parseline(self, line):
368 self.text_wiki += "%s\n" % line
371 elif line.startswith('* '):
372 self.endcur([self.stUL])
373 if self.state != self.stUL:
374 self.text_html += '<ul>'
375 self.state = self.stUL
376 self.text_html += '<li>'
377 self.addtext(line[1:])
378 self.text_html += '</li>'
379 elif line.startswith('# '):
380 self.endcur([self.stOL])
381 if self.state != self.stOL:
382 self.text_html += '<ol>'
383 self.state = self.stOL
384 self.text_html += '<li>'
385 self.addtext(line[1:])
386 self.text_html += '</li>'
388 self.endcur([self.stPARA])
389 if self.state == self.stNONE:
390 self.text_html += '<p>'
391 self.state = self.stPARA
392 elif self.state == self.stPARA:
393 self.text_html += ' '
400 # Parse multiple lines of description as written in a metadata file, returning
401 # a single string in wiki format. Used for the Maintainer Notes field as well,
402 # because it's the same format.
403 def description_wiki(lines):
404 ps = DescriptionFormatter(None)
411 # Parse multiple lines of description as written in a metadata file, returning
412 # a single string in HTML format.
413 def description_html(lines, linkres):
414 ps = DescriptionFormatter(linkres)
421 def parse_srclib(metadatapath):
425 # Defaults for fields that come from metadata
426 thisinfo['Repo Type'] = ''
427 thisinfo['Repo'] = ''
428 thisinfo['Subdir'] = None
429 thisinfo['Prepare'] = None
431 if not os.path.exists(metadatapath):
434 metafile = open(metadatapath, "r")
437 for line in metafile:
439 line = line.rstrip('\r\n')
440 if not line or line.startswith("#"):
444 field, value = line.split(':', 1)
446 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
448 if field == "Subdir":
449 thisinfo[field] = value.split(',')
451 thisinfo[field] = value
457 """Read all srclib metadata.
459 The information read will be accessible as metadata.srclibs, which is a
460 dictionary, keyed on srclib name, with the values each being a dictionary
461 in the same format as that returned by the parse_srclib function.
463 A MetaDataException is raised if there are any problems with the srclib
468 # They were already loaded
469 if srclibs is not None:
475 if not os.path.exists(srcdir):
478 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
479 srclibname = os.path.basename(metadatapath[:-4])
480 srclibs[srclibname] = parse_srclib(metadatapath)
483 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
484 # returned by the parse_txt_metadata function.
485 def read_metadata(xref=True):
487 # Always read the srclibs before the apps, since they can use a srlib as
488 # their source repository.
493 for basedir in ('metadata', 'tmp'):
494 if not os.path.exists(basedir):
497 # If there are multiple metadata files for a single appid, then the first
498 # file that is parsed wins over all the others, and the rest throw an
499 # exception. So the original .txt format is parsed first, at least until
500 # newer formats stabilize.
502 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
503 + glob.glob(os.path.join('metadata', '*.json'))
504 + glob.glob(os.path.join('metadata', '*.xml'))
505 + glob.glob(os.path.join('metadata', '*.yaml'))):
506 appid, appinfo = parse_metadata(apps, metadatapath)
507 check_metadata(appinfo)
508 apps[appid] = appinfo
511 # Parse all descriptions at load time, just to ensure cross-referencing
512 # errors are caught early rather than when they hit the build server.
515 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
516 raise MetaDataException("Cannot resolve app id " + appid)
518 for appid, app in apps.iteritems():
520 description_html(app['Description'], linkres)
521 except MetaDataException, e:
522 raise MetaDataException("Problem with description of " + appid +
528 # Get the type expected for a given metadata field.
529 def metafieldtype(name):
530 if name in ['Description', 'Maintainer Notes']:
532 if name in ['Categories', 'AntiFeatures']:
534 if name == 'Build Version':
538 if name == 'Use Built':
540 if name not in app_defaults:
546 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
547 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
550 if name in ['init', 'prebuild', 'build']:
552 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
558 def fill_build_defaults(build):
560 def get_build_type():
561 for t in ['maven', 'gradle', 'kivy']:
568 for flag, value in flag_defaults.iteritems():
572 build['type'] = get_build_type()
573 build['ndk_path'] = common.get_ndk_path(build['ndk'])
576 def split_list_values(s):
577 # Port legacy ';' separators
578 l = [v.strip() for v in s.replace(';', ',').split(',')]
579 return [v for v in l if v]
582 def get_default_app_info_list(apps, metadatapath=None):
583 if metadatapath is None:
586 appid = os.path.splitext(os.path.basename(metadatapath))[0]
588 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
589 % (metadatapath, appid, apps[appid]['metadatapath']))
593 thisinfo.update(app_defaults)
594 thisinfo['metadatapath'] = metadatapath
595 if appid is not None:
596 thisinfo['id'] = appid
598 # General defaults...
599 thisinfo['builds'] = []
600 thisinfo['comments'] = []
602 return appid, thisinfo
605 def sorted_builds(builds):
606 return sorted(builds, key=lambda build: int(build['vercode']))
609 def post_metadata_parse(thisinfo):
611 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
612 for k, v in thisinfo.iteritems():
613 if k not in supported_metadata:
614 raise MetaDataException("Unrecognised metadata: {0}: {1}"
616 if type(v) in (float, int):
619 # convert to the odd internal format
620 for k in ('Description', 'Maintainer Notes'):
621 if isinstance(thisinfo[k], basestring):
622 text = thisinfo[k].rstrip().lstrip()
623 thisinfo[k] = text.split('\n')
625 supported_flags = (flag_defaults.keys()
626 + ['vercode', 'version', 'versionCode', 'versionName'])
627 esc_newlines = re.compile('\\\\( |\\n)')
629 for build in thisinfo['builds']:
630 for k, v in build.items():
631 if k not in supported_flags:
632 raise MetaDataException("Unrecognised build flag: {0}={1}"
635 if k == 'versionCode':
636 build['vercode'] = str(v)
637 del build['versionCode']
638 elif k == 'versionName':
639 build['version'] = str(v)
640 del build['versionName']
641 elif type(v) in (float, int):
644 keyflagtype = flagtype(k)
645 if keyflagtype == 'list':
646 # these can be bools, strings or lists, but ultimately are lists
647 if isinstance(v, basestring):
649 elif isinstance(v, bool):
654 elif keyflagtype == 'script':
655 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
656 elif keyflagtype == 'bool':
657 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
658 if isinstance(v, basestring):
664 if not thisinfo['Description']:
665 thisinfo['Description'].append('No description available')
667 for build in thisinfo['builds']:
668 fill_build_defaults(build)
670 thisinfo['builds'] = sorted_builds(thisinfo['builds'])
673 # Parse metadata for a single application.
675 # 'metadatapath' - the filename to read. The package id for the application comes
676 # from this filename. Pass None to get a blank entry.
678 # Returns a dictionary containing all the details of the application. There are
679 # two major kinds of information in the dictionary. Keys beginning with capital
680 # letters correspond directory to identically named keys in the metadata file.
681 # Keys beginning with lower case letters are generated in one way or another,
682 # and are not found verbatim in the metadata.
684 # Known keys not originating from the metadata are:
686 # 'builds' - a list of dictionaries containing build information
687 # for each defined build
688 # 'comments' - a list of comments from the metadata file. Each is
689 # a list of the form [field, comment] where field is
690 # the name of the field it preceded in the metadata
691 # file. Where field is None, the comment goes at the
692 # end of the file. Alternatively, 'build:version' is
693 # for a comment before a particular build version.
694 # 'descriptionlines' - original lines of description as formatted in the
699 def _decode_list(data):
700 '''convert items in a list from unicode to basestring'''
703 if isinstance(item, unicode):
704 item = item.encode('utf-8')
705 elif isinstance(item, list):
706 item = _decode_list(item)
707 elif isinstance(item, dict):
708 item = _decode_dict(item)
713 def _decode_dict(data):
714 '''convert items in a dict from unicode to basestring'''
716 for key, value in data.iteritems():
717 if isinstance(key, unicode):
718 key = key.encode('utf-8')
719 if isinstance(value, unicode):
720 value = value.encode('utf-8')
721 elif isinstance(value, list):
722 value = _decode_list(value)
723 elif isinstance(value, dict):
724 value = _decode_dict(value)
729 def parse_metadata(apps, metadatapath):
730 root, ext = os.path.splitext(metadatapath)
731 metadataformat = ext[1:]
732 accepted = common.config['accepted_formats']
733 if metadataformat not in accepted:
734 logging.critical('"' + metadatapath
735 + '" is not in an accepted format, '
736 + 'convert to: ' + ', '.join(accepted))
739 if metadataformat == 'txt':
740 return parse_txt_metadata(apps, metadatapath)
741 elif metadataformat == 'json':
742 return parse_json_metadata(apps, metadatapath)
743 elif metadataformat == 'xml':
744 return parse_xml_metadata(apps, metadatapath)
745 elif metadataformat == 'yaml':
746 return parse_yaml_metadata(apps, metadatapath)
748 logging.critical('Unknown metadata format: ' + metadatapath)
752 def parse_json_metadata(apps, metadatapath):
754 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
756 # fdroid metadata is only strings and booleans, no floats or ints. And
757 # json returns unicode, and fdroidserver still uses plain python strings
758 # TODO create schema using https://pypi.python.org/pypi/jsonschema
759 jsoninfo = json.load(open(metadatapath, 'r'),
760 object_hook=_decode_dict,
761 parse_int=lambda s: s,
762 parse_float=lambda s: s)
763 thisinfo.update(jsoninfo)
764 post_metadata_parse(thisinfo)
766 return (appid, thisinfo)
769 def parse_xml_metadata(apps, metadatapath):
771 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
773 tree = ElementTree.ElementTree(file=metadatapath)
774 root = tree.getroot()
776 if root.tag != 'resources':
777 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
780 supported_metadata = app_defaults.keys()
782 if child.tag != 'builds':
783 # builds does not have name="" attrib
784 name = child.attrib['name']
785 if name not in supported_metadata:
786 raise MetaDataException("Unrecognised metadata: <"
787 + child.tag + ' name="' + name + '">'
789 + "</" + child.tag + '>')
791 if child.tag == 'string':
792 thisinfo[name] = child.text
793 elif child.tag == 'string-array':
796 items.append(item.text)
797 thisinfo[name] = items
798 elif child.tag == 'builds':
803 builddict[key.tag] = key.text
804 builds.append(builddict)
805 thisinfo['builds'] = builds
807 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
808 if not isinstance(thisinfo['Requires Root'], bool):
809 if thisinfo['Requires Root'] == 'true':
810 thisinfo['Requires Root'] = True
812 thisinfo['Requires Root'] = False
814 post_metadata_parse(thisinfo)
816 return (appid, thisinfo)
819 def parse_yaml_metadata(apps, metadatapath):
821 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
823 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
824 thisinfo.update(yamlinfo)
825 post_metadata_parse(thisinfo)
827 return (appid, thisinfo)
830 def parse_txt_metadata(apps, metadatapath):
834 def add_buildflag(p, thisbuild):
836 raise MetaDataException("Empty build flag at {1}"
837 .format(buildlines[0], linedesc))
840 raise MetaDataException("Invalid build flag at {0} in {1}"
841 .format(buildlines[0], linedesc))
844 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
845 .format(pk, thisbuild['version'], linedesc))
848 if pk not in flag_defaults:
849 raise MetaDataException("Unrecognised build flag at {0} in {1}"
850 .format(p, linedesc))
853 pv = split_list_values(pv)
855 if len(pv) == 1 and pv[0] in ['main', 'yes']:
858 elif t == 'string' or t == 'script':
865 logging.debug("...ignoring bool flag %s" % p)
868 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
871 def parse_buildline(lines):
872 value = "".join(lines)
873 parts = [p.replace("\\,", ",")
874 for p in re.split(r"(?<!\\),", value)]
876 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
878 thisbuild['origlines'] = lines
879 thisbuild['version'] = parts[0]
880 thisbuild['vercode'] = parts[1]
881 if parts[2].startswith('!'):
882 # For backwards compatibility, handle old-style disabling,
883 # including attempting to extract the commit from the message
884 thisbuild['disable'] = parts[2][1:]
885 commit = 'unknown - see disabled'
886 index = parts[2].rfind('at ')
888 commit = parts[2][index + 3:]
889 if commit.endswith(')'):
891 thisbuild['commit'] = commit
893 thisbuild['commit'] = parts[2]
895 add_buildflag(p, thisbuild)
899 def add_comments(key):
902 for comment in curcomments:
903 thisinfo['comments'].append([key, comment])
906 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
907 metafile = open(metadatapath, "r")
916 for line in metafile:
918 linedesc = "%s:%d" % (metafile.name, c)
919 line = line.rstrip('\r\n')
921 if not any(line.startswith(s) for s in (' ', '\t')):
922 commit = curbuild['commit'] if 'commit' in curbuild else None
923 if not commit and 'disable' not in curbuild:
924 raise MetaDataException("No commit specified for {0} in {1}"
925 .format(curbuild['version'], linedesc))
927 thisinfo['builds'].append(curbuild)
928 add_comments('build:' + curbuild['vercode'])
931 if line.endswith('\\'):
932 buildlines.append(line[:-1].lstrip())
934 buildlines.append(line.lstrip())
935 bl = ''.join(buildlines)
936 add_buildflag(bl, curbuild)
942 if line.startswith("#"):
943 curcomments.append(line)
946 field, value = line.split(':', 1)
948 raise MetaDataException("Invalid metadata in " + linedesc)
949 if field != field.strip() or value != value.strip():
950 raise MetaDataException("Extra spacing found in " + linedesc)
952 # Translate obsolete fields...
953 if field == 'Market Version':
954 field = 'Current Version'
955 if field == 'Market Version Code':
956 field = 'Current Version Code'
958 fieldtype = metafieldtype(field)
959 if fieldtype not in ['build', 'buildv2']:
961 if fieldtype == 'multiline':
965 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
966 elif fieldtype == 'string':
967 thisinfo[field] = value
968 elif fieldtype == 'list':
969 thisinfo[field] = split_list_values(value)
970 elif fieldtype == 'build':
971 if value.endswith("\\"):
973 buildlines = [value[:-1]]
975 curbuild = parse_buildline([value])
976 thisinfo['builds'].append(curbuild)
977 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
978 elif fieldtype == 'buildv2':
980 vv = value.split(',')
982 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
983 .format(value, linedesc))
984 curbuild['version'] = vv[0]
985 curbuild['vercode'] = vv[1]
986 if curbuild['vercode'] in vc_seen:
987 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
988 curbuild['vercode'], linedesc))
989 vc_seen[curbuild['vercode']] = True
992 elif fieldtype == 'obsolete':
993 pass # Just throw it away!
995 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
996 elif mode == 1: # Multiline field
1000 thisinfo[field].append(line)
1001 elif mode == 2: # Line continuation mode in Build Version
1002 if line.endswith("\\"):
1003 buildlines.append(line[:-1])
1005 buildlines.append(line)
1006 curbuild = parse_buildline(buildlines)
1007 thisinfo['builds'].append(curbuild)
1008 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1012 # Mode at end of file should always be 0...
1014 raise MetaDataException(field + " not terminated in " + metafile.name)
1016 raise MetaDataException("Unterminated continuation in " + metafile.name)
1018 raise MetaDataException("Unterminated build in " + metafile.name)
1020 post_metadata_parse(thisinfo)
1022 return (appid, thisinfo)
1025 # Write a metadata file.
1027 # 'dest' - The path to the output file
1028 # 'app' - The app data
1029 def write_metadata(dest, app):
1031 def writecomments(key):
1033 for pf, comment in app['comments']:
1035 mf.write("%s\n" % comment)
1038 logging.debug("...writing comments for " + (key or 'EOF'))
1040 def writefield(field, value=None):
1041 writecomments(field)
1044 t = metafieldtype(field)
1046 value = ','.join(value)
1047 mf.write("%s:%s\n" % (field, value))
1049 def writefield_nonempty(field, value=None):
1053 writefield(field, value)
1055 mf = open(dest, 'w')
1056 writefield_nonempty('Disabled')
1057 if app['AntiFeatures']:
1058 writefield('AntiFeatures')
1059 writefield_nonempty('Provides')
1060 writefield('Categories')
1061 writefield('License')
1062 writefield('Web Site')
1063 writefield('Source Code')
1064 writefield('Issue Tracker')
1065 writefield_nonempty('Changelog')
1066 writefield_nonempty('Donate')
1067 writefield_nonempty('FlattrID')
1068 writefield_nonempty('Bitcoin')
1069 writefield_nonempty('Litecoin')
1070 writefield_nonempty('Dogecoin')
1072 writefield_nonempty('Name')
1073 writefield_nonempty('Auto Name')
1074 writefield('Summary')
1075 writefield('Description', '')
1076 for line in app['Description']:
1077 mf.write("%s\n" % line)
1080 if app['Requires Root']:
1081 writefield('Requires Root', 'yes')
1083 if app['Repo Type']:
1084 writefield('Repo Type')
1087 writefield('Binaries')
1089 for build in sorted_builds(app['builds']):
1091 if build['version'] == "Ignore":
1094 writecomments('build:' + build['vercode'])
1095 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1097 def write_builditem(key, value):
1099 if key in ['version', 'vercode']:
1102 if value == flag_defaults[key]:
1107 logging.debug("...writing {0} : {1}".format(key, value))
1108 outline = ' %s=' % key
1115 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1117 outline += ','.join(value) if type(value) == list else value
1122 for flag in flag_defaults:
1125 write_builditem(flag, value)
1128 if app['Maintainer Notes']:
1129 writefield('Maintainer Notes', '')
1130 for line in app['Maintainer Notes']:
1131 mf.write("%s\n" % line)
1135 writefield_nonempty('Archive Policy')
1136 writefield('Auto Update Mode')
1137 writefield('Update Check Mode')
1138 writefield_nonempty('Update Check Ignore')
1139 writefield_nonempty('Vercode Operation')
1140 writefield_nonempty('Update Check Name')
1141 writefield_nonempty('Update Check Data')
1142 if app['Current Version']:
1143 writefield('Current Version')
1144 writefield('Current Version Code')
1146 if app['No Source Since']:
1147 writefield('No Source Since')