xref: /aosp_15_r20/external/angle/third_party/spirv-tools/src/utils/check_copyright.py (revision 8975f5c5ed3d1c378011245431ada316dfb6f244)
1#!/usr/bin/env python3
2# coding=utf-8
3# Copyright (c) 2016 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16"""Checks for copyright notices in all the files that need them under the
17
18current directory.  Optionally insert them.  When inserting, replaces
19an MIT or Khronos free use license with Apache 2.
20"""
21
22import argparse
23import fileinput
24import fnmatch
25import inspect
26import os
27import re
28import sys
29
30# List of designated copyright owners.
31AUTHORS = ['The Khronos Group Inc.',
32           'LunarG Inc.',
33           'Google Inc.',
34           'Google LLC',
35           'Pierre Moreau',
36           'Samsung Inc',
37           'André Perez Maselco',
38           'Vasyl Teliman',
39           'Advanced Micro Devices, Inc.',
40           'Stefano Milizia',
41           'Alastair F. Donaldson',
42           'Mostafa Ashraf',
43           'Shiyu Liu',
44           'ZHOU He',
45           'Nintendo',
46           'Epic Games, Inc.',
47           'NVIDIA Corporation']
48CURRENT_YEAR = 2023
49
50FIRST_YEAR = 2014
51FINAL_YEAR = CURRENT_YEAR + 5
52# A regular expression to match the valid years in the copyright information.
53YEAR_REGEX = '(' + '|'.join(
54    str(year) for year in range(FIRST_YEAR, FINAL_YEAR + 1)) + ')'
55
56# A regular expression to make a range of years in the form <year1>-<year2>.
57YEAR_RANGE_REGEX = '('
58for year1 in range(FIRST_YEAR, FINAL_YEAR + 1):
59  for year2 in range(year1 + 1, FINAL_YEAR + 1):
60    YEAR_RANGE_REGEX += str(year1) + '-' + str(year2) + '|'
61YEAR_RANGE_REGEX = YEAR_RANGE_REGEX[:-1] + ')'
62
63# In the copyright info, the year can be a single year or a range.  This is a
64# regex to make sure it matches one of them.
65YEAR_OR_RANGE_REGEX = '(' + YEAR_REGEX + '|' + YEAR_RANGE_REGEX + ')'
66
67# The final regular expression to match a valid copyright line.
68COPYRIGHT_RE = re.compile('Copyright \(c\) {} ({})'.format(
69    YEAR_OR_RANGE_REGEX, '|'.join(AUTHORS)))
70
71MIT_BEGIN_RE = re.compile('Permission is hereby granted, '
72                          'free of charge, to any person obtaining a')
73MIT_END_RE = re.compile('MATERIALS OR THE USE OR OTHER DEALINGS IN '
74                        'THE MATERIALS.')
75APACHE2_BEGIN_RE = re.compile('Licensed under the Apache License, '
76                              'Version 2.0 \(the "License"\);')
77APACHE2_END_RE = re.compile('limitations under the License.')
78
79LICENSED = """Licensed under the Apache License, Version 2.0 (the "License");
80you may not use this file except in compliance with the License.
81You may obtain a copy of the License at
82
83    http://www.apache.org/licenses/LICENSE-2.0
84
85Unless required by applicable law or agreed to in writing, software
86distributed under the License is distributed on an "AS IS" BASIS,
87WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
88See the License for the specific language governing permissions and
89limitations under the License."""
90LICENSED_LEN = 10 # Number of lines in LICENSED
91
92
93def find(top, filename_glob, skip_glob_dir_list, skip_glob_files_list):
94    """Returns files in the tree rooted at top matching filename_glob but not
95    in directories matching skip_glob_dir_list nor files matching
96    skip_glob_dir_list."""
97
98    file_list = []
99    for path, dirs, files in os.walk(top):
100        for glob in skip_glob_dir_list:
101            for match in fnmatch.filter(dirs, glob):
102                dirs.remove(match)
103        for filename in fnmatch.filter(files, filename_glob):
104            full_file = os.path.join(path, filename)
105            if full_file not in skip_glob_files_list:
106                file_list.append(full_file)
107    return file_list
108
109
110def filtered_descendants(glob):
111    """Returns glob-matching filenames under the current directory, but skips
112    some irrelevant paths."""
113    return find('.', glob, ['third_party', 'external', 'CompilerIdCXX',
114        'build*', 'out*'], ['./utils/clang-format-diff.py'])
115
116
117def skip(line):
118    """Returns true if line is all whitespace or shebang."""
119    stripped = line.lstrip()
120    return stripped == '' or stripped.startswith('#!')
121
122
123def comment(text, prefix):
124    """Returns commented-out text.
125
126    Each line of text will be prefixed by prefix and a space character.  Any
127    trailing whitespace will be trimmed.
128    """
129    accum = ['{} {}'.format(prefix, line).rstrip() for line in text.split('\n')]
130    return '\n'.join(accum)
131
132
133def insert_copyright(author, glob, comment_prefix):
134    """Finds all glob-matching files under the current directory and inserts the
135    copyright message, and license notice.  An MIT license or Khronos free
136    use license (modified MIT) is replaced with an Apache 2 license.
137
138    The copyright message goes into the first non-whitespace, non-shebang line
139    in a file.  The license notice follows it.  Both are prefixed on each line
140    by comment_prefix and a space.
141    """
142
143    copyright = comment('Copyright (c) {} {}'.format(CURRENT_YEAR, author),
144                        comment_prefix) + '\n\n'
145    licensed = comment(LICENSED, comment_prefix) + '\n\n'
146    for file in filtered_descendants(glob):
147        # Parsing states are:
148        #   0 Initial: Have not seen a copyright declaration.
149        #   1 Seen a copyright line and no other interesting lines
150        #   2 In the middle of an MIT or Khronos free use license
151        #   9 Exited any of the above
152        state = 0
153        update_file = False
154        for line in fileinput.input(file, inplace=1):
155            emit = True
156            if state == 0:
157                if COPYRIGHT_RE.search(line):
158                    state = 1
159                elif skip(line):
160                    pass
161                else:
162                    # Didn't see a copyright. Inject copyright and license.
163                    sys.stdout.write(copyright)
164                    sys.stdout.write(licensed)
165                    # Assume there isn't a previous license notice.
166                    state = 1
167            elif state == 1:
168                if MIT_BEGIN_RE.search(line):
169                    state = 2
170                    emit = False
171                elif APACHE2_BEGIN_RE.search(line):
172                    # Assume an Apache license is preceded by a copyright
173                    # notice.  So just emit it like the rest of the file.
174                    state = 9
175            elif state == 2:
176                # Replace the MIT license with Apache 2
177                emit = False
178                if MIT_END_RE.search(line):
179                    state = 9
180                    sys.stdout.write(licensed)
181            if emit:
182                sys.stdout.write(line)
183
184
185def alert_if_no_copyright(glob, comment_prefix):
186    """Prints names of all files missing either a copyright or Apache 2 license.
187
188    Finds all glob-matching files under the current directory and checks if they
189    contain the copyright message and license notice.  Prints the names of all the
190    files that don't meet both criteria.
191
192    Returns the total number of file names printed.
193    """
194    printed_count = 0
195    for file in filtered_descendants(glob):
196        has_copyright = False
197        has_apache2 = False
198        line_num = 0
199        apache_expected_end = 0
200        with open(file, encoding='utf-8') as contents:
201            for line in contents:
202                line_num += 1
203                if COPYRIGHT_RE.search(line):
204                    has_copyright = True
205                if APACHE2_BEGIN_RE.search(line):
206                    apache_expected_end = line_num + LICENSED_LEN
207                if (line_num is apache_expected_end) and APACHE2_END_RE.search(line):
208                    has_apache2 = True
209        if not (has_copyright and has_apache2):
210            message = file
211            if not has_copyright:
212                message += ' has no copyright'
213            if not has_apache2:
214                message += ' has no Apache 2 license notice'
215            print(message)
216            printed_count += 1
217    return printed_count
218
219
220class ArgParser(argparse.ArgumentParser):
221    def __init__(self):
222        super(ArgParser, self).__init__(
223                description=inspect.getdoc(sys.modules[__name__]))
224        self.add_argument('--update', dest='author', action='store',
225                          help='For files missing a copyright notice, insert '
226                               'one for the given author, and add a license '
227                               'notice.  The author must be in the AUTHORS '
228                               'list in the script.')
229
230
231def main():
232    glob_comment_pairs = [('*.h', '//'), ('*.hpp', '//'), ('*.sh', '#'),
233                          ('*.py', '#'), ('*.cpp', '//'),
234                          ('CMakeLists.txt', '#')]
235    argparser = ArgParser()
236    args = argparser.parse_args()
237
238    if args.author:
239        if args.author not in AUTHORS:
240            print('error: --update argument must be in the AUTHORS list in '
241                  'check_copyright.py: {}'.format(AUTHORS))
242            sys.exit(1)
243        for pair in glob_comment_pairs:
244            insert_copyright(args.author, *pair)
245        sys.exit(0)
246    else:
247        count = sum([alert_if_no_copyright(*p) for p in glob_comment_pairs])
248        sys.exit(count > 0)
249
250
251if __name__ == '__main__':
252    main()
253