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):
583 appid = os.path.splitext(os.path.basename(metadatapath))[0]
585 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
586 % (metadatapath, appid, apps[appid]['metadatapath']))
590 thisinfo.update(app_defaults)
591 thisinfo['metadatapath'] = metadatapath
592 if appid is not None:
593 thisinfo['id'] = appid
595 # General defaults...
596 thisinfo['builds'] = []
597 thisinfo['comments'] = []
599 return appid, thisinfo
602 def post_metadata_parse(thisinfo):
604 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
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 # 'metadatapath' - 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_metadata(apps, metadatapath):
723 root, ext = os.path.splitext(metadatapath)
724 metadataformat = ext[1:]
725 accepted = common.config['accepted_formats']
726 if metadataformat not in accepted:
727 logging.critical('"' + metadatapath
728 + '" is not in an accepted format, '
729 + 'convert to: ' + ', '.join(accepted))
732 if metadataformat == 'txt':
733 return parse_txt_metadata(apps, metadatapath)
734 elif metadataformat == 'json':
735 return parse_json_metadata(apps, metadatapath)
736 elif metadataformat == 'xml':
737 return parse_xml_metadata(apps, metadatapath)
738 elif metadataformat == 'yaml':
739 return parse_yaml_metadata(apps, metadatapath)
741 logging.critical('Unknown metadata format: ' + metadatapath)
745 def parse_json_metadata(apps, metadatapath):
747 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
749 # fdroid metadata is only strings and booleans, no floats or ints. And
750 # json returns unicode, and fdroidserver still uses plain python strings
751 # TODO create schema using https://pypi.python.org/pypi/jsonschema
752 jsoninfo = json.load(open(metadatapath, 'r'),
753 object_hook=_decode_dict,
754 parse_int=lambda s: s,
755 parse_float=lambda s: s)
756 thisinfo.update(jsoninfo)
757 post_metadata_parse(thisinfo)
759 return (appid, thisinfo)
762 def parse_xml_metadata(apps, metadatapath):
764 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
766 tree = ElementTree.ElementTree(file=metadatapath)
767 root = tree.getroot()
769 if root.tag != 'resources':
770 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
773 supported_metadata = app_defaults.keys()
775 if child.tag != 'builds':
776 # builds does not have name="" attrib
777 name = child.attrib['name']
778 if name not in supported_metadata:
779 raise MetaDataException("Unrecognised metadata: <"
780 + child.tag + ' name="' + name + '">'
782 + "</" + child.tag + '>')
784 if child.tag == 'string':
785 thisinfo[name] = child.text
786 elif child.tag == 'string-array':
789 items.append(item.text)
790 thisinfo[name] = items
791 elif child.tag == 'builds':
796 builddict[key.tag] = key.text
797 builds.append(builddict)
798 thisinfo['builds'] = builds
800 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
801 if not isinstance(thisinfo['Requires Root'], bool):
802 if thisinfo['Requires Root'] == 'true':
803 thisinfo['Requires Root'] = True
805 thisinfo['Requires Root'] = False
807 post_metadata_parse(thisinfo)
809 return (appid, thisinfo)
812 def parse_yaml_metadata(apps, metadatapath):
814 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
816 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
817 thisinfo.update(yamlinfo)
818 post_metadata_parse(thisinfo)
820 return (appid, thisinfo)
823 def parse_txt_metadata(apps, metadatapath):
827 def add_buildflag(p, thisbuild):
829 raise MetaDataException("Empty build flag at {1}"
830 .format(buildlines[0], linedesc))
833 raise MetaDataException("Invalid build flag at {0} in {1}"
834 .format(buildlines[0], linedesc))
837 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
838 .format(pk, thisbuild['version'], linedesc))
841 if pk not in flag_defaults:
842 raise MetaDataException("Unrecognised build flag at {0} in {1}"
843 .format(p, linedesc))
846 pv = split_list_values(pv)
848 if len(pv) == 1 and pv[0] in ['main', 'yes']:
851 elif t == 'string' or t == 'script':
858 logging.debug("...ignoring bool flag %s" % p)
861 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
864 def parse_buildline(lines):
865 value = "".join(lines)
866 parts = [p.replace("\\,", ",")
867 for p in re.split(r"(?<!\\),", value)]
869 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
871 thisbuild['origlines'] = lines
872 thisbuild['version'] = parts[0]
873 thisbuild['vercode'] = parts[1]
874 if parts[2].startswith('!'):
875 # For backwards compatibility, handle old-style disabling,
876 # including attempting to extract the commit from the message
877 thisbuild['disable'] = parts[2][1:]
878 commit = 'unknown - see disabled'
879 index = parts[2].rfind('at ')
881 commit = parts[2][index + 3:]
882 if commit.endswith(')'):
884 thisbuild['commit'] = commit
886 thisbuild['commit'] = parts[2]
888 add_buildflag(p, thisbuild)
892 def add_comments(key):
895 for comment in curcomments:
896 thisinfo['comments'].append([key, comment])
899 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
900 metafile = open(metadatapath, "r")
909 for line in metafile:
911 linedesc = "%s:%d" % (metafile.name, c)
912 line = line.rstrip('\r\n')
914 if not any(line.startswith(s) for s in (' ', '\t')):
915 commit = curbuild['commit'] if 'commit' in curbuild else None
916 if not commit and 'disable' not in curbuild:
917 raise MetaDataException("No commit specified for {0} in {1}"
918 .format(curbuild['version'], linedesc))
920 thisinfo['builds'].append(curbuild)
921 add_comments('build:' + curbuild['vercode'])
924 if line.endswith('\\'):
925 buildlines.append(line[:-1].lstrip())
927 buildlines.append(line.lstrip())
928 bl = ''.join(buildlines)
929 add_buildflag(bl, curbuild)
935 if line.startswith("#"):
936 curcomments.append(line)
939 field, value = line.split(':', 1)
941 raise MetaDataException("Invalid metadata in " + linedesc)
942 if field != field.strip() or value != value.strip():
943 raise MetaDataException("Extra spacing found in " + linedesc)
945 # Translate obsolete fields...
946 if field == 'Market Version':
947 field = 'Current Version'
948 if field == 'Market Version Code':
949 field = 'Current Version Code'
951 fieldtype = metafieldtype(field)
952 if fieldtype not in ['build', 'buildv2']:
954 if fieldtype == 'multiline':
958 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
959 elif fieldtype == 'string':
960 thisinfo[field] = value
961 elif fieldtype == 'list':
962 thisinfo[field] = split_list_values(value)
963 elif fieldtype == 'build':
964 if value.endswith("\\"):
966 buildlines = [value[:-1]]
968 curbuild = parse_buildline([value])
969 thisinfo['builds'].append(curbuild)
970 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
971 elif fieldtype == 'buildv2':
973 vv = value.split(',')
975 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
976 .format(value, linedesc))
977 curbuild['version'] = vv[0]
978 curbuild['vercode'] = vv[1]
979 if curbuild['vercode'] in vc_seen:
980 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
981 curbuild['vercode'], linedesc))
982 vc_seen[curbuild['vercode']] = True
985 elif fieldtype == 'obsolete':
986 pass # Just throw it away!
988 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
989 elif mode == 1: # Multiline field
993 thisinfo[field].append(line)
994 elif mode == 2: # Line continuation mode in Build Version
995 if line.endswith("\\"):
996 buildlines.append(line[:-1])
998 buildlines.append(line)
999 curbuild = parse_buildline(buildlines)
1000 thisinfo['builds'].append(curbuild)
1001 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1005 # Mode at end of file should always be 0...
1007 raise MetaDataException(field + " not terminated in " + metafile.name)
1009 raise MetaDataException("Unterminated continuation in " + metafile.name)
1011 raise MetaDataException("Unterminated build in " + metafile.name)
1013 post_metadata_parse(thisinfo)
1015 return (appid, thisinfo)
1018 # Write a metadata file.
1020 # 'dest' - The path to the output file
1021 # 'app' - The app data
1022 def write_metadata(dest, app):
1024 def writecomments(key):
1026 for pf, comment in app['comments']:
1028 mf.write("%s\n" % comment)
1031 logging.debug("...writing comments for " + (key or 'EOF'))
1033 def writefield(field, value=None):
1034 writecomments(field)
1037 t = metafieldtype(field)
1039 value = ','.join(value)
1040 mf.write("%s:%s\n" % (field, value))
1042 def writefield_nonempty(field, value=None):
1046 writefield(field, value)
1048 mf = open(dest, 'w')
1049 writefield_nonempty('Disabled')
1050 writefield('AntiFeatures')
1051 writefield_nonempty('Provides')
1052 writefield('Categories')
1053 writefield('License')
1054 writefield('Web Site')
1055 writefield('Source Code')
1056 writefield('Issue Tracker')
1057 writefield_nonempty('Changelog')
1058 writefield_nonempty('Donate')
1059 writefield_nonempty('FlattrID')
1060 writefield_nonempty('Bitcoin')
1061 writefield_nonempty('Litecoin')
1062 writefield_nonempty('Dogecoin')
1064 writefield_nonempty('Name')
1065 writefield_nonempty('Auto Name')
1066 writefield('Summary')
1067 writefield('Description', '')
1068 for line in app['Description']:
1069 mf.write("%s\n" % line)
1072 if app['Requires Root']:
1073 writefield('Requires Root', 'yes')
1075 if app['Repo Type']:
1076 writefield('Repo Type')
1079 writefield('Binaries')
1081 for build in app['builds']:
1083 if build['version'] == "Ignore":
1086 writecomments('build:' + build['vercode'])
1087 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1089 def write_builditem(key, value):
1091 if key in ['version', 'vercode']:
1094 if value == flag_defaults[key]:
1099 logging.debug("...writing {0} : {1}".format(key, value))
1100 outline = ' %s=' % key
1107 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1109 outline += ','.join(value) if type(value) == list else value
1114 for flag in flag_defaults:
1117 write_builditem(flag, value)
1120 if app['Maintainer Notes']:
1121 writefield('Maintainer Notes', '')
1122 for line in app['Maintainer Notes']:
1123 mf.write("%s\n" % line)
1127 writefield_nonempty('Archive Policy')
1128 writefield('Auto Update Mode')
1129 writefield('Update Check Mode')
1130 writefield_nonempty('Update Check Ignore')
1131 writefield_nonempty('Vercode Operation')
1132 writefield_nonempty('Update Check Name')
1133 writefield_nonempty('Update Check Data')
1134 if app['Current Version']:
1135 writefield('Current Version')
1136 writefield('Current Version Code')
1138 if app['No Source Since']:
1139 writefield('No Source Since')