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/>.
28 # use the C implementation when available
29 import xml.etree.cElementTree as ElementTree
31 from collections import OrderedDict
38 class MetaDataException(Exception):
40 def __init__(self, value):
46 # In the order in which they are laid out on files
47 app_defaults = OrderedDict([
51 ('Categories', ['None']),
52 ('License', 'Unknown'),
55 ('Issue Tracker', ''),
66 ('Requires Root', False),
70 ('Maintainer Notes', []),
71 ('Archive Policy', None),
72 ('Auto Update Mode', 'None'),
73 ('Update Check Mode', 'None'),
74 ('Update Check Ignore', None),
75 ('Vercode Operation', None),
76 ('Update Check Name', None),
77 ('Update Check Data', None),
78 ('Current Version', ''),
79 ('Current Version Code', '0'),
80 ('No Source Since', ''),
84 # In the order in which they are laid out on files
85 # Sorted by their action and their place in the build timeline
86 # These variables can have varying datatypes. For example, anything with
87 # flagtype(v) == 'list' is inited as False, then set as a list of strings.
88 flag_defaults = OrderedDict([
92 ('submodules', False),
100 ('oldsdkloc', False),
102 ('forceversion', False),
103 ('forcevercode', False),
107 ('update', ['auto']),
113 ('ndk', 'r10e'), # defaults to latest
116 ('antcommands', None),
121 # Designates a metadata field type and checks that it matches
123 # 'name' - The long name of the field type
124 # 'matching' - List of possible values or regex expression
125 # 'sep' - Separator to use if value may be a list
126 # 'fields' - Metadata fields (Field:Value) of this type
127 # 'attrs' - Build attributes (attr=value) of this type
129 class FieldValidator():
131 def __init__(self, name, matching, sep, fields, attrs):
133 self.matching = matching
134 if type(matching) is str:
135 self.compiled = re.compile(matching)
140 def _assert_regex(self, values, appid):
142 if not self.compiled.match(v):
143 raise MetaDataException("'%s' is not a valid %s in %s. "
144 % (v, self.name, appid) +
145 "Regex pattern: %s" % (self.matching))
147 def _assert_list(self, values, appid):
149 if v not in self.matching:
150 raise MetaDataException("'%s' is not a valid %s in %s. "
151 % (v, self.name, appid) +
152 "Possible values: %s" % (", ".join(self.matching)))
154 def check(self, value, appid):
155 if type(value) is not str or not value:
157 if self.sep is not None:
158 values = value.split(self.sep)
161 if type(self.matching) is list:
162 self._assert_list(values, appid)
164 self._assert_regex(values, appid)
167 # Generic value types
169 FieldValidator("Integer",
170 r'^[1-9][0-9]*$', None,
174 FieldValidator("Hexadecimal",
175 r'^[0-9a-f]+$', None,
179 FieldValidator("HTTP link",
180 r'^http[s]?://', None,
181 ["Web Site", "Source Code", "Issue Tracker", "Changelog", "Donate"], []),
183 FieldValidator("Bitcoin address",
184 r'^[a-zA-Z0-9]{27,34}$', None,
188 FieldValidator("Litecoin address",
189 r'^L[a-zA-Z0-9]{33}$', None,
193 FieldValidator("Dogecoin address",
194 r'^D[a-zA-Z0-9]{33}$', None,
198 FieldValidator("bool",
199 r'([Yy]es|[Nn]o|[Tt]rue|[Ff]alse)', None,
201 ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
204 FieldValidator("Repo Type",
205 ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
209 FieldValidator("Binaries",
210 r'^http[s]?://', None,
214 FieldValidator("Archive Policy",
215 r'^[0-9]+ versions$', None,
219 FieldValidator("Anti-Feature",
220 ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
224 FieldValidator("Auto Update Mode",
225 r"^(Version .+|None)$", None,
226 ["Auto Update Mode"],
229 FieldValidator("Update Check Mode",
230 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
231 ["Update Check Mode"],
236 # Check an app's metadata information for integrity errors
237 def check_metadata(info):
239 for field in v.fields:
240 v.check(info[field], info['id'])
241 for build in info['builds']:
243 v.check(build[attr], info['id'])
246 # Formatter for descriptions. Create an instance, and call parseline() with
247 # each line of the description source from the metadata. At the end, call
248 # end() and then text_wiki and text_html will contain the result.
249 class DescriptionFormatter:
261 def __init__(self, linkres):
262 self.linkResolver = linkres
264 def endcur(self, notstates=None):
265 if notstates and self.state in notstates:
267 if self.state == self.stPARA:
269 elif self.state == self.stUL:
271 elif self.state == self.stOL:
275 self.text_html += '</p>'
276 self.state = self.stNONE
279 self.text_html += '</ul>'
280 self.state = self.stNONE
283 self.text_html += '</ol>'
284 self.state = self.stNONE
286 def formatted(self, txt, html):
289 txt = cgi.escape(txt)
291 index = txt.find("''")
293 return formatted + txt
294 formatted += txt[:index]
296 if txt.startswith("'''"):
302 self.bold = not self.bold
310 self.ital = not self.ital
313 def linkify(self, txt):
317 index = txt.find("[")
319 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
320 linkified_plain += self.formatted(txt[:index], False)
321 linkified_html += self.formatted(txt[:index], True)
323 if txt.startswith("[["):
324 index = txt.find("]]")
326 raise MetaDataException("Unterminated ]]")
328 if self.linkResolver:
329 url, urltext = self.linkResolver(url)
332 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
333 linkified_plain += urltext
334 txt = txt[index + 2:]
336 index = txt.find("]")
338 raise MetaDataException("Unterminated ]")
340 index2 = url.find(' ')
344 urltxt = url[index2 + 1:]
347 raise MetaDataException("Url title is just the URL - use [url]")
348 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
349 linkified_plain += urltxt
351 linkified_plain += ' (' + url + ')'
352 txt = txt[index + 1:]
354 def addtext(self, txt):
355 p, h = self.linkify(txt)
358 def parseline(self, line):
359 self.text_wiki += "%s\n" % line
362 elif line.startswith('* '):
363 self.endcur([self.stUL])
364 if self.state != self.stUL:
365 self.text_html += '<ul>'
366 self.state = self.stUL
367 self.text_html += '<li>'
368 self.addtext(line[1:])
369 self.text_html += '</li>'
370 elif line.startswith('# '):
371 self.endcur([self.stOL])
372 if self.state != self.stOL:
373 self.text_html += '<ol>'
374 self.state = self.stOL
375 self.text_html += '<li>'
376 self.addtext(line[1:])
377 self.text_html += '</li>'
379 self.endcur([self.stPARA])
380 if self.state == self.stNONE:
381 self.text_html += '<p>'
382 self.state = self.stPARA
383 elif self.state == self.stPARA:
384 self.text_html += ' '
391 # Parse multiple lines of description as written in a metadata file, returning
392 # a single string in wiki format. Used for the Maintainer Notes field as well,
393 # because it's the same format.
394 def description_wiki(lines):
395 ps = DescriptionFormatter(None)
402 # Parse multiple lines of description as written in a metadata file, returning
403 # a single string in HTML format.
404 def description_html(lines, linkres):
405 ps = DescriptionFormatter(linkres)
412 def parse_srclib(metafile):
415 if metafile and not isinstance(metafile, file):
416 metafile = open(metafile, "r")
418 # Defaults for fields that come from metadata
419 thisinfo['Repo Type'] = ''
420 thisinfo['Repo'] = ''
421 thisinfo['Subdir'] = None
422 thisinfo['Prepare'] = None
428 for line in metafile:
430 line = line.rstrip('\r\n')
431 if not line or line.startswith("#"):
435 field, value = line.split(':', 1)
437 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
439 if field == "Subdir":
440 thisinfo[field] = value.split(',')
442 thisinfo[field] = value
448 """Read all srclib metadata.
450 The information read will be accessible as metadata.srclibs, which is a
451 dictionary, keyed on srclib name, with the values each being a dictionary
452 in the same format as that returned by the parse_srclib function.
454 A MetaDataException is raised if there are any problems with the srclib
459 # They were already loaded
460 if srclibs is not None:
466 if not os.path.exists(srcdir):
469 for metafile in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
470 srclibname = os.path.basename(metafile[:-4])
471 srclibs[srclibname] = parse_srclib(metafile)
474 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
475 # returned by the parse_txt_metadata function.
476 def read_metadata(xref=True):
478 # Always read the srclibs before the apps, since they can use a srlib as
479 # their source repository.
484 for basedir in ('metadata', 'tmp'):
485 if not os.path.exists(basedir):
488 for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
489 appid, appinfo = parse_txt_metadata(metafile)
490 check_metadata(appinfo)
491 apps[appid] = appinfo
493 for metafile in sorted(glob.glob(os.path.join('metadata', '*.json'))):
494 appid, appinfo = parse_json_metadata(metafile)
495 check_metadata(appinfo)
496 apps[appid] = appinfo
498 for metafile in sorted(glob.glob(os.path.join('metadata', '*.xml'))):
499 appid, appinfo = parse_xml_metadata(metafile)
500 check_metadata(appinfo)
501 apps[appid] = appinfo
504 # Parse all descriptions at load time, just to ensure cross-referencing
505 # errors are caught early rather than when they hit the build server.
508 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
509 raise MetaDataException("Cannot resolve app id " + appid)
511 for appid, app in apps.iteritems():
513 description_html(app['Description'], linkres)
514 except MetaDataException, e:
515 raise MetaDataException("Problem with description of " + appid +
521 # Get the type expected for a given metadata field.
522 def metafieldtype(name):
523 if name in ['Description', 'Maintainer Notes']:
525 if name in ['Categories', 'AntiFeatures']:
527 if name == 'Build Version':
531 if name == 'Use Built':
533 if name not in app_defaults:
539 if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni', 'preassemble',
540 'update', 'scanignore', 'scandelete', 'gradle', 'antcommands',
543 if name in ['init', 'prebuild', 'build']:
545 if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
551 def fill_build_defaults(build):
553 def get_build_type():
554 for t in ['maven', 'gradle', 'kivy']:
561 for flag, value in flag_defaults.iteritems():
565 build['type'] = get_build_type()
566 build['ndk_path'] = common.get_ndk_path(build['ndk'])
569 def split_list_values(s):
570 # Port legacy ';' separators
571 l = [v.strip() for v in s.replace(';', ',').split(',')]
572 return [v for v in l if v]
575 def get_default_app_info_list(appid=None):
577 thisinfo.update(app_defaults)
578 if appid is not None:
579 thisinfo['id'] = appid
581 # General defaults...
582 thisinfo['builds'] = []
583 thisinfo['comments'] = []
588 def post_metadata_parse(thisinfo):
590 supported_metadata = app_defaults.keys() + ['comments', 'builds', 'id']
591 for k, v in thisinfo.iteritems():
592 if k not in supported_metadata:
593 raise MetaDataException("Unrecognised metadata: {0}: {1}"
595 if type(v) in (float, int):
598 # convert to the odd internal format
599 for k in ('Description', 'Maintainer Notes'):
600 if isinstance(thisinfo[k], basestring):
601 text = thisinfo[k].rstrip().lstrip()
602 thisinfo[k] = text.split('\n')
604 supported_flags = (flag_defaults.keys()
605 + ['vercode', 'version', 'versionCode', 'versionName'])
606 esc_newlines = re.compile('\\\\( |\\n)')
608 for build in thisinfo['builds']:
609 for k, v in build.items():
610 if k not in supported_flags:
611 raise MetaDataException("Unrecognised build flag: {0}={1}"
614 if k == 'versionCode':
615 build['vercode'] = str(v)
616 del build['versionCode']
617 elif k == 'versionName':
618 build['version'] = str(v)
619 del build['versionName']
620 elif type(v) in (float, int):
623 keyflagtype = flagtype(k)
624 if keyflagtype == 'list':
625 # these can be bools, strings or lists, but ultimately are lists
626 if isinstance(v, basestring):
628 elif isinstance(v, bool):
633 elif keyflagtype == 'script':
634 build[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
635 elif keyflagtype == 'bool':
636 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
637 if isinstance(v, basestring):
643 if not thisinfo['Description']:
644 thisinfo['Description'].append('No description available')
646 for build in thisinfo['builds']:
647 fill_build_defaults(build)
649 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
652 # Parse metadata for a single application.
654 # 'metafile' - the filename to read. The package id for the application comes
655 # from this filename. Pass None to get a blank entry.
657 # Returns a dictionary containing all the details of the application. There are
658 # two major kinds of information in the dictionary. Keys beginning with capital
659 # letters correspond directory to identically named keys in the metadata file.
660 # Keys beginning with lower case letters are generated in one way or another,
661 # and are not found verbatim in the metadata.
663 # Known keys not originating from the metadata are:
665 # 'builds' - a list of dictionaries containing build information
666 # for each defined build
667 # 'comments' - a list of comments from the metadata file. Each is
668 # a list of the form [field, comment] where field is
669 # the name of the field it preceded in the metadata
670 # file. Where field is None, the comment goes at the
671 # end of the file. Alternatively, 'build:version' is
672 # for a comment before a particular build version.
673 # 'descriptionlines' - original lines of description as formatted in the
678 def _decode_list(data):
679 '''convert items in a list from unicode to basestring'''
682 if isinstance(item, unicode):
683 item = item.encode('utf-8')
684 elif isinstance(item, list):
685 item = _decode_list(item)
686 elif isinstance(item, dict):
687 item = _decode_dict(item)
692 def _decode_dict(data):
693 '''convert items in a dict from unicode to basestring'''
695 for key, value in data.iteritems():
696 if isinstance(key, unicode):
697 key = key.encode('utf-8')
698 if isinstance(value, unicode):
699 value = value.encode('utf-8')
700 elif isinstance(value, list):
701 value = _decode_list(value)
702 elif isinstance(value, dict):
703 value = _decode_dict(value)
708 def parse_json_metadata(metafile):
710 appid = os.path.basename(metafile)[0:-5] # strip path and .json
711 thisinfo = get_default_app_info_list(appid)
713 # fdroid metadata is only strings and booleans, no floats or ints. And
714 # json returns unicode, and fdroidserver still uses plain python strings
715 # TODO create schema using https://pypi.python.org/pypi/jsonschema
716 jsoninfo = json.load(open(metafile, 'r'),
717 object_hook=_decode_dict,
718 parse_int=lambda s: s,
719 parse_float=lambda s: s)
720 thisinfo.update(jsoninfo)
721 post_metadata_parse(thisinfo)
723 return (appid, thisinfo)
726 def parse_xml_metadata(metafile):
728 appid = os.path.basename(metafile)[0:-4] # strip path and .xml
729 thisinfo = get_default_app_info_list(appid)
731 tree = ElementTree.ElementTree(file=metafile)
732 root = tree.getroot()
734 if root.tag != 'resources':
735 logging.critical(metafile + ' does not have root as <resources></resources>!')
738 supported_metadata = app_defaults.keys()
740 if child.tag != 'builds':
741 # builds does not have name="" attrib
742 name = child.attrib['name']
743 if name not in supported_metadata:
744 raise MetaDataException("Unrecognised metadata: <"
745 + child.tag + ' name="' + name + '">'
747 + "</" + child.tag + '>')
749 if child.tag == 'string':
750 thisinfo[name] = child.text
751 elif child.tag == 'string-array':
754 items.append(item.text)
755 thisinfo[name] = items
756 elif child.tag == 'builds':
761 builddict[key.tag] = key.text
762 builds.append(builddict)
763 thisinfo['builds'] = builds
765 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
766 if not isinstance(thisinfo['Requires Root'], bool):
767 if thisinfo['Requires Root'] == 'true':
768 thisinfo['Requires Root'] = True
770 thisinfo['Requires Root'] = False
772 post_metadata_parse(thisinfo)
774 return (appid, thisinfo)
777 def parse_txt_metadata(metafile):
782 def add_buildflag(p, thisbuild):
784 raise MetaDataException("Empty build flag at {1}"
785 .format(buildlines[0], linedesc))
788 raise MetaDataException("Invalid build flag at {0} in {1}"
789 .format(buildlines[0], linedesc))
792 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
793 .format(pk, thisbuild['version'], linedesc))
796 if pk not in flag_defaults:
797 raise MetaDataException("Unrecognised build flag at {0} in {1}"
798 .format(p, linedesc))
801 pv = split_list_values(pv)
803 if len(pv) == 1 and pv[0] in ['main', 'yes']:
806 elif t == 'string' or t == 'script':
813 logging.debug("...ignoring bool flag %s" % p)
816 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
819 def parse_buildline(lines):
820 value = "".join(lines)
821 parts = [p.replace("\\,", ",")
822 for p in re.split(r"(?<!\\),", value)]
824 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
826 thisbuild['origlines'] = lines
827 thisbuild['version'] = parts[0]
828 thisbuild['vercode'] = parts[1]
829 if parts[2].startswith('!'):
830 # For backwards compatibility, handle old-style disabling,
831 # including attempting to extract the commit from the message
832 thisbuild['disable'] = parts[2][1:]
833 commit = 'unknown - see disabled'
834 index = parts[2].rfind('at ')
836 commit = parts[2][index + 3:]
837 if commit.endswith(')'):
839 thisbuild['commit'] = commit
841 thisbuild['commit'] = parts[2]
843 add_buildflag(p, thisbuild)
847 def add_comments(key):
850 for comment in curcomments:
851 thisinfo['comments'].append([key, comment])
854 thisinfo = get_default_app_info_list()
856 if not isinstance(metafile, file):
857 metafile = open(metafile, "r")
858 appid = metafile.name[9:-4]
859 thisinfo['id'] = appid
861 return appid, thisinfo
870 for line in metafile:
872 linedesc = "%s:%d" % (metafile.name, c)
873 line = line.rstrip('\r\n')
875 if not any(line.startswith(s) for s in (' ', '\t')):
876 commit = curbuild['commit'] if 'commit' in curbuild else None
877 if not commit and 'disable' not in curbuild:
878 raise MetaDataException("No commit specified for {0} in {1}"
879 .format(curbuild['version'], linedesc))
881 thisinfo['builds'].append(curbuild)
882 add_comments('build:' + curbuild['vercode'])
885 if line.endswith('\\'):
886 buildlines.append(line[:-1].lstrip())
888 buildlines.append(line.lstrip())
889 bl = ''.join(buildlines)
890 add_buildflag(bl, curbuild)
896 if line.startswith("#"):
897 curcomments.append(line)
900 field, value = line.split(':', 1)
902 raise MetaDataException("Invalid metadata in " + linedesc)
903 if field != field.strip() or value != value.strip():
904 raise MetaDataException("Extra spacing found in " + linedesc)
906 # Translate obsolete fields...
907 if field == 'Market Version':
908 field = 'Current Version'
909 if field == 'Market Version Code':
910 field = 'Current Version Code'
912 fieldtype = metafieldtype(field)
913 if fieldtype not in ['build', 'buildv2']:
915 if fieldtype == 'multiline':
919 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
920 elif fieldtype == 'string':
921 thisinfo[field] = value
922 elif fieldtype == 'list':
923 thisinfo[field] = split_list_values(value)
924 elif fieldtype == 'build':
925 if value.endswith("\\"):
927 buildlines = [value[:-1]]
929 curbuild = parse_buildline([value])
930 thisinfo['builds'].append(curbuild)
931 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
932 elif fieldtype == 'buildv2':
934 vv = value.split(',')
936 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
937 .format(value, linedesc))
938 curbuild['version'] = vv[0]
939 curbuild['vercode'] = vv[1]
940 if curbuild['vercode'] in vc_seen:
941 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
942 curbuild['vercode'], linedesc))
943 vc_seen[curbuild['vercode']] = True
946 elif fieldtype == 'obsolete':
947 pass # Just throw it away!
949 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
950 elif mode == 1: # Multiline field
954 thisinfo[field].append(line)
955 elif mode == 2: # Line continuation mode in Build Version
956 if line.endswith("\\"):
957 buildlines.append(line[:-1])
959 buildlines.append(line)
960 curbuild = parse_buildline(buildlines)
961 thisinfo['builds'].append(curbuild)
962 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
966 # Mode at end of file should always be 0...
968 raise MetaDataException(field + " not terminated in " + metafile.name)
970 raise MetaDataException("Unterminated continuation in " + metafile.name)
972 raise MetaDataException("Unterminated build in " + metafile.name)
974 post_metadata_parse(thisinfo)
976 return (appid, thisinfo)
979 # Write a metadata file.
981 # 'dest' - The path to the output file
982 # 'app' - The app data
983 def write_metadata(dest, app):
985 def writecomments(key):
987 for pf, comment in app['comments']:
989 mf.write("%s\n" % comment)
992 logging.debug("...writing comments for " + (key or 'EOF'))
994 def writefield(field, value=None):
998 t = metafieldtype(field)
1000 value = ','.join(value)
1001 mf.write("%s:%s\n" % (field, value))
1003 def writefield_nonempty(field, value=None):
1007 writefield(field, value)
1009 mf = open(dest, 'w')
1010 writefield_nonempty('Disabled')
1011 writefield('AntiFeatures')
1012 writefield_nonempty('Provides')
1013 writefield('Categories')
1014 writefield('License')
1015 writefield('Web Site')
1016 writefield('Source Code')
1017 writefield('Issue Tracker')
1018 writefield_nonempty('Changelog')
1019 writefield_nonempty('Donate')
1020 writefield_nonempty('FlattrID')
1021 writefield_nonempty('Bitcoin')
1022 writefield_nonempty('Litecoin')
1023 writefield_nonempty('Dogecoin')
1025 writefield_nonempty('Name')
1026 writefield_nonempty('Auto Name')
1027 writefield('Summary')
1028 writefield('Description', '')
1029 for line in app['Description']:
1030 mf.write("%s\n" % line)
1033 if app['Requires Root']:
1034 writefield('Requires Root', 'yes')
1036 if app['Repo Type']:
1037 writefield('Repo Type')
1040 writefield('Binaries')
1042 for build in app['builds']:
1044 if build['version'] == "Ignore":
1047 writecomments('build:' + build['vercode'])
1048 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1050 def write_builditem(key, value):
1052 if key in ['version', 'vercode']:
1055 if value == flag_defaults[key]:
1060 logging.debug("...writing {0} : {1}".format(key, value))
1061 outline = ' %s=' % key
1068 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1070 outline += ','.join(value) if type(value) == list else value
1075 for flag in flag_defaults:
1078 write_builditem(flag, value)
1081 if app['Maintainer Notes']:
1082 writefield('Maintainer Notes', '')
1083 for line in app['Maintainer Notes']:
1084 mf.write("%s\n" % line)
1088 writefield_nonempty('Archive Policy')
1089 writefield('Auto Update Mode')
1090 writefield('Update Check Mode')
1091 writefield_nonempty('Update Check Ignore')
1092 writefield_nonempty('Vercode Operation')
1093 writefield_nonempty('Update Check Name')
1094 writefield_nonempty('Update Check Data')
1095 if app['Current Version']:
1096 writefield('Current Version')
1097 writefield('Current Version Code')
1099 if app['No Source Since']:
1100 writefield('No Source Since')