1#!/usr/bin/env python3
2#
3# Arm SCP/MCP Software
4# Copyright (c) 2015-2023, Arm Limited and Contributors. All rights reserved.
5#
6# SPDX-License-Identifier: BSD-3-Clause
7#
8
9"""
10Check if a given file includes the correct license header.
11This checker supports the following comment styles:
12    * Used by .c, .h and .s/.S (GCC) files
13    ; Used by .s (ARM toolchain) and .scat (scatter file) files
14    # Used by Makefile (including .mk), .py (Python) and dxy (Doxygen) files
15"""
16import os
17import fnmatch
18import re
19import sys
20import datetime
21import subprocess
22import glob
23from itertools import islice
24
25#
26# Directories to exclude
27#
28
29# Exclude all mod_test "mocks" directories
30UNIT_TEST_MOCKS = glob.glob('module/*/test/**/mocks', recursive=True)
31
32EXCLUDE_DIRECTORIES = [
33    '.git',
34    'build',
35    'contrib/cmsis/git',
36    "contrib/run-clang-format/git",
37    "contrib/cmock/git",
38    'product/rcar/src/CMSIS-FreeRTOS',
39    'unit_test/unity_mocks',
40] + UNIT_TEST_MOCKS
41
42#
43# Supported file types
44#
45FILE_TYPES = [
46    'Makefile',
47    '*.mk',
48    '*.c',
49    '*.h',
50    '*.s',
51    '*.S',
52    '*.py',
53    '*.scat',
54    '*CMakeLists.txt',
55    '*.cmake',
56    "*.rb",
57    "*.yaml",
58    "*.yml",
59]
60
61#
62# Supported comment styles (Python regex)
63#
64COMMENT_PATTERN = '^(( \\*)|(;)|(\\#))'
65COMPANY_PATTERN = '(Arm|Renesas|Linaro)'
66COMPANY_FULL_NAME_PATTERN = \
67    '(Arm Limited and Contributors|Renesas Electronics Corporation|'\
68    'Linaro Limited and Contributors)'
69
70#
71# git command using diff-filter to include Added (A), Copied (C), Modified (M),
72# Renamed (R), type changed (T), Unmerged (U), Unknown (X) files
73# Deleted files (D) are not included
74#
75GIT_CMD = \
76    'git diff-tree --name-only --no-commit-id -r --diff-filter=ACMRTUX HEAD'
77
78#
79# License pattern to match
80#
81LICENSE_PATTERN = \
82    '{0} {1} SCP/MCP Software$\n'\
83    '({0} Copyright \\(c\\) (?P<years>[0-9]{{4}}(-[0-9]{{4}})?), {2}.'\
84    ' All rights(( )|(\n{0} ))reserved.$\n)+'\
85    '{0}$\n'\
86    '{0} SPDX-License-Identifier: BSD-3-Clause$\n'\
87    .format(COMMENT_PATTERN, COMPANY_PATTERN, COMPANY_FULL_NAME_PATTERN)
88
89#
90# The number of lines from the beginning of the file to search for the
91# copyright header. This limit avoids the tool searching the whole file when
92# the header always appears near the top.
93#
94# Note: The copyright notice does not usually start on the first line of the
95# file. The value should be enough to include the all of the lines in the
96# LICENSE_PATTERN, plus any extra lines that appears before the license. The
97# performance of the tool may degrade if this value is increased significantly.
98#
99HEAD_LINE_COUNT = 10
100
101
102class ErrorYear(Exception):
103    pass
104
105
106class ErrorCopyright(Exception):
107    pass
108
109
110class ErrorYearNotCurrent(Exception):
111    pass
112
113
114def is_valid_directory(filename):
115    for dir in EXCLUDE_DIRECTORIES:
116        if filename.startswith(dir):
117            return False
118    return True
119
120
121def is_valid_file_type(filename):
122    for file_type in FILE_TYPES:
123        if fnmatch.fnmatch(filename, file_type):
124            return True
125    return False
126
127
128def check_copyright(pattern, filename):
129    with open(filename, encoding="utf-8") as file:
130        # Read just the first HEAD_LINE_COUNT lines of a file
131        head_lines = islice(file, HEAD_LINE_COUNT)
132        head = ''
133        for line in head_lines:
134            head += line
135
136        match = pattern.search(head)
137        if not match:
138            raise ErrorCopyright
139
140        years = match.group('years').split('-')
141        if len(years) > 1:
142            if years[0] > years[1]:
143                raise ErrorYear
144
145        now = datetime.datetime.now()
146        final_year = len(years) - 1
147        if int(years[final_year]) != now.year:
148            raise ErrorYearNotCurrent
149
150
151def main():
152    pattern = re.compile(LICENSE_PATTERN, re.MULTILINE)
153    error_year_count = 0
154    error_copyright_count = 0
155    error_incorrect_year_count = 0
156
157    print("Checking the copyrights in the code...")
158
159    cwd = os.getcwd()
160    print("Executing from {}".format(cwd))
161
162    try:
163        result = subprocess.Popen(
164            GIT_CMD,
165            shell=True,
166            stdout=subprocess.PIPE,
167            stderr=subprocess.PIPE)
168    except subprocess.CalledProcessError as e:
169        print("ERROR " + e.returncode + ": Failed to get last changed files")
170        return 1
171
172    for line in result.stdout:
173        filename = line.decode("utf-8").strip('\n')
174
175        if is_valid_file_type(filename) and is_valid_directory(filename):
176            try:
177                check_copyright(pattern, filename)
178            except ErrorYear:
179                print("{}: Invalid year format.".format(filename))
180                error_year_count += 1
181
182            except ErrorCopyright:
183                print("{}: Invalid copyright header.".format(filename))
184                error_copyright_count += 1
185
186            except ErrorYearNotCurrent:
187                print("{}: Outdated copyright year range.".
188                      format(filename))
189                error_incorrect_year_count += 1
190
191    if error_year_count != 0 or error_copyright_count != 0 or \
192       error_incorrect_year_count != 0:
193        print("\t{} files with invalid year(s) format."
194              .format(error_year_count))
195        print("\t{} files with invalid copyright."
196              .format(error_copyright_count))
197        print("\t{} files with incorrect year ranges."
198              .format(error_incorrect_year_count))
199
200        return 1
201    else:
202        print("Check copyright - No errors found.")
203        return 0
204
205
206if __name__ == "__main__":
207    sys.exit(main())
208