| #!/usr/bin/env python |
| # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Utility for checking and processing licensing information in third_party |
| directories. |
| |
| Usage: licenses.py <command> |
| |
| Commands: |
| scan scan third_party directories, verifying that we have licensing info |
| credits generate about:credits on stdout |
| |
| (You can also import this as a module.) |
| """ |
| |
| import cgi |
| import os |
| import sys |
| |
| # Paths from the root of the tree to directories to skip. |
| PRUNE_PATHS = set([ |
| # Same module occurs in crypto/third_party/nss and net/third_party/nss, so |
| # skip this one. |
| os.path.join('third_party','nss'), |
| |
| # Placeholder directory only, not third-party code. |
| os.path.join('third_party','adobe'), |
| |
| # Build files only, not third-party code. |
| os.path.join('third_party','widevine'), |
| |
| # Only binaries, used during development. |
| os.path.join('third_party','valgrind'), |
| |
| # Used for development and test, not in the shipping product. |
| os.path.join('build','secondary'), |
| os.path.join('third_party','bison'), |
| os.path.join('third_party','blanketjs'), |
| os.path.join('third_party','gnu_binutils'), |
| os.path.join('third_party','gold'), |
| os.path.join('third_party','gperf'), |
| os.path.join('third_party','lighttpd'), |
| os.path.join('third_party','llvm'), |
| os.path.join('third_party','llvm-build'), |
| os.path.join('third_party','nacl_sdk_binaries'), |
| os.path.join('third_party','pefile'), |
| os.path.join('third_party','perl'), |
| os.path.join('third_party','pylib'), |
| os.path.join('third_party','pywebsocket'), |
| os.path.join('third_party','qunit'), |
| os.path.join('third_party','sinonjs'), |
| os.path.join('third_party','syzygy'), |
| os.path.join('tools', 'profile_chrome', 'third_party'), |
| |
| # Chromium code in third_party. |
| os.path.join('third_party','fuzzymatch'), |
| os.path.join('tools', 'swarming_client'), |
| |
| # Stuff pulled in from chrome-internal for official builds/tools. |
| os.path.join('third_party', 'clear_cache'), |
| os.path.join('third_party', 'gnu'), |
| os.path.join('third_party', 'googlemac'), |
| os.path.join('third_party', 'pcre'), |
| os.path.join('third_party', 'psutils'), |
| os.path.join('third_party', 'sawbuck'), |
| ]) |
| |
| # Directories we don't scan through. |
| VCS_METADATA_DIRS = ('.svn', '.git') |
| PRUNE_DIRS = (VCS_METADATA_DIRS + |
| ('out', 'Debug', 'Release', # build files |
| 'tests')) # lots of subdirs, not shipped. |
| |
| ADDITIONAL_PATHS = ( |
| os.path.join('breakpad'), |
| os.path.join('chrome', 'common', 'extensions', 'docs', 'examples'), |
| os.path.join('chrome', 'test', 'chromeos', 'autotest'), |
| os.path.join('chrome', 'test', 'data'), |
| os.path.join('native_client'), |
| os.path.join('net', 'tools', 'spdyshark'), |
| os.path.join('sdch', 'open-vcdiff'), |
| os.path.join('testing', 'gmock'), |
| os.path.join('testing', 'gtest'), |
| os.path.join('tools', 'grit'), |
| os.path.join('tools', 'gyp'), |
| os.path.join('tools', 'page_cycler', 'acid3'), |
| os.path.join('url', 'third_party', 'mozilla'), |
| os.path.join('v8'), |
| # Fake directory so we can include the strongtalk license. |
| os.path.join('v8', 'strongtalk'), |
| os.path.join('v8', 'third_party', 'fdlibm'), |
| ) |
| |
| |
| # Directories where we check out directly from upstream, and therefore |
| # can't provide a README.chromium. Please prefer a README.chromium |
| # wherever possible. |
| SPECIAL_CASES = { |
| os.path.join('native_client'): { |
| "Name": "native client", |
| "URL": "http://code.google.com/p/nativeclient", |
| "License": "BSD", |
| }, |
| os.path.join('sdch', 'open-vcdiff'): { |
| "Name": "open-vcdiff", |
| "URL": "http://code.google.com/p/open-vcdiff", |
| "License": "Apache 2.0, MIT, GPL v2 and custom licenses", |
| "License Android Compatible": "yes", |
| }, |
| os.path.join('testing', 'gmock'): { |
| "Name": "gmock", |
| "URL": "http://code.google.com/p/googlemock", |
| "License": "BSD", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('testing', 'gtest'): { |
| "Name": "gtest", |
| "URL": "http://code.google.com/p/googletest", |
| "License": "BSD", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('third_party', 'angle'): { |
| "Name": "Almost Native Graphics Layer Engine", |
| "URL": "http://code.google.com/p/angleproject/", |
| "License": "BSD", |
| }, |
| os.path.join('third_party', 'cros_system_api'): { |
| "Name": "Chromium OS system API", |
| "URL": "http://www.chromium.org/chromium-os", |
| "License": "BSD", |
| # Absolute path here is resolved as relative to the source root. |
| "License File": "/LICENSE.chromium_os", |
| }, |
| os.path.join('third_party', 'lss'): { |
| "Name": "linux-syscall-support", |
| "URL": "http://code.google.com/p/linux-syscall-support/", |
| "License": "BSD", |
| "License File": "/LICENSE", |
| }, |
| os.path.join('third_party', 'ots'): { |
| "Name": "OTS (OpenType Sanitizer)", |
| "URL": "http://code.google.com/p/ots/", |
| "License": "BSD", |
| }, |
| os.path.join('third_party', 'pdfium'): { |
| "Name": "PDFium", |
| "URL": "http://code.google.com/p/pdfium/", |
| "License": "BSD", |
| }, |
| os.path.join('third_party', 'pdfsqueeze'): { |
| "Name": "pdfsqueeze", |
| "URL": "http://code.google.com/p/pdfsqueeze/", |
| "License": "Apache 2.0", |
| "License File": "COPYING", |
| }, |
| os.path.join('third_party', 'ppapi'): { |
| "Name": "ppapi", |
| "URL": "http://code.google.com/p/ppapi/", |
| }, |
| os.path.join('third_party', 'scons-2.0.1'): { |
| "Name": "scons-2.0.1", |
| "URL": "http://www.scons.org", |
| "License": "MIT", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('third_party', 'trace-viewer'): { |
| "Name": "trace-viewer", |
| "URL": "http://code.google.com/p/trace-viewer", |
| "License": "BSD", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('third_party', 'v8-i18n'): { |
| "Name": "Internationalization Library for v8", |
| "URL": "http://code.google.com/p/v8-i18n/", |
| "License": "Apache 2.0", |
| }, |
| os.path.join('third_party', 'WebKit'): { |
| "Name": "WebKit", |
| "URL": "http://webkit.org/", |
| "License": "BSD and GPL v2", |
| # Absolute path here is resolved as relative to the source root. |
| "License File": "/webkit/LICENSE", |
| }, |
| os.path.join('third_party', 'webpagereplay'): { |
| "Name": "webpagereplay", |
| "URL": "http://code.google.com/p/web-page-replay", |
| "License": "Apache 2.0", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('tools', 'grit'): { |
| "Name": "grit", |
| "URL": "http://code.google.com/p/grit-i18n", |
| "License": "BSD", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('tools', 'gyp'): { |
| "Name": "gyp", |
| "URL": "http://code.google.com/p/gyp", |
| "License": "BSD", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('v8'): { |
| "Name": "V8 JavaScript Engine", |
| "URL": "http://code.google.com/p/v8", |
| "License": "BSD", |
| }, |
| os.path.join('v8', 'strongtalk'): { |
| "Name": "Strongtalk", |
| "URL": "http://www.strongtalk.org/", |
| "License": "BSD", |
| # Absolute path here is resolved as relative to the source root. |
| "License File": "/v8/LICENSE.strongtalk", |
| }, |
| os.path.join('v8', 'third_party', 'fdlibm'): { |
| "Name": "fdlibm", |
| "URL": "http://www.netlib.org/fdlibm/", |
| "License": "Freely Distributable", |
| # Absolute path here is resolved as relative to the source root. |
| "License File" : "/v8/third_party/fdlibm/LICENSE", |
| "License Android Compatible" : "yes", |
| }, |
| os.path.join('third_party', 'khronos_glcts'): { |
| # These sources are not shipped, are not public, and it isn't |
| # clear why they're tripping the license check. |
| "Name": "khronos_glcts", |
| "URL": "http://no-public-url", |
| "License": "Khronos", |
| "License File": "NOT_SHIPPED", |
| }, |
| os.path.join('tools', 'telemetry', 'third_party', 'gsutil'): { |
| "Name": "gsutil", |
| "URL": "https://cloud.google.com/storage/docs/gsutil", |
| "License": "Apache 2.0", |
| "License File": "NOT_SHIPPED", |
| }, |
| } |
| |
| # Special value for 'License File' field used to indicate that the license file |
| # should not be used in about:credits. |
| NOT_SHIPPED = "NOT_SHIPPED" |
| |
| |
| class LicenseError(Exception): |
| """We raise this exception when a directory's licensing info isn't |
| fully filled out.""" |
| pass |
| |
| def AbsolutePath(path, filename, root): |
| """Convert a path in README.chromium to be absolute based on the source |
| root.""" |
| if filename.startswith('/'): |
| # Absolute-looking paths are relative to the source root |
| # (which is the directory we're run from). |
| absolute_path = os.path.join(root, filename[1:]) |
| else: |
| absolute_path = os.path.join(root, path, filename) |
| if os.path.exists(absolute_path): |
| return absolute_path |
| return None |
| |
| def ParseDir(path, root, require_license_file=True, optional_keys=None): |
| """Examine a third_party/foo component and extract its metadata.""" |
| |
| # Parse metadata fields out of README.chromium. |
| # We examine "LICENSE" for the license file by default. |
| metadata = { |
| "License File": "LICENSE", # Relative path to license text. |
| "Name": None, # Short name (for header on about:credits). |
| "URL": None, # Project home page. |
| "License": None, # Software license. |
| } |
| |
| if optional_keys is None: |
| optional_keys = [] |
| |
| if path in SPECIAL_CASES: |
| metadata.update(SPECIAL_CASES[path]) |
| else: |
| # Try to find README.chromium. |
| readme_path = os.path.join(root, path, 'README.chromium') |
| if not os.path.exists(readme_path): |
| raise LicenseError("missing README.chromium or licenses.py " |
| "SPECIAL_CASES entry") |
| |
| for line in open(readme_path): |
| line = line.strip() |
| if not line: |
| break |
| for key in metadata.keys() + optional_keys: |
| field = key + ": " |
| if line.startswith(field): |
| metadata[key] = line[len(field):] |
| |
| # Check that all expected metadata is present. |
| for key, value in metadata.iteritems(): |
| if not value: |
| raise LicenseError("couldn't find '" + key + "' line " |
| "in README.chromium or licences.py " |
| "SPECIAL_CASES") |
| |
| # Special-case modules that aren't in the shipping product, so don't need |
| # their license in about:credits. |
| if metadata["License File"] != NOT_SHIPPED: |
| # Check that the license file exists. |
| for filename in (metadata["License File"], "COPYING"): |
| license_path = AbsolutePath(path, filename, root) |
| if license_path is not None: |
| break |
| |
| if require_license_file and not license_path: |
| raise LicenseError("License file not found. " |
| "Either add a file named LICENSE, " |
| "import upstream's COPYING if available, " |
| "or add a 'License File:' line to " |
| "README.chromium with the appropriate path.") |
| metadata["License File"] = license_path |
| |
| return metadata |
| |
| |
| def ContainsFiles(path, root): |
| """Determines whether any files exist in a directory or in any of its |
| subdirectories.""" |
| for _, dirs, files in os.walk(os.path.join(root, path)): |
| if files: |
| return True |
| for vcs_metadata in VCS_METADATA_DIRS: |
| if vcs_metadata in dirs: |
| dirs.remove(vcs_metadata) |
| return False |
| |
| |
| def FilterDirsWithFiles(dirs_list, root): |
| # If a directory contains no files, assume it's a DEPS directory for a |
| # project not used by our current configuration and skip it. |
| return [x for x in dirs_list if ContainsFiles(x, root)] |
| |
| |
| def FindThirdPartyDirs(prune_paths, root): |
| """Find all third_party directories underneath the source root.""" |
| third_party_dirs = set() |
| for path, dirs, files in os.walk(root): |
| path = path[len(root)+1:] # Pretty up the path. |
| |
| if path in prune_paths: |
| dirs[:] = [] |
| continue |
| |
| # Prune out directories we want to skip. |
| # (Note that we loop over PRUNE_DIRS so we're not iterating over a |
| # list that we're simultaneously mutating.) |
| for skip in PRUNE_DIRS: |
| if skip in dirs: |
| dirs.remove(skip) |
| |
| if os.path.basename(path) == 'third_party': |
| # Add all subdirectories that are not marked for skipping. |
| for dir in dirs: |
| dirpath = os.path.join(path, dir) |
| if dirpath not in prune_paths: |
| third_party_dirs.add(dirpath) |
| |
| # Don't recurse into any subdirs from here. |
| dirs[:] = [] |
| continue |
| |
| # Don't recurse into paths in ADDITIONAL_PATHS, like we do with regular |
| # third_party/foo paths. |
| if path in ADDITIONAL_PATHS: |
| dirs[:] = [] |
| |
| for dir in ADDITIONAL_PATHS: |
| if dir not in prune_paths: |
| third_party_dirs.add(dir) |
| |
| return third_party_dirs |
| |
| |
| def ScanThirdPartyDirs(root=None): |
| """Scan a list of directories and report on any problems we find.""" |
| if root is None: |
| root = os.getcwd() |
| third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root) |
| third_party_dirs = FilterDirsWithFiles(third_party_dirs, root) |
| |
| errors = [] |
| for path in sorted(third_party_dirs): |
| try: |
| metadata = ParseDir(path, root) |
| except LicenseError, e: |
| errors.append((path, e.args[0])) |
| continue |
| |
| for path, error in sorted(errors): |
| print path + ": " + error |
| |
| return len(errors) == 0 |
| |
| |
| def GenerateCredits(): |
| """Generate about:credits.""" |
| |
| if len(sys.argv) not in (2, 3): |
| print 'usage: licenses.py credits [output_file]' |
| return False |
| |
| def EvaluateTemplate(template, env, escape=True): |
| """Expand a template with variables like {{foo}} using a |
| dictionary of expansions.""" |
| for key, val in env.items(): |
| if escape: |
| val = cgi.escape(val) |
| template = template.replace('{{%s}}' % key, val) |
| return template |
| |
| root = os.path.join(os.path.dirname(__file__), '..') |
| third_party_dirs = FindThirdPartyDirs(PRUNE_PATHS, root) |
| templates_dir = os.path.join(os.path.dirname(__file__), 'resources') |
| |
| entry_template = open(os.path.join(templates_dir, |
| 'about_credits_entry.tmpl'), 'rb').read() |
| entries = [] |
| for path in sorted(third_party_dirs): |
| try: |
| metadata = ParseDir(path, root) |
| except LicenseError: |
| # TODO(phajdan.jr): Convert to fatal error (http://crbug.com/39240). |
| continue |
| if metadata['License File'] == NOT_SHIPPED: |
| continue |
| env = { |
| 'name': metadata['Name'], |
| 'url': metadata['URL'], |
| 'license': open(metadata['License File'], 'rb').read(), |
| } |
| entries.append(EvaluateTemplate(entry_template, env)) |
| |
| file_template = open(os.path.join(templates_dir, |
| 'about_credits.tmpl'), 'rb').read() |
| template_contents = EvaluateTemplate(file_template, |
| {'entries': '\n'.join(entries)}, |
| escape=False) |
| |
| if len(sys.argv) == 3: |
| with open(sys.argv[2], 'w') as output_file: |
| output_file.write(template_contents) |
| elif len(sys.argv) == 2: |
| print template_contents |
| |
| return True |
| |
| |
| def main(): |
| command = 'help' |
| if len(sys.argv) > 1: |
| command = sys.argv[1] |
| |
| if command == 'scan': |
| if not ScanThirdPartyDirs(): |
| return 1 |
| elif command == 'credits': |
| if not GenerateCredits(): |
| return 1 |
| else: |
| print __doc__ |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |