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 appid, appinfo = parse_txt_metadata(apps, metadatapath)
504 check_metadata(appinfo)
505 apps[appid] = appinfo
507 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.json'))):
508 appid, appinfo = parse_json_metadata(apps, metadatapath)
509 check_metadata(appinfo)
510 apps[appid] = appinfo
512 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.xml'))):
513 appid, appinfo = parse_xml_metadata(apps, metadatapath)
514 check_metadata(appinfo)
515 apps[appid] = appinfo
517 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.yaml'))):
518 appid, appinfo = parse_yaml_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):
595 appid = os.path.splitext(os.path.basename(metadatapath))[0]
597 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
598 % (metadatapath, appid, apps[appid]['metadatapath']))
602 thisinfo.update(app_defaults)
603 thisinfo['metadatapath'] = metadatapath
604 if appid is not None:
605 thisinfo['id'] = appid
607 # General defaults...
608 thisinfo['builds'] = []
609 thisinfo['comments'] = []
611 return appid, thisinfo
614 def post_metadata_parse(thisinfo):
616 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
617 for k, v in thisinfo.iteritems():
618 if k not in supported_metadata:
619 raise MetaDataException("Unrecognised metadata: {0}: {1}"
621 if type(v) in (float, int):
624 # convert to the odd internal format
625 for k in ('Description', 'Maintainer Notes'):
626 if isinstance(thisinfo[k], basestring):
627 text = thisinfo[k].rstrip().lstrip()
628 thisinfo[k] = text.split('\n')
630 supported_flags = (flag_defaults.keys()
631 + ['vercode', 'version', 'versionCode', 'versionName'])
632 esc_newlines = re.compile('\\\\( |\\n)')
634 for build in thisinfo['builds']:
635 for k, v in build.items():
636 if k not in supported_flags:
637 raise MetaDataException("Unrecognised build flag: {0}={1}"
640 if k == 'versionCode':
641 build['vercode'] = str(v)
642 del build['versionCode']
643 elif k == 'versionName':
644 build['version'] = str(v)
645 del build['versionName']
646 elif type(v) in (float, int):
649 keyflagtype = flagtype(k)
650 if keyflagtype == 'list':
651 # these can be bools, strings or lists, but ultimately are lists
652 if isinstance(v, basestring):
654 elif isinstance(v, bool):
659 elif keyflagtype == 'script':
660 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
661 elif keyflagtype == 'bool':
662 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
663 if isinstance(v, basestring):
669 if not thisinfo['Description']:
670 thisinfo['Description'].append('No description available')
672 for build in thisinfo['builds']:
673 fill_build_defaults(build)
675 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
678 # Parse metadata for a single application.
680 # 'metadatapath' - the filename to read. The package id for the application comes
681 # from this filename. Pass None to get a blank entry.
683 # Returns a dictionary containing all the details of the application. There are
684 # two major kinds of information in the dictionary. Keys beginning with capital
685 # letters correspond directory to identically named keys in the metadata file.
686 # Keys beginning with lower case letters are generated in one way or another,
687 # and are not found verbatim in the metadata.
689 # Known keys not originating from the metadata are:
691 # 'builds' - a list of dictionaries containing build information
692 # for each defined build
693 # 'comments' - a list of comments from the metadata file. Each is
694 # a list of the form [field, comment] where field is
695 # the name of the field it preceded in the metadata
696 # file. Where field is None, the comment goes at the
697 # end of the file. Alternatively, 'build:version' is
698 # for a comment before a particular build version.
699 # 'descriptionlines' - original lines of description as formatted in the
704 def _decode_list(data):
705 '''convert items in a list from unicode to basestring'''
708 if isinstance(item, unicode):
709 item = item.encode('utf-8')
710 elif isinstance(item, list):
711 item = _decode_list(item)
712 elif isinstance(item, dict):
713 item = _decode_dict(item)
718 def _decode_dict(data):
719 '''convert items in a dict from unicode to basestring'''
721 for key, value in data.iteritems():
722 if isinstance(key, unicode):
723 key = key.encode('utf-8')
724 if isinstance(value, unicode):
725 value = value.encode('utf-8')
726 elif isinstance(value, list):
727 value = _decode_list(value)
728 elif isinstance(value, dict):
729 value = _decode_dict(value)
734 def parse_json_metadata(apps, metadatapath):
736 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
738 # fdroid metadata is only strings and booleans, no floats or ints. And
739 # json returns unicode, and fdroidserver still uses plain python strings
740 # TODO create schema using https://pypi.python.org/pypi/jsonschema
741 jsoninfo = json.load(open(metadatapath, 'r'),
742 object_hook=_decode_dict,
743 parse_int=lambda s: s,
744 parse_float=lambda s: s)
745 thisinfo.update(jsoninfo)
746 post_metadata_parse(thisinfo)
748 return (appid, thisinfo)
751 def parse_xml_metadata(apps, metadatapath):
753 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
755 tree = ElementTree.ElementTree(file=metadatapath)
756 root = tree.getroot()
758 if root.tag != 'resources':
759 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
762 supported_metadata = app_defaults.keys()
764 if child.tag != 'builds':
765 # builds does not have name="" attrib
766 name = child.attrib['name']
767 if name not in supported_metadata:
768 raise MetaDataException("Unrecognised metadata: <"
769 + child.tag + ' name="' + name + '">'
771 + "</" + child.tag + '>')
773 if child.tag == 'string':
774 thisinfo[name] = child.text
775 elif child.tag == 'string-array':
778 items.append(item.text)
779 thisinfo[name] = items
780 elif child.tag == 'builds':
785 builddict[key.tag] = key.text
786 builds.append(builddict)
787 thisinfo['builds'] = builds
789 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
790 if not isinstance(thisinfo['Requires Root'], bool):
791 if thisinfo['Requires Root'] == 'true':
792 thisinfo['Requires Root'] = True
794 thisinfo['Requires Root'] = False
796 post_metadata_parse(thisinfo)
798 return (appid, thisinfo)
801 def parse_yaml_metadata(apps, metadatapath):
803 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
805 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
806 thisinfo.update(yamlinfo)
807 post_metadata_parse(thisinfo)
809 return (appid, thisinfo)
812 def parse_txt_metadata(apps, metadatapath):
816 def add_buildflag(p, thisbuild):
818 raise MetaDataException("Empty build flag at {1}"
819 .format(buildlines[0], linedesc))
822 raise MetaDataException("Invalid build flag at {0} in {1}"
823 .format(buildlines[0], linedesc))
826 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
827 .format(pk, thisbuild['version'], linedesc))
830 if pk not in flag_defaults:
831 raise MetaDataException("Unrecognised build flag at {0} in {1}"
832 .format(p, linedesc))
835 pv = split_list_values(pv)
837 if len(pv) == 1 and pv[0] in ['main', 'yes']:
840 elif t == 'string' or t == 'script':
847 logging.debug("...ignoring bool flag %s" % p)
850 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
853 def parse_buildline(lines):
854 value = "".join(lines)
855 parts = [p.replace("\\,", ",")
856 for p in re.split(r"(?<!\\),", value)]
858 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
860 thisbuild['origlines'] = lines
861 thisbuild['version'] = parts[0]
862 thisbuild['vercode'] = parts[1]
863 if parts[2].startswith('!'):
864 # For backwards compatibility, handle old-style disabling,
865 # including attempting to extract the commit from the message
866 thisbuild['disable'] = parts[2][1:]
867 commit = 'unknown - see disabled'
868 index = parts[2].rfind('at ')
870 commit = parts[2][index + 3:]
871 if commit.endswith(')'):
873 thisbuild['commit'] = commit
875 thisbuild['commit'] = parts[2]
877 add_buildflag(p, thisbuild)
881 def add_comments(key):
884 for comment in curcomments:
885 thisinfo['comments'].append([key, comment])
888 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
889 metafile = open(metadatapath, "r")
898 for line in metafile:
900 linedesc = "%s:%d" % (metafile.name, c)
901 line = line.rstrip('\r\n')
903 if not any(line.startswith(s) for s in (' ', '\t')):
904 commit = curbuild['commit'] if 'commit' in curbuild else None
905 if not commit and 'disable' not in curbuild:
906 raise MetaDataException("No commit specified for {0} in {1}"
907 .format(curbuild['version'], linedesc))
909 thisinfo['builds'].append(curbuild)
910 add_comments('build:' + curbuild['vercode'])
913 if line.endswith('\\'):
914 buildlines.append(line[:-1].lstrip())
916 buildlines.append(line.lstrip())
917 bl = ''.join(buildlines)
918 add_buildflag(bl, curbuild)
924 if line.startswith("#"):
925 curcomments.append(line)
928 field, value = line.split(':', 1)
930 raise MetaDataException("Invalid metadata in " + linedesc)
931 if field != field.strip() or value != value.strip():
932 raise MetaDataException("Extra spacing found in " + linedesc)
934 # Translate obsolete fields...
935 if field == 'Market Version':
936 field = 'Current Version'
937 if field == 'Market Version Code':
938 field = 'Current Version Code'
940 fieldtype = metafieldtype(field)
941 if fieldtype not in ['build', 'buildv2']:
943 if fieldtype == 'multiline':
947 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
948 elif fieldtype == 'string':
949 thisinfo[field] = value
950 elif fieldtype == 'list':
951 thisinfo[field] = split_list_values(value)
952 elif fieldtype == 'build':
953 if value.endswith("\\"):
955 buildlines = [value[:-1]]
957 curbuild = parse_buildline([value])
958 thisinfo['builds'].append(curbuild)
959 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
960 elif fieldtype == 'buildv2':
962 vv = value.split(',')
964 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
965 .format(value, linedesc))
966 curbuild['version'] = vv[0]
967 curbuild['vercode'] = vv[1]
968 if curbuild['vercode'] in vc_seen:
969 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
970 curbuild['vercode'], linedesc))
971 vc_seen[curbuild['vercode']] = True
974 elif fieldtype == 'obsolete':
975 pass # Just throw it away!
977 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
978 elif mode == 1: # Multiline field
982 thisinfo[field].append(line)
983 elif mode == 2: # Line continuation mode in Build Version
984 if line.endswith("\\"):
985 buildlines.append(line[:-1])
987 buildlines.append(line)
988 curbuild = parse_buildline(buildlines)
989 thisinfo['builds'].append(curbuild)
990 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
994 # Mode at end of file should always be 0...
996 raise MetaDataException(field + " not terminated in " + metafile.name)
998 raise MetaDataException("Unterminated continuation in " + metafile.name)
1000 raise MetaDataException("Unterminated build in " + metafile.name)
1002 post_metadata_parse(thisinfo)
1004 return (appid, thisinfo)
1007 # Write a metadata file.
1009 # 'dest' - The path to the output file
1010 # 'app' - The app data
1011 def write_metadata(dest, app):
1013 def writecomments(key):
1015 for pf, comment in app['comments']:
1017 mf.write("%s\n" % comment)
1020 logging.debug("...writing comments for " + (key or 'EOF'))
1022 def writefield(field, value=None):
1023 writecomments(field)
1026 t = metafieldtype(field)
1028 value = ','.join(value)
1029 mf.write("%s:%s\n" % (field, value))
1031 def writefield_nonempty(field, value=None):
1035 writefield(field, value)
1037 mf = open(dest, 'w')
1038 writefield_nonempty('Disabled')
1039 writefield('AntiFeatures')
1040 writefield_nonempty('Provides')
1041 writefield('Categories')
1042 writefield('License')
1043 writefield('Web Site')
1044 writefield('Source Code')
1045 writefield('Issue Tracker')
1046 writefield_nonempty('Changelog')
1047 writefield_nonempty('Donate')
1048 writefield_nonempty('FlattrID')
1049 writefield_nonempty('Bitcoin')
1050 writefield_nonempty('Litecoin')
1051 writefield_nonempty('Dogecoin')
1053 writefield_nonempty('Name')
1054 writefield_nonempty('Auto Name')
1055 writefield('Summary')
1056 writefield('Description', '')
1057 for line in app['Description']:
1058 mf.write("%s\n" % line)
1061 if app['Requires Root']:
1062 writefield('Requires Root', 'yes')
1064 if app['Repo Type']:
1065 writefield('Repo Type')
1068 writefield('Binaries')
1070 for build in app['builds']:
1072 if build['version'] == "Ignore":
1075 writecomments('build:' + build['vercode'])
1076 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1078 def write_builditem(key, value):
1080 if key in ['version', 'vercode']:
1083 if value == flag_defaults[key]:
1088 logging.debug("...writing {0} : {1}".format(key, value))
1089 outline = ' %s=' % key
1096 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1098 outline += ','.join(value) if type(value) == list else value
1103 for flag in flag_defaults:
1106 write_builditem(flag, value)
1109 if app['Maintainer Notes']:
1110 writefield('Maintainer Notes', '')
1111 for line in app['Maintainer Notes']:
1112 mf.write("%s\n" % line)
1116 writefield_nonempty('Archive Policy')
1117 writefield('Auto Update Mode')
1118 writefield('Update Check Mode')
1119 writefield_nonempty('Update Check Ignore')
1120 writefield_nonempty('Vercode Operation')
1121 writefield_nonempty('Update Check Name')
1122 writefield_nonempty('Update Check Data')
1123 if app['Current Version']:
1124 writefield('Current Version')
1125 writefield('Current Version Code')
1127 if app['No Source Since']:
1128 writefield('No Source Since')