1 # -*- coding: utf-8 -*-
3 # metadata.py - part of the FDroid server tools
4 # Copyright (C) 2013, Ciaran Gultnieks, ciaran@ciarang.com
5 # Copyright (C) 2013-2014 Daniel Martà <mvdan@mvdan.cc>
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 # use libyaml if it is available
31 from yaml import CLoader
34 from yaml import Loader
37 # use the C implementation when available
38 import xml.etree.cElementTree as ElementTree
40 from collections import OrderedDict
47 class MetaDataException(Exception):
49 def __init__(self, value):
55 # In the order in which they are laid out on files
56 app_defaults = OrderedDict([
60 ('Categories', ['None']),
61 ('License', 'Unknown'),
64 ('Issue Tracker', ''),
75 ('Requires Root', False),
79 ('Maintainer Notes', []),
80 ('Archive Policy', None),
81 ('Auto Update Mode', 'None'),
82 ('Update Check Mode', 'None'),
83 ('Update Check Ignore', None),
84 ('Vercode Operation', None),
85 ('Update Check Name', None),
86 ('Update Check Data', None),
87 ('Current Version', ''),
88 ('Current Version Code', '0'),
89 ('No Source Since', ''),
93 # In the order in which they are laid out on files
94 # Sorted by their action and their place in the build timeline
95 # These variables can have varying datatypes. For example, anything with
96 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
97 flag_defaults = OrderedDict([
101 ('submodules', False),
109 ('oldsdkloc', False),
111 ('forceversion', False),
112 ('forcevercode', False),
116 ('update', ['auto']),
122 ('ndk', 'r10e'), # defaults to latest
125 ('antcommands', None),
130 # Designates a metadata field type and checks that it matches
132 # 'name' - The long name of the field type
133 # 'matching' - List of possible values or regex expression
134 # 'sep' - Separator to use if value may be a list
135 # 'fields' - Metadata fields (Field:Value) of this type
136 # 'attrs' - Build attributes (attr=value) of this type
138 class FieldValidator():
140 def __init__(self, name, matching, sep, fields, attrs):
142 self.matching = matching
143 if type(matching) is str:
144 self.compiled = re.compile(matching)
149 def _assert_regex(self, values, appid):
151 if not self.compiled.match(v):
152 raise MetaDataException("'%s' is not a valid %s in %s. "
153 % (v, self.name, appid) +
154 "Regex pattern: %s" % (self.matching))
156 def _assert_list(self, values, appid):
158 if v not in self.matching:
159 raise MetaDataException("'%s' is not a valid %s in %s. "
160 % (v, self.name, appid) +
161 "Possible values: %s" % (", ".join(self.matching)))
163 def check(self, value, appid):
164 if type(value) is not str or not value:
166 if self.sep is not None:
167 values = value.split(self.sep)
170 if type(self.matching) is list:
171 self._assert_list(values, appid)
173 self._assert_regex(values, appid)
176 # Generic value types
178 FieldValidator("Integer",
179 r'^[1-9][0-9]*$', None,
183 FieldValidator("Hexadecimal",
184 r'^[0-9a-f]+$', None,
188 FieldValidator("HTTP link",
189 r'^http[s]?://', None,
190 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
192 FieldValidator("Bitcoin address",
193 r'^[a-zA-Z0-9]{27,34}$', None,
197 FieldValidator("Litecoin address",
198 r'^L[a-zA-Z0-9]{33}$', None,
202 FieldValidator("Dogecoin address",
203 r'^D[a-zA-Z0-9]{33}$', None,
207 FieldValidator("bool",
208 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
210 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
213 FieldValidator("Repo Type",
214 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
218 FieldValidator("Binaries",
219 r'^http[s]?://', None,
223 FieldValidator("Archive Policy",
224 r'^[0-9]+ versions$', None,
228 FieldValidator("Anti-Feature",
229 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
233 FieldValidator("Auto Update Mode",
234 r"^(Version .+|None)$", None,
235 ["Auto Update Mode"],
238 FieldValidator("Update Check Mode",
239 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
240 ["Update Check Mode"],
245 # Check an app's metadata information for integrity errors
246 def check_metadata(info):
248 for field in v.fields:
249 v.check(info[field], info['id'])
250 for build in info['builds']:
252 v.check(build[attr], info['id'])
255 # Formatter for descriptions. Create an instance, and call parseline() with
256 # each line of the description source from the metadata. At the end, call
257 # end() and then text_wiki and text_html will contain the result.
258 class DescriptionFormatter:
270 def __init__(self, linkres):
271 self.linkResolver = linkres
273 def endcur(self, notstates=None):
274 if notstates and self.state in notstates:
276 if self.state == self.stPARA:
278 elif self.state == self.stUL:
280 elif self.state == self.stOL:
284 self.text_html += '</p>'
285 self.state = self.stNONE
288 self.text_html += '</ul>'
289 self.state = self.stNONE
292 self.text_html += '</ol>'
293 self.state = self.stNONE
295 def formatted(self, txt, html):
298 txt = cgi.escape(txt)
300 index = txt.find("''")
302 return formatted + txt
303 formatted += txt[:index]
305 if txt.startswith("'''"):
311 self.bold = not self.bold
319 self.ital = not self.ital
322 def linkify(self, txt):
326 index = txt.find("[")
328 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
329 linkified_plain += self.formatted(txt[:index], False)
330 linkified_html += self.formatted(txt[:index], True)
332 if txt.startswith("[["):
333 index = txt.find("]]")
335 raise MetaDataException("Unterminated ]]")
337 if self.linkResolver:
338 url, urltext = self.linkResolver(url)
341 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
342 linkified_plain += urltext
343 txt = txt[index + 2:]
345 index = txt.find("]")
347 raise MetaDataException("Unterminated ]")
349 index2 = url.find(' ')
353 urltxt = url[index2 + 1:]
356 raise MetaDataException("Url title is just the URL - use [url]")
357 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
358 linkified_plain += urltxt
360 linkified_plain += ' (' + url + ')'
361 txt = txt[index + 1:]
363 def addtext(self, txt):
364 p, h = self.linkify(txt)
367 def parseline(self, line):
368 self.text_wiki += "%s\n" % line
371 elif line.startswith('* '):
372 self.endcur([self.stUL])
373 if self.state != self.stUL:
374 self.text_html += '<ul>'
375 self.state = self.stUL
376 self.text_html += '<li>'
377 self.addtext(line[1:])
378 self.text_html += '</li>'
379 elif line.startswith('# '):
380 self.endcur([self.stOL])
381 if self.state != self.stOL:
382 self.text_html += '<ol>'
383 self.state = self.stOL
384 self.text_html += '<li>'
385 self.addtext(line[1:])
386 self.text_html += '</li>'
388 self.endcur([self.stPARA])
389 if self.state == self.stNONE:
390 self.text_html += '<p>'
391 self.state = self.stPARA
392 elif self.state == self.stPARA:
393 self.text_html += ' '
400 # Parse multiple lines of description as written in a metadata file, returning
401 # a single string in wiki format. Used for the Maintainer Notes field as well,
402 # because it's the same format.
403 def description_wiki(lines):
404 ps = DescriptionFormatter(None)
411 # Parse multiple lines of description as written in a metadata file, returning
412 # a single string in HTML format.
413 def description_html(lines, linkres):
414 ps = DescriptionFormatter(linkres)
421 def parse_srclib(metadatapath):
425 # Defaults for fields that come from metadata
426 thisinfo['Repo Type'] = ''
427 thisinfo['Repo'] = ''
428 thisinfo['Subdir'] = None
429 thisinfo['Prepare'] = None
431 if not os.path.exists(metadatapath):
434 metafile = open(metadatapath, "r")
437 for line in metafile:
439 line = line.rstrip('\r\n')
440 if not line or line.startswith("#"):
444 field, value = line.split(':', 1)
446 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
448 if field == "Subdir":
449 thisinfo[field] = value.split(',')
451 thisinfo[field] = value
457 """Read all srclib metadata.
459 The information read will be accessible as metadata.srclibs, which is a
460 dictionary, keyed on srclib name, with the values each being a dictionary
461 in the same format as that returned by the parse_srclib function.
463 A MetaDataException is raised if there are any problems with the srclib
468 # They were already loaded
469 if srclibs is not None:
475 if not os.path.exists(srcdir):
478 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
479 srclibname = os.path.basename(metadatapath[:-4])
480 srclibs[srclibname] = parse_srclib(metadatapath)
483 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
484 # returned by the parse_txt_metadata function.
485 def read_metadata(xref=True):
487 # Always read the srclibs before the apps, since they can use a srlib as
488 # their source repository.
493 for basedir in ('metadata', 'tmp'):
494 if not os.path.exists(basedir):
497 # If there are multiple metadata files for a single appid, then the first
498 # file that is parsed wins over all the others, and the rest throw an
499 # exception. So the original .txt format is parsed first, at least until
500 # newer formats stabilize.
502 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
503 + glob.glob(os.path.join('metadata', '*.json'))
504 + glob.glob(os.path.join('metadata', '*.xml'))
505 + glob.glob(os.path.join('metadata', '*.yaml'))):
506 appid, appinfo = parse_metadata(apps, metadatapath)
507 check_metadata(appinfo)
508 apps[appid] = appinfo
511 # Parse all descriptions at load time, just to ensure cross-referencing
512 # errors are caught early rather than when they hit the build server.
515 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
516 raise MetaDataException("Cannot resolve app id " + appid)
518 for appid, app in apps.iteritems():
520 description_html(app['Description'], linkres)
521 except MetaDataException, e:
522 raise MetaDataException("Problem with description of " + appid +
528 # Get the type expected for a given metadata field.
529 def metafieldtype(name):
530 if name in ['Description', 'Maintainer Notes']:
532 if name in ['Categories', 'AntiFeatures']:
534 if name == 'Build Version':
538 if name == 'Use Built':
540 if name not in app_defaults:
546 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
547 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
550 if name in ['init', 'prebuild', 'build']:
552 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
558 def fill_build_defaults(build):
560 def get_build_type():
561 for t in ['maven', 'gradle', 'kivy']:
568 for flag, value in flag_defaults.iteritems():
572 build['type'] = get_build_type()
573 build['ndk_path'] = common.get_ndk_path(build['ndk'])
576 def split_list_values(s):
577 # Port legacy ';' separators
578 l = [v.strip() for v in s.replace(';', ',').split(',')]
579 return [v for v in l if v]
582 def get_default_app_info_list(apps, metadatapath=None):
583 if metadatapath is None:
586 appid = os.path.splitext(os.path.basename(metadatapath))[0]
588 logging.critical("'%s' is a duplicate! '%s' is already provided by '%s'"
589 % (metadatapath, appid, apps[appid]['metadatapath']))
593 thisinfo.update(app_defaults)
594 thisinfo['metadatapath'] = metadatapath
595 if appid is not None:
596 thisinfo['id'] = appid
598 # General defaults...
599 thisinfo['builds'] = []
600 thisinfo['comments'] = []
602 return appid, thisinfo
605 def post_metadata_parse(thisinfo):
607 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id', 'metadatapath']
608 for k, v in thisinfo.iteritems():
609 if k not in supported_metadata:
610 raise MetaDataException("Unrecognised metadata: {0}: {1}"
612 if type(v) in (float, int):
615 # convert to the odd internal format
616 for k in ('Description', 'Maintainer Notes'):
617 if isinstance(thisinfo[k], basestring):
618 text = thisinfo[k].rstrip().lstrip()
619 thisinfo[k] = text.split('\n')
621 supported_flags = (flag_defaults.keys()
622 + ['vercode', 'version', 'versionCode', 'versionName'])
623 esc_newlines = re.compile('\\\\( |\\n)')
625 for build in thisinfo['builds']:
626 for k, v in build.items():
627 if k not in supported_flags:
628 raise MetaDataException("Unrecognised build flag: {0}={1}"
631 if k == 'versionCode':
632 build['vercode'] = str(v)
633 del build['versionCode']
634 elif k == 'versionName':
635 build['version'] = str(v)
636 del build['versionName']
637 elif type(v) in (float, int):
640 keyflagtype = flagtype(k)
641 if keyflagtype == 'list':
642 # these can be bools, strings or lists, but ultimately are lists
643 if isinstance(v, basestring):
645 elif isinstance(v, bool):
650 elif keyflagtype == 'script':
651 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
652 elif keyflagtype == 'bool':
653 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
654 if isinstance(v, basestring):
660 if not thisinfo['Description']:
661 thisinfo['Description'].append('No description available')
663 for build in thisinfo['builds']:
664 fill_build_defaults(build)
666 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
669 # Parse metadata for a single application.
671 # 'metadatapath' - the filename to read. The package id for the application comes
672 # from this filename. Pass None to get a blank entry.
674 # Returns a dictionary containing all the details of the application. There are
675 # two major kinds of information in the dictionary. Keys beginning with capital
676 # letters correspond directory to identically named keys in the metadata file.
677 # Keys beginning with lower case letters are generated in one way or another,
678 # and are not found verbatim in the metadata.
680 # Known keys not originating from the metadata are:
682 # 'builds' - a list of dictionaries containing build information
683 # for each defined build
684 # 'comments' - a list of comments from the metadata file. Each is
685 # a list of the form [field, comment] where field is
686 # the name of the field it preceded in the metadata
687 # file. Where field is None, the comment goes at the
688 # end of the file. Alternatively, 'build:version' is
689 # for a comment before a particular build version.
690 # 'descriptionlines' - original lines of description as formatted in the
695 def _decode_list(data):
696 '''convert items in a list from unicode to basestring'''
699 if isinstance(item, unicode):
700 item = item.encode('utf-8')
701 elif isinstance(item, list):
702 item = _decode_list(item)
703 elif isinstance(item, dict):
704 item = _decode_dict(item)
709 def _decode_dict(data):
710 '''convert items in a dict from unicode to basestring'''
712 for key, value in data.iteritems():
713 if isinstance(key, unicode):
714 key = key.encode('utf-8')
715 if isinstance(value, unicode):
716 value = value.encode('utf-8')
717 elif isinstance(value, list):
718 value = _decode_list(value)
719 elif isinstance(value, dict):
720 value = _decode_dict(value)
725 def parse_metadata(apps, metadatapath):
726 root, ext = os.path.splitext(metadatapath)
727 metadataformat = ext[1:]
728 accepted = common.config['accepted_formats']
729 if metadataformat not in accepted:
730 logging.critical('"' + metadatapath
731 + '" is not in an accepted format, '
732 + 'convert to: ' + ', '.join(accepted))
735 if metadataformat == 'txt':
736 return parse_txt_metadata(apps, metadatapath)
737 elif metadataformat == 'json':
738 return parse_json_metadata(apps, metadatapath)
739 elif metadataformat == 'xml':
740 return parse_xml_metadata(apps, metadatapath)
741 elif metadataformat == 'yaml':
742 return parse_yaml_metadata(apps, metadatapath)
744 logging.critical('Unknown metadata format: ' + metadatapath)
748 def parse_json_metadata(apps, metadatapath):
750 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
752 # fdroid metadata is only strings and booleans, no floats or ints. And
753 # json returns unicode, and fdroidserver still uses plain python strings
754 # TODO create schema using https://pypi.python.org/pypi/jsonschema
755 jsoninfo = json.load(open(metadatapath, 'r'),
756 object_hook=_decode_dict,
757 parse_int=lambda s: s,
758 parse_float=lambda s: s)
759 thisinfo.update(jsoninfo)
760 post_metadata_parse(thisinfo)
762 return (appid, thisinfo)
765 def parse_xml_metadata(apps, metadatapath):
767 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
769 tree = ElementTree.ElementTree(file=metadatapath)
770 root = tree.getroot()
772 if root.tag != 'resources':
773 logging.critical(metadatapath + ' does not have root as <resources></resources>!')
776 supported_metadata = app_defaults.keys()
778 if child.tag != 'builds':
779 # builds does not have name="" attrib
780 name = child.attrib['name']
781 if name not in supported_metadata:
782 raise MetaDataException("Unrecognised metadata: <"
783 + child.tag + ' name="' + name + '">'
785 + "</" + child.tag + '>')
787 if child.tag == 'string':
788 thisinfo[name] = child.text
789 elif child.tag == 'string-array':
792 items.append(item.text)
793 thisinfo[name] = items
794 elif child.tag == 'builds':
799 builddict[key.tag] = key.text
800 builds.append(builddict)
801 thisinfo['builds'] = builds
803 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
804 if not isinstance(thisinfo['Requires Root'], bool):
805 if thisinfo['Requires Root'] == 'true':
806 thisinfo['Requires Root'] = True
808 thisinfo['Requires Root'] = False
810 post_metadata_parse(thisinfo)
812 return (appid, thisinfo)
815 def parse_yaml_metadata(apps, metadatapath):
817 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
819 yamlinfo = yaml.load(open(metadatapath, 'r'), Loader=YamlLoader)
820 thisinfo.update(yamlinfo)
821 post_metadata_parse(thisinfo)
823 return (appid, thisinfo)
826 def parse_txt_metadata(apps, metadatapath):
830 def add_buildflag(p, thisbuild):
832 raise MetaDataException("Empty build flag at {1}"
833 .format(buildlines[0], linedesc))
836 raise MetaDataException("Invalid build flag at {0} in {1}"
837 .format(buildlines[0], linedesc))
840 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
841 .format(pk, thisbuild['version'], linedesc))
844 if pk not in flag_defaults:
845 raise MetaDataException("Unrecognised build flag at {0} in {1}"
846 .format(p, linedesc))
849 pv = split_list_values(pv)
851 if len(pv) == 1 and pv[0] in ['main', 'yes']:
854 elif t == 'string' or t == 'script':
861 logging.debug("...ignoring bool flag %s" % p)
864 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
867 def parse_buildline(lines):
868 value = "".join(lines)
869 parts = [p.replace("\\,", ",")
870 for p in re.split(r"(?<!\\),", value)]
872 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
874 thisbuild['origlines'] = lines
875 thisbuild['version'] = parts[0]
876 thisbuild['vercode'] = parts[1]
877 if parts[2].startswith('!'):
878 # For backwards compatibility, handle old-style disabling,
879 # including attempting to extract the commit from the message
880 thisbuild['disable'] = parts[2][1:]
881 commit = 'unknown - see disabled'
882 index = parts[2].rfind('at ')
884 commit = parts[2][index + 3:]
885 if commit.endswith(')'):
887 thisbuild['commit'] = commit
889 thisbuild['commit'] = parts[2]
891 add_buildflag(p, thisbuild)
895 def add_comments(key):
898 for comment in curcomments:
899 thisinfo['comments'].append([key, comment])
902 appid, thisinfo = get_default_app_info_list(apps, metadatapath)
903 metafile = open(metadatapath, "r")
912 for line in metafile:
914 linedesc = "%s:%d" % (metafile.name, c)
915 line = line.rstrip('\r\n')
917 if not any(line.startswith(s) for s in (' ', '\t')):
918 commit = curbuild['commit'] if 'commit' in curbuild else None
919 if not commit and 'disable' not in curbuild:
920 raise MetaDataException("No commit specified for {0} in {1}"
921 .format(curbuild['version'], linedesc))
923 thisinfo['builds'].append(curbuild)
924 add_comments('build:' + curbuild['vercode'])
927 if line.endswith('\\'):
928 buildlines.append(line[:-1].lstrip())
930 buildlines.append(line.lstrip())
931 bl = ''.join(buildlines)
932 add_buildflag(bl, curbuild)
938 if line.startswith("#"):
939 curcomments.append(line)
942 field, value = line.split(':', 1)
944 raise MetaDataException("Invalid metadata in " + linedesc)
945 if field != field.strip() or value != value.strip():
946 raise MetaDataException("Extra spacing found in " + linedesc)
948 # Translate obsolete fields...
949 if field == 'Market Version':
950 field = 'Current Version'
951 if field == 'Market Version Code':
952 field = 'Current Version Code'
954 fieldtype = metafieldtype(field)
955 if fieldtype not in ['build', 'buildv2']:
957 if fieldtype == 'multiline':
961 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
962 elif fieldtype == 'string':
963 thisinfo[field] = value
964 elif fieldtype == 'list':
965 thisinfo[field] = split_list_values(value)
966 elif fieldtype == 'build':
967 if value.endswith("\\"):
969 buildlines = [value[:-1]]
971 curbuild = parse_buildline([value])
972 thisinfo['builds'].append(curbuild)
973 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
974 elif fieldtype == 'buildv2':
976 vv = value.split(',')
978 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
979 .format(value, linedesc))
980 curbuild['version'] = vv[0]
981 curbuild['vercode'] = vv[1]
982 if curbuild['vercode'] in vc_seen:
983 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
984 curbuild['vercode'], linedesc))
985 vc_seen[curbuild['vercode']] = True
988 elif fieldtype == 'obsolete':
989 pass # Just throw it away!
991 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
992 elif mode == 1: # Multiline field
996 thisinfo[field].append(line)
997 elif mode == 2: # Line continuation mode in Build Version
998 if line.endswith("\\"):
999 buildlines.append(line[:-1])
1001 buildlines.append(line)
1002 curbuild = parse_buildline(buildlines)
1003 thisinfo['builds'].append(curbuild)
1004 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
1008 # Mode at end of file should always be 0...
1010 raise MetaDataException(field + " not terminated in " + metafile.name)
1012 raise MetaDataException("Unterminated continuation in " + metafile.name)
1014 raise MetaDataException("Unterminated build in " + metafile.name)
1016 post_metadata_parse(thisinfo)
1018 return (appid, thisinfo)
1021 # Write a metadata file.
1023 # 'dest' - The path to the output file
1024 # 'app' - The app data
1025 def write_metadata(dest, app):
1027 def writecomments(key):
1029 for pf, comment in app['comments']:
1031 mf.write("%s\n" % comment)
1034 logging.debug("...writing comments for " + (key or 'EOF'))
1036 def writefield(field, value=None):
1037 writecomments(field)
1040 t = metafieldtype(field)
1042 value = ','.join(value)
1043 mf.write("%s:%s\n" % (field, value))
1045 def writefield_nonempty(field, value=None):
1049 writefield(field, value)
1051 mf = open(dest, 'w')
1052 writefield_nonempty('Disabled')
1053 if app['AntiFeatures']:
1054 writefield('AntiFeatures')
1055 writefield_nonempty('Provides')
1056 writefield('Categories')
1057 writefield('License')
1058 writefield('Web Site')
1059 writefield('Source Code')
1060 writefield('Issue Tracker')
1061 writefield_nonempty('Changelog')
1062 writefield_nonempty('Donate')
1063 writefield_nonempty('FlattrID')
1064 writefield_nonempty('Bitcoin')
1065 writefield_nonempty('Litecoin')
1066 writefield_nonempty('Dogecoin')
1068 writefield_nonempty('Name')
1069 writefield_nonempty('Auto Name')
1070 writefield('Summary')
1071 writefield('Description', '')
1072 for line in app['Description']:
1073 mf.write("%s\n" % line)
1076 if app['Requires Root']:
1077 writefield('Requires Root', 'yes')
1079 if app['Repo Type']:
1080 writefield('Repo Type')
1083 writefield('Binaries')
1085 for build in app['builds']:
1087 if build['version'] == "Ignore":
1090 writecomments('build:' + build['vercode'])
1091 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1093 def write_builditem(key, value):
1095 if key in ['version', 'vercode']:
1098 if value == flag_defaults[key]:
1103 logging.debug("...writing {0} : {1}".format(key, value))
1104 outline = ' %s=' % key
1111 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1113 outline += ','.join(value) if type(value) == list else value
1118 for flag in flag_defaults:
1121 write_builditem(flag, value)
1124 if app['Maintainer Notes']:
1125 writefield('Maintainer Notes', '')
1126 for line in app['Maintainer Notes']:
1127 mf.write("%s\n" % line)
1131 writefield_nonempty('Archive Policy')
1132 writefield('Auto Update Mode')
1133 writefield('Update Check Mode')
1134 writefield_nonempty('Update Check Ignore')
1135 writefield_nonempty('Vercode Operation')
1136 writefield_nonempty('Update Check Name')
1137 writefield_nonempty('Update Check Data')
1138 if app['Current Version']:
1139 writefield('Current Version')
1140 writefield('Current Version Code')
1142 if app['No Source Since']:
1143 writefield('No Source Since')