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