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