From 211fbbd16d441343d8ce2cdba4acd853762db117 Mon Sep 17 00:00:00 2001 From: Nick Brassel Date: Sat, 11 Nov 2023 20:50:33 +1100 Subject: [PATCH] Add `qmk license-check` developer-level CLI command. (#22075) --- lib/python/qmk/cli/__init__.py | 1 + lib/python/qmk/cli/license_check.py | 116 ++++++++++++++++++++++++++ lib/python/qmk/constants.py | 123 ++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+) create mode 100644 lib/python/qmk/cli/license_check.py diff --git a/lib/python/qmk/cli/__init__.py b/lib/python/qmk/cli/__init__.py index b8bc99aa0d..695a180066 100644 --- a/lib/python/qmk/cli/__init__.py +++ b/lib/python/qmk/cli/__init__.py @@ -69,6 +69,7 @@ subcommands = [ 'qmk.cli.import.keymap', 'qmk.cli.info', 'qmk.cli.json2c', + 'qmk.cli.license_check', 'qmk.cli.lint', 'qmk.cli.kle2json', 'qmk.cli.list.keyboards', diff --git a/lib/python/qmk/cli/license_check.py b/lib/python/qmk/cli/license_check.py new file mode 100644 index 0000000000..4bda272ec9 --- /dev/null +++ b/lib/python/qmk/cli/license_check.py @@ -0,0 +1,116 @@ +# Copyright 2023 Nick Brassel (@tzarc) +# SPDX-License-Identifier: GPL-2.0-or-later +import re +from pathlib import Path +from milc import cli +from qmk.constants import LICENSE_TEXTS + +L_PAREN = re.compile(r'\(\[\{\<') +R_PAREN = re.compile(r'\)\]\}\>') +PUNCTUATION = re.compile(r'[\.,;:]+') +TRASH_PREFIX = re.compile(r'^(\s|/|\*|#)+') +TRASH_SUFFIX = re.compile(r'(\s|/|\*|#|\\)+$') +SPACE = re.compile(r'\s+') +SUFFIXES = ['.c', '.h', '.cpp', '.cxx', '.hpp', '.hxx'] + + +def _simplify_text(input): + lines = input.lower().split('\n') + lines = [PUNCTUATION.sub('', line) for line in lines] + lines = [TRASH_PREFIX.sub('', line) for line in lines] + lines = [TRASH_SUFFIX.sub('', line) for line in lines] + lines = [SPACE.sub(' ', line) for line in lines] + lines = [L_PAREN.sub('(', line) for line in lines] + lines = [R_PAREN.sub(')', line) for line in lines] + lines = [line.strip() for line in lines] + lines = [line for line in lines if line is not None and line != ''] + return ' '.join(lines) + + +def _detect_license_from_file_contents(filename, absolute=False): + data = filename.read_text(encoding='utf-8', errors='ignore') + filename_out = str(filename.absolute()) if absolute else str(filename) + + if 'SPDX-License-Identifier:' in data: + res = data.split('SPDX-License-Identifier:') + license = re.split(r'\s|//|\*', res[1].strip())[0].strip() + found = False + for short_license, _ in LICENSE_TEXTS: + if license.lower() == short_license.lower(): + license = short_license + found = True + break + + if not found: + if cli.args.short: + print(f'{filename_out} UNKNOWN') + else: + cli.log.error(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- unknown license, or no license detected!') + return False + + if cli.args.short: + print(f'{filename_out} {license}') + else: + cli.log.info(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- license detected: {license} (SPDX License Identifier)') + return True + + else: + simple_text = _simplify_text(data) + for short_license, long_licenses in LICENSE_TEXTS: + for long_license in long_licenses: + if long_license in simple_text: + if cli.args.short: + print(f'{filename_out} {short_license}') + else: + cli.log.info(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- license detected: {short_license} (Full text)') + return True + + if cli.args.short: + print(f'{filename_out} UNKNOWN') + else: + cli.log.error(f'{{fg_cyan}}{filename_out}{{fg_reset}} -- unknown license, or no license detected!') + + return False + + +@cli.argument('inputs', nargs='*', arg_only=True, type=Path, help='List of input files or directories.') +@cli.argument('-s', '--short', action='store_true', help='Short output.') +@cli.argument('-a', '--absolute', action='store_true', help='Print absolute paths.') +@cli.argument('-e', '--extension', arg_only=True, action='append', default=[], help='Override list of extensions. Can be specified multiple times for multiple extensions.') +@cli.subcommand('File license check.', hidden=False if cli.config.user.developer else True) +def license_check(cli): + def _default_suffix_condition(s): + return s in SUFFIXES + + conditional = _default_suffix_condition + + if len(cli.args.extension) > 0: + suffixes = [f'.{s}' if not s.startswith('.') else s for s in cli.args.extension] + + def _specific_suffix_condition(s): + return s in suffixes + + conditional = _specific_suffix_condition + + # Pre-format all the licenses + for _, long_licenses in LICENSE_TEXTS: + for i in range(len(long_licenses)): + long_licenses[i] = _simplify_text(long_licenses[i]) + + check_list = set() + for filename in sorted(cli.args.inputs): + if filename.is_dir(): + for file in sorted(filename.rglob('*')): + if file.is_file() and conditional(file.suffix): + check_list.add(file) + elif filename.is_file(): + if conditional(filename.suffix): + check_list.add(filename) + + failed = False + for filename in sorted(check_list): + if not _detect_license_from_file_contents(filename, absolute=cli.args.absolute): + failed = True + + if failed: + return False diff --git a/lib/python/qmk/constants.py b/lib/python/qmk/constants.py index 97bd84aa23..1967441fc8 100644 --- a/lib/python/qmk/constants.py +++ b/lib/python/qmk/constants.py @@ -189,3 +189,126 @@ GENERATED_HEADER_SH_LIKE = '''\ # ################################################################################ ''' + +LICENSE_TEXTS = [ + ( + 'GPL-2.0-or-later', [ + """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + """, """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or any later version. + """ + ] + ), + ('GPL-2.0-only', ["""\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; version 2. + """]), + ( + 'GPL-3.0-or-later', [ + """\ + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of + the License, or (at your option) any later version. + """, """\ + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of + the License, or any later version. + """ + ] + ), + ('GPL-3.0-only', ["""\ + This program is free software: you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, version 3. + """]), + ( + 'LGPL-2.1-or-later', [ + """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 2.1 + of the License, or (at your option) any later version. + """, """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 2.1 + of the License, or any later version. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 2.1 + of the License, or (at your option) any later version. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 2.1 + of the License, or any later version. + """ + ] + ), + ( + 'LGPL-2.1-only', [ + """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; version 2.1. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; version 2.1. + """ + ] + ), + ( + 'LGPL-3.0-or-later', [ + """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 3 + of the License, or (at your option) any later version. + """, """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 3 + of the License, or any later version. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 3 + of the License, or (at your option) any later version. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License + as published by the Free Software Foundation; either version 3 + of the License, or any later version. + """ + ] + ), + ( + 'LGPL-3.0-only', [ + """\ + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; version 3. + """, """\ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; version 3. + """ + ] + ), + ('Apache-2.0', ["""\ + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + """]), +]