#!/usr/bin/env python3 """Every VLC source file and header compiled into a libvlc.so, from the build's own dependency files. usage: license-audit.py [ ...] For each build dir (vlc/build-android-): the modules linked in (ndk/libvlcjni-modules.c), each module's automake dependency files (.deps/lib_plugin_la-*.Plo), libvlccore's and libvlc's, and every file they name inside the VLC tree (not the contribs, which are checked by the build's --license a). Each file is classified by its licence grant, SPDX tag and any VLC_MODULE_LICENSE define. Exit 1 if anything is GPL, unclassified (and not in READ_BY_HAND), or a module has no dependency files. """ import os import re import sys SRC_EXT = ('.c', '.h', '.cpp', '.hpp', '.m', '.S', '.asm', '.inc') # Files without licence text in VLC 3.0 (8d489ca9b3), read by hand. READ_BY_HAND = { 'modules/audio_filter/spatializer/revmodel.hpp': 'Freeverb (Jezar at Dreampoint), public domain', 'modules/codec/omxil/qcom.h': 'declarations for qcom.c (LGPL)', 'modules/stream_out/transcode/transcode.h': 'declarations for transcode.c (LGPL)', 'src/revision.c': 'generated at build time: the revision string', } OK = ('LGPL', 'BSD/ISC/MIT-style', 'public domain', 'WTFPL', 'read by hand') def classify(path): try: with open(path, encoding='utf-8', errors='replace') as f: text = f.read() except OSError: return 'missing' if re.search(r'VLC_MODULE_LICENSE\s+VLC_LICENSE_GPL', text): return 'GPL (VLC_MODULE_LICENSE)' head = text[:6000] spdx = re.search(r'SPDX-License-Identifier:\s*([\w.+-]+)', head) if spdx: return 'GPL' if re.match(r'GPL', spdx.group(1)) else ('LGPL' if spdx.group(1).startswith('LGPL') else f'SPDX {spdx.group(1)}') # The grant sentence decides ("...under the terms of the GNU Lesser General Public License..."); some # LGPL headers end with a copy-pasted "You should have received a copy of the GNU General Public License". grant = re.search(r'under\s+the\s+terms\s+of\s+the\s+GNU\s+(Lesser\s+|Library\s+)?General\s+Public\s+License', re.sub(r'\s*\n\s*[*#/]*\s*', ' ', head), re.I) if grant: return 'LGPL' if grant.group(1) else 'GPL' lesser = re.search(r'(Lesser|Library) General Public', head, re.I) gpl = re.search(r'(? 10: return value return re.sub(r'\$\((\w+)\)', lambda m: expand(variables.get(m.group(1), ''), variables, depth + 1), value) def objects(makefile, library): """The .lo/.o objects a library target is built from, as paths relative to the Makefile's directory.""" variables = makefile_vars(makefile) out = [] for kind in ('am_', 'nodist_'): out += expand(variables.get(f'{kind}{library}_OBJECTS', ''), variables).split() return [o for o in out if o.endswith(('.lo', '.o'))] def main(): vlc = os.path.realpath(sys.argv[1]) contrib = os.path.join(vlc, 'contrib') + os.sep bad = False for build in sys.argv[2:]: build = os.path.realpath(build) with open(os.path.join(build, 'ndk', 'libvlcjni-modules.c'), encoding='utf-8') as f: modules = sorted(set(re.findall(r'vlc_entry__(\w+),', f.read()))) targets = [(m, os.path.join(build, 'modules'), f'lib{m}_plugin_la') for m in modules] targets += [('libvlccore', os.path.join(build, 'src'), 'libvlccore_la'), ('libvlc', os.path.join(build, 'lib'), 'libvlc_la'), ('libcompat', os.path.join(build, 'compat'), 'libcompat_la')] units = {} no_deps = [] for unit, directory, library in targets: plos = [] for obj in objects(os.path.join(directory, 'Makefile'), library): stem = re.sub(r'\.l?o$', '', obj) plo = [p for p in (os.path.join(directory, os.path.dirname(stem), '.deps', os.path.basename(stem) + ext) for ext in ('.Plo', '.Po')) if os.path.exists(p)] if plo: plos.append((directory, plo[0])) else: no_deps.append(f'{unit}:{obj}') if not plos: no_deps.append(unit) units[unit] = plos files = {} for unit, plo_paths in units.items(): for compile_dir, plo in plo_paths: for dep in deps_of(plo): full = os.path.realpath(dep if os.path.isabs(dep) else os.path.join(compile_dir, dep)) if not full.startswith(vlc + os.sep) or full.startswith(contrib) or full.startswith(build + os.sep): continue files.setdefault(os.path.relpath(full, vlc), set()).add(unit) verdicts = {} for rel in files: verdict = classify(os.path.join(vlc, rel)) if verdict == 'NO LICENCE TEXT' and rel.replace(os.sep, '/') in READ_BY_HAND: verdict = 'read by hand' verdicts.setdefault(verdict, []).append(rel) abi = os.path.basename(build) print(f'== {abi}: {len(modules)} modules, {len(files)} VLC files') for verdict in sorted(verdicts): print(f' {verdict}: {len(verdicts[verdict])}') for verdict in sorted(verdicts): if verdict in OK: continue bad = True for rel in sorted(verdicts[verdict]): print(f' {verdict}: {rel} <- {", ".join(sorted(files[rel]))}') if no_deps: bad = True print(f' modules with no dependency files (not checked): {" ".join(no_deps)}') sys.exit(1 if bad else 0) if __name__ == '__main__': main()