chiark / gitweb /
e746f2691ebb8e0ed19a579b476768603b8dac86
[fdroidserver.git] / fdroidserver / lint.py
1 #!/usr/bin/env python3
2 #
3 # lint.py - part of the FDroid server tool
4 # Copyright (C) 2013-2014 Daniel Martí <mvdan@mvdan.cc>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU Affero General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See th
14 # GNU Affero General Public License for more details.
15 #
16 # You should have received a copy of the GNU Affero General Public Licen
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 from argparse import ArgumentParser
20 import glob
21 import os
22 import re
23 import sys
24
25 from . import _
26 from . import common
27 from . import metadata
28 from . import rewritemeta
29
30 config = None
31 options = None
32
33
34 def enforce_https(domain):
35     return (re.compile(r'^[^h][^t][^t][^p][^s]://[^/]*' + re.escape(domain) + r'(/.*)?', re.IGNORECASE),
36             domain + " URLs should always use https://")
37
38
39 https_enforcings = [
40     enforce_https('github.com'),
41     enforce_https('gitlab.com'),
42     enforce_https('bitbucket.org'),
43     enforce_https('apache.org'),
44     enforce_https('google.com'),
45     enforce_https('git.code.sf.net'),
46     enforce_https('svn.code.sf.net'),
47     enforce_https('anongit.kde.org'),
48     enforce_https('savannah.nongnu.org'),
49     enforce_https('git.savannah.nongnu.org'),
50     enforce_https('download.savannah.nongnu.org'),
51     enforce_https('savannah.gnu.org'),
52     enforce_https('git.savannah.gnu.org'),
53     enforce_https('download.savannah.gnu.org'),
54     enforce_https('github.io'),
55     enforce_https('gitlab.io'),
56     enforce_https('githubusercontent.com'),
57 ]
58
59
60 def forbid_shortener(domain):
61     return (re.compile(r'https?://[^/]*' + re.escape(domain) + r'/.*'),
62             _("URL shorteners should not be used"))
63
64
65 http_url_shorteners = [
66     forbid_shortener('1url.com'),
67     forbid_shortener('adf.ly'),
68     forbid_shortener('bc.vc'),
69     forbid_shortener('bit.do'),
70     forbid_shortener('bit.ly'),
71     forbid_shortener('bitly.com'),
72     forbid_shortener('budurl.com'),
73     forbid_shortener('buzurl.com'),
74     forbid_shortener('cli.gs'),
75     forbid_shortener('cur.lv'),
76     forbid_shortener('cutt.us'),
77     forbid_shortener('db.tt'),
78     forbid_shortener('filoops.info'),
79     forbid_shortener('goo.gl'),
80     forbid_shortener('is.gd'),
81     forbid_shortener('ity.im'),
82     forbid_shortener('j.mp'),
83     forbid_shortener('l.gg'),
84     forbid_shortener('lnkd.in'),
85     forbid_shortener('moourl.com'),
86     forbid_shortener('ow.ly'),
87     forbid_shortener('para.pt'),
88     forbid_shortener('po.st'),
89     forbid_shortener('q.gs'),
90     forbid_shortener('qr.ae'),
91     forbid_shortener('qr.net'),
92     forbid_shortener('rdlnk.com'),
93     forbid_shortener('scrnch.me'),
94     forbid_shortener('short.nr'),
95     forbid_shortener('sn.im'),
96     forbid_shortener('snipurl.com'),
97     forbid_shortener('su.pr'),
98     forbid_shortener('t.co'),
99     forbid_shortener('tiny.cc'),
100     forbid_shortener('tinyarrows.com'),
101     forbid_shortener('tinyurl.com'),
102     forbid_shortener('tr.im'),
103     forbid_shortener('tweez.me'),
104     forbid_shortener('twitthis.com'),
105     forbid_shortener('twurl.nl'),
106     forbid_shortener('tyn.ee'),
107     forbid_shortener('u.bb'),
108     forbid_shortener('u.to'),
109     forbid_shortener('ur1.ca'),
110     forbid_shortener('urlof.site'),
111     forbid_shortener('v.gd'),
112     forbid_shortener('vzturl.com'),
113     forbid_shortener('x.co'),
114     forbid_shortener('xrl.us'),
115     forbid_shortener('yourls.org'),
116     forbid_shortener('zip.net'),
117     forbid_shortener('✩.ws'),
118     forbid_shortener('➡.ws'),
119 ]
120
121 http_checks = https_enforcings + http_url_shorteners + [
122     (re.compile(r'.*github\.com/[^/]+/[^/]+\.git'),
123      _("Appending .git is not necessary")),
124     (re.compile(r'.*://[^/]*(github|gitlab|bitbucket|rawgit)[^/]*/([^/]+/){1,3}master'),
125      _("Use /HEAD instead of /master to point at a file in the default branch")),
126 ]
127
128 regex_checks = {
129     'WebSite': http_checks,
130     'SourceCode': http_checks,
131     'Repo': https_enforcings,
132     'UpdateCheckMode': https_enforcings,
133     'IssueTracker': http_checks + [
134         (re.compile(r'.*github\.com/[^/]+/[^/]+/*$'),
135          _("/issues is missing")),
136         (re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'),
137          _("/issues is missing")),
138     ],
139     'Donate': http_checks + [
140         (re.compile(r'.*flattr\.com'),
141          _("Flattr donation methods belong in the FlattrID flag")),
142         (re.compile(r'.*liberapay\.com'),
143          _("Liberapay donation methods belong in the LiberapayID flag")),
144     ],
145     'Changelog': http_checks,
146     'Author Name': [
147         (re.compile(r'^\s'),
148          _("Unnecessary leading space")),
149         (re.compile(r'.*\s$'),
150          _("Unnecessary trailing space")),
151     ],
152     'Summary': [
153         (re.compile(r'.*\b(free software|open source)\b.*', re.IGNORECASE),
154          _("No need to specify that the app is Free Software")),
155         (re.compile(r'.*((your|for).*android|android.*(app|device|client|port|version))', re.IGNORECASE),
156          _("No need to specify that the app is for Android")),
157         (re.compile(r'.*[a-z0-9][.!?]( |$)'),
158          _("Punctuation should be avoided")),
159         (re.compile(r'^\s'),
160          _("Unnecessary leading space")),
161         (re.compile(r'.*\s$'),
162          _("Unnecessary trailing space")),
163     ],
164     'Description': https_enforcings + http_url_shorteners + [
165         (re.compile(r'\s*[*#][^ .]'),
166          _("Invalid bulleted list")),
167         (re.compile(r'^\s'),
168          _("Unnecessary leading space")),
169         (re.compile(r'.*\s$'),
170          _("Unnecessary trailing space")),
171         (re.compile(r'.*<(applet|base|body|button|embed|form|head|html|iframe|img|input|link|object|picture|script|source|style|svg|video).*', re.IGNORECASE),
172          _("Forbidden HTML tags")),
173         (re.compile(r'''.*\s+src=["']javascript:.*'''),
174          _("Javascript in HTML src attributes")),
175     ],
176 }
177
178 locale_pattern = re.compile(r'^[a-z]{2,3}(-[A-Z][A-Z])?$')
179
180
181 def check_regexes(app):
182     for f, checks in regex_checks.items():
183         for m, r in checks:
184             v = app.get(f)
185             t = metadata.fieldtype(f)
186             if t == metadata.TYPE_MULTILINE:
187                 for l in v.splitlines():
188                     if m.match(l):
189                         yield "%s at line '%s': %s" % (f, l, r)
190             else:
191                 if v is None:
192                     continue
193                 if m.match(v):
194                     yield "%s '%s': %s" % (f, v, r)
195
196
197 def get_lastbuild(builds):
198     lowest_vercode = -1
199     lastbuild = None
200     for build in builds:
201         if not build.disable:
202             vercode = int(build.versionCode)
203             if lowest_vercode == -1 or vercode < lowest_vercode:
204                 lowest_vercode = vercode
205         if not lastbuild or int(build.versionCode) > int(lastbuild.versionCode):
206             lastbuild = build
207     return lastbuild
208
209
210 def check_ucm_tags(app):
211     lastbuild = get_lastbuild(app.builds)
212     if (lastbuild is not None
213             and lastbuild.commit
214             and app.UpdateCheckMode == 'RepoManifest'
215             and not lastbuild.commit.startswith('unknown')
216             and lastbuild.versionCode == app.CurrentVersionCode
217             and not lastbuild.forcevercode
218             and any(s in lastbuild.commit for s in '.,_-/')):
219         yield _("Last used commit '{commit}' looks like a tag, but Update Check Mode is '{ucm}'")\
220             .format(commit=lastbuild.commit, ucm=app.UpdateCheckMode)
221
222
223 def check_char_limits(app):
224     limits = config['char_limits']
225
226     if len(app.Summary) > limits['summary']:
227         yield _("Summary of length {length} is over the {limit} char limit")\
228             .format(length=len(app.Summary), limit=limits['summary'])
229
230     if len(app.Description) > limits['description']:
231         yield _("Description of length {length} is over the {limit} char limit")\
232             .format(length=len(app.Description), limit=limits['description'])
233
234
235 def check_old_links(app):
236     usual_sites = [
237         'github.com',
238         'gitlab.com',
239         'bitbucket.org',
240     ]
241     old_sites = [
242         'gitorious.org',
243         'code.google.com',
244     ]
245     if any(s in app.Repo for s in usual_sites):
246         for f in ['WebSite', 'SourceCode', 'IssueTracker', 'Changelog']:
247             v = app.get(f)
248             if any(s in v for s in old_sites):
249                 yield _("App is in '{repo}' but has a link to {url}")\
250                     .format(repo=app.Repo, url=v)
251
252
253 def check_useless_fields(app):
254     if app.UpdateCheckName == app.id:
255         yield _("Update Check Name is set to the known app id - it can be removed")
256
257
258 filling_ucms = re.compile(r'^(Tags.*|RepoManifest.*)')
259
260
261 def check_checkupdates_ran(app):
262     if filling_ucms.match(app.UpdateCheckMode):
263         if not app.AutoName and not app.CurrentVersion and app.CurrentVersionCode == '0':
264             yield _("UCM is set but it looks like checkupdates hasn't been run yet")
265
266
267 def check_empty_fields(app):
268     if not app.Categories:
269         yield _("Categories are not set")
270
271
272 all_categories = set([
273     "Connectivity",
274     "Development",
275     "Games",
276     "Graphics",
277     "Internet",
278     "Money",
279     "Multimedia",
280     "Navigation",
281     "Phone & SMS",
282     "Reading",
283     "Science & Education",
284     "Security",
285     "Sports & Health",
286     "System",
287     "Theming",
288     "Time",
289     "Writing",
290 ])
291
292
293 def check_categories(app):
294     for categ in app.Categories:
295         if categ not in all_categories:
296             yield _("Category '%s' is not valid" % categ)
297
298
299 def check_duplicates(app):
300     if app.Name and app.Name == app.AutoName:
301         yield _("Name '%s' is just the auto name - remove it") % app.Name
302
303     links_seen = set()
304     for f in ['Source Code', 'Web Site', 'Issue Tracker', 'Changelog']:
305         v = app.get(f)
306         if not v:
307             continue
308         v = v.lower()
309         if v in links_seen:
310             yield _("Duplicate link in '{field}': {url}").format(field=f, url=v)
311         else:
312             links_seen.add(v)
313
314     name = app.Name or app.AutoName
315     if app.Summary and name:
316         if app.Summary.lower() == name.lower():
317             yield _("Summary '%s' is just the app's name") % app.Summary
318
319     if app.Summary and app.Description and len(app.Description) == 1:
320         if app.Summary.lower() == app.Description[0].lower():
321             yield _("Description '%s' is just the app's summary") % app.Summary
322
323     seenlines = set()
324     for l in app.Description.splitlines():
325         if len(l) < 1:
326             continue
327         if l in seenlines:
328             yield _("Description has a duplicate line")
329         seenlines.add(l)
330
331
332 desc_url = re.compile(r'(^|[^[])\[([^ ]+)( |\]|$)')
333
334
335 def check_mediawiki_links(app):
336     wholedesc = ' '.join(app.Description)
337     for um in desc_url.finditer(wholedesc):
338         url = um.group(1)
339         for m, r in http_checks:
340             if m.match(url):
341                 yield _("URL {url} in Description: {error}").format(url=url, error=r)
342
343
344 def check_bulleted_lists(app):
345     validchars = ['*', '#']
346     lchar = ''
347     lcount = 0
348     for l in app.Description.splitlines():
349         if len(l) < 1:
350             lcount = 0
351             continue
352
353         if l[0] == lchar and l[1] == ' ':
354             lcount += 1
355             if lcount > 2 and lchar not in validchars:
356                 yield _("Description has a list (%s) but it isn't bulleted (*) nor numbered (#)") % lchar
357                 break
358         else:
359             lchar = l[0]
360             lcount = 1
361
362
363 def check_builds(app):
364     supported_flags = set(metadata.build_flags)
365     # needed for YAML and JSON
366     for build in app.builds:
367         if build.disable:
368             if build.disable.startswith('Generated by import.py'):
369                 yield _("Build generated by `fdroid import` - remove disable line once ready")
370             continue
371         for s in ['master', 'origin', 'HEAD', 'default', 'trunk']:
372             if build.commit and build.commit.startswith(s):
373                 yield _("Branch '{branch}' used as commit in build '{versionName}'")\
374                     .format(branch=s, versionName=build.versionName)
375             for srclib in build.srclibs:
376                 if '@' in srclib:
377                     ref = srclib.split('@')[1].split('/')[0]
378                     if ref.startswith(s):
379                         yield _("Branch '{branch}' used as commit in srclib '{srclib}'")\
380                             .format(branch=s, srclib=srclib)
381                 else:
382                     yield _('srclibs missing name and/or @') + ' (srclibs: ' + srclib + ')'
383         for key in build.keys():
384             if key not in supported_flags:
385                 yield _('%s is not an accepted build field') % key
386
387
388 def check_files_dir(app):
389     dir_path = os.path.join('metadata', app.id)
390     if not os.path.isdir(dir_path):
391         return
392     files = set()
393     for name in os.listdir(dir_path):
394         path = os.path.join(dir_path, name)
395         if not (os.path.isfile(path) or name == 'signatures' or locale_pattern.match(name)):
396             yield _("Found non-file at %s") % path
397             continue
398         files.add(name)
399
400     used = {'signatures', }
401     for build in app.builds:
402         for fname in build.patch:
403             if fname not in files:
404                 yield _("Unknown file '{filename}' in build '{versionName}'")\
405                     .format(filename=fname, versionName=build.versionName)
406             else:
407                 used.add(fname)
408
409     for name in files.difference(used):
410         if locale_pattern.match(name):
411             continue
412         yield _("Unused file at %s") % os.path.join(dir_path, name)
413
414
415 def check_format(app):
416     if options.format and not rewritemeta.proper_format(app):
417         yield _("Run rewritemeta to fix formatting")
418
419
420 def check_license_tag(app):
421     '''Ensure all license tags are in https://spdx.org/license-list'''
422     if app.License.rstrip('+') not in SPDX:
423         yield _('Invalid license tag "%s"! Use only tags from https://spdx.org/license-list') \
424             % (app.License)
425
426
427 def check_extlib_dir(apps):
428     dir_path = os.path.join('build', 'extlib')
429     unused_extlib_files = set()
430     for root, dirs, files in os.walk(dir_path):
431         for name in files:
432             unused_extlib_files.add(os.path.join(root, name)[len(dir_path) + 1:])
433
434     used = set()
435     for app in apps:
436         for build in app.builds:
437             for path in build.extlibs:
438                 if path not in unused_extlib_files:
439                     yield _("{appid}: Unknown extlib {path} in build '{versionName}'")\
440                         .format(appid=app.id, path=path, versionName=build.versionName)
441                 else:
442                     used.add(path)
443
444     for path in unused_extlib_files.difference(used):
445         if any(path.endswith(s) for s in [
446                 '.gitignore',
447                 'source.txt', 'origin.txt', 'md5.txt',
448                 'LICENSE', 'LICENSE.txt',
449                 'COPYING', 'COPYING.txt',
450                 'NOTICE', 'NOTICE.txt',
451                 ]):
452             continue
453         yield _("Unused extlib at %s") % os.path.join(dir_path, path)
454
455
456 def check_for_unsupported_metadata_files(basedir=""):
457     """Checks whether any non-metadata files are in metadata/"""
458
459     global config
460
461     return_value = False
462     formats = config['accepted_formats']
463     for f in glob.glob(basedir + 'metadata/*') + glob.glob(basedir + 'metadata/.*'):
464         if os.path.isdir(f):
465             exists = False
466             for t in formats:
467                 exists = exists or os.path.exists(f + '.' + t)
468             if not exists:
469                 print(_('"%s/" has no matching metadata file!') % f)
470                 return_value = True
471         elif not os.path.splitext(f)[1][1:] in formats:
472             print('"' + f.replace(basedir, '')
473                   + '" is not a supported file format: (' + ','.join(formats) + ')')
474             return_value = True
475
476     return return_value
477
478
479 def main():
480
481     global config, options
482
483     # Parse command line...
484     parser = ArgumentParser(usage="%(prog)s [options] [APPID [APPID ...]]")
485     common.setup_global_opts(parser)
486     parser.add_argument("-f", "--format", action="store_true", default=False,
487                         help=_("Also warn about formatting issues, like rewritemeta -l"))
488     parser.add_argument("appid", nargs='*', help=_("applicationId in the form APPID"))
489     metadata.add_metadata_arguments(parser)
490     options = parser.parse_args()
491     metadata.warnings_action = options.W
492
493     config = common.read_config(options)
494
495     # Get all apps...
496     allapps = metadata.read_metadata(xref=True)
497     apps = common.read_app_args(options.appid, allapps, False)
498
499     anywarns = check_for_unsupported_metadata_files()
500
501     apps_check_funcs = []
502     if len(options.appid) == 0:
503         # otherwise it finds tons of unused extlibs
504         apps_check_funcs.append(check_extlib_dir)
505     for check_func in apps_check_funcs:
506         for warn in check_func(apps.values()):
507             anywarns = True
508             print(warn)
509
510     for appid, app in apps.items():
511         if app.Disabled:
512             continue
513
514         app_check_funcs = [
515             check_regexes,
516             check_ucm_tags,
517             check_char_limits,
518             check_old_links,
519             check_checkupdates_ran,
520             check_useless_fields,
521             check_empty_fields,
522             check_categories,
523             check_duplicates,
524             check_mediawiki_links,
525             check_bulleted_lists,
526             check_builds,
527             check_files_dir,
528             check_format,
529             check_license_tag,
530         ]
531
532         for check_func in app_check_funcs:
533             for warn in check_func(app):
534                 anywarns = True
535                 print("%s: %s" % (appid, warn))
536
537     if anywarns:
538         sys.exit(1)
539
540
541 # A compiled, public domain list of official SPDX license tags from:
542 # https://github.com/sindresorhus/spdx-license-list/blob/v3.0.1/spdx-simple.json
543 # The deprecated license tags have been removed from the list, they are at the
544 # bottom, starting after the last license tags that start with Z.
545 # This is at the bottom, since its a long list of data
546 SPDX = [
547     "PublicDomain",  # an F-Droid addition, until we can enforce a better option
548     "Glide",
549     "Abstyles",
550     "AFL-1.1",
551     "AFL-1.2",
552     "AFL-2.0",
553     "AFL-2.1",
554     "AFL-3.0",
555     "AMPAS",
556     "APL-1.0",
557     "Adobe-Glyph",
558     "APAFML",
559     "Adobe-2006",
560     "AGPL-1.0",
561     "Afmparse",
562     "Aladdin",
563     "ADSL",
564     "AMDPLPA",
565     "ANTLR-PD",
566     "Apache-1.0",
567     "Apache-1.1",
568     "Apache-2.0",
569     "AML",
570     "APSL-1.0",
571     "APSL-1.1",
572     "APSL-1.2",
573     "APSL-2.0",
574     "Artistic-1.0",
575     "Artistic-1.0-Perl",
576     "Artistic-1.0-cl8",
577     "Artistic-2.0",
578     "AAL",
579     "Bahyph",
580     "Barr",
581     "Beerware",
582     "BitTorrent-1.0",
583     "BitTorrent-1.1",
584     "BSL-1.0",
585     "Borceux",
586     "BSD-2-Clause",
587     "BSD-2-Clause-FreeBSD",
588     "BSD-2-Clause-NetBSD",
589     "BSD-3-Clause",
590     "BSD-3-Clause-Clear",
591     "BSD-3-Clause-No-Nuclear-License",
592     "BSD-3-Clause-No-Nuclear-License-2014",
593     "BSD-3-Clause-No-Nuclear-Warranty",
594     "BSD-4-Clause",
595     "BSD-Protection",
596     "BSD-Source-Code",
597     "BSD-3-Clause-Attribution",
598     "0BSD",
599     "BSD-4-Clause-UC",
600     "bzip2-1.0.5",
601     "bzip2-1.0.6",
602     "Caldera",
603     "CECILL-1.0",
604     "CECILL-1.1",
605     "CECILL-2.0",
606     "CECILL-2.1",
607     "CECILL-B",
608     "CECILL-C",
609     "ClArtistic",
610     "MIT-CMU",
611     "CNRI-Jython",
612     "CNRI-Python",
613     "CNRI-Python-GPL-Compatible",
614     "CPOL-1.02",
615     "CDDL-1.0",
616     "CDDL-1.1",
617     "CPAL-1.0",
618     "CPL-1.0",
619     "CATOSL-1.1",
620     "Condor-1.1",
621     "CC-BY-1.0",
622     "CC-BY-2.0",
623     "CC-BY-2.5",
624     "CC-BY-3.0",
625     "CC-BY-4.0",
626     "CC-BY-ND-1.0",
627     "CC-BY-ND-2.0",
628     "CC-BY-ND-2.5",
629     "CC-BY-ND-3.0",
630     "CC-BY-ND-4.0",
631     "CC-BY-NC-1.0",
632     "CC-BY-NC-2.0",
633     "CC-BY-NC-2.5",
634     "CC-BY-NC-3.0",
635     "CC-BY-NC-4.0",
636     "CC-BY-NC-ND-1.0",
637     "CC-BY-NC-ND-2.0",
638     "CC-BY-NC-ND-2.5",
639     "CC-BY-NC-ND-3.0",
640     "CC-BY-NC-ND-4.0",
641     "CC-BY-NC-SA-1.0",
642     "CC-BY-NC-SA-2.0",
643     "CC-BY-NC-SA-2.5",
644     "CC-BY-NC-SA-3.0",
645     "CC-BY-NC-SA-4.0",
646     "CC-BY-SA-1.0",
647     "CC-BY-SA-2.0",
648     "CC-BY-SA-2.5",
649     "CC-BY-SA-3.0",
650     "CC-BY-SA-4.0",
651     "CC0-1.0",
652     "Crossword",
653     "CrystalStacker",
654     "CUA-OPL-1.0",
655     "Cube",
656     "curl",
657     "D-FSL-1.0",
658     "diffmark",
659     "WTFPL",
660     "DOC",
661     "Dotseqn",
662     "DSDP",
663     "dvipdfm",
664     "EPL-1.0",
665     "ECL-1.0",
666     "ECL-2.0",
667     "eGenix",
668     "EFL-1.0",
669     "EFL-2.0",
670     "MIT-advertising",
671     "MIT-enna",
672     "Entessa",
673     "ErlPL-1.1",
674     "EUDatagrid",
675     "EUPL-1.0",
676     "EUPL-1.1",
677     "Eurosym",
678     "Fair",
679     "MIT-feh",
680     "Frameworx-1.0",
681     "FreeImage",
682     "FTL",
683     "FSFAP",
684     "FSFUL",
685     "FSFULLR",
686     "Giftware",
687     "GL2PS",
688     "Glulxe",
689     "AGPL-3.0",
690     "GFDL-1.1",
691     "GFDL-1.2",
692     "GFDL-1.3",
693     "GPL-1.0",
694     "GPL-2.0",
695     "GPL-3.0",
696     "LGPL-2.1",
697     "LGPL-3.0",
698     "LGPL-2.0",
699     "gnuplot",
700     "gSOAP-1.3b",
701     "HaskellReport",
702     "HPND",
703     "IBM-pibs",
704     "IPL-1.0",
705     "ICU",
706     "ImageMagick",
707     "iMatix",
708     "Imlib2",
709     "IJG",
710     "Info-ZIP",
711     "Intel-ACPI",
712     "Intel",
713     "Interbase-1.0",
714     "IPA",
715     "ISC",
716     "JasPer-2.0",
717     "JSON",
718     "LPPL-1.0",
719     "LPPL-1.1",
720     "LPPL-1.2",
721     "LPPL-1.3a",
722     "LPPL-1.3c",
723     "Latex2e",
724     "BSD-3-Clause-LBNL",
725     "Leptonica",
726     "LGPLLR",
727     "Libpng",
728     "libtiff",
729     "LAL-1.2",
730     "LAL-1.3",
731     "LiLiQ-P-1.1",
732     "LiLiQ-Rplus-1.1",
733     "LiLiQ-R-1.1",
734     "LPL-1.02",
735     "LPL-1.0",
736     "MakeIndex",
737     "MTLL",
738     "MS-PL",
739     "MS-RL",
740     "MirOS",
741     "MITNFA",
742     "MIT",
743     "Motosoto",
744     "MPL-1.0",
745     "MPL-1.1",
746     "MPL-2.0",
747     "MPL-2.0-no-copyleft-exception",
748     "mpich2",
749     "Multics",
750     "Mup",
751     "NASA-1.3",
752     "Naumen",
753     "NBPL-1.0",
754     "Net-SNMP",
755     "NetCDF",
756     "NGPL",
757     "NOSL",
758     "NPL-1.0",
759     "NPL-1.1",
760     "Newsletr",
761     "NLPL",
762     "Nokia",
763     "NPOSL-3.0",
764     "NLOD-1.0",
765     "Noweb",
766     "NRL",
767     "NTP",
768     "Nunit",
769     "OCLC-2.0",
770     "ODbL-1.0",
771     "PDDL-1.0",
772     "OCCT-PL",
773     "OGTSL",
774     "OLDAP-2.2.2",
775     "OLDAP-1.1",
776     "OLDAP-1.2",
777     "OLDAP-1.3",
778     "OLDAP-1.4",
779     "OLDAP-2.0",
780     "OLDAP-2.0.1",
781     "OLDAP-2.1",
782     "OLDAP-2.2",
783     "OLDAP-2.2.1",
784     "OLDAP-2.3",
785     "OLDAP-2.4",
786     "OLDAP-2.5",
787     "OLDAP-2.6",
788     "OLDAP-2.7",
789     "OLDAP-2.8",
790     "OML",
791     "OPL-1.0",
792     "OSL-1.0",
793     "OSL-1.1",
794     "OSL-2.0",
795     "OSL-2.1",
796     "OSL-3.0",
797     "OpenSSL",
798     "OSET-PL-2.1",
799     "PHP-3.0",
800     "PHP-3.01",
801     "Plexus",
802     "PostgreSQL",
803     "psfrag",
804     "psutils",
805     "Python-2.0",
806     "QPL-1.0",
807     "Qhull",
808     "Rdisc",
809     "RPSL-1.0",
810     "RPL-1.1",
811     "RPL-1.5",
812     "RHeCos-1.1",
813     "RSCPL",
814     "RSA-MD",
815     "Ruby",
816     "SAX-PD",
817     "Saxpath",
818     "SCEA",
819     "SWL",
820     "SMPPL",
821     "Sendmail",
822     "SGI-B-1.0",
823     "SGI-B-1.1",
824     "SGI-B-2.0",
825     "OFL-1.0",
826     "OFL-1.1",
827     "SimPL-2.0",
828     "Sleepycat",
829     "SNIA",
830     "Spencer-86",
831     "Spencer-94",
832     "Spencer-99",
833     "SMLNJ",
834     "SugarCRM-1.1.3",
835     "SISSL",
836     "SISSL-1.2",
837     "SPL-1.0",
838     "Watcom-1.0",
839     "TCL",
840     "TCP-wrappers",
841     "Unlicense",
842     "TMate",
843     "TORQUE-1.1",
844     "TOSL",
845     "Unicode-DFS-2015",
846     "Unicode-DFS-2016",
847     "Unicode-TOU",
848     "UPL-1.0",
849     "NCSA",
850     "Vim",
851     "VOSTROM",
852     "VSL-1.0",
853     "W3C-20150513",
854     "W3C-19980720",
855     "W3C",
856     "Wsuipa",
857     "Xnet",
858     "X11",
859     "Xerox",
860     "XFree86-1.1",
861     "xinetd",
862     "xpp",
863     "XSkat",
864     "YPL-1.0",
865     "YPL-1.1",
866     "Zed",
867     "Zend-2.0",
868     "Zimbra-1.3",
869     "Zimbra-1.4",
870     "Zlib",
871     "zlib-acknowledgement",
872     "ZPL-1.1",
873     "ZPL-2.0",
874     "ZPL-2.1",
875 ]
876
877 if __name__ == "__main__":
878     main()