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 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
498 appid, appinfo = parse_txt_metadata(metadatapath)
499 check_metadata(appinfo)
500 apps[appid] = appinfo
502 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.json'))):
503 appid, appinfo = parse_json_metadata(metadatapath)
504 check_metadata(appinfo)
505 apps[appid] = appinfo
507 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.xml'))):
508 appid, appinfo = parse_xml_metadata(metadatapath)
509 check_metadata(appinfo)
510 apps[appid] = appinfo
512 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.yaml'))):
513 appid, appinfo = parse_yaml_metadata(metadatapath)
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(metadatapath):
590 appid = os.path.splitext(os.path.basename(metadatapath))[0]
592 thisinfo.update(app_defaults)
593 if appid is not None:
594 thisinfo['id'] = appid
596 # General defaults...
597 thisinfo['builds'] = []
598 thisinfo['comments'] = []
600 return appid, thisinfo
603 def post_metadata_parse(thisinfo):
605 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id']
606 for k, v in thisinfo.iteritems():
607 if k not in supported_metadata:
608 raise MetaDataException("Unrecognised metadata: {0}: {1}"
610 if type(v) in (float, int):
613 # convert to the odd internal format
614 for k in ('Description', 'Maintainer Notes'):
615 if isinstance(thisinfo[k], basestring):
616 text = thisinfo[k].rstrip().lstrip()
617 thisinfo[k] = text.split('\n')
619 supported_flags = (flag_defaults.keys()
620 + ['vercode', 'version', 'versionCode', 'versionName'])
621 esc_newlines = re.compile('\\\\( |\\n)')
623 for build in thisinfo['builds']:
624 for k, v in build.items():
625 if k not in supported_flags:
626 raise MetaDataException("Unrecognised build flag: {0}={1}"
629 if k == 'versionCode':
630 build['vercode'] = str(v)
631 del build['versionCode']
632 elif k == 'versionName':
633 build['version'] = str(v)
634 del build['versionName']
635 elif type(v) in (float, int):
638 keyflagtype = flagtype(k)
639 if keyflagtype == 'list':
640 # these can be bools, strings or lists, but ultimately are lists
641 if isinstance(v, basestring):
643 elif isinstance(v, bool):
648 elif keyflagtype == 'script':
649 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
650 elif keyflagtype == 'bool':
651 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
652 if isinstance(v, basestring):
658 if not thisinfo['Description']:
659 thisinfo['Description'].append('No description available')
661 for build in thisinfo['builds']:
662 fill_build_defaults(build)
664 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
667 # Parse metadata for a single application.
669 # 'metadatapath' - the filename to read. The package id for the application comes
670 # from this filename. Pass None to get a blank entry.
672 # Returns a dictionary containing all the details of the application. There are
673 # two major kinds of information in the dictionary. Keys beginning with capital
674 # letters correspond directory to identically named keys in the metadata file.
675 # Keys beginning with lower case letters are generated in one way or another,
676 # and are not found verbatim in the metadata.
678 # Known keys not originating from the metadata are:
680 # 'builds' - a list of dictionaries containing build information
681 # for each defined build
682 # 'comments' - a list of comments from the metadata file. Each is
683 # a list of the form [field, comment] where field is
684 # the name of the field it preceded in the metadata
685 # file. Where field is None, the comment goes at the
686 # end of the file. Alternatively, 'build:version' is
687 # for a comment before a particular build version.
688 # 'descriptionlines' - original lines of description as formatted in the
693 def _decode_list(data):
694 '''convert items in a list from unicode to basestring'''
697 if isinstance(item, unicode):
698 item = item.encode('utf-8')
699 elif isinstance(item, list):
700 item = _decode_list(item)
701 elif isinstance(item, dict):
702 item = _decode_dict(item)
707 def _decode_dict(data):
708 '''convert items in a dict from unicode to basestring'''
710 for key, value in data.iteritems():
711 if isinstance(key, unicode):
712 key = key.encode('utf-8')
713 if isinstance(value, unicode):
714 value = value.encode('utf-8')
715 elif isinstance(value, list):
716 value = _decode_list(value)
717 elif isinstance(value, dict):
718 value = _decode_dict(value)
723 def parse_json_metadata(metadatapath):
725 appid, thisinfo = get_default_app_info_list(metadatapath)
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(metadatapath, '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(metadatapath):
742 appid, thisinfo = get_default_app_info_list(metadatapath)
744 tree = ElementTree.ElementTree(file=metadatapath)
745 root = tree.getroot()
747 if root.tag != 'resources':
748 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
751 supported_metadata = app_defaults.keys()
753 if child.tag != 'builds':
754 # builds does not have name="" attrib
755 name = child.attrib['name']
756 if name not in supported_metadata:
757 raise MetaDataException("Unrecognised metadata: <"
758 + child.tag + ' name="' + name + '">'
760 + "</" + child.tag + '>')
762 if child.tag == 'string':
763 thisinfo[name] = child.text
764 elif child.tag == 'string-array':
767 items.append(item.text)
768 thisinfo[name] = items
769 elif child.tag == 'builds':
774 builddict[key.tag] = key.text
775 builds.append(builddict)
776 thisinfo['builds'] = builds
778 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
779 if not isinstance(thisinfo['Requires Root'], bool):
780 if thisinfo['Requires Root'] == 'true':
781 thisinfo['Requires Root'] = True
783 thisinfo['Requires Root'] = False
785 post_metadata_parse(thisinfo)
787 return (appid, thisinfo)
790 def parse_yaml_metadata(metadatapath):
792 appid, thisinfo = get_default_app_info_list(metadatapath)
794 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
795 thisinfo.update(yamlinfo)
796 post_metadata_parse(thisinfo)
798 return (appid, thisinfo)
801 def parse_txt_metadata(metadatapath):
805 def add_buildflag(p, thisbuild):
807 raise MetaDataException("Empty build flag at {1}"
808 .format(buildlines[0], linedesc))
811 raise MetaDataException("Invalid build flag at {0} in {1}"
812 .format(buildlines[0], linedesc))
815 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
816 .format(pk, thisbuild['version'], linedesc))
819 if pk not in flag_defaults:
820 raise MetaDataException("Unrecognised build flag at {0} in {1}"
821 .format(p, linedesc))
824 pv = split_list_values(pv)
826 if len(pv) == 1 and pv[0] in ['main', 'yes']:
829 elif t == 'string' or t == 'script':
836 logging.debug("...ignoring bool flag %s" % p)
839 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
842 def parse_buildline(lines):
843 value = "".join(lines)
844 parts = [p.replace("\\,", ",")
845 for p in re.split(r"(?<!\\),", value)]
847 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
849 thisbuild['origlines'] = lines
850 thisbuild['version'] = parts[0]
851 thisbuild['vercode'] = parts[1]
852 if parts[2].startswith('!'):
853 # For backwards compatibility, handle old-style disabling,
854 # including attempting to extract the commit from the message
855 thisbuild['disable'] = parts[2][1:]
856 commit = 'unknown - see disabled'
857 index = parts[2].rfind('at ')
859 commit = parts[2][index + 3:]
860 if commit.endswith(')'):
862 thisbuild['commit'] = commit
864 thisbuild['commit'] = parts[2]
866 add_buildflag(p, thisbuild)
870 def add_comments(key):
873 for comment in curcomments:
874 thisinfo['comments'].append([key, comment])
877 appid, thisinfo = get_default_app_info_list(metadatapath)
878 metafile = open(metadatapath, "r")
887 for line in metafile:
889 linedesc = "%s:%d" % (metafile.name, c)
890 line = line.rstrip('\r\n')
892 if not any(line.startswith(s) for s in (' ', '\t')):
893 commit = curbuild['commit'] if 'commit' in curbuild else None
894 if not commit and 'disable' not in curbuild:
895 raise MetaDataException("No commit specified for {0} in {1}"
896 .format(curbuild['version'], linedesc))
898 thisinfo['builds'].append(curbuild)
899 add_comments('build:' + curbuild['vercode'])
902 if line.endswith('\\'):
903 buildlines.append(line[:-1].lstrip())
905 buildlines.append(line.lstrip())
906 bl = ''.join(buildlines)
907 add_buildflag(bl, curbuild)
913 if line.startswith("#"):
914 curcomments.append(line)
917 field, value = line.split(':', 1)
919 raise MetaDataException("Invalid metadata in " + linedesc)
920 if field != field.strip() or value != value.strip():
921 raise MetaDataException("Extra spacing found in " + linedesc)
923 # Translate obsolete fields...
924 if field == 'Market Version':
925 field = 'Current Version'
926 if field == 'Market Version Code':
927 field = 'Current Version Code'
929 fieldtype = metafieldtype(field)
930 if fieldtype not in ['build', 'buildv2']:
932 if fieldtype == 'multiline':
936 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
937 elif fieldtype == 'string':
938 thisinfo[field] = value
939 elif fieldtype == 'list':
940 thisinfo[field] = split_list_values(value)
941 elif fieldtype == 'build':
942 if value.endswith("\\"):
944 buildlines = [value[:-1]]
946 curbuild = parse_buildline([value])
947 thisinfo['builds'].append(curbuild)
948 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
949 elif fieldtype == 'buildv2':
951 vv = value.split(',')
953 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
954 .format(value, linedesc))
955 curbuild['version'] = vv[0]
956 curbuild['vercode'] = vv[1]
957 if curbuild['vercode'] in vc_seen:
958 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
959 curbuild['vercode'], linedesc))
960 vc_seen[curbuild['vercode']] = True
963 elif fieldtype == 'obsolete':
964 pass # Just throw it away!
966 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
967 elif mode == 1: # Multiline field
971 thisinfo[field].append(line)
972 elif mode == 2: # Line continuation mode in Build Version
973 if line.endswith("\\"):
974 buildlines.append(line[:-1])
976 buildlines.append(line)
977 curbuild = parse_buildline(buildlines)
978 thisinfo['builds'].append(curbuild)
979 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
983 # Mode at end of file should always be 0...
985 raise MetaDataException(field + " not terminated in " + metafile.name)
987 raise MetaDataException("Unterminated continuation in " + metafile.name)
989 raise MetaDataException("Unterminated build in " + metafile.name)
991 post_metadata_parse(thisinfo)
993 return (appid, thisinfo)
996 # Write a metadata file.
998 # 'dest' - The path to the output file
999 # 'app' - The app data
1000 def write_metadata(dest, app):
1002 def writecomments(key):
1004 for pf, comment in app['comments']:
1006 mf.write("%s\n" % comment)
1009 logging.debug("...writing comments for " + (key or 'EOF'))
1011 def writefield(field, value=None):
1012 writecomments(field)
1015 t = metafieldtype(field)
1017 value = ','.join(value)
1018 mf.write("%s:%s\n" % (field, value))
1020 def writefield_nonempty(field, value=None):
1024 writefield(field, value)
1026 mf = open(dest, 'w')
1027 writefield_nonempty('Disabled')
1028 writefield('AntiFeatures')
1029 writefield_nonempty('Provides')
1030 writefield('Categories')
1031 writefield('License')
1032 writefield('Web Site')
1033 writefield('Source Code')
1034 writefield('Issue Tracker')
1035 writefield_nonempty('Changelog')
1036 writefield_nonempty('Donate')
1037 writefield_nonempty('FlattrID')
1038 writefield_nonempty('Bitcoin')
1039 writefield_nonempty('Litecoin')
1040 writefield_nonempty('Dogecoin')
1042 writefield_nonempty('Name')
1043 writefield_nonempty('Auto Name')
1044 writefield('Summary')
1045 writefield('Description', '')
1046 for line in app['Description']:
1047 mf.write("%s\n" % line)
1050 if app['Requires Root']:
1051 writefield('Requires Root', 'yes')
1053 if app['Repo Type']:
1054 writefield('Repo Type')
1057 writefield('Binaries')
1059 for build in app['builds']:
1061 if build['version'] == "Ignore":
1064 writecomments('build:' + build['vercode'])
1065 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1067 def write_builditem(key, value):
1069 if key in ['version', 'vercode']:
1072 if value == flag_defaults[key]:
1077 logging.debug("...writing {0} : {1}".format(key, value))
1078 outline = ' %s=' % key
1085 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1087 outline += ','.join(value) if type(value) == list else value
1092 for flag in flag_defaults:
1095 write_builditem(flag, value)
1098 if app['Maintainer Notes']:
1099 writefield('Maintainer Notes', '')
1100 for line in app['Maintainer Notes']:
1101 mf.write("%s\n" % line)
1105 writefield_nonempty('Archive Policy')
1106 writefield('Auto Update Mode')
1107 writefield('Update Check Mode')
1108 writefield_nonempty('Update Check Ignore')
1109 writefield_nonempty('Vercode Operation')
1110 writefield_nonempty('Update Check Name')
1111 writefield_nonempty('Update Check Data')
1112 if app['Current Version']:
1113 writefield('Current Version')
1114 writefield('Current Version Code')
1116 if app['No Source Since']:
1117 writefield('No Source Since')