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