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