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>
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.
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.
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/>.
29 # use libyaml if it is available
31 from yaml import CLoader
34 from yaml import Loader
37 # use the C implementation when available
38 import xml.etree.cElementTree as ElementTree
40 import fdroidserver.common
45 class MetaDataException(Exception):
47 def __init__(self, value):
53 # To filter which ones should be written to the metadata files if
83 'Update Check Ignore',
88 'Current Version Code',
91 'comments', # For formats that don't do inline comments
92 'builds', # For formats that do builds as a list
100 self.AntiFeatures = []
102 self.Categories = ['None']
103 self.License = 'Unknown'
104 self.AuthorName = None
105 self.AuthorEmail = None
108 self.IssueTracker = ''
117 self.Description = ''
118 self.RequiresRoot = False
122 self.MaintainerNotes = ''
123 self.ArchivePolicy = None
124 self.AutoUpdateMode = 'None'
125 self.UpdateCheckMode = 'None'
126 self.UpdateCheckIgnore = None
127 self.VercodeOperation = None
128 self.UpdateCheckName = None
129 self.UpdateCheckData = None
130 self.CurrentVersion = ''
131 self.CurrentVersionCode = None
132 self.NoSourceSince = ''
135 self.metadatapath = None
139 self.lastupdated = None
140 self._modified = set()
142 # Translates human-readable field names to attribute names, e.g.
143 # 'Auto Name' to 'AutoName'
145 def field_to_attr(cls, f):
146 return f.replace(' ', '')
148 # Translates attribute names to human-readable field names, e.g.
149 # 'AutoName' to 'Auto Name'
151 def attr_to_field(cls, k):
154 f = re.sub(r'([a-z])([A-Z])', r'\1 \2', k)
157 # Constructs an old-fashioned dict with the human-readable field
158 # names. Should only be used for tests.
159 def field_dict(self):
161 for k, v in self.__dict__.items():
165 b = {k: v for k, v in build.__dict__.items() if not k.startswith('_')}
166 d['builds'].append(b)
167 elif not k.startswith('_'):
168 f = App.attr_to_field(k)
172 # Gets the value associated to a field name, e.g. 'Auto Name'
173 def get_field(self, f):
174 if f not in app_fields:
175 raise MetaDataException('Unrecognised app field: ' + f)
176 k = App.field_to_attr(f)
177 return getattr(self, k)
179 # Sets the value associated to a field name, e.g. 'Auto Name'
180 def set_field(self, f, v):
181 if f not in app_fields:
182 raise MetaDataException('Unrecognised app field: ' + f)
183 k = App.field_to_attr(f)
185 self._modified.add(k)
187 # Appends to the value associated to a field name, e.g. 'Auto Name'
188 def append_field(self, f, v):
189 if f not in app_fields:
190 raise MetaDataException('Unrecognised app field: ' + f)
191 k = App.field_to_attr(f)
192 if k not in self.__dict__:
193 self.__dict__[k] = [v]
195 self.__dict__[k].append(v)
197 # Like dict.update(), but using human-readable field names
198 def update_fields(self, d):
199 for f, v in d.items():
203 build.update_flags(b)
204 self.builds.append(build)
219 'Description': TYPE_MULTILINE,
220 'Maintainer Notes': TYPE_MULTILINE,
221 'Categories': TYPE_LIST,
222 'AntiFeatures': TYPE_LIST,
223 'Build Version': TYPE_BUILD,
224 'Build': TYPE_BUILD_V2,
225 'Use Built': TYPE_OBSOLETE,
230 if name in fieldtypes:
231 return fieldtypes[name]
235 # In the order in which they are laid out on files
236 build_flags_order = [
269 build_flags = set(build_flags_order + ['version', 'vercode'])
278 self.submodules = False
286 self.oldsdkloc = False
288 self.forceversion = False
289 self.forcevercode = False
300 self.preassemble = []
301 self.gradleprops = []
302 self.antcommands = []
303 self.novcheck = False
305 self._modified = set()
307 def get_flag(self, f):
308 if f not in build_flags:
309 raise MetaDataException('Unrecognised build flag: ' + f)
310 return getattr(self, f)
312 def set_flag(self, f, v):
313 if f == 'versionName':
315 if f == 'versionCode':
317 if f not in build_flags:
318 raise MetaDataException('Unrecognised build flag: ' + f)
320 self._modified.add(f)
322 def append_flag(self, f, v):
323 if f not in build_flags:
324 raise MetaDataException('Unrecognised build flag: ' + f)
325 if f not in self.__dict__:
326 self.__dict__[f] = [v]
328 self.__dict__[f].append(v)
330 def build_method(self):
331 for f in ['maven', 'gradle', 'kivy']:
338 # like build_method, but prioritize output=
339 def output_method(self):
342 for f in ['maven', 'gradle', 'kivy']:
350 version = 'r10e' # falls back to latest
351 paths = fdroidserver.common.config['ndk_paths']
352 if version not in paths:
354 return paths[version]
356 def update_flags(self, d):
357 for f, v in d.items():
361 'extlibs': TYPE_LIST,
362 'srclibs': TYPE_LIST,
365 'buildjni': TYPE_LIST,
366 'preassemble': TYPE_LIST,
368 'scanignore': TYPE_LIST,
369 'scandelete': TYPE_LIST,
371 'antcommands': TYPE_LIST,
372 'gradleprops': TYPE_LIST,
374 'prebuild': TYPE_SCRIPT,
375 'build': TYPE_SCRIPT,
376 'submodules': TYPE_BOOL,
377 'oldsdkloc': TYPE_BOOL,
378 'forceversion': TYPE_BOOL,
379 'forcevercode': TYPE_BOOL,
380 'novcheck': TYPE_BOOL,
385 if name in flagtypes:
386 return flagtypes[name]
390 # Designates a metadata field type and checks that it matches
392 # 'name' - The long name of the field type
393 # 'matching' - List of possible values or regex expression
394 # 'sep' - Separator to use if value may be a list
395 # 'fields' - Metadata fields (Field:Value) of this type
396 # 'flags' - Build flags (flag=value) of this type
398 class FieldValidator():
400 def __init__(self, name, matching, fields, flags):
402 self.matching = matching
403 self.compiled = re.compile(matching)
407 def check(self, v, appid):
415 if not self.compiled.match(v):
416 raise MetaDataException("'%s' is not a valid %s in %s. Regex pattern: %s"
417 % (v, self.name, appid, self.matching))
419 # Generic value types
421 FieldValidator("Integer",
426 FieldValidator("Hexadecimal",
431 FieldValidator("HTTP link",
433 ["WebSite", "SourceCode", "IssueTracker", "Changelog", "Donate"], []),
435 FieldValidator("Email",
436 r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
437 ["AuthorEmail"], []),
439 FieldValidator("Bitcoin address",
440 r'^[a-zA-Z0-9]{27,34}$',
444 FieldValidator("Litecoin address",
445 r'^L[a-zA-Z0-9]{33}$',
449 FieldValidator("Repo Type",
450 r'^(git|git-svn|svn|hg|bzr|srclib)$',
454 FieldValidator("Binaries",
459 FieldValidator("Archive Policy",
460 r'^[0-9]+ versions$',
464 FieldValidator("Anti-Feature",
465 r'^(Ads|Tracking|NonFreeNet|NonFreeDep|NonFreeAdd|UpstreamNonFree|NonFreeAssets)$',
469 FieldValidator("Auto Update Mode",
470 r"^(Version .+|None)$",
474 FieldValidator("Update Check Mode",
475 r"^(Tags|Tags .+|RepoManifest|RepoManifest/.+|RepoTrunk|HTTP|Static|None)$",
481 # Check an app's metadata information for integrity errors
482 def check_metadata(app):
485 if k not in app._modified:
487 v.check(app.__dict__[k], app.id)
488 for build in app.builds:
490 if k not in build._modified:
492 v.check(build.__dict__[k], app.id)
495 # Formatter for descriptions. Create an instance, and call parseline() with
496 # each line of the description source from the metadata. At the end, call
497 # end() and then text_txt and text_html will contain the result.
498 class DescriptionFormatter:
505 def __init__(self, linkres):
508 self.state = self.stNONE
509 self.laststate = self.stNONE
512 self.html = io.StringIO()
513 self.text = io.StringIO()
515 self.linkResolver = None
516 self.linkResolver = linkres
518 def endcur(self, notstates=None):
519 if notstates and self.state in notstates:
521 if self.state == self.stPARA:
523 elif self.state == self.stUL:
525 elif self.state == self.stOL:
529 self.laststate = self.state
530 self.state = self.stNONE
531 whole_para = ' '.join(self.para_lines)
532 self.addtext(whole_para)
533 wrapped = textwrap.fill(whole_para, 80,
534 break_long_words=False,
535 break_on_hyphens=False)
536 self.text.write(wrapped)
537 self.html.write('</p>')
538 del self.para_lines[:]
541 self.html.write('</ul>')
542 self.laststate = self.state
543 self.state = self.stNONE
546 self.html.write('</ol>')
547 self.laststate = self.state
548 self.state = self.stNONE
550 def formatted(self, txt, html):
553 txt = cgi.escape(txt)
555 index = txt.find("''")
560 if txt.startswith("'''"):
566 self.bold = not self.bold
574 self.ital = not self.ital
577 def linkify(self, txt):
581 index = txt.find("[")
583 return (res_plain + self.formatted(txt, False), res_html + self.formatted(txt, True))
584 res_plain += self.formatted(txt[:index], False)
585 res_html += self.formatted(txt[:index], True)
587 if txt.startswith("[["):
588 index = txt.find("]]")
590 raise MetaDataException("Unterminated ]]")
592 if self.linkResolver:
593 url, urltext = self.linkResolver(url)
596 res_html += '<a href="' + url + '">' + cgi.escape(urltext) + '</a>'
598 txt = txt[index + 2:]
600 index = txt.find("]")
602 raise MetaDataException("Unterminated ]")
604 index2 = url.find(' ')
608 urltxt = url[index2 + 1:]
611 raise MetaDataException("Url title is just the URL - use [url]")
612 res_html += '<a href="' + url + '">' + cgi.escape(urltxt) + '</a>'
615 res_plain += ' (' + url + ')'
616 txt = txt[index + 1:]
618 def addtext(self, txt):
619 p, h = self.linkify(txt)
622 def parseline(self, line):
625 elif line.startswith('* '):
626 self.endcur([self.stUL])
627 if self.state != self.stUL:
628 self.html.write('<ul>')
629 self.state = self.stUL
630 if self.laststate != self.stNONE:
631 self.text.write('\n\n')
633 self.text.write('\n')
634 self.text.write(line)
635 self.html.write('<li>')
636 self.addtext(line[1:])
637 self.html.write('</li>')
638 elif line.startswith('# '):
639 self.endcur([self.stOL])
640 if self.state != self.stOL:
641 self.html.write('<ol>')
642 self.state = self.stOL
643 if self.laststate != self.stNONE:
644 self.text.write('\n\n')
646 self.text.write('\n')
647 self.text.write(line)
648 self.html.write('<li>')
649 self.addtext(line[1:])
650 self.html.write('</li>')
652 self.para_lines.append(line)
653 self.endcur([self.stPARA])
654 if self.state == self.stNONE:
655 self.state = self.stPARA
656 if self.laststate != self.stNONE:
657 self.text.write('\n\n')
658 self.html.write('<p>')
662 self.text_txt = self.text.getvalue()
663 self.text_html = self.html.getvalue()
668 # Parse multiple lines of description as written in a metadata file, returning
669 # a single string in text format and wrapped to 80 columns.
670 def description_txt(s):
671 ps = DescriptionFormatter(None)
672 for line in s.splitlines():
678 # Parse multiple lines of description as written in a metadata file, returning
679 # a single string in wiki format. Used for the Maintainer Notes field as well,
680 # because it's the same format.
681 def description_wiki(s):
685 # Parse multiple lines of description as written in a metadata file, returning
686 # a single string in HTML format.
687 def description_html(s, linkres):
688 ps = DescriptionFormatter(linkres)
689 for line in s.splitlines():
695 def parse_srclib(metadatapath):
699 # Defaults for fields that come from metadata
700 thisinfo['Repo Type'] = ''
701 thisinfo['Repo'] = ''
702 thisinfo['Subdir'] = None
703 thisinfo['Prepare'] = None
705 if not os.path.exists(metadatapath):
708 metafile = open(metadatapath, "r")
711 for line in metafile:
713 line = line.rstrip('\r\n')
714 if not line or line.startswith("#"):
718 f, v = line.split(':', 1)
720 raise MetaDataException("Invalid metadata in %s:%d" % (line, n))
723 thisinfo[f] = v.split(',')
733 """Read all srclib metadata.
735 The information read will be accessible as metadata.srclibs, which is a
736 dictionary, keyed on srclib name, with the values each being a dictionary
737 in the same format as that returned by the parse_srclib function.
739 A MetaDataException is raised if there are any problems with the srclib
744 # They were already loaded
745 if srclibs is not None:
751 if not os.path.exists(srcdir):
754 for metadatapath in sorted(glob.glob(os.path.join(srcdir, '*.txt'))):
755 srclibname = os.path.basename(metadatapath[:-4])
756 srclibs[srclibname] = parse_srclib(metadatapath)
759 # Read all metadata. Returns a list of 'app' objects (which are dictionaries as
760 # returned by the parse_txt_metadata function.
761 def read_metadata(xref=True):
763 # Always read the srclibs before the apps, since they can use a srlib as
764 # their source repository.
769 for basedir in ('metadata', 'tmp'):
770 if not os.path.exists(basedir):
773 # If there are multiple metadata files for a single appid, then the first
774 # file that is parsed wins over all the others, and the rest throw an
775 # exception. So the original .txt format is parsed first, at least until
776 # newer formats stabilize.
778 for metadatapath in sorted(glob.glob(os.path.join('metadata', '*.txt'))
779 + glob.glob(os.path.join('metadata', '*.json'))
780 + glob.glob(os.path.join('metadata', '*.xml'))
781 + glob.glob(os.path.join('metadata', '*.yaml'))):
782 app = parse_metadata(metadatapath)
784 raise MetaDataException("Found multiple metadata files for " + app.id)
789 # Parse all descriptions at load time, just to ensure cross-referencing
790 # errors are caught early rather than when they hit the build server.
793 return ("fdroid.app:" + appid, "Dummy name - don't know yet")
794 raise MetaDataException("Cannot resolve app id " + appid)
796 for appid, app in apps.items():
798 description_html(app.Description, linkres)
799 except MetaDataException as e:
800 raise MetaDataException("Problem with description of " + appid +
805 # Port legacy ';' separators
806 list_sep = re.compile(r'[,;]')
809 def split_list_values(s):
811 for v in re.split(list_sep, s):
821 def get_default_app_info(metadatapath=None):
822 if metadatapath is None:
825 appid, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
828 app.metadatapath = metadatapath
829 if appid is not None:
835 def sorted_builds(builds):
836 return sorted(builds, key=lambda build: int(build.vercode))
839 esc_newlines = re.compile(r'\\( |\n)')
842 # This function uses __dict__ to be faster
843 def post_metadata_parse(app):
845 for k, v in app.__dict__.items():
846 if k not in app._modified:
848 if type(v) in (float, int):
849 app.__dict__[k] = str(v)
851 for build in app.builds:
852 for k, v in build.__dict__.items():
854 if k not in build._modified:
856 if type(v) in (float, int):
857 build.__dict__[k] = str(v)
861 if ftype == TYPE_SCRIPT:
862 build.__dict__[k] = re.sub(esc_newlines, '', v).lstrip().rstrip()
863 elif ftype == TYPE_BOOL:
864 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
865 if isinstance(v, str):
866 build.__dict__[k] = _decode_bool(v)
867 elif ftype == TYPE_STRING:
868 if isinstance(v, bool) and v:
869 build.__dict__[k] = 'yes'
871 if not app.Description:
872 app.Description = 'No description available'
874 app.builds = sorted_builds(app.builds)
877 # Parse metadata for a single application.
879 # 'metadatapath' - the filename to read. The package id for the application comes
880 # from this filename. Pass None to get a blank entry.
882 # Returns a dictionary containing all the details of the application. There are
883 # two major kinds of information in the dictionary. Keys beginning with capital
884 # letters correspond directory to identically named keys in the metadata file.
885 # Keys beginning with lower case letters are generated in one way or another,
886 # and are not found verbatim in the metadata.
888 # Known keys not originating from the metadata are:
890 # 'builds' - a list of dictionaries containing build information
891 # for each defined build
892 # 'comments' - a list of comments from the metadata file. Each is
893 # a list of the form [field, comment] where field is
894 # the name of the field it preceded in the metadata
895 # file. Where field is None, the comment goes at the
896 # end of the file. Alternatively, 'build:version' is
897 # for a comment before a particular build version.
898 # 'descriptionlines' - original lines of description as formatted in the
903 bool_true = re.compile(r'([Yy]es|[Tt]rue)')
904 bool_false = re.compile(r'([Nn]o|[Ff]alse)')
908 if bool_true.match(s):
910 if bool_false.match(s):
912 raise MetaDataException("Invalid bool '%s'" % s)
915 def parse_metadata(metadatapath):
916 _, ext = fdroidserver.common.get_extension(metadatapath)
917 accepted = fdroidserver.common.config['accepted_formats']
918 if ext not in accepted:
919 raise MetaDataException('"%s" is not an accepted format, convert to: %s' % (
920 metadatapath, ', '.join(accepted)))
923 app.metadatapath = metadatapath
924 app.id, _ = fdroidserver.common.get_extension(os.path.basename(metadatapath))
926 with open(metadatapath, 'r') as mf:
928 parse_txt_metadata(mf, app)
930 parse_json_metadata(mf, app)
932 parse_xml_metadata(mf, app)
934 parse_yaml_metadata(mf, app)
936 raise MetaDataException('Unknown metadata format: %s' % metadatapath)
938 post_metadata_parse(app)
942 def parse_json_metadata(mf, app):
944 # fdroid metadata is only strings and booleans, no floats or ints.
945 # TODO create schema using https://pypi.python.org/pypi/jsonschema
946 jsoninfo = json.load(mf, parse_int=lambda s: s,
947 parse_float=lambda s: s)
948 app.update_fields(jsoninfo)
949 for f in ['Description', 'Maintainer Notes']:
951 app.set_field(f, '\n'.join(v))
955 def parse_xml_metadata(mf, app):
957 tree = ElementTree.ElementTree(file=mf)
958 root = tree.getroot()
960 if root.tag != 'resources':
961 raise MetaDataException('resources file does not have root element <resources/>')
964 if child.tag != 'builds':
965 # builds does not have name="" attrib
966 name = child.attrib['name']
968 if child.tag == 'string':
969 app.set_field(name, child.text)
970 elif child.tag == 'string-array':
972 app.append_field(name, item.text)
973 elif child.tag == 'builds':
977 build.set_flag(key.tag, key.text)
978 app.builds.append(build)
980 # TODO handle this using <xsd:element type="xsd:boolean> in a schema
981 if not isinstance(app.RequiresRoot, bool):
982 app.RequiresRoot = app.RequiresRoot == 'true'
987 def parse_yaml_metadata(mf, app):
989 yamlinfo = yaml.load(mf, Loader=YamlLoader)
990 app.update_fields(yamlinfo)
994 build_line_sep = re.compile(r'(?<!\\),')
995 build_cont = re.compile(r'^[ \t]')
998 def parse_txt_metadata(mf, app):
1002 def add_buildflag(p, build):
1004 raise MetaDataException("Empty build flag at {1}"
1005 .format(buildlines[0], linedesc))
1006 bv = p.split('=', 1)
1008 raise MetaDataException("Invalid build flag at {0} in {1}"
1009 .format(buildlines[0], linedesc))
1015 pv = split_list_values(pv)
1016 build.set_flag(pk, pv)
1017 elif t == TYPE_STRING or t == TYPE_SCRIPT:
1018 build.set_flag(pk, pv)
1019 elif t == TYPE_BOOL:
1020 build.set_flag(pk, _decode_bool(pv))
1022 def parse_buildline(lines):
1024 parts = [p.replace("\\,", ",") for p in re.split(build_line_sep, v)]
1026 raise MetaDataException("Invalid build format: " + v + " in " + mf.name)
1028 build.version = parts[0]
1029 build.vercode = parts[1]
1030 if parts[2].startswith('!'):
1031 # For backwards compatibility, handle old-style disabling,
1032 # including attempting to extract the commit from the message
1033 build.disable = parts[2][1:]
1034 commit = 'unknown - see disabled'
1035 index = parts[2].rfind('at ')
1037 commit = parts[2][index + 3:]
1038 if commit.endswith(')'):
1039 commit = commit[:-1]
1040 build.commit = commit
1042 build.commit = parts[2]
1044 add_buildflag(p, build)
1048 def add_comments(key):
1051 app.comments[key] = list(curcomments)
1056 multiline_lines = []
1064 linedesc = "%s:%d" % (mf.name, c)
1065 line = line.rstrip('\r\n')
1067 if build_cont.match(line):
1068 if line.endswith('\\'):
1069 buildlines.append(line[:-1].lstrip())
1071 buildlines.append(line.lstrip())
1072 bl = ''.join(buildlines)
1073 add_buildflag(bl, build)
1076 if not build.commit and not build.disable:
1077 raise MetaDataException("No commit specified for {0} in {1}"
1078 .format(build.version, linedesc))
1080 app.builds.append(build)
1081 add_comments('build:' + build.vercode)
1087 if line.startswith("#"):
1088 curcomments.append(line[1:].strip())
1091 f, v = line.split(':', 1)
1093 raise MetaDataException("Invalid metadata in " + linedesc)
1095 # Translate obsolete fields...
1096 if f == 'Market Version':
1097 f = 'Current Version'
1098 if f == 'Market Version Code':
1099 f = 'Current Version Code'
1101 ftype = fieldtype(f)
1102 if ftype not in [TYPE_BUILD, TYPE_BUILD_V2]:
1104 if ftype == TYPE_MULTILINE:
1107 raise MetaDataException("Unexpected text on same line as " + f + " in " + linedesc)
1108 elif ftype == TYPE_STRING:
1110 elif ftype == TYPE_LIST:
1111 app.set_field(f, split_list_values(v))
1112 elif ftype == TYPE_BUILD:
1113 if v.endswith("\\"):
1116 buildlines.append(v[:-1])
1118 build = parse_buildline([v])
1119 app.builds.append(build)
1120 add_comments('build:' + app.builds[-1].vercode)
1121 elif ftype == TYPE_BUILD_V2:
1124 raise MetaDataException('Build should have comma-separated version and vercode, not "{0}", in {1}'
1125 .format(v, linedesc))
1127 build.version = vv[0]
1128 build.vercode = vv[1]
1129 if build.vercode in vc_seen:
1130 raise MetaDataException('Duplicate build recipe found for vercode %s in %s' % (
1131 build.vercode, linedesc))
1132 vc_seen.add(build.vercode)
1135 elif ftype == TYPE_OBSOLETE:
1136 pass # Just throw it away!
1138 raise MetaDataException("Unrecognised field '" + f + "' in " + linedesc)
1139 elif mode == 1: # Multiline field
1142 app.set_field(f, '\n'.join(multiline_lines))
1143 del multiline_lines[:]
1145 multiline_lines.append(line)
1146 elif mode == 2: # Line continuation mode in Build Version
1147 if line.endswith("\\"):
1148 buildlines.append(line[:-1])
1150 buildlines.append(line)
1151 build = parse_buildline(buildlines)
1152 app.builds.append(build)
1153 add_comments('build:' + app.builds[-1].vercode)
1157 # Mode at end of file should always be 0
1159 raise MetaDataException(f + " not terminated in " + mf.name)
1161 raise MetaDataException("Unterminated continuation in " + mf.name)
1163 raise MetaDataException("Unterminated build in " + mf.name)
1168 def write_plaintext_metadata(mf, app, w_comment, w_field, w_build):
1170 def w_comments(key):
1171 if key not in app.comments:
1173 for line in app.comments[key]:
1176 def w_field_always(f, v=None):
1178 v = app.get_field(f)
1182 def w_field_nonempty(f, v=None):
1184 v = app.get_field(f)
1189 w_field_nonempty('Disabled')
1190 w_field_nonempty('AntiFeatures')
1191 w_field_nonempty('Provides')
1192 w_field_always('Categories')
1193 w_field_always('License')
1194 w_field_nonempty('Author Name')
1195 w_field_nonempty('Author Email')
1196 w_field_always('Web Site')
1197 w_field_always('Source Code')
1198 w_field_always('Issue Tracker')
1199 w_field_nonempty('Changelog')
1200 w_field_nonempty('Donate')
1201 w_field_nonempty('FlattrID')
1202 w_field_nonempty('Bitcoin')
1203 w_field_nonempty('Litecoin')
1205 w_field_nonempty('Name')
1206 w_field_nonempty('Auto Name')
1207 w_field_always('Summary')
1208 w_field_always('Description', description_txt(app.Description))
1210 if app.RequiresRoot:
1211 w_field_always('Requires Root', 'yes')
1214 w_field_always('Repo Type')
1215 w_field_always('Repo')
1217 w_field_always('Binaries')
1220 for build in sorted_builds(app.builds):
1222 if build.version == "Ignore":
1225 w_comments('build:' + build.vercode)
1229 if app.MaintainerNotes:
1230 w_field_always('Maintainer Notes', app.MaintainerNotes)
1233 w_field_nonempty('Archive Policy')
1234 w_field_always('Auto Update Mode')
1235 w_field_always('Update Check Mode')
1236 w_field_nonempty('Update Check Ignore')
1237 w_field_nonempty('Vercode Operation')
1238 w_field_nonempty('Update Check Name')
1239 w_field_nonempty('Update Check Data')
1240 if app.CurrentVersion:
1241 w_field_always('Current Version')
1242 w_field_always('Current Version Code')
1243 if app.NoSourceSince:
1245 w_field_always('No Source Since')
1249 # Write a metadata file in txt format.
1251 # 'mf' - Writer interface (file, StringIO, ...)
1252 # 'app' - The app data
1253 def write_txt_metadata(mf, app):
1255 def w_comment(line):
1256 mf.write("# %s\n" % line)
1262 elif t == TYPE_MULTILINE:
1263 v = '\n' + v + '\n.'
1264 mf.write("%s:%s\n" % (f, v))
1267 mf.write("Build:%s,%s\n" % (build.version, build.vercode))
1269 for f in build_flags_order:
1270 v = build.get_flag(f)
1275 mf.write(' %s=' % f)
1276 if t == TYPE_STRING:
1278 elif t == TYPE_BOOL:
1280 elif t == TYPE_SCRIPT:
1282 for s in v.split(' && '):
1286 mf.write(' && \\\n ')
1288 elif t == TYPE_LIST:
1289 mf.write(','.join(v))
1293 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1296 def write_yaml_metadata(mf, app):
1298 def w_comment(line):
1299 mf.write("# %s\n" % line)
1304 if any(c in v for c in [': ', '%', '@', '*']):
1305 return "'" + v.replace("'", "''") + "'"
1308 def w_field(f, v, prefix='', t=None):
1315 v += prefix + ' - ' + escape(e) + '\n'
1316 elif t == TYPE_MULTILINE:
1318 for l in v.splitlines():
1320 v += prefix + ' ' + l + '\n'
1323 elif t == TYPE_BOOL:
1325 elif t == TYPE_SCRIPT:
1326 cmds = [s + '&& \\' for s in v.split('&& ')]
1328 cmds[-1] = cmds[-1][:-len('&& \\')]
1329 w_field(f, cmds, prefix, 'multiline')
1332 v = ' ' + escape(v) + '\n'
1345 mf.write("builds:\n")
1348 w_field('versionName', build.version, ' - ', TYPE_STRING)
1349 w_field('versionCode', build.vercode, ' ', TYPE_STRING)
1350 for f in build_flags_order:
1351 v = build.get_flag(f)
1355 w_field(f, v, ' ', flagtype(f))
1357 write_plaintext_metadata(mf, app, w_comment, w_field, w_build)
1360 def write_metadata(fmt, mf, app):
1362 return write_txt_metadata(mf, app)
1364 return write_yaml_metadata(mf, app)
1365 raise MetaDataException("Unknown metadata format given")