OLD | NEW |
---|---|
1 #!/usr/bin/python | 1 #!/usr/bin/python |
2 # Copyright 2014 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 |
(...skipping 18 matching lines...) Expand all Loading... | |
29 REPOSITORY_ROOT = os.path.abspath(os.path.join( | 29 REPOSITORY_ROOT = os.path.abspath(os.path.join( |
30 os.path.dirname(__file__), '..', '..')) | 30 os.path.dirname(__file__), '..', '..')) |
31 | 31 |
32 # 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 |
33 # 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. |
34 sys.dont_write_bytecode = True | 34 sys.dont_write_bytecode = True |
35 third_party = \ | 35 third_party = \ |
36 imp.load_source('PRESUBMIT', \ | 36 imp.load_source('PRESUBMIT', \ |
37 os.path.join(REPOSITORY_ROOT, 'third_party', 'PRESUBMIT.py')) | 37 os.path.join(REPOSITORY_ROOT, 'third_party', 'PRESUBMIT.py')) |
38 | 38 |
39 sys.path.append(os.path.join(REPOSITORY_ROOT, 'build/android/gyp/util')) | |
40 import build_utils | |
39 sys.path.append(os.path.join(REPOSITORY_ROOT, 'third_party')) | 41 sys.path.append(os.path.join(REPOSITORY_ROOT, 'third_party')) |
40 import jinja2 | 42 import jinja2 |
41 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) | 43 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) |
42 from copyright_scanner import copyright_scanner | 44 from copyright_scanner import copyright_scanner |
43 import licenses | 45 import licenses |
44 | 46 |
45 | 47 |
46 class InputApi(object): | 48 class InputApi(object): |
47 def __init__(self): | 49 def __init__(self): |
48 self.os_path = os.path | 50 self.os_path = os.path |
(...skipping 114 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
163 third_party_dirs, whitelisted_files) | 165 third_party_dirs, whitelisted_files) |
164 | 166 |
165 problem_paths.extend(more_problem_paths) | 167 problem_paths.extend(more_problem_paths) |
166 | 168 |
167 return (licenses_check if all_licenses_valid else ScanResult.Errors, | 169 return (licenses_check if all_licenses_valid else ScanResult.Errors, |
168 problem_paths) | 170 problem_paths) |
169 | 171 |
170 | 172 |
171 class TemplateEntryGenerator(object): | 173 class TemplateEntryGenerator(object): |
172 def __init__(self): | 174 def __init__(self): |
173 self._generate_licenses_file_list_only = False | |
174 self._toc_index = 0 | 175 self._toc_index = 0 |
175 | 176 |
176 def SetGenerateLicensesFileListOnly(self, generate_licenses_file_list_only): | |
177 self._generate_licenses_file_list_only = generate_licenses_file_list_only | |
178 | |
179 def _ReadFileGuessEncoding(self, name): | 177 def _ReadFileGuessEncoding(self, name): |
180 if self._generate_licenses_file_list_only: | |
181 return '' | |
182 contents = '' | 178 contents = '' |
183 with open(name, 'rb') as input_file: | 179 with open(name, 'rb') as input_file: |
184 contents = input_file.read() | 180 contents = input_file.read() |
185 try: | 181 try: |
186 return contents.decode('utf8') | 182 return contents.decode('utf8') |
187 except UnicodeDecodeError: | 183 except UnicodeDecodeError: |
188 pass | 184 pass |
189 # If it's not UTF-8, it must be CP-1252. Fail otherwise. | 185 # If it's not UTF-8, it must be CP-1252. Fail otherwise. |
190 return contents.decode('cp1252') | 186 return contents.decode('cp1252') |
191 | 187 |
192 def MetadataToTemplateEntry(self, metadata): | 188 def MetadataToTemplateEntry(self, metadata): |
193 self._toc_index += 1 | 189 self._toc_index += 1 |
194 return { | 190 return { |
195 'name': metadata['Name'], | 191 'name': metadata['Name'], |
196 'url': metadata['URL'], | 192 'url': metadata['URL'], |
197 'license_file': metadata['License File'], | 193 'license_file': metadata['License File'], |
198 'license': self._ReadFileGuessEncoding(metadata['License File']), | 194 'license': self._ReadFileGuessEncoding(metadata['License File']), |
199 'toc_href': 'entry' + str(self._toc_index), | 195 'toc_href': 'entry' + str(self._toc_index), |
200 } | 196 } |
201 | 197 |
202 | 198 |
203 def GenerateNoticeFile(generate_licenses_file_list_only=False): | 199 def GenerateNoticeFile(): |
204 """Generates the contents of an Android NOTICE file for the third-party code. | 200 """Generates the contents of an Android NOTICE file for the third-party code. |
205 This is used by the snapshot tool. | 201 This is used by the snapshot tool. |
206 Returns: | 202 Returns: |
207 The contents of the NOTICE file. | 203 A tuple of (input paths, contents of the NOTICE file). |
208 """ | 204 """ |
209 | |
210 generator = TemplateEntryGenerator() | 205 generator = TemplateEntryGenerator() |
211 generator.SetGenerateLicensesFileListOnly(generate_licenses_file_list_only) | |
212 # Start from Chromium's LICENSE file | 206 # Start from Chromium's LICENSE file |
213 entries = [generator.MetadataToTemplateEntry({ | 207 entries = [generator.MetadataToTemplateEntry({ |
214 'Name': 'The Chromium Project', | 208 'Name': 'The Chromium Project', |
215 'URL': 'http://www.chromium.org', | 209 'URL': 'http://www.chromium.org', |
216 'License File': os.path.join(REPOSITORY_ROOT, 'LICENSE') }) | 210 'License File': os.path.join(REPOSITORY_ROOT, 'LICENSE') }) |
217 ] | 211 ] |
218 | 212 |
219 third_party_dirs = licenses.FindThirdPartyDirsWithFiles(REPOSITORY_ROOT) | 213 third_party_dirs = licenses.FindThirdPartyDirsWithFiles(REPOSITORY_ROOT) |
220 # We provide attribution for all third-party directories. | 214 # We provide attribution for all third-party directories. |
221 # TODO(mnaganov): Limit this to only code used by the WebView binary. | 215 # TODO(mnaganov): Limit this to only code used by the WebView binary. |
222 for directory in sorted(third_party_dirs): | 216 for directory in sorted(third_party_dirs): |
223 try: | 217 try: |
224 metadata = licenses.ParseDir(directory, REPOSITORY_ROOT, | 218 metadata = licenses.ParseDir(directory, REPOSITORY_ROOT, |
225 require_license_file=False) | 219 require_license_file=False) |
226 except licenses.LicenseError: | 220 except licenses.LicenseError: |
227 # Since this code is called during project files generation, | 221 # Since this code is called during project files generation, |
228 # we don't want to break the it. But we assume that release | 222 # we don't want to break the it. But we assume that release |
229 # WebView apks are built using checkouts that pass | 223 # WebView apks are built using checkouts that pass |
230 # 'webview_licenses.py scan' check, thus they don't contain | 224 # 'webview_licenses.py scan' check, thus they don't contain |
231 # projects with non-compatible licenses. | 225 # projects with non-compatible licenses. |
232 continue | 226 continue |
233 license_file = metadata['License File'] | 227 license_file = metadata['License File'] |
234 if license_file and license_file != licenses.NOT_SHIPPED: | 228 if license_file and license_file != licenses.NOT_SHIPPED: |
235 entries.append(generator.MetadataToTemplateEntry(metadata)) | 229 entries.append(generator.MetadataToTemplateEntry(metadata)) |
236 | 230 |
237 if generate_licenses_file_list_only: | 231 entries.sort(key=lambda entry: entry['name']) |
238 return [entry['license_file'] for entry in entries] | 232 |
239 else: | 233 license_file_list = sorted(set([entry['license_file'] for entry in entries])) |
240 env = jinja2.Environment( | 234 license_file_list = [os.path.relpath(p) for p in license_file_list] |
241 loader=jinja2.FileSystemLoader(os.path.dirname(__file__)), | 235 env = jinja2.Environment( |
242 extensions=['jinja2.ext.autoescape']) | 236 loader=jinja2.FileSystemLoader(os.path.dirname(__file__)), |
243 template = env.get_template('licenses_notice.tmpl') | 237 extensions=['jinja2.ext.autoescape']) |
244 return template.render({ 'entries': entries }).encode('utf8') | 238 template = env.get_template('licenses_notice.tmpl') |
239 notice_file_contents = template.render({'entries': entries}).encode('utf8') | |
240 return (license_file_list, notice_file_contents) | |
245 | 241 |
246 | 242 |
247 def main(): | 243 def main(): |
248 class FormatterWithNewLines(optparse.IndentedHelpFormatter): | 244 class FormatterWithNewLines(optparse.IndentedHelpFormatter): |
249 def format_description(self, description): | 245 def format_description(self, description): |
250 paras = description.split('\n') | 246 paras = description.split('\n') |
251 formatted_paras = [textwrap.fill(para, self.width) for para in paras] | 247 formatted_paras = [textwrap.fill(para, self.width) for para in paras] |
252 return '\n'.join(formatted_paras) + '\n' | 248 return '\n'.join(formatted_paras) + '\n' |
253 | 249 |
254 parser = optparse.OptionParser(formatter=FormatterWithNewLines(), | 250 parser = optparse.OptionParser(formatter=FormatterWithNewLines(), |
255 usage='%prog [options]') | 251 usage='%prog [options]') |
256 parser.add_option('--json', help='Path to JSON output file') | 252 parser.add_option('--json', help='Path to JSON output file') |
253 build_utils.AddDepfileOption(parser) | |
257 parser.description = (__doc__ + | 254 parser.description = (__doc__ + |
258 '\nCommands:\n' | 255 '\nCommands:\n' |
259 ' scan Check licenses.\n' | 256 ' scan Check licenses.\n' |
260 ' notice_deps Generate the list of dependencies for ' | |
261 'Android NOTICE file.\n' | 257 'Android NOTICE file.\n' |
262 ' notice [file] Generate Android NOTICE file on ' | 258 ' notice [file] Generate Android NOTICE file on ' |
263 'stdout or into |file|.\n' | 259 'stdout or into |file|.\n' |
264 ' display_copyrights Display autorship on the files' | 260 ' display_copyrights Display autorship on the files' |
265 ' using names provided via stdin.\n') | 261 ' using names provided via stdin.\n') |
266 (options, args) = parser.parse_args() | 262 options, args = parser.parse_args() |
267 if len(args) < 1: | 263 if len(args) < 1: |
268 parser.print_help() | 264 parser.print_help() |
269 return ScanResult.Errors | 265 return ScanResult.Errors |
270 | 266 |
271 if args[0] == 'scan': | 267 if args[0] == 'scan': |
272 scan_result, problem_paths = _Scan() | 268 scan_result, problem_paths = _Scan() |
273 if scan_result == ScanResult.Ok: | 269 if scan_result == ScanResult.Ok: |
274 print 'OK!' | 270 print 'OK!' |
275 if options.json: | 271 if options.json: |
276 with open(options.json, 'w') as f: | 272 with open(options.json, 'w') as f: |
277 json.dump(problem_paths, f) | 273 json.dump(problem_paths, f) |
278 return scan_result | 274 return scan_result |
279 elif args[0] == 'notice_deps': | |
280 # 'set' is used to eliminate duplicate references to the same license file. | |
281 print ' '.join( | |
282 sorted(set(GenerateNoticeFile(generate_licenses_file_list_only=True)))) | |
283 return ScanResult.Ok | |
284 elif args[0] == 'gn_notice_deps': | |
285 # generate list for gn. | |
286 # 'set' is used to eliminate duplicate references to the same license file. | |
287 gn_file_list = ['"' + f + '"' for f in | |
288 sorted(set(GenerateNoticeFile(generate_licenses_file_list_only=True)))] | |
289 print '[%s] ' % ','.join(gn_file_list) | |
290 return ScanResult.Ok | |
291 elif args[0] == 'notice': | 275 elif args[0] == 'notice': |
292 notice_file_contents = GenerateNoticeFile() | 276 license_file_list, notice_file_contents = GenerateNoticeFile() |
293 if len(args) == 1: | 277 if len(args) == 1: |
294 print notice_file_contents | 278 print notice_file_contents |
295 else: | 279 else: |
296 with open(args[1], 'w') as output_file: | 280 with open(args[1], 'w') as output_file: |
297 output_file.write(notice_file_contents) | 281 output_file.write(notice_file_contents) |
282 if options.depfile: | |
283 assert args[1] | |
284 # Add in build.ninja so that the target will be considered dirty whenever | |
285 # gn gen is run. Otherwise, it will fail to notice new files being added. | |
286 # This is still no perfect, as it will fail if no build files are changed, | |
287 # but a new README.chromium / LICENSE is added. This shouldn't happen in | |
288 # practice however. | |
289 build_utils.WriteDepfile(options.depfile, args[1], | |
290 license_file_list + ['build.ninja']) | |
291 | |
Dirk Pranke
2017/02/10 00:15:59
I don't understand this block. Why do you want to
agrieve
2017/02/10 00:32:35
Before this change, it was scanning the filesystem
| |
298 return ScanResult.Ok | 292 return ScanResult.Ok |
299 elif args[0] == 'display_copyrights': | 293 elif args[0] == 'display_copyrights': |
300 files = sys.stdin.read().splitlines() | 294 files = sys.stdin.read().splitlines() |
301 for f, c in \ | 295 for f, c in \ |
302 zip(files, copyright_scanner.FindCopyrights(InputApi(), '.', files)): | 296 zip(files, copyright_scanner.FindCopyrights(InputApi(), '.', files)): |
303 print f, '\t', ' / '.join(sorted(c)) | 297 print f, '\t', ' / '.join(sorted(c)) |
304 return ScanResult.Ok | 298 return ScanResult.Ok |
305 parser.print_help() | 299 parser.print_help() |
306 return ScanResult.Errors | 300 return ScanResult.Errors |
307 | 301 |
308 if __name__ == '__main__': | 302 if __name__ == '__main__': |
309 sys.exit(main()) | 303 sys.exit(main()) |
OLD | NEW |