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