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