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