OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. |
| 5 |
| 6 """Utility for checking and processing licensing information in third_party |
| 7 directories. |
| 8 |
| 9 Usage: licenses.py <command> |
| 10 |
| 11 Commands: |
| 12 scan scan third_party directories, verifying that we have licensing info |
| 13 credits generate about:credits on stdout |
| 14 |
| 15 (You can also import this as a module.) |
| 16 """ |
| 17 |
| 18 import cgi |
| 19 import os |
| 20 import sys |
| 21 |
| 22 # Paths from the root of the tree to directories to skip. |
| 23 PRUNE_PATHS = set([ |
| 24 # Same module occurs in crypto/third_party/nss and net/third_party/nss, so |
| 25 # skip this one. |
| 26 os.path.join('third_party','nss'), |
| 27 |
| 28 # Placeholder directory only, not third-party code. |
| 29 os.path.join('third_party','adobe'), |
| 30 |
| 31 # Build files only, not third-party code. |
| 32 os.path.join('third_party','widevine'), |
| 33 |
| 34 # Only binaries, used during development. |
| 35 os.path.join('third_party','valgrind'), |
| 36 |
| 37 # Used for development and test, not in the shipping product. |
| 38 os.path.join('build','secondary'), |
| 39 os.path.join('third_party','bison'), |
| 40 os.path.join('third_party','blanketjs'), |
| 41 os.path.join('third_party','gnu_binutils'), |
| 42 os.path.join('third_party','gold'), |
| 43 os.path.join('third_party','gperf'), |
| 44 os.path.join('third_party','lighttpd'), |
| 45 os.path.join('third_party','llvm'), |
| 46 os.path.join('third_party','llvm-build'), |
| 47 os.path.join('third_party','nacl_sdk_binaries'), |
| 48 os.path.join('third_party','pefile'), |
| 49 os.path.join('third_party','perl'), |
| 50 os.path.join('third_party','pylib'), |
| 51 os.path.join('third_party','pywebsocket'), |
| 52 os.path.join('third_party','qunit'), |
| 53 os.path.join('third_party','sinonjs'), |
| 54 os.path.join('third_party','syzygy'), |
| 55 os.path.join('tools', 'profile_chrome', 'third_party'), |
| 56 |
| 57 # Chromium code in third_party. |
| 58 os.path.join('third_party','fuzzymatch'), |
| 59 os.path.join('tools', 'swarming_client'), |
| 60 |
| 61 # Stuff pulled in from chrome-internal for official builds/tools. |
| 62 os.path.join('third_party', 'clear_cache'), |
| 63 os.path.join('third_party', 'gnu'), |
| 64 os.path.join('third_party', 'googlemac'), |
| 65 os.path.join('third_party', 'pcre'), |
| 66 os.path.join('third_party', 'psutils'), |
| 67 os.path.join('third_party', 'sawbuck'), |
| 68 ]) |
| 69 |
| 70 # Directories we don't scan through. |
| 71 VCS_METADATA_DIRS = ('.svn', '.git') |
| 72 PRUNE_DIRS = (VCS_METADATA_DIRS + |
| 73 ('out', 'Debug', 'Release', # build files |
| 74 'tests')) # lots of subdirs, not shipped. |
| 75 |
| 76 ADDITIONAL_PATHS = ( |
| 77 os.path.join('breakpad'), |
| 78 os.path.join('chrome', 'common', 'extensions', 'docs', 'examples'), |
| 79 os.path.join('chrome', 'test', 'chromeos', 'autotest'), |
| 80 os.path.join('chrome', 'test', 'data'), |
| 81 os.path.join('native_client'), |
| 82 os.path.join('net', 'tools', 'spdyshark'), |
| 83 os.path.join('sdch', 'open-vcdiff'), |
| 84 os.path.join('testing', 'gmock'), |
| 85 os.path.join('testing', 'gtest'), |
| 86 os.path.join('tools', 'grit'), |
| 87 os.path.join('tools', 'gyp'), |
| 88 os.path.join('tools', 'page_cycler', 'acid3'), |
| 89 os.path.join('url', 'third_party', 'mozilla'), |
| 90 os.path.join('v8'), |
| 91 # Fake directory so we can include the strongtalk license. |
| 92 os.path.join('v8', 'strongtalk'), |
| 93 os.path.join('v8', 'third_party', 'fdlibm'), |
| 94 ) |
| 95 |
| 96 |
| 97 # Directories where we check out directly from upstream, and therefore |
| 98 # can't provide a README.chromium. Please prefer a README.chromium |
| 99 # wherever possible. |
| 100 SPECIAL_CASES = { |
| 101 os.path.join('native_client'): { |
| 102 "Name": "native client", |
| 103 "URL": "http://code.google.com/p/nativeclient", |
| 104 "License": "BSD", |
| 105 }, |
| 106 os.path.join('sdch', 'open-vcdiff'): { |
| 107 "Name": "open-vcdiff", |
| 108 "URL": "http://code.google.com/p/open-vcdiff", |
| 109 "License": "Apache 2.0, MIT, GPL v2 and custom licenses", |
| 110 "License Android Compatible": "yes", |
| 111 }, |
| 112 os.path.join('testing', 'gmock'): { |
| 113 "Name": "gmock", |
| 114 "URL": "http://code.google.com/p/googlemock", |
| 115 "License": "BSD", |
| 116 "License File": "NOT_SHIPPED", |
| 117 }, |
| 118 os.path.join('testing', 'gtest'): { |
| 119 "Name": "gtest", |
| 120 "URL": "http://code.google.com/p/googletest", |
| 121 "License": "BSD", |
| 122 "License File": "NOT_SHIPPED", |
| 123 }, |
| 124 os.path.join('third_party', 'angle'): { |
| 125 "Name": "Almost Native Graphics Layer Engine", |
| 126 "URL": "http://code.google.com/p/angleproject/", |
| 127 "License": "BSD", |
| 128 }, |
| 129 os.path.join('third_party', 'cros_system_api'): { |
| 130 "Name": "Chromium OS system API", |
| 131 "URL": "http://www.chromium.org/chromium-os", |
| 132 "License": "BSD", |
| 133 # Absolute path here is resolved as relative to the source root. |
| 134 "License File": "/LICENSE.chromium_os", |
| 135 }, |
| 136 os.path.join('third_party', 'lss'): { |
| 137 "Name": "linux-syscall-support", |
| 138 "URL": "http://code.google.com/p/linux-syscall-support/", |
| 139 "License": "BSD", |
| 140 "License File": "/LICENSE", |
| 141 }, |
| 142 os.path.join('third_party', 'ots'): { |
| 143 "Name": "OTS (OpenType Sanitizer)", |
| 144 "URL": "http://code.google.com/p/ots/", |
| 145 "License": "BSD", |
| 146 }, |
| 147 os.path.join('third_party', 'pdfium'): { |
| 148 "Name": "PDFium", |
| 149 "URL": "http://code.google.com/p/pdfium/", |
| 150 "License": "BSD", |
| 151 }, |
| 152 os.path.join('third_party', 'pdfsqueeze'): { |
| 153 "Name": "pdfsqueeze", |
| 154 "URL": "http://code.google.com/p/pdfsqueeze/", |
| 155 "License": "Apache 2.0", |
| 156 "License File": "COPYING", |
| 157 }, |
| 158 os.path.join('third_party', 'ppapi'): { |
| 159 "Name": "ppapi", |
| 160 "URL": "http://code.google.com/p/ppapi/", |
| 161 }, |
| 162 os.path.join('third_party', 'scons-2.0.1'): { |
| 163 "Name": "scons-2.0.1", |
| 164 "URL": "http://www.scons.org", |
| 165 "License": "MIT", |
| 166 "License File": "NOT_SHIPPED", |
| 167 }, |
| 168 os.path.join('third_party', 'trace-viewer'): { |
| 169 "Name": "trace-viewer", |
| 170 "URL": "http://code.google.com/p/trace-viewer", |
| 171 "License": "BSD", |
| 172 "License File": "NOT_SHIPPED", |
| 173 }, |
| 174 os.path.join('third_party', 'v8-i18n'): { |
| 175 "Name": "Internationalization Library for v8", |
| 176 "URL": "http://code.google.com/p/v8-i18n/", |
| 177 "License": "Apache 2.0", |
| 178 }, |
| 179 os.path.join('third_party', 'WebKit'): { |
| 180 "Name": "WebKit", |
| 181 "URL": "http://webkit.org/", |
| 182 "License": "BSD and GPL v2", |
| 183 # Absolute path here is resolved as relative to the source root. |
| 184 "License File": "/webkit/LICENSE", |
| 185 }, |
| 186 os.path.join('third_party', 'webpagereplay'): { |
| 187 "Name": "webpagereplay", |
| 188 "URL": "http://code.google.com/p/web-page-replay", |
| 189 "License": "Apache 2.0", |
| 190 "License File": "NOT_SHIPPED", |
| 191 }, |
| 192 os.path.join('tools', 'grit'): { |
| 193 "Name": "grit", |
| 194 "URL": "http://code.google.com/p/grit-i18n", |
| 195 "License": "BSD", |
| 196 "License File": "NOT_SHIPPED", |
| 197 }, |
| 198 os.path.join('tools', 'gyp'): { |
| 199 "Name": "gyp", |
| 200 "URL": "http://code.google.com/p/gyp", |
| 201 "License": "BSD", |
| 202 "License File": "NOT_SHIPPED", |
| 203 }, |
| 204 os.path.join('v8'): { |
| 205 "Name": "V8 JavaScript Engine", |
| 206 "URL": "http://code.google.com/p/v8", |
| 207 "License": "BSD", |
| 208 }, |
| 209 os.path.join('v8', 'strongtalk'): { |
| 210 "Name": "Strongtalk", |
| 211 "URL": "http://www.strongtalk.org/", |
| 212 "License": "BSD", |
| 213 # Absolute path here is resolved as relative to the source root. |
| 214 "License File": "/v8/LICENSE.strongtalk", |
| 215 }, |
| 216 os.path.join('v8', 'third_party', 'fdlibm'): { |
| 217 "Name": "fdlibm", |
| 218 "URL": "http://www.netlib.org/fdlibm/", |
| 219 "License": "Freely Distributable", |
| 220 # Absolute path here is resolved as relative to the source root. |
| 221 "License File" : "/v8/third_party/fdlibm/LICENSE", |
| 222 "License Android Compatible" : "yes", |
| 223 }, |
| 224 os.path.join('third_party', 'khronos_glcts'): { |
| 225 # These sources are not shipped, are not public, and it isn't |
| 226 # clear why they're tripping the license check. |
| 227 "Name": "khronos_glcts", |
| 228 "URL": "http://no-public-url", |
| 229 "License": "Khronos", |
| 230 "License File": "NOT_SHIPPED", |
| 231 }, |
| 232 os.path.join('tools', 'telemetry', 'third_party', 'gsutil'): { |
| 233 "Name": "gsutil", |
| 234 "URL": "https://cloud.google.com/storage/docs/gsutil", |
| 235 "License": "Apache 2.0", |
| 236 "License File": "NOT_SHIPPED", |
| 237 }, |
| 238 } |
| 239 |
| 240 # Special value for 'License File' field used to indicate that the license file |
| 241 # should not be used in about:credits. |
| 242 NOT_SHIPPED = "NOT_SHIPPED" |
| 243 |
| 244 |
| 245 class LicenseError(Exception): |
| 246 """We raise this exception when a directory's licensing info isn't |
| 247 fully filled out.""" |
| 248 pass |
| 249 |
| 250 def AbsolutePath(path, filename, root): |
| 251 """Convert a path in README.chromium to be absolute based on the source |
| 252 root.""" |
| 253 if filename.startswith('/'): |
| 254 # Absolute-looking paths are relative to the source root |
| 255 # (which is the directory we're run from). |
| 256 absolute_path = os.path.join(root, filename[1:]) |
| 257 else: |
| 258 absolute_path = os.path.join(root, path, filename) |
| 259 if os.path.exists(absolute_path): |
| 260 return absolute_path |
| 261 return None |
| 262 |
| 263 def ParseDir(path, root, require_license_file=True, optional_keys=None): |
| 264 """Examine a third_party/foo component and extract its metadata.""" |
| 265 |
| 266 # Parse metadata fields out of README.chromium. |
| 267 # We examine "LICENSE" for the license file by default. |
| 268 metadata = { |
| 269 "License File": "LICENSE", # Relative path to license text. |
| 270 "Name": None, # Short name (for header on about:credits). |
| 271 "URL": None, # Project home page. |
| 272 "License": None, # Software license. |
| 273 } |
| 274 |
| 275 if optional_keys is None: |
| 276 optional_keys = [] |
| 277 |
| 278 if path in SPECIAL_CASES: |
| 279 metadata.update(SPECIAL_CASES[path]) |
| 280 else: |
| 281 # Try to find README.chromium. |
| 282 readme_path = os.path.join(root, path, 'README.chromium') |
| 283 if not os.path.exists(readme_path): |
| 284 raise LicenseError("missing README.chromium or licenses.py " |
| 285 "SPECIAL_CASES entry") |
| 286 |
| 287 for line in open(readme_path): |
| 288 line = line.strip() |
| 289 if not line: |
| 290 break |
| 291 for key in metadata.keys() + optional_keys: |
| 292 field = key + ": " |
| 293 if line.startswith(field): |
| 294 metadata[key] = line[len(field):] |
| 295 |
| 296 # Check that all expected metadata is present. |
| 297 for key, value in metadata.iteritems(): |
| 298 if not value: |
| 299 raise LicenseError("couldn't find '" + key + "' line " |
| 300 "in README.chromium or licences.py " |
| 301 "SPECIAL_CASES") |
| 302 |
| 303 # Special-case modules that aren't in the shipping product, so don't need |
| 304 # their license in about:credits. |
| 305 if metadata["License File"] != NOT_SHIPPED: |
| 306 # Check that the license file exists. |
| 307 for filename in (metadata["License File"], "COPYING"): |
| 308 license_path = AbsolutePath(path, filename, root) |
| 309 if license_path is not None: |
| 310 break |
| 311 |
| 312 if require_license_file and not license_path: |
| 313 raise LicenseError("License file not found. " |
| 314 "Either add a file named LICENSE, " |
| 315 "import upstream's COPYING if available, " |
| 316 "or add a 'License File:' line to " |
| 317 "README.chromium with the appropriate path.") |
| 318 metadata["License File"] = license_path |
| 319 |
| 320 return metadata |
| 321 |
| 322 |
| 323 def ContainsFiles(path, root): |
| 324 """Determines whether any files exist in a directory or in any of its |
| 325 subdirectories.""" |
| 326 for _, dirs, files in os.walk(os.path.join(root, path)): |
| 327 if files: |
| 328 return True |
| 329 for vcs_metadata in VCS_METADATA_DIRS: |
| 330 if vcs_metadata in dirs: |
| 331 dirs.remove(vcs_metadata) |
| 332 return False |
| 333 |
| 334 |
| 335 def FilterDirsWithFiles(dirs_list, root): |
| 336 # If a directory contains no files, assume it's a DEPS directory for a |
| 337 # project not used by our current configuration and skip it. |
| 338 return [x for x in dirs_list if ContainsFiles(x, root)] |
| 339 |
| 340 |
| 341 def FindThirdPartyDirs(prune_paths, root): |
| 342 """Find all third_party directories underneath the source root.""" |
| 343 third_party_dirs = set() |
| 344 for path, dirs, files in os.walk(root): |
| 345 path = path[len(root)+1:] # Pretty up the path. |
| 346 |
| 347 if path in prune_paths: |
| 348 dirs[:] = [] |
| 349 continue |
| 350 |
| 351 # Prune out directories we want to skip. |
| 352 # (Note that we loop over PRUNE_DIRS so we're not iterating over a |
| 353 # list that we're simultaneously mutating.) |
| 354 for skip in PRUNE_DIRS: |
| 355 if skip in dirs: |
| 356 dirs.remove(skip) |
| 357 |
| 358 if os.path.basename(path) == 'third_party': |
| 359 # Add all subdirectories that are not marked for skipping. |
| 360 for dir in dirs: |
| 361 dirpath = os.path.join(path, dir) |
| 362 if dirpath not in prune_paths: |
| 363 third_party_dirs.add(dirpath) |
| 364 |
| 365 # Don't recurse into any subdirs from here. |
| 366 dirs[:] = [] |
| 367 continue |
| 368 |
| 369 # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular |
| 370 # third_party/foo paths. |
| 371 if path in ADDITIONAL_PATHS: |
| 372 dirs[:] = [] |
| 373 |
| 374 for dir in ADDITIONAL_PATHS: |
| 375 if dir not in prune_paths: |
| 376 third_party_dirs.add(dir) |
| 377 |
| 378 return third_party_dirs |
| 379 |
| 380 |
| 381 def ScanThirdPartyDirs(root=None): |
| 382 """Scan a list of directories and report on any problems we find.""" |
| 383 if root is None: |
| 384 root = os.getcwd() |
| 385 third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root) |
| 386 third_party_dirs = FilterDirsWithFiles(third_party_dirs, root) |
| 387 |
| 388 errors = [] |
| 389 for path in sorted(third_party_dirs): |
| 390 try: |
| 391 metadata = ParseDir(path, root) |
| 392 except LicenseError, e: |
| 393 errors.append((path, e.args[0])) |
| 394 continue |
| 395 |
| 396 for path, error in sorted(errors): |
| 397 print path + ": " + error |
| 398 |
| 399 return len(errors) == 0 |
| 400 |
| 401 |
| 402 def GenerateCredits(): |
| 403 """Generate about:credits.""" |
| 404 |
| 405 if len(sys.argv) not in (2, 3): |
| 406 print 'usage: licenses.py credits [output_file]' |
| 407 return False |
| 408 |
| 409 def EvaluateTemplate(template, env, escape=True): |
| 410 """Expand a template with variables like {{foo}} using a |
| 411 dictionary of expansions.""" |
| 412 for key, val in env.items(): |
| 413 if escape: |
| 414 val = cgi.escape(val) |
| 415 template = template.replace('{{%s}}' % key, val) |
| 416 return template |
| 417 |
| 418 root = os.path.join(os.path.dirname(__file__), '..') |
| 419 third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root) |
| 420 templates_dir = os.path.join(os.path.dirname(__file__), 'resources') |
| 421 |
| 422 entry_template = open(os.path.join(templates_dir, |
| 423 'about_credits_entry.tmpl'), 'rb').read() |
| 424 entries = [] |
| 425 for path in sorted(third_party_dirs): |
| 426 try: |
| 427 metadata = ParseDir(path, root) |
| 428 except LicenseError: |
| 429 # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240). |
| 430 continue |
| 431 if metadata['License File'] == NOT_SHIPPED: |
| 432 continue |
| 433 env = { |
| 434 'name': metadata['Name'], |
| 435 'url': metadata['URL'], |
| 436 'license': open(metadata['License File'], 'rb').read(), |
| 437 } |
| 438 entries.append(EvaluateTemplate(entry_template, env)) |
| 439 |
| 440 file_template = open(os.path.join(templates_dir, |
| 441 'about_credits.tmpl'), 'rb').read() |
| 442 template_contents = EvaluateTemplate(file_template, |
| 443 {'entries': '\n'.join(entries)}, |
| 444 escape=False) |
| 445 |
| 446 if len(sys.argv) == 3: |
| 447 with open(sys.argv[2], 'w') as output_file: |
| 448 output_file.write(template_contents) |
| 449 elif len(sys.argv) == 2: |
| 450 print template_contents |
| 451 |
| 452 return True |
| 453 |
| 454 |
| 455 def main(): |
| 456 command = 'help' |
| 457 if len(sys.argv) > 1: |
| 458 command = sys.argv[1] |
| 459 |
| 460 if command == 'scan': |
| 461 if not ScanThirdPartyDirs(): |
| 462 return 1 |
| 463 elif command == 'credits': |
| 464 if not GenerateCredits(): |
| 465 return 1 |
| 466 else: |
| 467 print __doc__ |
| 468 return 1 |
| 469 |
| 470 |
| 471 if __name__ == '__main__': |
| 472 sys.exit(main()) |
OLD | NEW |