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(metafile):
424 if metafile and not isinstance(metafile, file):
425 metafile = open(metafile, "r")
427 # Defaults for fields that come from metadata
428 thisinfo['Repo Type'] = ''
429 thisinfo['Repo'] = ''
430 thisinfo['Subdir'] = None
431 thisinfo['Prepare'] = None
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 metafile in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
479 srclibname = os.path.basename(metafile[:-4])
480 srclibs[srclibname] = parse_srclib(metafile)
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 for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
498 appid, appinfo = parse_txt_metadata(metafile)
499 check_metadata(appinfo)
500 apps[appid] = appinfo
502 for metafile in sorted(glob.glob(os.path.join('metadata', '*.json'))):
503 appid, appinfo = parse_json_metadata(metafile)
504 check_metadata(appinfo)
505 apps[appid] = appinfo
507 for metafile in sorted(glob.glob(os.path.join('metadata', '*.xml'))):
508 appid, appinfo = parse_xml_metadata(metafile)
509 check_metadata(appinfo)
510 apps[appid] = appinfo
512 for metafile in sorted(glob.glob(os.path.join('metadata', '*.yaml'))):
513 appid, appinfo = parse_yaml_metadata(metafile)
514 check_metadata(appinfo)
515 apps[appid] = appinfo
518 # Parse all descriptions at load time, just to ensure cross-referencing
519 # errors are caught early rather than when they hit the build server.
522 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
523 raise MetaDataException("Cannot resolve app id " + appid)
525 for appid, app in apps.iteritems():
527 description_html(app['Description'], linkres)
528 except MetaDataException, e:
529 raise MetaDataException("Problem with description of " + appid +
535 # Get the type expected for a given metadata field.
536 def metafieldtype(name):
537 if name in ['Description', 'Maintainer Notes']:
539 if name in ['Categories', 'AntiFeatures']:
541 if name == 'Build Version':
545 if name == 'Use Built':
547 if name not in app_defaults:
553 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
554 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
557 if name in ['init', 'prebuild', 'build']:
559 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
565 def fill_build_defaults(build):
567 def get_build_type():
568 for t in ['maven', 'gradle', 'kivy']:
575 for flag, value in flag_defaults.iteritems():
579 build['type'] = get_build_type()
580 build['ndk_path'] = common.get_ndk_path(build['ndk'])
583 def split_list_values(s):
584 # Port legacy ';' separators
585 l = [v.strip() for v in s.replace(';', ',').split(',')]
586 return [v for v in l if v]
589 def get_default_app_info_list(appid=None):
591 thisinfo.update(app_defaults)
592 if appid is not None:
593 thisinfo['id'] = appid
595 # General defaults...
596 thisinfo['builds'] = []
597 thisinfo['comments'] = []
602 def post_metadata_parse(thisinfo):
604 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id']
605 for k, v in thisinfo.iteritems():
606 if k not in supported_metadata:
607 raise MetaDataException("Unrecognised metadata: {0}: {1}"
609 if type(v) in (float, int):
612 # convert to the odd internal format
613 for k in ('Description', 'Maintainer Notes'):
614 if isinstance(thisinfo[k], basestring):
615 text = thisinfo[k].rstrip().lstrip()
616 thisinfo[k] = text.split('\n')
618 supported_flags = (flag_defaults.keys()
619 + ['vercode', 'version', 'versionCode', 'versionName'])
620 esc_newlines = re.compile('\\\\( |\\n)')
622 for build in thisinfo['builds']:
623 for k, v in build.items():
624 if k not in supported_flags:
625 raise MetaDataException("Unrecognised build flag: {0}={1}"
628 if k == 'versionCode':
629 build['vercode'] = str(v)
630 del build['versionCode']
631 elif k == 'versionName':
632 build['version'] = str(v)
633 del build['versionName']
634 elif type(v) in (float, int):
637 keyflagtype = flagtype(k)
638 if keyflagtype == 'list':
639 # these can be bools, strings or lists, but ultimately are lists
640 if isinstance(v, basestring):
642 elif isinstance(v, bool):
647 elif keyflagtype == 'script':
648 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
649 elif keyflagtype == 'bool':
650 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
651 if isinstance(v, basestring):
657 if not thisinfo['Description']:
658 thisinfo['Description'].append('No description available')
660 for build in thisinfo['builds']:
661 fill_build_defaults(build)
663 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
666 # Parse metadata for a single application.
668 # 'metafile' - the filename to read. The package id for the application comes
669 # from this filename. Pass None to get a blank entry.
671 # Returns a dictionary containing all the details of the application. There are
672 # two major kinds of information in the dictionary. Keys beginning with capital
673 # letters correspond directory to identically named keys in the metadata file.
674 # Keys beginning with lower case letters are generated in one way or another,
675 # and are not found verbatim in the metadata.
677 # Known keys not originating from the metadata are:
679 # 'builds' - a list of dictionaries containing build information
680 # for each defined build
681 # 'comments' - a list of comments from the metadata file. Each is
682 # a list of the form [field, comment] where field is
683 # the name of the field it preceded in the metadata
684 # file. Where field is None, the comment goes at the
685 # end of the file. Alternatively, 'build:version' is
686 # for a comment before a particular build version.
687 # 'descriptionlines' - original lines of description as formatted in the
692 def _decode_list(data):
693 '''convert items in a list from unicode to basestring'''
696 if isinstance(item, unicode):
697 item = item.encode('utf-8')
698 elif isinstance(item, list):
699 item = _decode_list(item)
700 elif isinstance(item, dict):
701 item = _decode_dict(item)
706 def _decode_dict(data):
707 '''convert items in a dict from unicode to basestring'''
709 for key, value in data.iteritems():
710 if isinstance(key, unicode):
711 key = key.encode('utf-8')
712 if isinstance(value, unicode):
713 value = value.encode('utf-8')
714 elif isinstance(value, list):
715 value = _decode_list(value)
716 elif isinstance(value, dict):
717 value = _decode_dict(value)
722 def parse_json_metadata(metafile):
724 appid = os.path.basename(metafile)[0:-5] # strip path and .json
725 thisinfo = get_default_app_info_list(appid)
727 # fdroid metadata is only strings and booleans, no floats or ints. And
728 # json returns unicode, and fdroidserver still uses plain python strings
729 # TODO create schema using https://pypi.python.org/pypi/jsonschema
730 jsoninfo = json.load(open(metafile, 'r'),
731 object_hook=_decode_dict,
732 parse_int=lambda s: s,
733 parse_float=lambda s: s)
734 thisinfo.update(jsoninfo)
735 post_metadata_parse(thisinfo)
737 return (appid, thisinfo)
740 def parse_xml_metadata(metafile):
742 appid = os.path.basename(metafile)[0:-4] # strip path and .xml
743 thisinfo = get_default_app_info_list(appid)
745 tree = ElementTree.ElementTree(file=metafile)
746 root = tree.getroot()
748 if root.tag != 'resources':
749 logging.critical(metafile + ' does not have root as <resources></resources>!')
752 supported_metadata = app_defaults.keys()
754 if child.tag != 'builds':
755 # builds does not have name="" attrib
756 name = child.attrib['name']
757 if name not in supported_metadata:
758 raise MetaDataException("Unrecognised metadata: <"
759 + child.tag + ' name="' + name + '">'
761 + "</" + child.tag + '>')
763 if child.tag == 'string':
764 thisinfo[name] = child.text
765 elif child.tag == 'string-array':
768 items.append(item.text)
769 thisinfo[name] = items
770 elif child.tag == 'builds':
775 builddict[key.tag] = key.text
776 builds.append(builddict)
777 thisinfo['builds'] = builds
779 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
780 if not isinstance(thisinfo['Requires Root'], bool):
781 if thisinfo['Requires Root'] == 'true':
782 thisinfo['Requires Root'] = True
784 thisinfo['Requires Root'] = False
786 post_metadata_parse(thisinfo)
788 return (appid, thisinfo)
791 def parse_yaml_metadata(metafile):
793 appid = os.path.basename(metafile)[0:-5] # strip path and .yaml
794 thisinfo = get_default_app_info_list(appid)
796 yamlinfo = yaml.load(open(metafile, 'r'), Loader=YamlLoader)
797 thisinfo.update(yamlinfo)
798 post_metadata_parse(thisinfo)
800 return (appid, thisinfo)
803 def parse_txt_metadata(metafile):
808 def add_buildflag(p, thisbuild):
810 raise MetaDataException("Empty build flag at {1}"
811 .format(buildlines[0], linedesc))
814 raise MetaDataException("Invalid build flag at {0} in {1}"
815 .format(buildlines[0], linedesc))
818 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
819 .format(pk, thisbuild['version'], linedesc))
822 if pk not in flag_defaults:
823 raise MetaDataException("Unrecognised build flag at {0} in {1}"
824 .format(p, linedesc))
827 pv = split_list_values(pv)
829 if len(pv) == 1 and pv[0] in ['main', 'yes']:
832 elif t == 'string' or t == 'script':
839 logging.debug("...ignoring bool flag %s" % p)
842 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
845 def parse_buildline(lines):
846 value = "".join(lines)
847 parts = [p.replace("\\,", ",")
848 for p in re.split(r"(?<!\\),", value)]
850 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
852 thisbuild['origlines'] = lines
853 thisbuild['version'] = parts[0]
854 thisbuild['vercode'] = parts[1]
855 if parts[2].startswith('!'):
856 # For backwards compatibility, handle old-style disabling,
857 # including attempting to extract the commit from the message
858 thisbuild['disable'] = parts[2][1:]
859 commit = 'unknown - see disabled'
860 index = parts[2].rfind('at ')
862 commit = parts[2][index + 3:]
863 if commit.endswith(')'):
865 thisbuild['commit'] = commit
867 thisbuild['commit'] = parts[2]
869 add_buildflag(p, thisbuild)
873 def add_comments(key):
876 for comment in curcomments:
877 thisinfo['comments'].append([key, comment])
880 thisinfo = get_default_app_info_list()
882 if not isinstance(metafile, file):
883 metafile = open(metafile, "r")
884 appid = metafile.name[9:-4]
885 thisinfo['id'] = appid
887 return appid, thisinfo
896 for line in metafile:
898 linedesc = "%s:%d" % (metafile.name, c)
899 line = line.rstrip('\r\n')
901 if not any(line.startswith(s) for s in (' ', '\t')):
902 commit = curbuild['commit'] if 'commit' in curbuild else None
903 if not commit and 'disable' not in curbuild:
904 raise MetaDataException("No commit specified for {0} in {1}"
905 .format(curbuild['version'], linedesc))
907 thisinfo['builds'].append(curbuild)
908 add_comments('build:' + curbuild['vercode'])
911 if line.endswith('\\'):
912 buildlines.append(line[:-1].lstrip())
914 buildlines.append(line.lstrip())
915 bl = ''.join(buildlines)
916 add_buildflag(bl, curbuild)
922 if line.startswith("#"):
923 curcomments.append(line)
926 field, value = line.split(':', 1)
928 raise MetaDataException("Invalid metadata in " + linedesc)
929 if field != field.strip() or value != value.strip():
930 raise MetaDataException("Extra spacing found in " + linedesc)
932 # Translate obsolete fields...
933 if field == 'Market Version':
934 field = 'Current Version'
935 if field == 'Market Version Code':
936 field = 'Current Version Code'
938 fieldtype = metafieldtype(field)
939 if fieldtype not in ['build', 'buildv2']:
941 if fieldtype == 'multiline':
945 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
946 elif fieldtype == 'string':
947 thisinfo[field] = value
948 elif fieldtype == 'list':
949 thisinfo[field] = split_list_values(value)
950 elif fieldtype == 'build':
951 if value.endswith("\\"):
953 buildlines = [value[:-1]]
955 curbuild = parse_buildline([value])
956 thisinfo['builds'].append(curbuild)
957 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
958 elif fieldtype == 'buildv2':
960 vv = value.split(',')
962 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
963 .format(value, linedesc))
964 curbuild['version'] = vv[0]
965 curbuild['vercode'] = vv[1]
966 if curbuild['vercode'] in vc_seen:
967 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
968 curbuild['vercode'], linedesc))
969 vc_seen[curbuild['vercode']] = True
972 elif fieldtype == 'obsolete':
973 pass # Just throw it away!
975 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
976 elif mode == 1: # Multiline field
980 thisinfo[field].append(line)
981 elif mode == 2: # Line continuation mode in Build Version
982 if line.endswith("\\"):
983 buildlines.append(line[:-1])
985 buildlines.append(line)
986 curbuild = parse_buildline(buildlines)
987 thisinfo['builds'].append(curbuild)
988 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
992 # Mode at end of file should always be 0...
994 raise MetaDataException(field + " not terminated in " + metafile.name)
996 raise MetaDataException("Unterminated continuation in " + metafile.name)
998 raise MetaDataException("Unterminated build in " + metafile.name)
1000 post_metadata_parse(thisinfo)
1002 return (appid, thisinfo)
1005 # Write a metadata file.
1007 # 'dest' - The path to the output file
1008 # 'app' - The app data
1009 def write_metadata(dest, app):
1011 def writecomments(key):
1013 for pf, comment in app['comments']:
1015 mf.write("%s\n" % comment)
1018 logging.debug("...writing comments for " + (key or 'EOF'))
1020 def writefield(field, value=None):
1021 writecomments(field)
1024 t = metafieldtype(field)
1026 value = ','.join(value)
1027 mf.write("%s:%s\n" % (field, value))
1029 def writefield_nonempty(field, value=None):
1033 writefield(field, value)
1035 mf = open(dest, 'w')
1036 writefield_nonempty('Disabled')
1037 writefield('AntiFeatures')
1038 writefield_nonempty('Provides')
1039 writefield('Categories')
1040 writefield('License')
1041 writefield('Web Site')
1042 writefield('Source Code')
1043 writefield('Issue Tracker')
1044 writefield_nonempty('Changelog')
1045 writefield_nonempty('Donate')
1046 writefield_nonempty('FlattrID')
1047 writefield_nonempty('Bitcoin')
1048 writefield_nonempty('Litecoin')
1049 writefield_nonempty('Dogecoin')
1051 writefield_nonempty('Name')
1052 writefield_nonempty('Auto Name')
1053 writefield('Summary')
1054 writefield('Description', '')
1055 for line in app['Description']:
1056 mf.write("%s\n" % line)
1059 if app['Requires Root']:
1060 writefield('Requires Root', 'yes')
1062 if app['Repo Type']:
1063 writefield('Repo Type')
1066 writefield('Binaries')
1068 for build in app['builds']:
1070 if build['version'] == "Ignore":
1073 writecomments('build:' + build['vercode'])
1074 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1076 def write_builditem(key, value):
1078 if key in ['version', 'vercode']:
1081 if value == flag_defaults[key]:
1086 logging.debug("...writing {0} : {1}".format(key, value))
1087 outline = ' %s=' % key
1094 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1096 outline += ','.join(value) if type(value) == list else value
1101 for flag in flag_defaults:
1104 write_builditem(flag, value)
1107 if app['Maintainer Notes']:
1108 writefield('Maintainer Notes', '')
1109 for line in app['Maintainer Notes']:
1110 mf.write("%s\n" % line)
1114 writefield_nonempty('Archive Policy')
1115 writefield('Auto Update Mode')
1116 writefield('Update Check Mode')
1117 writefield_nonempty('Update Check Ignore')
1118 writefield_nonempty('Vercode Operation')
1119 writefield_nonempty('Update Check Name')
1120 writefield_nonempty('Update Check Data')
1121 if app['Current Version']:
1122 writefield('Current Version')
1123 writefield('Current Version Code')
1125 if app['No Source Since']:
1126 writefield('No Source Since')