Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add qmk license-check developer-level CLI command. #22075

Merged
merged 14 commits into from
Nov 11, 2023
1 change: 1 addition & 0 deletions lib/python/qmk/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
'qmk.cli.import.keymap',
'qmk.cli.info',
'qmk.cli.json2c',
'qmk.cli.license_check',
'qmk.cli.lint',
'qmk.cli.kle2json',
'qmk.cli.list.keyboards',
Expand Down
116 changes: 116 additions & 0 deletions lib/python/qmk/cli/license_check.py
Original file line number Diff line number Diff line change
@@ -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
123 changes: 123 additions & 0 deletions lib/python/qmk/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,126 @@
#
################################################################################
'''

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', ["""\
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inconsistency in formatting bugs me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, but this is what the formatter spits out. Annoying.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could change the data structure, but even that does not make the formatting fully consistent between the cases with single and multiple values:

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.
        """],

If all else fails, you could slap # yapf:disable after the final closing bracket and format the value manually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel a strong enough need to fight the formatter. This is something that'll be touched only once every blue moon.

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.
"""]),
]