| # SPDX-FileCopyrightText: 2020 Efabless Corporation |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| import argparse |
| import logging |
| import os |
| from pathlib import Path |
| |
| from strsimpy.sorensen_dice import SorensenDice |
| |
| # Default values for base license files |
| APPROVED_LICENSES_PATH = Path(__file__).parent / "_licenses/_approved_licenses" |
| PROHIBITED_LICENSES_PATH = Path(__file__).parent / "_licenses/_prohibited_licenses" |
| |
| # Directories, files and file_extensions ignored for license check |
| IGNORED_DIRS = [".git", ".github", "caravel", "gl", "third_party"] |
| IGNORED_EXTS = [".cfg", ".csv", ".def", ".drc", ".gds", ".gz", ".hex", ".jpg", ".lef", ".log", ".mag", ".md", ".out", ".pdf", ".png", ".pyc", ".rdb", ".spice", ".svg", ".txt", ".vcd", ".xml"] |
| IGNORED_FILES = [".git", ".gitignore", ".gitmodules", "info.yaml", "LICENSE", "manifest", "OPENLANE_VERSION", "PDK_SOURCES"] |
| |
| # Default values for base license files |
| LICENSE_FILENAME = "LICENSE" |
| SPDX_COPYRIGHT_HEADER = "SPDX-FileCopyrightText" |
| SPDX_LICENSE_HEADER = "SPDX-License-Identifier" |
| |
| |
| def check_license(target_license_path, licenses_path): |
| confidence_map = [] |
| try: |
| target_license_file_content = target_license_path.open(encoding="utf-8").read() |
| except FileNotFoundError: |
| logging.error(f"LICENSE FILE NOT FOUND in {target_license_path.parent}") |
| return None |
| |
| for license_file in licenses_path.iterdir(): |
| license_file_content = license_file.open(encoding="utf-8").read() |
| confidence = 1 - SorensenDice().distance(license_file_content.strip(), target_license_file_content.strip()) |
| confidence_map.append({"license_key": license_file.stem, "confidence": confidence * 100}) |
| license_check_result = max(confidence_map, key=lambda x: x["confidence"]) |
| return license_check_result["license_key"] if license_check_result["confidence"] > 95 else None |
| |
| |
| def verify_license_compliance(path): |
| path = path / LICENSE_FILENAME |
| try: |
| prohibited_license = check_license(path, PROHIBITED_LICENSES_PATH) |
| if prohibited_license: |
| logging.warning(f"A prohibited LICENSE ({prohibited_license}) was found in {path.parent}.") |
| return False |
| else: |
| approved_license = check_license(path, APPROVED_LICENSES_PATH) |
| if approved_license: |
| logging.info(f"An approved LICENSE ({approved_license}) was found in {path.parent}.") |
| return True |
| else: |
| logging.warning(f"An identifiable LICENSE file was not found in {path.parent}.") |
| return False |
| except Exception as e: |
| logging.fatal(f"VERIFY LICENSE EXCEPTION in ({path}): {e}") |
| raise |
| |
| |
| def check_submodules_licenses(path): |
| submodules = [] |
| for root, dirs, files in os.walk(path): # note: root = submodule |
| if ".git" not in dirs or root == path: |
| continue |
| submodules.append(verify_license_compliance(path)) |
| return False if False in submodules else True |
| |
| |
| def check_third_party_libs_licenses(path): |
| libs = [] |
| for lib_path in next(os.walk(path))[1]: |
| lib_path = Path(path) / lib_path |
| libs.append(verify_license_compliance(lib_path)) |
| return False if False in libs else True |
| |
| |
| def check_dir_spdx_compliance(non_compliant_list, path, license_key=None): |
| for root, dirs, files in os.walk(path): |
| for file in files: |
| file_under_test = Path(root) / file |
| if not any(ignored_dir in str(file_under_test.parent) for ignored_dir in IGNORED_DIRS): |
| result = check_file_spdx_compliance(file_under_test, license_key) |
| if result: |
| non_compliant_list.append(result) |
| return non_compliant_list |
| |
| |
| def check_file_spdx_compliance(file_path, license_key): |
| spdx_license_header = SPDX_LICENSE_HEADER if not license_key else f"{'SPDX-License-Identifier'}: {license_key}" |
| spdx_compliant = spdx_cp_compliant = spdx_ls_compliant = False |
| |
| file_base = file_path.name |
| if file_base in IGNORED_FILES: |
| return None |
| file_ext = file_path.suffix |
| if file_ext in IGNORED_EXTS: |
| return None |
| |
| try: |
| with open(file_path, "tr", encoding="utf-8") as f: |
| lines = [x.rstrip() for x in f.readlines()] |
| if lines and list(filter(None, lines)): |
| for line in lines: |
| if line: |
| if SPDX_COPYRIGHT_HEADER in line: |
| spdx_cp_compliant = True |
| if spdx_license_header in line: |
| spdx_ls_compliant = True |
| |
| if spdx_cp_compliant and spdx_ls_compliant: |
| spdx_compliant = True |
| break |
| return file_path if not spdx_compliant else None |
| except UnicodeDecodeError as unicode_error: |
| logging.error(f"SPDX COMPLIANCE FILE UNICODE DECODE EXCEPTION in ({file_path}): {unicode_error}") |
| except FileNotFoundError as file_not_found_error: |
| if not Path(file_not_found_error.filename).is_symlink(): |
| logging.error(f"SPDX COMPLIANCE FILE NOT FOUND in {file_path}") |
| else: |
| logging.error(f"SPDX COMPLIANCE SYMLINK FILE NOT FOUND in {file_path}") |
| return None |
| |
| |
| if __name__ == "__main__": |
| logging.basicConfig(level=logging.DEBUG, format=f"%(asctime)s | %(levelname)-7s | %(message)s", datefmt='%d-%b-%Y %H:%M:%S') |
| default_input_directory = Path(__file__).parents[2] / "_default_content" |
| parser = argparse.ArgumentParser(description='Runs a set of license checks on a given directory.') |
| parser.add_argument('--input_directory', '-i', required=False, default=default_input_directory, help='Yaml Path') |
| args = parser.parse_args() |
| |
| if verify_license_compliance(Path(args.input_directory)): |
| logging.info("License Clean") |
| else: |
| logging.warning("License Dirty") |
| |
| if check_third_party_libs_licenses(Path(args.input_directory)): |
| logging.info("Third Party Libraries Clean") |
| else: |
| logging.warning("Third Party Libraries Dirty") |
| |
| if check_submodules_licenses(Path(args.input_directory)): |
| logging.info("Submodules Clean") |
| else: |
| logging.warning("Submodules Dirty") |
| |
| spdx_non_compliant_list = [str(x) for x in check_dir_spdx_compliance([], Path(args.input_directory))] |
| if not spdx_non_compliant_list: |
| logging.info("Project is compliant with the SPDX Standard") |
| else: |
| logging.warning(f"Project is not compliant with the SPDX Standard." |
| f" {spdx_non_compliant_list.__len__()} non-compliant files found: {spdx_non_compliant_list}") |