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