chiark / gitweb /
Centralise management of srclib metadata
[fdroidserver.git] / fdroidserver / metadata.py
1 # -*- coding: utf-8 -*-
2 #
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>
6 #
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.
11 #
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.
16 #
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/>.
19
20 import os
21 import re
22 import glob
23 import cgi
24 import logging
25
26 srclibs = {}
27
28 class MetaDataException(Exception):
29     def __init__(self, value):
30         self.value = value
31
32     def __str__(self):
33         return self.value
34
35 app_defaults = {
36     'Name': None,
37     'Provides': None,
38     'Auto Name': '',
39     'Categories': ['None'],
40     'Description': [],
41     'Summary': '',
42     'License': 'Unknown',
43     'Web Site': '',
44     'Source Code': '',
45     'Issue Tracker': '',
46     'Donate': None,
47     'FlattrID': None,
48     'Bitcoin': None,
49     'Litecoin': None,
50     'Dogecoin': None,
51     'Disabled': None,
52     'AntiFeatures': None,
53     'Archive Policy': None,
54     'Update Check Mode': 'None',
55     'Update Check Ignore': None,
56     'Update Check Name': None,
57     'Update Check Data': None,
58     'Vercode Operation': None,
59     'Auto Update Mode': 'None',
60     'Current Version': '',
61     'Current Version Code': '0',
62     'Repo Type': '',
63     'Repo': '',
64     'Requires Root': False,
65     'No Source Since': ''
66     }
67
68
69 # This defines the preferred order for the build items - as in the
70 # manual, they're roughly in order of application.
71 ordered_flags = [
72     'disable', 'commit', 'subdir', 'submodules', 'init',
73     'gradle', 'maven', 'kivy', 'output', 'oldsdkloc', 'target',
74     'update', 'encoding', 'forceversion', 'forcevercode', 'rm',
75     'extlibs', 'srclibs', 'patch', 'prebuild', 'scanignore',
76     'scandelete', 'build', 'buildjni', 'preassemble', 'bindir',
77     'antcommand', 'novcheck'
78     ]
79
80
81 # Designates a metadata field type and checks that it matches
82 #
83 # 'name'     - The long name of the field type
84 # 'matching' - List of possible values or regex expression
85 # 'sep'      - Separator to use if value may be a list
86 # 'fields'   - Metadata fields (Field:Value) of this type
87 # 'attrs'    - Build attributes (attr=value) of this type
88 #
89 class FieldType():
90     def __init__(self, name, matching, sep, fields, attrs):
91         self.name = name
92         self.matching = matching
93         if type(matching) is str:
94             self.compiled = re.compile(matching)
95         self.sep = sep
96         self.fields = fields
97         self.attrs = attrs
98
99     def _assert_regex(self, values, appid):
100         for v in values:
101             if not self.compiled.match(v):
102                 raise MetaDataException("'%s' is not a valid %s in %s. "
103                                         % (v, self.name, appid) +
104                                         "Regex pattern: %s" % (self.matching))
105
106     def _assert_list(self, values, appid):
107         for v in values:
108             if v not in self.matching:
109                 raise MetaDataException("'%s' is not a valid %s in %s. "
110                                         % (v, self.name, appid) +
111                                         "Possible values: %s" % (", ".join(self.matching)))
112
113     def check(self, value, appid):
114         if type(value) is not str or not value:
115             return
116         if self.sep is not None:
117             values = value.split(self.sep)
118         else:
119             values = [value]
120         if type(self.matching) is list:
121             self._assert_list(values, appid)
122         else:
123             self._assert_regex(values, appid)
124
125
126 # Generic value types
127 valuetypes = {
128     'int': FieldType("Integer",
129                      r'^[1-9][0-9]*$', None,
130                      ['FlattrID'],
131                      ['vercode']),
132
133     'http': FieldType("HTTP link",
134                       r'^http[s]?://', None,
135                       ["Web Site", "Source Code", "Issue Tracker", "Donate"], []),
136
137     'bitcoin': FieldType("Bitcoin address",
138                          r'^[a-zA-Z0-9]{27,34}$', None,
139                          ["Bitcoin"],
140                          []),
141
142     'litecoin': FieldType("Litecoin address",
143                           r'^L[a-zA-Z0-9]{33}$', None,
144                           ["Litecoin"],
145                           []),
146
147     'dogecoin': FieldType("Dogecoin address",
148                           r'^D[a-zA-Z0-9]{33}$', None,
149                           ["Dogecoin"],
150                           []),
151
152     'Bool': FieldType("Boolean",
153                       ['Yes', 'No'], None,
154                       ["Requires Root"],
155                       []),
156
157     'bool': FieldType("Boolean",
158                       ['yes', 'no'], None,
159                       [],
160                       ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
161                        'novcheck']),
162
163     'Repo Type': FieldType("Repo Type",
164                            ['git', 'git-svn', 'svn', 'hg', 'bzr', 'srclib'], None,
165                            ["Repo Type"],
166                            []),
167
168     'archive': FieldType("Archive Policy",
169                          r'^[0-9]+ versions$', None,
170                          ["Archive Policy"],
171                          []),
172
173     'antifeatures': FieldType("Anti-Feature",
174                               ["Ads", "Tracking", "NonFreeNet", "NonFreeDep", "NonFreeAdd", "UpstreamNonFree"], ',',
175                               ["AntiFeatures"],
176                               []),
177
178     'autoupdatemodes': FieldType("Auto Update Mode",
179                                  r"^(Version .+|None)$", None,
180                                  ["Auto Update Mode"],
181                                  []),
182
183     'updatecheckmodes': FieldType("Update Check Mode",
184                                   r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$", None,
185                                   ["Update Check Mode"],
186                                   [])
187     }
188
189
190 # Check an app's metadata information for integrity errors
191 def check_metadata(info):
192     for k, t in valuetypes.iteritems():
193         for field in t.fields:
194             if field in info:
195                 t.check(info[field], info['id'])
196                 if k == 'Bool':
197                     info[field] = info[field] == "Yes"
198         for build in info['builds']:
199             for attr in t.attrs:
200                 if attr in build:
201                     t.check(build[attr], info['id'])
202                     if k == 'bool':
203                         build[attr] = build[attr] == "yes"
204                 elif k == 'bool':
205                     build[attr] = False
206
207
208 # Formatter for descriptions. Create an instance, and call parseline() with
209 # each line of the description source from the metadata. At the end, call
210 # end() and then text_plain, text_wiki and text_html will contain the result.
211 class DescriptionFormatter:
212     stNONE = 0
213     stPARA = 1
214     stUL = 2
215     stOL = 3
216     bold = False
217     ital = False
218     state = stNONE
219     text_plain = ''
220     text_wiki = ''
221     text_html = ''
222     linkResolver = None
223
224     def __init__(self, linkres):
225         self.linkResolver = linkres
226
227     def endcur(self, notstates=None):
228         if notstates and self.state in notstates:
229             return
230         if self.state == self.stPARA:
231             self.endpara()
232         elif self.state == self.stUL:
233             self.endul()
234         elif self.state == self.stOL:
235             self.endol()
236
237     def endpara(self):
238         self.text_plain += '\n'
239         self.text_html += '</p>'
240         self.state = self.stNONE
241
242     def endul(self):
243         self.text_html += '</ul>'
244         self.state = self.stNONE
245
246     def endol(self):
247         self.text_html += '</ol>'
248         self.state = self.stNONE
249
250     def formatted(self, txt, html):
251         formatted = ''
252         if html:
253             txt = cgi.escape(txt)
254         while True:
255             index = txt.find("''")
256             if index == -1:
257                 return formatted + txt
258             formatted += txt[:index]
259             txt = txt[index:]
260             if txt.startswith("'''"):
261                 if html:
262                     if self.bold:
263                         formatted += '</b>'
264                     else:
265                         formatted += '<b>'
266                 self.bold = not self.bold
267                 txt = txt[3:]
268             else:
269                 if html:
270                     if self.ital:
271                         formatted += '</i>'
272                     else:
273                         formatted += '<i>'
274                 self.ital = not self.ital
275                 txt = txt[2:]
276
277     def linkify(self, txt):
278         linkified_plain = ''
279         linkified_html = ''
280         while True:
281             index = txt.find("[")
282             if index == -1:
283                 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
284             linkified_plain += self.formatted(txt[:index], False)
285             linkified_html += self.formatted(txt[:index], True)
286             txt = txt[index:]
287             if txt.startswith("[["):
288                 index = txt.find("]]")
289                 if index == -1:
290                     raise MetaDataException("Unterminated ]]")
291                 url = txt[2:index]
292                 if self.linkResolver:
293                     url, urltext = self.linkResolver(url)
294                 else:
295                     urltext = url
296                 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
297                 linkified_plain += urltext
298                 txt = txt[index + 2:]
299             else:
300                 index = txt.find("]")
301                 if index == -1:
302                     raise MetaDataException("Unterminated ]")
303                 url = txt[1:index]
304                 index2 = url.find(' ')
305                 if index2 == -1:
306                     urltxt = url
307                 else:
308                     urltxt = url[index2 + 1:]
309                     url = url[:index2]
310                 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
311                 linkified_plain += urltxt
312                 if urltxt != url:
313                     linkified_plain += ' (' + url + ')'
314                 txt = txt[index + 1:]
315
316     def addtext(self, txt):
317         p, h = self.linkify(txt)
318         self.text_plain += p
319         self.text_html += h
320
321     def parseline(self, line):
322         self.text_wiki += "%s\n" % line
323         if not line:
324             self.endcur()
325         elif line.startswith('* '):
326             self.endcur([self.stUL])
327             if self.state != self.stUL:
328                 self.text_html += '<ul>'
329                 self.state = self.stUL
330             self.text_html += '<li>'
331             self.text_plain += '* '
332             self.addtext(line[1:])
333             self.text_html += '</li>'
334         elif line.startswith('# '):
335             self.endcur([self.stOL])
336             if self.state != self.stOL:
337                 self.text_html += '<ol>'
338                 self.state = self.stOL
339             self.text_html += '<li>'
340             self.text_plain += '* '  # TODO: lazy - put the numbers in!
341             self.addtext(line[1:])
342             self.text_html += '</li>'
343         else:
344             self.endcur([self.stPARA])
345             if self.state == self.stNONE:
346                 self.text_html += '<p>'
347                 self.state = self.stPARA
348             elif self.state == self.stPARA:
349                 self.text_html += ' '
350                 self.text_plain += ' '
351             self.addtext(line)
352
353     def end(self):
354         self.endcur()
355
356
357 # Parse multiple lines of description as written in a metadata file, returning
358 # a single string in plain text format.
359 def description_plain(lines, linkres):
360     ps = DescriptionFormatter(linkres)
361     for line in lines:
362         ps.parseline(line)
363     ps.end()
364     return ps.text_plain
365
366
367 # Parse multiple lines of description as written in a metadata file, returning
368 # a single string in wiki format. Used for the Maintainer Notes field as well,
369 # because it's the same format.
370 def description_wiki(lines):
371     ps = DescriptionFormatter(None)
372     for line in lines:
373         ps.parseline(line)
374     ps.end()
375     return ps.text_wiki
376
377
378 # Parse multiple lines of description as written in a metadata file, returning
379 # a single string in HTML format.
380 def description_html(lines, linkres):
381     ps = DescriptionFormatter(linkres)
382     for line in lines:
383         ps.parseline(line)
384     ps.end()
385     return ps.text_html
386
387
388 def parse_srclib(metafile):
389
390     thisinfo = {}
391     if metafile and not isinstance(metafile, file):
392         metafile = open(metafile, "r")
393
394     # Defaults for fields that come from metadata
395     thisinfo['Repo Type'] = ''
396     thisinfo['Repo'] = ''
397     thisinfo['Subdir'] = None
398     thisinfo['Prepare'] = None
399     thisinfo['Srclibs'] = None
400
401     if metafile is None:
402         return thisinfo
403
404     n = 0
405     for line in metafile:
406         n += 1
407         line = line.rstrip('\r\n')
408         if not line or line.startswith("#"):
409             continue
410
411         try:
412             field, value = line.split(':', 1)
413         except ValueError:
414             raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
415
416         if field == "Subdir":
417             thisinfo[field] = value.split(',')
418         else:
419             thisinfo[field] = value
420
421     return thisinfo
422
423
424 def read_srclibs():
425     """Read all srclib metadata.
426
427     The information read will be accessible as metadata.srclibs, which is a
428     dictionary, keyed on srclib name, with the values each being a dictionary
429     in the same format as that returned by the parse_srclib function.
430
431     A MetaDataException is raised if there are any problems with the srclib
432     metadata.
433     """
434     global srclibs
435     srclibs = {}
436
437     srcdir = 'srclibs'
438     if not os.path.exists(srcdir):
439         os.makedirs(srcdir)
440
441     for metafile in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
442         srclibname = os.path.basename(metafile[:-4])
443         srclibs[srclibname] = parse_srclib(metafile)
444
445
446 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
447 # returned by the parse_metadata function.
448 def read_metadata(xref=True):
449     apps = []
450
451     for basedir in ('metadata', 'tmp'):
452         if not os.path.exists(basedir):
453             os.makedirs(basedir)
454
455     for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
456         appinfo = parse_metadata(metafile)
457         check_metadata(appinfo)
458         apps.append(appinfo)
459
460     if xref:
461         # Parse all descriptions at load time, just to ensure cross-referencing
462         # errors are caught early rather than when they hit the build server.
463         def linkres(link):
464             for app in apps:
465                 if app['id'] == link:
466                     return ("fdroid.app:" + link, "Dummy name - don't know yet")
467             raise MetaDataException("Cannot resolve app id " + link)
468         for app in apps:
469             try:
470                 description_html(app['Description'], linkres)
471             except Exception, e:
472                 raise MetaDataException("Problem with description of " + app['id'] +
473                                         " - " + str(e))
474
475     return apps
476
477
478 # Get the type expected for a given metadata field.
479 def metafieldtype(name):
480     if name in ['Description', 'Maintainer Notes']:
481         return 'multiline'
482     if name in ['Categories']:
483         return 'list'
484     if name == 'Build Version':
485         return 'build'
486     if name == 'Build':
487         return 'buildv2'
488     if name == 'Use Built':
489         return 'obsolete'
490     if name not in app_defaults:
491         return 'unknown'
492     return 'string'
493
494
495 def flagtype(name):
496     if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni',
497                 'update', 'scanignore', 'scandelete']:
498         return 'list'
499     if name in ['init', 'prebuild', 'build']:
500         return 'script'
501     return 'string'
502
503
504 # Parse metadata for a single application.
505 #
506 #  'metafile' - the filename to read. The package id for the application comes
507 #               from this filename. Pass None to get a blank entry.
508 #
509 # Returns a dictionary containing all the details of the application. There are
510 # two major kinds of information in the dictionary. Keys beginning with capital
511 # letters correspond directory to identically named keys in the metadata file.
512 # Keys beginning with lower case letters are generated in one way or another,
513 # and are not found verbatim in the metadata.
514 #
515 # Known keys not originating from the metadata are:
516 #
517 #  'id'               - the application's package ID
518 #  'builds'           - a list of dictionaries containing build information
519 #                       for each defined build
520 #  'comments'         - a list of comments from the metadata file. Each is
521 #                       a tuple of the form (field, comment) where field is
522 #                       the name of the field it preceded in the metadata
523 #                       file. Where field is None, the comment goes at the
524 #                       end of the file. Alternatively, 'build:version' is
525 #                       for a comment before a particular build version.
526 #  'descriptionlines' - original lines of description as formatted in the
527 #                       metadata file.
528 #
529 def parse_metadata(metafile):
530
531     linedesc = None
532
533     def add_buildflag(p, thisbuild):
534         bv = p.split('=', 1)
535         if len(bv) != 2:
536             raise MetaDataException("Invalid build flag at {0} in {1}"
537                                     .format(buildlines[0], linedesc))
538         pk, pv = bv
539         if pk in thisbuild:
540             raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
541                                     .format(pk, thisbuild['version'], linedesc))
542
543         pk = pk.lstrip()
544         if pk not in ordered_flags:
545             raise MetaDataException("Unrecognised build flag at {0} in {1}"
546                                     .format(p, linedesc))
547         t = flagtype(pk)
548         if t == 'list':
549             # Port legacy ';' separators
550             thisbuild[pk] = [v.strip() for v in pv.replace(';', ',').split(',')]
551         elif t == 'string':
552             thisbuild[pk] = pv
553         elif t == 'script':
554             thisbuild[pk] = pv
555         else:
556             raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
557                                     % (t, p, linedesc))
558
559     def parse_buildline(lines):
560         value = "".join(lines)
561         parts = [p.replace("\\,", ",")
562                  for p in re.split(r"(?<!\\),", value)]
563         if len(parts) < 3:
564             raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
565         thisbuild = {}
566         thisbuild['origlines'] = lines
567         thisbuild['version'] = parts[0]
568         thisbuild['vercode'] = parts[1]
569         if parts[2].startswith('!'):
570             # For backwards compatibility, handle old-style disabling,
571             # including attempting to extract the commit from the message
572             thisbuild['disable'] = parts[2][1:]
573             commit = 'unknown - see disabled'
574             index = parts[2].rfind('at ')
575             if index != -1:
576                 commit = parts[2][index + 3:]
577                 if commit.endswith(')'):
578                     commit = commit[:-1]
579             thisbuild['commit'] = commit
580         else:
581             thisbuild['commit'] = parts[2]
582         for p in parts[3:]:
583             add_buildflag(p, thisbuild)
584
585         return thisbuild
586
587     def add_comments(key):
588         if not curcomments:
589             return
590         for comment in curcomments:
591             thisinfo['comments'].append((key, comment))
592         del curcomments[:]
593
594     def get_build_type(build):
595         for t in ['maven', 'gradle', 'kivy']:
596             if build.get(t, 'no') != 'no':
597                 return t
598         if 'output' in build:
599             return 'raw'
600         return 'ant'
601
602     thisinfo = {}
603     if metafile:
604         if not isinstance(metafile, file):
605             metafile = open(metafile, "r")
606         thisinfo['id'] = metafile.name[9:-4]
607     else:
608         thisinfo['id'] = None
609
610     thisinfo.update(app_defaults)
611
612     # General defaults...
613     thisinfo['builds'] = []
614     thisinfo['comments'] = []
615
616     if metafile is None:
617         return thisinfo
618
619     mode = 0
620     buildlines = []
621     curcomments = []
622     curbuild = None
623
624     c = 0
625     for line in metafile:
626         c += 1
627         linedesc = "%s:%d" % (metafile.name, c)
628         line = line.rstrip('\r\n')
629         if mode == 3:
630             if not any(line.startswith(s) for s in (' ', '\t')):
631                 if 'commit' not in curbuild and 'disable' not in curbuild:
632                     raise MetaDataException("No commit specified for {0} in {1}"
633                                             .format(curbuild['version'], linedesc))
634                 thisinfo['builds'].append(curbuild)
635                 add_comments('build:' + curbuild['version'])
636                 mode = 0
637             else:
638                 if line.endswith('\\'):
639                     buildlines.append(line[:-1].lstrip())
640                 else:
641                     buildlines.append(line.lstrip())
642                     bl = ''.join(buildlines)
643                     add_buildflag(bl, curbuild)
644                     buildlines = []
645
646         if mode == 0:
647             if not line:
648                 continue
649             if line.startswith("#"):
650                 curcomments.append(line)
651                 continue
652             try:
653                 field, value = line.split(':', 1)
654             except ValueError:
655                 raise MetaDataException("Invalid metadata in " + linedesc)
656             if field != field.strip() or value != value.strip():
657                 raise MetaDataException("Extra spacing found in " + linedesc)
658
659             # Translate obsolete fields...
660             if field == 'Market Version':
661                 field = 'Current Version'
662             if field == 'Market Version Code':
663                 field = 'Current Version Code'
664
665             fieldtype = metafieldtype(field)
666             if fieldtype not in ['build', 'buildv2']:
667                 add_comments(field)
668             if fieldtype == 'multiline':
669                 mode = 1
670                 thisinfo[field] = []
671                 if value:
672                     raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
673             elif fieldtype == 'string':
674                 thisinfo[field] = value
675             elif fieldtype == 'list':
676                 thisinfo[field] = [v.strip() for v in value.replace(';', ',').split(',')]
677             elif fieldtype == 'build':
678                 if value.endswith("\\"):
679                     mode = 2
680                     buildlines = [value[:-1]]
681                 else:
682                     thisinfo['builds'].append(parse_buildline([value]))
683                     add_comments('build:' + thisinfo['builds'][-1]['version'])
684             elif fieldtype == 'buildv2':
685                 curbuild = {}
686                 vv = value.split(',')
687                 if len(vv) != 2:
688                     raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
689                                             .format(value, linedesc))
690                 curbuild['version'] = vv[0]
691                 curbuild['vercode'] = vv[1]
692                 buildlines = []
693                 mode = 3
694             elif fieldtype == 'obsolete':
695                 pass        # Just throw it away!
696             else:
697                 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
698         elif mode == 1:     # Multiline field
699             if line == '.':
700                 mode = 0
701             else:
702                 thisinfo[field].append(line)
703         elif mode == 2:     # Line continuation mode in Build Version
704             if line.endswith("\\"):
705                 buildlines.append(line[:-1])
706             else:
707                 buildlines.append(line)
708                 thisinfo['builds'].append(
709                     parse_buildline(buildlines))
710                 add_comments('build:' + thisinfo['builds'][-1]['version'])
711                 mode = 0
712     add_comments(None)
713
714     # Mode at end of file should always be 0...
715     if mode == 1:
716         raise MetaDataException(field + " not terminated in " + metafile.name)
717     elif mode == 2:
718         raise MetaDataException("Unterminated continuation in " + metafile.name)
719     elif mode == 3:
720         raise MetaDataException("Unterminated build in " + metafile.name)
721
722     if not thisinfo['Description']:
723         thisinfo['Description'].append('No description available')
724
725     for build in thisinfo['builds']:
726         build['type'] = get_build_type(build)
727
728     return thisinfo
729
730
731 # Write a metadata file.
732 #
733 # 'dest'    - The path to the output file
734 # 'app'     - The app data
735 def write_metadata(dest, app):
736
737     def writecomments(key):
738         written = 0
739         for pf, comment in app['comments']:
740             if pf == key:
741                 mf.write("%s\n" % comment)
742                 written += 1
743         if written > 0:
744             logging.debug("...writing comments for " + (key if key else 'EOF'))
745
746     def writefield(field, value=None):
747         writecomments(field)
748         if value is None:
749             value = app[field]
750         t = metafieldtype(field)
751         if t == 'list':
752             value = ','.join(value)
753         mf.write("%s:%s\n" % (field, value))
754
755     mf = open(dest, 'w')
756     if app['Disabled']:
757         writefield('Disabled')
758     if app['AntiFeatures']:
759         writefield('AntiFeatures')
760     if app['Provides']:
761         writefield('Provides')
762     writefield('Categories')
763     writefield('License')
764     writefield('Web Site')
765     writefield('Source Code')
766     writefield('Issue Tracker')
767     if app['Donate']:
768         writefield('Donate')
769     if app['FlattrID']:
770         writefield('FlattrID')
771     if app['Bitcoin']:
772         writefield('Bitcoin')
773     if app['Litecoin']:
774         writefield('Litecoin')
775     if app['Dogecoin']:
776         writefield('Dogecoin')
777     mf.write('\n')
778     if app['Name']:
779         writefield('Name')
780     if app['Auto Name']:
781         writefield('Auto Name')
782     writefield('Summary')
783     writefield('Description', '')
784     for line in app['Description']:
785         mf.write("%s\n" % line)
786     mf.write('.\n')
787     mf.write('\n')
788     if app['Requires Root']:
789         writefield('Requires Root', 'Yes')
790         mf.write('\n')
791     if app['Repo Type']:
792         writefield('Repo Type')
793         writefield('Repo')
794         mf.write('\n')
795     for build in app['builds']:
796         writecomments('build:' + build['version'])
797         mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
798
799         def write_builditem(key, value):
800             if key in ['version', 'vercode', 'origlines', 'type']:
801                 return
802             if key in valuetypes['bool'].attrs:
803                 if not value:
804                     return
805                 value = 'yes'
806             t = flagtype(key)
807             logging.debug("...writing {0} : {1}".format(key, value))
808             outline = '    %s=' % key
809             if t == 'string':
810                 outline += value
811             elif t == 'script':
812                 outline += '&& \\\n        '.join([s.lstrip() for s in value.split('&& ')])
813             elif t == 'list':
814                 outline += ','.join(value) if type(value) == list else value
815             outline += '\n'
816             mf.write(outline)
817
818         for key in ordered_flags:
819             if key in build:
820                 write_builditem(key, build[key])
821         mf.write('\n')
822
823     if 'Maintainer Notes' in app:
824         writefield('Maintainer Notes', '')
825         for line in app['Maintainer Notes']:
826             mf.write("%s\n" % line)
827         mf.write('.\n')
828         mf.write('\n')
829
830     if app['Archive Policy']:
831         writefield('Archive Policy')
832     writefield('Auto Update Mode')
833     writefield('Update Check Mode')
834     if app['Update Check Ignore']:
835         writefield('Update Check Ignore')
836     if app['Vercode Operation']:
837         writefield('Vercode Operation')
838     if app['Update Check Data']:
839         writefield('Update Check Data')
840     if app['Current Version']:
841         writefield('Current Version')
842         writefield('Current Version Code')
843     mf.write('\n')
844     if app['No Source Since']:
845         writefield('No Source Since')
846         mf.write('\n')
847     writecomments(None)
848     mf.close()