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