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