From: Thanh Ha Date: Wed, 19 Jul 2017 22:50:24 +0000 (-0400) Subject: Add a license header scanner X-Git-Tag: v0.7.0~8^2 X-Git-Url: https://gerrit.linuxfoundation.org/infra/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F00%2F5600%2F6;p=releng%2Flftools.git Add a license header scanner Simple scanner to check code files for a license header. Does not care about the exact formatting of the license header as long as all the text exists in the correct order. This version only supports headers using '#' as the comment string. Issue: RELENG-279 Change-Id: Id4030f040c3de4350c59776ed21eed497e5d6f8d Signed-off-by: Thanh Ha --- diff --git a/docs/commands/index.rst b/docs/commands/index.rst index 687e59ca..c4bd35b5 100644 --- a/docs/commands/index.rst +++ b/docs/commands/index.rst @@ -9,6 +9,7 @@ bash. It supports the following commands. :maxdepth: 2 deploy + license nexus openstack sign diff --git a/docs/commands/license.rst b/docs/commands/license.rst new file mode 100644 index 00000000..f088f45a --- /dev/null +++ b/docs/commands/license.rst @@ -0,0 +1,16 @@ +******* +License +******* + +.. program-output:: lftools license --help + +Commands +======== + +.. contents:: License Commands + :local: + +check +----- + +.. program-output:: lftools license check --help diff --git a/lftools/cli/__init__.py b/lftools/cli/__init__.py index 823bd8d4..022c5fc3 100644 --- a/lftools/cli/__init__.py +++ b/lftools/cli/__init__.py @@ -16,6 +16,7 @@ import click from lftools.cli.deploy import deploy from lftools.cli.jenkins import jenkins_cli +from lftools.cli.license import license from lftools.cli.nexus import nexus from lftools.cli.sign import sign from lftools.cli.version import version @@ -32,6 +33,7 @@ def cli(ctx): cli.add_command(deploy) cli.add_command(jenkins_cli, name='jenkins') +cli.add_command(license) cli.add_command(nexus) cli.add_command(openstack) cli.add_command(sign) diff --git a/lftools/cli/license.py b/lftools/cli/license.py new file mode 100644 index 00000000..4c4d7608 --- /dev/null +++ b/lftools/cli/license.py @@ -0,0 +1,66 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# Copyright (c) 2017 The Linux Foundation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## +"""Scan code for license headers.""" + +__author__ = 'Thanh Ha' + + +import sys + +import click + +from lftools.license import check_license +from lftools.license import check_license_directory + + +@click.group() +@click.pass_context +def license(ctx): + """Scan code for license headers.""" + pass + + +@click.command() +@click.argument('source') +@click.option('-l', '--license', default='license-header.txt', + help='License header file to compare against.') +@click.pass_context +def check(ctx, license, source): + """Check files for missing license headers. + + Does not care if about line formatting of the license as long as all of the + text is there and in the correct order. + + Note: This code only supports '#' comments for license headers. + """ + exit_code = check_license(license, source) + sys.exit(exit_code) + + +@click.command(name='check-dir') +@click.argument('directory') +@click.option('-e', '--extension', default='py', + help='File extension to search for.') +@click.option('-l', '--license', default='license-header.txt', + help='License header file to compare against.') +@click.pass_context +def check_directory(ctx, license, directory, extension): + """Check directory for files missing license headers. + + Does not care if about line formatting of the license as long as all of the + text is there and in the correct order. + + Note: This code only supports '#' comments for license headers. + """ + check_license_directory(license, directory, extension) + + +license.add_command(check) +license.add_command(check_directory) diff --git a/lftools/license.py b/lftools/license.py new file mode 100644 index 00000000..93cfdaa4 --- /dev/null +++ b/lftools/license.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# Copyright (c) 2017 The Linux Foundation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## +"""Scans code for a valid license header.""" + +__author__ = 'Thanh Ha' + + +import os +import re +import sys + + +def get_header_text(_file): + """Scan a file and pulls out the license header. + + Returns a string containing the license header with newlines and copyright + lines stripped. + + Note: This function only supports '#' comments for license headers. + """ + text = '' + with open(_file, 'r') as data: + lines = data.readlines() + for line in lines: + result = re.search(r'\s*[#]', line) + if not result: + break + string = re.sub(r'^\s*#+', '', line).strip() + if bool(re.match('Copyright', string, re.I)): # Ignore the Copyright line + continue + text += ' {}'.format(string) + # Strip unnecessary spacing + text = re.sub('\s+', ' ', text).strip() + return text + + +def check_license(license_file, code_file): + """Compare a file with the provided license header. + + Reports if license header is missing or does not match the text of + license_file. + """ + license_header = get_header_text(license_file) + code_header = get_header_text(code_file) + + if not license_header in code_header: + print('ERROR: {} is missing or has incorrect license header.'.format(code_file)) + return 1 + + return 0 + + +def check_license_directory(license_file, directory, extension="py"): + """Search a directory for files and calls check_license().""" + missing_license = False + + for root, dirs, files in os.walk(directory): + for file in files: + if file.endswith(".{}".format(extension)): + if check_license(license_file, os.path.join(root, file)): + missing_license = True + + if missing_license: + sys.exit(1) + + print('Scan completed did not detect any files missing license headers.') diff --git a/license-header.txt b/license-header.txt new file mode 100644 index 00000000..6370a913 --- /dev/null +++ b/license-header.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# COPYRIGHT +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## diff --git a/tests/fixtures/license/license-header.txt b/tests/fixtures/license/license-header.txt new file mode 100644 index 00000000..6370a913 --- /dev/null +++ b/tests/fixtures/license/license-header.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# COPYRIGHT +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## diff --git a/tests/fixtures/license/license.py b/tests/fixtures/license/license.py new file mode 100644 index 00000000..0ad9deac --- /dev/null +++ b/tests/fixtures/license/license.py @@ -0,0 +1,10 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# Copyright (c) 2017 The Linux Foundation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## +"""Test code file with license header.""" diff --git a/tests/fixtures/license/no_license1.py b/tests/fixtures/license/no_license1.py new file mode 100644 index 00000000..527be5de --- /dev/null +++ b/tests/fixtures/license/no_license1.py @@ -0,0 +1 @@ +"""Test code file without license header.""" diff --git a/tests/fixtures/license/no_license2.py b/tests/fixtures/license/no_license2.py new file mode 100644 index 00000000..527be5de --- /dev/null +++ b/tests/fixtures/license/no_license2.py @@ -0,0 +1 @@ +"""Test code file without license header.""" diff --git a/tests/test_license.py b/tests/test_license.py new file mode 100644 index 00000000..08d1263b --- /dev/null +++ b/tests/test_license.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# Copyright (c) 2017 The Linux Foundation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## +"""Test license command.""" + +import os + +import pytest + +from lftools import cli + +FIXTURE_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'fixtures', + ) + + +@pytest.mark.datafiles( + os.path.join(FIXTURE_DIR, 'license'), + ) +def test_check_license(cli_runner, datafiles): + """Test check_license() command.""" + os.chdir(str(datafiles)) + + # Check that license checker passes when file has license. + result = cli_runner.invoke(cli.cli, ['license', 'check', 'license.py']) + # noqa: B101 . + assert result.exit_code == 0 + + # Check that license checker fails when file is missing license. + result = cli_runner.invoke(cli.cli, ['license', 'check', 'no_license1.py']) + # noqa: B101 . + assert result.exit_code == 1 + + +@pytest.mark.datafiles( + os.path.join(FIXTURE_DIR, 'license'), + ) +def test_check_license_directory(cli_runner, datafiles): + """Test check_license_directory() command.""" + os.chdir(str(datafiles)) + + # Check that check-dir fails due to directory containing files + # with no license. + result = cli_runner.invoke(cli.cli, ['license', 'check-dir', '.']) + # noqa: B101 . + assert result.exit_code == 1 + + # Check that check-dir passes when directory contains files with licenses + os.remove('no_license1.py') + os.remove('no_license2.py') + result = cli_runner.invoke(cli.cli, ['license', 'check-dir', '.']) + # noqa: B101 . + assert result.exit_code == 0