OLD | NEW |
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright 2014 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Checks third-party licenses for the purposes of the Android WebView build. | 6 """Checks third-party licenses for the purposes of the Android WebView build. |
7 | 7 |
8 The Android tree includes a snapshot of Chromium in order to power the system | 8 The Android tree includes a snapshot of Chromium in order to power the system |
9 WebView. This tool checks that all code uses open-source licenses compatible | 9 WebView. This tool checks that all code uses open-source licenses compatible |
10 with Android, and that we meet the requirements of those licenses. It can also | 10 with Android, and that we meet the requirements of those licenses. It can also |
11 be used to generate an Android NOTICE file for the third-party code. | 11 be used to generate an Android NOTICE file for the third-party code. |
12 | 12 |
13 It makes use of src/tools/licenses.py and the README.chromium files on which | 13 It makes use of src/tools/licenses.py and the README.chromium files on which |
14 it depends. It also makes use of a data file, third_party_files_whitelist.txt, | 14 it depends. It also makes use of a data file, third_party_files_whitelist.txt, |
15 which whitelists indicidual files which contain third-party code but which | 15 which whitelists indicidual files which contain third-party code but which |
16 aren't in a third-party directory with a README.chromium file. | 16 aren't in a third-party directory with a README.chromium file. |
17 """ | 17 """ |
18 | 18 |
19 import glob | 19 import glob |
20 import imp | 20 import imp |
21 import multiprocessing | 21 import multiprocessing |
22 import optparse | 22 import optparse |
23 import os | 23 import os |
24 import re | 24 import re |
25 import subprocess | |
26 import sys | 25 import sys |
27 import textwrap | 26 import textwrap |
28 | 27 |
29 | 28 |
30 REPOSITORY_ROOT = os.path.abspath(os.path.join( | 29 REPOSITORY_ROOT = os.path.abspath(os.path.join( |
31 os.path.dirname(__file__), '..', '..')) | 30 os.path.dirname(__file__), '..', '..')) |
32 | 31 |
33 # Import third_party/PRESUBMIT.py via imp to avoid importing a random | 32 # Import third_party/PRESUBMIT.py via imp to avoid importing a random |
34 # PRESUBMIT.py from $PATH, also make sure we don't generate a .pyc file. | 33 # PRESUBMIT.py from $PATH, also make sure we don't generate a .pyc file. |
35 sys.dont_write_bytecode = True | 34 sys.dont_write_bytecode = True |
36 third_party = \ | 35 third_party = \ |
37 imp.load_source('PRESUBMIT', \ | 36 imp.load_source('PRESUBMIT', \ |
38 os.path.join(REPOSITORY_ROOT, 'third_party', 'PRESUBMIT.py')) | 37 os.path.join(REPOSITORY_ROOT, 'third_party', 'PRESUBMIT.py')) |
39 | 38 |
40 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) | 39 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) |
41 import licenses | 40 import licenses |
42 | 41 |
| 42 import copyright_scanner |
43 import known_issues | 43 import known_issues |
44 | 44 |
45 class InputApi(object): | 45 class InputApi(object): |
46 def __init__(self): | 46 def __init__(self): |
47 self.re = re | 47 self.re = re |
48 | 48 |
49 def GetIncompatibleDirectories(): | 49 def GetIncompatibleDirectories(): |
50 """Gets a list of third-party directories which use licenses incompatible | 50 """Gets a list of third-party directories which use licenses incompatible |
51 with Android. This is used by the snapshot tool. | 51 with Android. This is used by the snapshot tool. |
52 Returns: | 52 Returns: |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
90 exclude = exclude_dirname | 90 exclude = exclude_dirname |
91 known_incompatible.append(os.path.normpath(os.path.join(path, exclude))) | 91 known_incompatible.append(os.path.normpath(os.path.join(path, exclude))) |
92 known_incompatible = frozenset(known_incompatible) | 92 known_incompatible = frozenset(known_incompatible) |
93 return incompatible_directories.difference(known_incompatible) | 93 return incompatible_directories.difference(known_incompatible) |
94 | 94 |
95 | 95 |
96 class ScanResult(object): | 96 class ScanResult(object): |
97 Ok, Warnings, Errors = range(3) | 97 Ok, Warnings, Errors = range(3) |
98 | 98 |
99 # Needs to be a top-level function for multiprocessing | 99 # Needs to be a top-level function for multiprocessing |
100 def _FindCopyrights(files_to_scan): | 100 def _FindCopyrightViolations(files_to_scan_as_string): |
101 args = [os.path.join('android_webview', 'tools', 'find_copyrights.pl')] | 101 return copyright_scanner.FindCopyrightViolations( |
102 p = subprocess.Popen( | 102 REPOSITORY_ROOT, files_to_scan_as_string) |
103 args=args, cwd=REPOSITORY_ROOT, | |
104 stdin=subprocess.PIPE, stdout=subprocess.PIPE) | |
105 lines = p.communicate(files_to_scan)[0].splitlines() | |
106 | 103 |
107 offending_files = [] | 104 def _ShardList(l, shard_len): |
108 allowed_copyrights = '^(?:\*No copyright\*' \ | 105 return [l[i:i + shard_len] for i in range(0, len(l), shard_len)] |
109 '|20[0-9][0-9](?:-20[0-9][0-9])? The Chromium Authors\. ' \ | |
110 'All rights reserved.*)$' | |
111 allowed_copyrights_re = re.compile(allowed_copyrights) | |
112 for l in lines: | |
113 entries = l.split('\t') | |
114 if entries[1] == "GENERATED FILE": | |
115 continue | |
116 copyrights = entries[1].split(' / ') | |
117 for c in copyrights: | |
118 if c and not allowed_copyrights_re.match(c): | |
119 offending_files.append(os.path.normpath(entries[0])) | |
120 break | |
121 return offending_files | |
122 | |
123 def _ShardString(s, delimiter, shard_len): | |
124 result = [] | |
125 index = 0 | |
126 last_pos = 0 | |
127 for m in re.finditer(delimiter, s): | |
128 index += 1 | |
129 if index % shard_len == 0: | |
130 result.append(s[last_pos:m.end()]) | |
131 last_pos = m.end() | |
132 if not index % shard_len == 0: | |
133 result.append(s[last_pos:]) | |
134 return result | |
135 | 106 |
136 def _CheckLicenseHeaders(excluded_dirs_list, whitelisted_files): | 107 def _CheckLicenseHeaders(excluded_dirs_list, whitelisted_files): |
137 """Checks that all files which are not in a listed third-party directory, | 108 """Checks that all files which are not in a listed third-party directory, |
138 and which do not use the standard Chromium license, are whitelisted. | 109 and which do not use the standard Chromium license, are whitelisted. |
139 Args: | 110 Args: |
140 excluded_dirs_list: The list of directories to exclude from scanning. | 111 excluded_dirs_list: The list of directories to exclude from scanning. |
141 whitelisted_files: The whitelist of files. | 112 whitelisted_files: The whitelist of files. |
142 Returns: | 113 Returns: |
143 ScanResult.Ok if all files with non-standard license headers are whitelisted | 114 ScanResult.Ok if all files with non-standard license headers are whitelisted |
144 and the whitelist contains no stale entries; | 115 and the whitelist contains no stale entries; |
(...skipping 33 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
178 excluded_dirs_list.append('tools/histograms') | 149 excluded_dirs_list.append('tools/histograms') |
179 # Swarming tools, doesn't exist in the snapshot | 150 # Swarming tools, doesn't exist in the snapshot |
180 excluded_dirs_list.append('tools/swarming_client') | 151 excluded_dirs_list.append('tools/swarming_client') |
181 # Arm sysroot tools, doesn't exist in the snapshot | 152 # Arm sysroot tools, doesn't exist in the snapshot |
182 excluded_dirs_list.append('arm-sysroot') | 153 excluded_dirs_list.append('arm-sysroot') |
183 # Data is not part of open source chromium, but are included on some bots. | 154 # Data is not part of open source chromium, but are included on some bots. |
184 excluded_dirs_list.append('data') | 155 excluded_dirs_list.append('data') |
185 # This is not part of open source chromium, but are included on some bots. | 156 # This is not part of open source chromium, but are included on some bots. |
186 excluded_dirs_list.append('skia/tools/clusterfuzz-data') | 157 excluded_dirs_list.append('skia/tools/clusterfuzz-data') |
187 | 158 |
188 args = [os.path.join('android_webview', 'tools', 'find_files.pl'), | 159 files_to_scan = copyright_scanner.FindFiles( |
189 '.' | 160 REPOSITORY_ROOT, ['.'], excluded_dirs_list) |
190 ] + excluded_dirs_list | 161 sharded_files_to_scan = _ShardList(files_to_scan, 2000) |
191 p = subprocess.Popen(args=args, cwd=REPOSITORY_ROOT, stdout=subprocess.PIPE) | |
192 files_to_scan = p.communicate()[0] | |
193 | |
194 sharded_files_to_scan = _ShardString(files_to_scan, '\n', 2000) | |
195 pool = multiprocessing.Pool() | 162 pool = multiprocessing.Pool() |
196 offending_files_chunks = pool.map_async( | 163 offending_files_chunks = pool.map_async( |
197 _FindCopyrights, sharded_files_to_scan).get(999999) | 164 _FindCopyrightViolations, sharded_files_to_scan).get(999999) |
198 pool.close() | 165 pool.close() |
199 pool.join() | 166 pool.join() |
200 # Flatten out the result | 167 # Flatten out the result |
201 offending_files = \ | 168 offending_files = \ |
202 [item for sublist in offending_files_chunks for item in sublist] | 169 [item for sublist in offending_files_chunks for item in sublist] |
203 | 170 |
204 unknown = set(offending_files) - set(whitelisted_files) | 171 unknown = set(offending_files) - set(whitelisted_files) |
205 if unknown: | 172 if unknown: |
206 print 'The following files contain a third-party license but are not in ' \ | 173 print 'The following files contain a third-party license but are not in ' \ |
207 'a listed third-party directory and are not whitelisted. You must ' \ | 174 'a listed third-party directory and are not whitelisted. You must ' \ |
(...skipping 19 matching lines...) Expand all Loading... |
227 | 194 |
228 | 195 |
229 def _ReadFile(path): | 196 def _ReadFile(path): |
230 """Reads a file from disk. | 197 """Reads a file from disk. |
231 Args: | 198 Args: |
232 path: The path of the file to read, relative to the root of the repository. | 199 path: The path of the file to read, relative to the root of the repository. |
233 Returns: | 200 Returns: |
234 The contents of the file as a string. | 201 The contents of the file as a string. |
235 """ | 202 """ |
236 | 203 |
237 return open(os.path.join(REPOSITORY_ROOT, path), 'rb').read() | 204 with open(os.path.join(REPOSITORY_ROOT, path), 'rb') as f: |
| 205 return f.read() |
238 | 206 |
239 | 207 |
240 def _FindThirdPartyDirs(): | 208 def _FindThirdPartyDirs(): |
241 """Gets the list of third-party directories. | 209 """Gets the list of third-party directories. |
242 Returns: | 210 Returns: |
243 The list of third-party directories. | 211 The list of third-party directories. |
244 """ | 212 """ |
245 | 213 |
246 # Please don't add here paths that have problems with license files, | 214 # Please don't add here paths that have problems with license files, |
247 # as they will end up included in Android WebView snapshot. | 215 # as they will end up included in Android WebView snapshot. |
(...skipping 92 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
340 def main(): | 308 def main(): |
341 class FormatterWithNewLines(optparse.IndentedHelpFormatter): | 309 class FormatterWithNewLines(optparse.IndentedHelpFormatter): |
342 def format_description(self, description): | 310 def format_description(self, description): |
343 paras = description.split('\n') | 311 paras = description.split('\n') |
344 formatted_paras = [textwrap.fill(para, self.width) for para in paras] | 312 formatted_paras = [textwrap.fill(para, self.width) for para in paras] |
345 return '\n'.join(formatted_paras) + '\n' | 313 return '\n'.join(formatted_paras) + '\n' |
346 | 314 |
347 parser = optparse.OptionParser(formatter=FormatterWithNewLines(), | 315 parser = optparse.OptionParser(formatter=FormatterWithNewLines(), |
348 usage='%prog [options]') | 316 usage='%prog [options]') |
349 parser.description = (__doc__ + | 317 parser.description = (__doc__ + |
350 '\nCommands:\n' \ | 318 '\nCommands:\n' |
351 ' scan Check licenses.\n' \ | 319 ' scan Check licenses.\n' |
352 ' notice Generate Android NOTICE file on stdout.\n' \ | 320 ' notice Generate Android NOTICE file on stdout.\n' |
353 ' incompatible_directories Scan for incompatibly' | 321 ' incompatible_directories Scan for incompatibly' |
354 ' licensed directories.\n' | 322 ' licensed directories.\n' |
355 ' all_incompatible_directories Scan for incompatibly' | 323 ' all_incompatible_directories Scan for incompatibly' |
356 ' licensed directories (even those in' | 324 ' licensed directories (even those in' |
357 ' known_issues.py).\n') | 325 ' known_issues.py).\n' |
| 326 ' display_copyrights Display autorship on the files' |
| 327 ' using names provided via stdin.\n') |
358 (_, args) = parser.parse_args() | 328 (_, args) = parser.parse_args() |
359 if len(args) != 1: | 329 if len(args) != 1: |
360 parser.print_help() | 330 parser.print_help() |
361 return ScanResult.Errors | 331 return ScanResult.Errors |
362 | 332 |
363 if args[0] == 'scan': | 333 if args[0] == 'scan': |
364 scan_result = _Scan() | 334 scan_result = _Scan() |
365 if scan_result == ScanResult.Ok: | 335 if scan_result == ScanResult.Ok: |
366 print 'OK!' | 336 print 'OK!' |
367 return scan_result | 337 return scan_result |
368 elif args[0] == 'notice': | 338 elif args[0] == 'notice': |
369 print GenerateNoticeFile() | 339 print GenerateNoticeFile() |
370 return ScanResult.Ok | 340 return ScanResult.Ok |
371 elif args[0] == 'incompatible_directories': | 341 elif args[0] == 'incompatible_directories': |
372 return _ProcessIncompatibleResult(GetUnknownIncompatibleDirectories()) | 342 return _ProcessIncompatibleResult(GetUnknownIncompatibleDirectories()) |
373 elif args[0] == 'all_incompatible_directories': | 343 elif args[0] == 'all_incompatible_directories': |
374 return _ProcessIncompatibleResult(GetIncompatibleDirectories()) | 344 return _ProcessIncompatibleResult(GetIncompatibleDirectories()) |
| 345 elif args[0] == 'display_copyrights': |
| 346 files = sys.stdin.read().splitlines() |
| 347 for f, c in zip(files, copyright_scanner.FindCopyrights('.', files)): |
| 348 print f, '\t', ' / '.join(sorted(c)) |
| 349 return ScanResult.Ok |
375 parser.print_help() | 350 parser.print_help() |
376 return ScanResult.Errors | 351 return ScanResult.Errors |
377 | 352 |
378 if __name__ == '__main__': | 353 if __name__ == '__main__': |
379 sys.exit(main()) | 354 sys.exit(main()) |
OLD | NEW |