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():
577 thisinfo.update(app_defaults)
579 # General defaults...
580 thisinfo['builds'] = []
581 thisinfo['comments'] = []
586 def post_metadata_parse(thisinfo):
588 for build in thisinfo['builds']:
589 for k, v in build.iteritems():
590 if k == 'versionCode':
591 build['vercode'] = str(v)
592 del build['versionCode']
593 elif k == 'versionName':
594 build['version'] = str(v)
595 del build['versionName']
597 if not thisinfo['Description']:
598 thisinfo['Description'].append('No description available')
600 for build in thisinfo['builds']:
601 fill_build_defaults(build)
603 thisinfo['builds'] = sorted(thisinfo['builds'], key=lambda build: int(build['vercode']))
606 # Parse metadata for a single application.
608 # 'metafile' - the filename to read. The package id for the application comes
609 # from this filename. Pass None to get a blank entry.
611 # Returns a dictionary containing all the details of the application. There are
612 # two major kinds of information in the dictionary. Keys beginning with capital
613 # letters correspond directory to identically named keys in the metadata file.
614 # Keys beginning with lower case letters are generated in one way or another,
615 # and are not found verbatim in the metadata.
617 # Known keys not originating from the metadata are:
619 # 'builds' - a list of dictionaries containing build information
620 # for each defined build
621 # 'comments' - a list of comments from the metadata file. Each is
622 # a list of the form [field, comment] where field is
623 # the name of the field it preceded in the metadata
624 # file. Where field is None, the comment goes at the
625 # end of the file. Alternatively, 'build:version' is
626 # for a comment before a particular build version.
627 # 'descriptionlines' - original lines of description as formatted in the
632 def _decode_list(data):
633 '''convert items in a list from unicode to basestring'''
636 if isinstance(item, unicode):
637 item = item.encode('utf-8')
638 elif isinstance(item, list):
639 item = _decode_list(item)
640 elif isinstance(item, dict):
641 item = _decode_dict(item)
646 def _decode_dict(data):
647 '''convert items in a dict from unicode to basestring'''
649 for key, value in data.iteritems():
650 if isinstance(key, unicode):
651 key = key.encode('utf-8')
652 if isinstance(value, unicode):
653 value = value.encode('utf-8')
654 elif isinstance(value, list):
655 value = _decode_list(value)
656 elif isinstance(value, dict):
657 value = _decode_dict(value)
662 def parse_json_metadata(metafile):
664 appid = os.path.basename(metafile)[0:-5] # strip path and .json
665 thisinfo = get_default_app_info_list()
666 thisinfo['id'] = appid
668 # fdroid metadata is only strings and booleans, no floats or ints. And
669 # json returns unicode, and fdroidserver still uses plain python strings
670 jsoninfo = json.load(open(metafile, 'r'),
671 object_hook=_decode_dict,
672 parse_int=lambda s: s,
673 parse_float=lambda s: s)
674 supported_metadata = app_defaults.keys() + ['builds', 'comments']
675 for k, v in jsoninfo.iteritems():
676 if k not in supported_metadata:
677 logging.warn(metafile + ' contains unknown metadata key, ignoring: ' + k)
678 thisinfo.update(jsoninfo)
680 for build in thisinfo['builds']:
681 for k, v in build.iteritems():
682 if k in ('buildjni', 'gradle', 'maven', 'kivy'):
683 # convert standard types to mixed datatype legacy format
684 if isinstance(v, bool):
690 # TODO create schema using https://pypi.python.org/pypi/jsonschema
691 post_metadata_parse(thisinfo)
693 return (appid, thisinfo)
696 def parse_xml_metadata(metafile):
698 appid = os.path.basename(metafile)[0:-4] # strip path and .xml
699 thisinfo = get_default_app_info_list()
700 thisinfo['id'] = appid
702 tree = ElementTree.ElementTree(file=metafile)
703 root = tree.getroot()
705 if root.tag != 'resources':
706 logging.critical(metafile + ' does not have root as <resources></resources>!')
709 supported_metadata = app_defaults.keys()
711 if child.tag != 'builds':
712 # builds does not have name="" attrib
713 name = child.attrib['name']
714 if name not in supported_metadata:
715 raise MetaDataException("Unrecognised metadata: <"
716 + child.tag + ' name="' + name + '">'
718 + "</" + child.tag + '>')
720 if child.tag == 'string':
721 thisinfo[name] = child.text
722 elif child.tag == 'string-array':
725 items.append(item.text)
726 thisinfo[name] = items
727 elif child.tag == 'builds':
732 builddict[key.tag] = key.text
733 builds.append(builddict)
734 thisinfo['builds'] = builds
736 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
737 if not isinstance(thisinfo['Requires Root'], bool):
738 if thisinfo['Requires Root'] == 'true':
739 thisinfo['Requires Root'] = True
741 thisinfo['Requires Root'] = False
743 # convert to the odd internal format
744 for k in ('Description', 'Maintainer Notes'):
745 if isinstance(thisinfo[k], basestring):
746 text = thisinfo[k].rstrip().lstrip()
747 thisinfo[k] = text.split('\n')
749 supported_flags = flag_defaults.keys() + ['versionCode', 'versionName']
750 for build in thisinfo['builds']:
751 for k, v in build.iteritems():
752 if k not in supported_flags:
753 raise MetaDataException("Unrecognised build flag: {0}={1}"
755 keyflagtype = flagtype(k)
756 if keyflagtype == 'bool':
757 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
758 if isinstance(v, basestring):
763 elif keyflagtype == 'list':
764 if isinstance(v, basestring):
767 post_metadata_parse(thisinfo)
769 return (appid, thisinfo)
772 def parse_txt_metadata(metafile):
777 def add_buildflag(p, thisbuild):
779 raise MetaDataException("Empty build flag at {1}"
780 .format(buildlines[0], linedesc))
783 raise MetaDataException("Invalid build flag at {0} in {1}"
784 .format(buildlines[0], linedesc))
787 raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
788 .format(pk, thisbuild['version'], linedesc))
791 if pk not in flag_defaults:
792 raise MetaDataException("Unrecognised build flag at {0} in {1}"
793 .format(p, linedesc))
796 pv = split_list_values(pv)
798 if len(pv) == 1 and pv[0] in ['main', 'yes']:
801 elif t == 'string' or t == 'script':
808 logging.debug("...ignoring bool flag %s" % p)
811 raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
814 def parse_buildline(lines):
815 value = "".join(lines)
816 parts = [p.replace("\\,", ",")
817 for p in re.split(r"(?<!\\),", value)]
819 raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
821 thisbuild['origlines'] = lines
822 thisbuild['version'] = parts[0]
823 thisbuild['vercode'] = parts[1]
824 if parts[2].startswith('!'):
825 # For backwards compatibility, handle old-style disabling,
826 # including attempting to extract the commit from the message
827 thisbuild['disable'] = parts[2][1:]
828 commit = 'unknown - see disabled'
829 index = parts[2].rfind('at ')
831 commit = parts[2][index + 3:]
832 if commit.endswith(')'):
834 thisbuild['commit'] = commit
836 thisbuild['commit'] = parts[2]
838 add_buildflag(p, thisbuild)
842 def add_comments(key):
845 for comment in curcomments:
846 thisinfo['comments'].append([key, comment])
849 thisinfo = get_default_app_info_list()
851 if not isinstance(metafile, file):
852 metafile = open(metafile, "r")
853 appid = metafile.name[9:-4]
854 thisinfo['id'] = appid
856 return appid, thisinfo
865 for line in metafile:
867 linedesc = "%s:%d" % (metafile.name, c)
868 line = line.rstrip('\r\n')
870 if not any(line.startswith(s) for s in (' ', '\t')):
871 commit = curbuild['commit'] if 'commit' in curbuild else None
872 if not commit and 'disable' not in curbuild:
873 raise MetaDataException("No commit specified for {0} in {1}"
874 .format(curbuild['version'], linedesc))
876 thisinfo['builds'].append(curbuild)
877 add_comments('build:' + curbuild['vercode'])
880 if line.endswith('\\'):
881 buildlines.append(line[:-1].lstrip())
883 buildlines.append(line.lstrip())
884 bl = ''.join(buildlines)
885 add_buildflag(bl, curbuild)
891 if line.startswith("#"):
892 curcomments.append(line)
895 field, value = line.split(':', 1)
897 raise MetaDataException("Invalid metadata in " + linedesc)
898 if field != field.strip() or value != value.strip():
899 raise MetaDataException("Extra spacing found in " + linedesc)
901 # Translate obsolete fields...
902 if field == 'Market Version':
903 field = 'Current Version'
904 if field == 'Market Version Code':
905 field = 'Current Version Code'
907 fieldtype = metafieldtype(field)
908 if fieldtype not in ['build', 'buildv2']:
910 if fieldtype == 'multiline':
914 raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
915 elif fieldtype == 'string':
916 thisinfo[field] = value
917 elif fieldtype == 'list':
918 thisinfo[field] = split_list_values(value)
919 elif fieldtype == 'build':
920 if value.endswith("\\"):
922 buildlines = [value[:-1]]
924 curbuild = parse_buildline([value])
925 thisinfo['builds'].append(curbuild)
926 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
927 elif fieldtype == 'buildv2':
929 vv = value.split(',')
931 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
932 .format(value, linedesc))
933 curbuild['version'] = vv[0]
934 curbuild['vercode'] = vv[1]
935 if curbuild['vercode'] in vc_seen:
936 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
937 curbuild['vercode'], linedesc))
938 vc_seen[curbuild['vercode']] = True
941 elif fieldtype == 'obsolete':
942 pass # Just throw it away!
944 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
945 elif mode == 1: # Multiline field
949 thisinfo[field].append(line)
950 elif mode == 2: # Line continuation mode in Build Version
951 if line.endswith("\\"):
952 buildlines.append(line[:-1])
954 buildlines.append(line)
955 curbuild = parse_buildline(buildlines)
956 thisinfo['builds'].append(curbuild)
957 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
961 # Mode at end of file should always be 0...
963 raise MetaDataException(field + " not terminated in " + metafile.name)
965 raise MetaDataException("Unterminated continuation in " + metafile.name)
967 raise MetaDataException("Unterminated build in " + metafile.name)
969 post_metadata_parse(thisinfo)
971 return (appid, thisinfo)
974 # Write a metadata file.
976 # 'dest' - The path to the output file
977 # 'app' - The app data
978 def write_metadata(dest, app):
980 def writecomments(key):
982 for pf, comment in app['comments']:
984 mf.write("%s\n" % comment)
987 logging.debug("...writing comments for " + (key or 'EOF'))
989 def writefield(field, value=None):
993 t = metafieldtype(field)
995 value = ','.join(value)
996 mf.write("%s:%s\n" % (field, value))
998 def writefield_nonempty(field, value=None):
1002 writefield(field, value)
1004 mf = open(dest, 'w')
1005 writefield_nonempty('Disabled')
1006 writefield('AntiFeatures')
1007 writefield_nonempty('Provides')
1008 writefield('Categories')
1009 writefield('License')
1010 writefield('Web Site')
1011 writefield('Source Code')
1012 writefield('Issue Tracker')
1013 writefield_nonempty('Changelog')
1014 writefield_nonempty('Donate')
1015 writefield_nonempty('FlattrID')
1016 writefield_nonempty('Bitcoin')
1017 writefield_nonempty('Litecoin')
1018 writefield_nonempty('Dogecoin')
1020 writefield_nonempty('Name')
1021 writefield_nonempty('Auto Name')
1022 writefield('Summary')
1023 writefield('Description', '')
1024 for line in app['Description']:
1025 mf.write("%s\n" % line)
1028 if app['Requires Root']:
1029 writefield('Requires Root', 'yes')
1031 if app['Repo Type']:
1032 writefield('Repo Type')
1035 writefield('Binaries')
1037 for build in app['builds']:
1039 if build['version'] == "Ignore":
1042 writecomments('build:' + build['vercode'])
1043 mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
1045 def write_builditem(key, value):
1047 if key in ['version', 'vercode']:
1050 if value == flag_defaults[key]:
1055 logging.debug("...writing {0} : {1}".format(key, value))
1056 outline = ' %s=' % key
1063 outline += '&& \\\n '.join([s.lstrip() for s in value.split('&& ')])
1065 outline += ','.join(value) if type(value) == list else value
1070 for flag in flag_defaults:
1073 write_builditem(flag, value)
1076 if app['Maintainer Notes']:
1077 writefield('Maintainer Notes', '')
1078 for line in app['Maintainer Notes']:
1079 mf.write("%s\n" % line)
1083 writefield_nonempty('Archive Policy')
1084 writefield('Auto Update Mode')
1085 writefield('Update Check Mode')
1086 writefield_nonempty('Update Check Ignore')
1087 writefield_nonempty('Vercode Operation')
1088 writefield_nonempty('Update Check Name')
1089 writefield_nonempty('Update Check Data')
1090 if app['Current Version']:
1091 writefield('Current Version')
1092 writefield('Current Version Code')
1094 if app['No Source Since']:
1095 writefield('No Source Since')