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