chiark / gitweb /
Fix bug in reading old recipes
[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 = {}
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             if field in info:
222                 v.check(info[field], info['id'])
223         for build in info['builds']:
224             for attr in v.attrs:
225                 if build[attr]:
226                     v.check(build[attr], info['id'])
227
228
229 # Formatter for descriptions. Create an instance, and call parseline() with
230 # each line of the description source from the metadata. At the end, call
231 # end() and then text_plain, text_wiki and text_html will contain the result.
232 class DescriptionFormatter:
233     stNONE = 0
234     stPARA = 1
235     stUL = 2
236     stOL = 3
237     bold = False
238     ital = False
239     state = stNONE
240     text_plain = ''
241     text_wiki = ''
242     text_html = ''
243     linkResolver = None
244
245     def __init__(self, linkres):
246         self.linkResolver = linkres
247
248     def endcur(self, notstates=None):
249         if notstates and self.state in notstates:
250             return
251         if self.state == self.stPARA:
252             self.endpara()
253         elif self.state == self.stUL:
254             self.endul()
255         elif self.state == self.stOL:
256             self.endol()
257
258     def endpara(self):
259         self.text_plain += '\n'
260         self.text_html += '</p>'
261         self.state = self.stNONE
262
263     def endul(self):
264         self.text_html += '</ul>'
265         self.state = self.stNONE
266
267     def endol(self):
268         self.text_html += '</ol>'
269         self.state = self.stNONE
270
271     def formatted(self, txt, html):
272         formatted = ''
273         if html:
274             txt = cgi.escape(txt)
275         while True:
276             index = txt.find("''")
277             if index == -1:
278                 return formatted + txt
279             formatted += txt[:index]
280             txt = txt[index:]
281             if txt.startswith("'''"):
282                 if html:
283                     if self.bold:
284                         formatted += '</b>'
285                     else:
286                         formatted += '<b>'
287                 self.bold = not self.bold
288                 txt = txt[3:]
289             else:
290                 if html:
291                     if self.ital:
292                         formatted += '</i>'
293                     else:
294                         formatted += '<i>'
295                 self.ital = not self.ital
296                 txt = txt[2:]
297
298     def linkify(self, txt):
299         linkified_plain = ''
300         linkified_html = ''
301         while True:
302             index = txt.find("[")
303             if index == -1:
304                 return (linkified_plain + self.formatted(txt, False), linkified_html + self.formatted(txt, True))
305             linkified_plain += self.formatted(txt[:index], False)
306             linkified_html += self.formatted(txt[:index], True)
307             txt = txt[index:]
308             if txt.startswith("[["):
309                 index = txt.find("]]")
310                 if index == -1:
311                     raise MetaDataException("Unterminated ]]")
312                 url = txt[2:index]
313                 if self.linkResolver:
314                     url, urltext = self.linkResolver(url)
315                 else:
316                     urltext = url
317                 linkified_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
318                 linkified_plain += urltext
319                 txt = txt[index + 2:]
320             else:
321                 index = txt.find("]")
322                 if index == -1:
323                     raise MetaDataException("Unterminated ]")
324                 url = txt[1:index]
325                 index2 = url.find(' ')
326                 if index2 == -1:
327                     urltxt = url
328                 else:
329                     urltxt = url[index2 + 1:]
330                     url = url[:index2]
331                 linkified_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
332                 linkified_plain += urltxt
333                 if urltxt != url:
334                     linkified_plain += ' (' + url + ')'
335                 txt = txt[index + 1:]
336
337     def addtext(self, txt):
338         p, h = self.linkify(txt)
339         self.text_plain += p
340         self.text_html += h
341
342     def parseline(self, line):
343         self.text_wiki += "%s\n" % line
344         if not line:
345             self.endcur()
346         elif line.startswith('* '):
347             self.endcur([self.stUL])
348             if self.state != self.stUL:
349                 self.text_html += '<ul>'
350                 self.state = self.stUL
351             self.text_html += '<li>'
352             self.text_plain += '* '
353             self.addtext(line[1:])
354             self.text_html += '</li>'
355         elif line.startswith('# '):
356             self.endcur([self.stOL])
357             if self.state != self.stOL:
358                 self.text_html += '<ol>'
359                 self.state = self.stOL
360             self.text_html += '<li>'
361             self.text_plain += '* '  # TODO: lazy - put the numbers in!
362             self.addtext(line[1:])
363             self.text_html += '</li>'
364         else:
365             self.endcur([self.stPARA])
366             if self.state == self.stNONE:
367                 self.text_html += '<p>'
368                 self.state = self.stPARA
369             elif self.state == self.stPARA:
370                 self.text_html += ' '
371                 self.text_plain += ' '
372             self.addtext(line)
373
374     def end(self):
375         self.endcur()
376
377
378 # Parse multiple lines of description as written in a metadata file, returning
379 # a single string in plain text format.
380 def description_plain(lines, linkres):
381     ps = DescriptionFormatter(linkres)
382     for line in lines:
383         ps.parseline(line)
384     ps.end()
385     return ps.text_plain
386
387
388 # Parse multiple lines of description as written in a metadata file, returning
389 # a single string in wiki format. Used for the Maintainer Notes field as well,
390 # because it's the same format.
391 def description_wiki(lines):
392     ps = DescriptionFormatter(None)
393     for line in lines:
394         ps.parseline(line)
395     ps.end()
396     return ps.text_wiki
397
398
399 # Parse multiple lines of description as written in a metadata file, returning
400 # a single string in HTML format.
401 def description_html(lines, linkres):
402     ps = DescriptionFormatter(linkres)
403     for line in lines:
404         ps.parseline(line)
405     ps.end()
406     return ps.text_html
407
408
409 def parse_srclib(metafile):
410
411     thisinfo = {}
412     if metafile and not isinstance(metafile, file):
413         metafile = open(metafile, "r")
414
415     # Defaults for fields that come from metadata
416     thisinfo['Repo Type'] = ''
417     thisinfo['Repo'] = ''
418     thisinfo['Subdir'] = None
419     thisinfo['Prepare'] = None
420     thisinfo['Srclibs'] = None
421
422     if metafile is None:
423         return thisinfo
424
425     n = 0
426     for line in metafile:
427         n += 1
428         line = line.rstrip('\r\n')
429         if not line or line.startswith("#"):
430             continue
431
432         try:
433             field, value = line.split(':', 1)
434         except ValueError:
435             raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
436
437         if field == "Subdir":
438             thisinfo[field] = value.split(',')
439         else:
440             thisinfo[field] = value
441
442     return thisinfo
443
444
445 def read_srclibs():
446     """Read all srclib metadata.
447
448     The information read will be accessible as metadata.srclibs, which is a
449     dictionary, keyed on srclib name, with the values each being a dictionary
450     in the same format as that returned by the parse_srclib function.
451
452     A MetaDataException is raised if there are any problems with the srclib
453     metadata.
454     """
455     global srclibs
456     srclibs = {}
457
458     srcdir = 'srclibs'
459     if not os.path.exists(srcdir):
460         os.makedirs(srcdir)
461
462     for metafile in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
463         srclibname = os.path.basename(metafile[:-4])
464         srclibs[srclibname] = parse_srclib(metafile)
465
466
467 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
468 # returned by the parse_metadata function.
469 def read_metadata(xref=True):
470     apps = []
471
472     for basedir in ('metadata', 'tmp'):
473         if not os.path.exists(basedir):
474             os.makedirs(basedir)
475
476     for metafile in sorted(glob.glob(os.path.join('metadata', '*.txt'))):
477         appinfo = parse_metadata(metafile)
478         check_metadata(appinfo)
479         apps.append(appinfo)
480
481     if xref:
482         # Parse all descriptions at load time, just to ensure cross-referencing
483         # errors are caught early rather than when they hit the build server.
484         def linkres(link):
485             for app in apps:
486                 if app['id'] == link:
487                     return ("fdroid.app:" + link, "Dummy name - don't know yet")
488             raise MetaDataException("Cannot resolve app id " + link)
489         for app in apps:
490             try:
491                 description_html(app['Description'], linkres)
492             except Exception, e:
493                 raise MetaDataException("Problem with description of " + app['id'] +
494                                         " - " + str(e))
495
496     return apps
497
498
499 # Get the type expected for a given metadata field.
500 def metafieldtype(name):
501     if name in ['Description', 'Maintainer Notes']:
502         return 'multiline'
503     if name in ['Categories']:
504         return 'list'
505     if name == 'Build Version':
506         return 'build'
507     if name == 'Build':
508         return 'buildv2'
509     if name == 'Use Built':
510         return 'obsolete'
511     if name not in app_defaults:
512         return 'unknown'
513     return 'string'
514
515
516 def flagtype(name):
517     if name in ['extlibs', 'srclibs', 'patch', 'rm', 'buildjni',
518                 'update', 'scanignore', 'scandelete']:
519         return 'list'
520     if name in ['init', 'prebuild', 'build']:
521         return 'script'
522     if name in ['submodules', 'oldsdkloc', 'forceversion', 'forcevercode',
523                 'novcheck']:
524         return 'bool'
525     return 'string'
526
527
528 # Parse metadata for a single application.
529 #
530 #  'metafile' - the filename to read. The package id for the application comes
531 #               from this filename. Pass None to get a blank entry.
532 #
533 # Returns a dictionary containing all the details of the application. There are
534 # two major kinds of information in the dictionary. Keys beginning with capital
535 # letters correspond directory to identically named keys in the metadata file.
536 # Keys beginning with lower case letters are generated in one way or another,
537 # and are not found verbatim in the metadata.
538 #
539 # Known keys not originating from the metadata are:
540 #
541 #  'id'               - the application's package ID
542 #  'builds'           - a list of dictionaries containing build information
543 #                       for each defined build
544 #  'comments'         - a list of comments from the metadata file. Each is
545 #                       a tuple of the form (field, comment) where field is
546 #                       the name of the field it preceded in the metadata
547 #                       file. Where field is None, the comment goes at the
548 #                       end of the file. Alternatively, 'build:version' is
549 #                       for a comment before a particular build version.
550 #  'descriptionlines' - original lines of description as formatted in the
551 #                       metadata file.
552 #
553 def parse_metadata(metafile):
554
555     linedesc = None
556
557     def add_buildflag(p, thisbuild):
558         bv = p.split('=', 1)
559         if len(bv) != 2:
560             raise MetaDataException("Invalid build flag at {0} in {1}"
561                                     .format(buildlines[0], linedesc))
562         pk, pv = bv
563         if pk in thisbuild:
564             raise MetaDataException("Duplicate definition on {0} in version {1} of {2}"
565                                     .format(pk, thisbuild['version'], linedesc))
566
567         pk = pk.lstrip()
568         if pk not in flag_defaults:
569             raise MetaDataException("Unrecognised build flag at {0} in {1}"
570                                     .format(p, linedesc))
571         t = flagtype(pk)
572         if t == 'list':
573             # Port legacy ';' separators
574             thisbuild[pk] = [v.strip() for v in pv.replace(';', ',').split(',')]
575         elif t == 'string' or t == 'script':
576             thisbuild[pk] = pv
577         elif t == 'bool':
578             value = pv == 'yes'
579             if value:
580                 thisbuild[pk] = True
581             else:
582                 logging.debug("...ignoring bool flag %s" % p)
583
584         else:
585             raise MetaDataException("Unrecognised build flag type '%s' at %s in %s"
586                                     % (t, p, linedesc))
587
588     def parse_buildline(lines):
589         value = "".join(lines)
590         parts = [p.replace("\\,", ",")
591                  for p in re.split(r"(?<!\\),", value)]
592         if len(parts) < 3:
593             raise MetaDataException("Invalid build format: " + value + " in " + metafile.name)
594         thisbuild = {}
595         thisbuild['origlines'] = lines
596         thisbuild['version'] = parts[0]
597         thisbuild['vercode'] = parts[1]
598         if parts[2].startswith('!'):
599             # For backwards compatibility, handle old-style disabling,
600             # including attempting to extract the commit from the message
601             thisbuild['disable'] = parts[2][1:]
602             commit = 'unknown - see disabled'
603             index = parts[2].rfind('at ')
604             if index != -1:
605                 commit = parts[2][index + 3:]
606                 if commit.endswith(')'):
607                     commit = commit[:-1]
608             thisbuild['commit'] = commit
609         else:
610             thisbuild['commit'] = parts[2]
611         for p in parts[3:]:
612             add_buildflag(p, thisbuild)
613
614         return thisbuild
615
616     def add_comments(key):
617         if not curcomments:
618             return
619         for comment in curcomments:
620             thisinfo['comments'].append((key, comment))
621         del curcomments[:]
622
623     def get_build_type(build):
624         for t in ['maven', 'gradle', 'kivy']:
625             if build[t]:
626                 return t
627         if build['output']:
628             return 'raw'
629         return 'ant'
630
631     thisinfo = {}
632     if metafile:
633         if not isinstance(metafile, file):
634             metafile = open(metafile, "r")
635         thisinfo['id'] = metafile.name[9:-4]
636     else:
637         thisinfo['id'] = None
638
639     thisinfo.update(app_defaults)
640
641     # General defaults...
642     thisinfo['builds'] = []
643     thisinfo['comments'] = []
644
645     if metafile is None:
646         return thisinfo
647
648     mode = 0
649     buildlines = []
650     curcomments = []
651     curbuild = None
652     vc_seen = {}
653
654     c = 0
655     for line in metafile:
656         c += 1
657         linedesc = "%s:%d" % (metafile.name, c)
658         line = line.rstrip('\r\n')
659         if mode == 3:
660             if not any(line.startswith(s) for s in (' ', '\t')):
661                 if 'commit' not in curbuild and 'disable' not in curbuild:
662                     raise MetaDataException("No commit specified for {0} in {1}"
663                                             .format(curbuild['version'], linedesc))
664
665                 thisinfo['builds'].append(curbuild)
666                 add_comments('build:' + curbuild['vercode'])
667                 mode = 0
668             else:
669                 if line.endswith('\\'):
670                     buildlines.append(line[:-1].lstrip())
671                 else:
672                     buildlines.append(line.lstrip())
673                     bl = ''.join(buildlines)
674                     add_buildflag(bl, curbuild)
675                     buildlines = []
676
677         if mode == 0:
678             if not line:
679                 continue
680             if line.startswith("#"):
681                 curcomments.append(line)
682                 continue
683             try:
684                 field, value = line.split(':', 1)
685             except ValueError:
686                 raise MetaDataException("Invalid metadata in " + linedesc)
687             if field != field.strip() or value != value.strip():
688                 raise MetaDataException("Extra spacing found in " + linedesc)
689
690             # Translate obsolete fields...
691             if field == 'Market Version':
692                 field = 'Current Version'
693             if field == 'Market Version Code':
694                 field = 'Current Version Code'
695
696             fieldtype = metafieldtype(field)
697             if fieldtype not in ['build', 'buildv2']:
698                 add_comments(field)
699             if fieldtype == 'multiline':
700                 mode = 1
701                 thisinfo[field] = []
702                 if value:
703                     raise MetaDataException("Unexpected text on same line as " + field + " in " + linedesc)
704             elif fieldtype == 'string':
705                 thisinfo[field] = value
706             elif fieldtype == 'list':
707                 thisinfo[field] = [v.strip() for v in value.replace(';', ',').split(',')]
708             elif fieldtype == 'build':
709                 if value.endswith("\\"):
710                     mode = 2
711                     buildlines = [value[:-1]]
712                 else:
713                     curbuild = parse_buildline([value])
714                     thisinfo['builds'].append(curbuild)
715                     add_comments('build:' + thisinfo['builds'][-1]['vercode'])
716             elif fieldtype == 'buildv2':
717                 curbuild = {}
718                 vv = value.split(',')
719                 if len(vv) != 2:
720                     raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
721                                             .format(value, linedesc))
722                 curbuild['version'] = vv[0]
723                 curbuild['vercode'] = vv[1]
724                 if curbuild['vercode'] in vc_seen:
725                     raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
726                                             curbuild['vercode'], linedesc))
727                 vc_seen[curbuild['vercode']] = True
728                 buildlines = []
729                 mode = 3
730             elif fieldtype == 'obsolete':
731                 pass        # Just throw it away!
732             else:
733                 raise MetaDataException("Unrecognised field type for " + field + " in " + linedesc)
734         elif mode == 1:     # Multiline field
735             if line == '.':
736                 mode = 0
737             else:
738                 thisinfo[field].append(line)
739         elif mode == 2:     # Line continuation mode in Build Version
740             if line.endswith("\\"):
741                 buildlines.append(line[:-1])
742             else:
743                 buildlines.append(line)
744                 curbuild = parse_buildline(buildlines)
745                 thisinfo['builds'].append(curbuild)
746                 add_comments('build:' + thisinfo['builds'][-1]['vercode'])
747                 mode = 0
748     add_comments(None)
749
750     # Mode at end of file should always be 0...
751     if mode == 1:
752         raise MetaDataException(field + " not terminated in " + metafile.name)
753     elif mode == 2:
754         raise MetaDataException("Unterminated continuation in " + metafile.name)
755     elif mode == 3:
756         raise MetaDataException("Unterminated build in " + metafile.name)
757
758     if not thisinfo['Description']:
759         thisinfo['Description'].append('No description available')
760
761     for build in thisinfo['builds']:
762         for flag, value in flag_defaults.iteritems():
763             if flag in build:
764                 continue
765             build[flag] = value
766         build['type'] = get_build_type(build)
767
768     return thisinfo
769
770
771 # Write a metadata file.
772 #
773 # 'dest'    - The path to the output file
774 # 'app'     - The app data
775 def write_metadata(dest, app):
776
777     def writecomments(key):
778         written = 0
779         for pf, comment in app['comments']:
780             if pf == key:
781                 mf.write("%s\n" % comment)
782                 written += 1
783         if written > 0:
784             logging.debug("...writing comments for " + (key if key else 'EOF'))
785
786     def writefield(field, value=None):
787         writecomments(field)
788         if value is None:
789             value = app[field]
790         t = metafieldtype(field)
791         if t == 'list':
792             value = ','.join(value)
793         mf.write("%s:%s\n" % (field, value))
794
795     mf = open(dest, 'w')
796     if app['Disabled']:
797         writefield('Disabled')
798     if app['AntiFeatures']:
799         writefield('AntiFeatures')
800     if app['Provides']:
801         writefield('Provides')
802     writefield('Categories')
803     writefield('License')
804     writefield('Web Site')
805     writefield('Source Code')
806     writefield('Issue Tracker')
807     if app['Donate']:
808         writefield('Donate')
809     if app['FlattrID']:
810         writefield('FlattrID')
811     if app['Bitcoin']:
812         writefield('Bitcoin')
813     if app['Litecoin']:
814         writefield('Litecoin')
815     if app['Dogecoin']:
816         writefield('Dogecoin')
817     mf.write('\n')
818     if app['Name']:
819         writefield('Name')
820     if app['Auto Name']:
821         writefield('Auto Name')
822     writefield('Summary')
823     writefield('Description', '')
824     for line in app['Description']:
825         mf.write("%s\n" % line)
826     mf.write('.\n')
827     mf.write('\n')
828     if app['Requires Root']:
829         writefield('Requires Root', 'Yes')
830         mf.write('\n')
831     if app['Repo Type']:
832         writefield('Repo Type')
833         writefield('Repo')
834         mf.write('\n')
835     for build in app['builds']:
836         writecomments('build:' + build['vercode'])
837         mf.write("Build:%s,%s\n" % (build['version'], build['vercode']))
838
839         def write_builditem(key, value):
840
841             if key in ['version', 'vercode']:
842                 return
843
844             if value == flag_defaults[key]:
845                 return
846
847             t = flagtype(key)
848
849             logging.debug("...writing {0} : {1}".format(key, value))
850             outline = '    %s=' % key
851
852             if t == 'string':
853                 outline += value
854             if t == 'bool':
855                 outline += 'yes'
856             elif t == 'script':
857                 outline += '&& \\\n        '.join([s.lstrip() for s in value.split('&& ')])
858             elif t == 'list':
859                 outline += ','.join(value) if type(value) == list else value
860
861             outline += '\n'
862             mf.write(outline)
863
864         for flag in flag_defaults:
865             value = build[flag]
866             if value:
867                 write_builditem(flag, value)
868         mf.write('\n')
869
870     if app['Maintainer Notes']:
871         writefield('Maintainer Notes', '')
872         for line in app['Maintainer Notes']:
873             mf.write("%s\n" % line)
874         mf.write('.\n')
875         mf.write('\n')
876
877     if app['Archive Policy']:
878         writefield('Archive Policy')
879     writefield('Auto Update Mode')
880     writefield('Update Check Mode')
881     if app['Update Check Ignore']:
882         writefield('Update Check Ignore')
883     if app['Vercode Operation']:
884         writefield('Vercode Operation')
885     if app['Update Check Name']:
886         writefield('Update Check Name')
887     if app['Update Check Data']:
888         writefield('Update Check Data')
889     if app['Current Version']:
890         writefield('Current Version')
891         writefield('Current Version Code')
892     mf.write('\n')
893     if app['No Source Since']:
894         writefield('No Source Since')
895         mf.write('\n')
896     writecomments(None)
897     mf.close()