1 #!/usr/bin/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 """Checks third-party licenses for the purposes of the Android WebView build. 7 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 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. 12 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, 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. 17 """ 18 19 import optparse 20 import os 21 import re 22 import subprocess 23 import sys 24 import textwrap 25 26 27 REPOSITORY_ROOT = os.path.abspath(os.path.join( 28 os.path.dirname(__file__), '..', '..')) 29 30 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) 31 import licenses 32 33 import known_issues 34 35 def GetIncompatibleDirectories(): 36 """Gets a list of third-party directories which use licenses incompatible 37 with Android. This is used by the snapshot tool. 38 Returns: 39 A list of directories. 40 """ 41 42 whitelist = [ 43 'Apache( Version)? 2(\.0)?', 44 '(New )?BSD( [23]-Clause)?( with advertising clause)?', 45 'L?GPL ?v?2(\.[01])?( or later)?', 46 'MIT(/X11)?(-like)?', 47 'MPL 1\.1 ?/ ?GPL 2(\.0)? ?/ ?LGPL 2\.1', 48 'MPL 2(\.0)?', 49 'Microsoft Limited Public License', 50 'Microsoft Permissive License', 51 'Public Domain', 52 'SGI Free Software License B', 53 'X11', 54 ] 55 regex = '^(%s)$' % '|'.join(whitelist) 56 result = [] 57 for directory in _FindThirdPartyDirs(): 58 if directory in known_issues.KNOWN_ISSUES: 59 result.append(directory) 60 continue 61 try: 62 metadata = licenses.ParseDir(directory, REPOSITORY_ROOT, 63 require_license_file=False) 64 except licenses.LicenseError as e: 65 print 'Got LicenseError while scanning ' + directory 66 raise 67 if metadata.get('License Android Compatible', 'no').upper() == 'YES': 68 continue 69 license = re.split(' [Ll]icenses?$', metadata['License'])[0] 70 tokens = [x.strip() for x in re.split(' and |,', license) if len(x) > 0] 71 for token in tokens: 72 if not re.match(regex, token, re.IGNORECASE): 73 result.append(directory) 74 break 75 return result 76 77 class ScanResult(object): 78 Ok, Warnings, Errors = range(3) 79 80 def _CheckLicenseHeaders(excluded_dirs_list, whitelisted_files): 81 """Checks that all files which are not in a listed third-party directory, 82 and which do not use the standard Chromium license, are whitelisted. 83 Args: 84 excluded_dirs_list: The list of directories to exclude from scanning. 85 whitelisted_files: The whitelist of files. 86 Returns: 87 ScanResult.Ok if all files with non-standard license headers are whitelisted 88 and the whitelist contains no stale entries; 89 ScanResult.Warnings if there are stale entries; 90 ScanResult.Errors if new non-whitelisted entries found. 91 """ 92 93 excluded_dirs_list = [d for d in excluded_dirs_list if not 'third_party' in d] 94 # Using a commond pattern for third-partyies makes the ignore regexp shorter 95 excluded_dirs_list.append('third_party') 96 # VCS dirs 97 excluded_dirs_list.append('.git') 98 excluded_dirs_list.append('.svn') 99 # Build output 100 excluded_dirs_list.append('out/Debug') 101 excluded_dirs_list.append('out/Release') 102 # 'Copyright' appears in license agreements 103 excluded_dirs_list.append('chrome/app/resources') 104 # This is a test output directory 105 excluded_dirs_list.append('chrome/tools/test/reference_build') 106 # This is tests directory, doesn't exist in the snapshot 107 excluded_dirs_list.append('content/test/data') 108 # This is a test output directory 109 excluded_dirs_list.append('data/dom_perf') 110 # Histogram tools, doesn't exist in the snapshot 111 excluded_dirs_list.append('tools/histograms') 112 # Arm sysroot tools, doesn't exist in the snapshot 113 excluded_dirs_list.append('arm-sysroot') 114 # Data is not part of open source chromium, but are included on some bots. 115 excluded_dirs_list.append('data') 116 117 args = ['android_webview/tools/find_copyrights.pl', 118 '.' 119 ] + excluded_dirs_list 120 p = subprocess.Popen(args=args, cwd=REPOSITORY_ROOT, stdout=subprocess.PIPE) 121 lines = p.communicate()[0].splitlines() 122 123 offending_files = [] 124 allowed_copyrights = '^(?:\*No copyright\*' \ 125 '|20[0-9][0-9](?:-20[0-9][0-9])? The Chromium Authors\. ' \ 126 'All rights reserved.*)$' 127 allowed_copyrights_re = re.compile(allowed_copyrights) 128 for l in lines: 129 entries = l.split('\t') 130 if entries[1] == "GENERATED FILE": 131 continue 132 copyrights = entries[1].split(' / ') 133 for c in copyrights: 134 if c and not allowed_copyrights_re.match(c): 135 offending_files.append(os.path.normpath(entries[0])) 136 break 137 138 unknown = set(offending_files) - set(whitelisted_files) 139 if unknown: 140 print 'The following files contain a third-party license but are not in ' \ 141 'a listed third-party directory and are not whitelisted. You must ' \ 142 'add the following files to the whitelist.\n%s' % \ 143 '\n'.join(sorted(unknown)) 144 145 stale = set(whitelisted_files) - set(offending_files) 146 if stale: 147 print 'The following files are whitelisted unnecessarily. You must ' \ 148 ' remove the following files from the whitelist.\n%s' % \ 149 '\n'.join(sorted(stale)) 150 151 if unknown: 152 return ScanResult.Errors 153 elif stale: 154 return ScanResult.Warnings 155 else: 156 return ScanResult.Ok 157 158 159 def _ReadFile(path): 160 """Reads a file from disk. 161 Args: 162 path: The path of the file to read, relative to the root of the repository. 163 Returns: 164 The contents of the file as a string. 165 """ 166 167 return open(os.path.join(REPOSITORY_ROOT, path), 'rb').read() 168 169 170 def _FindThirdPartyDirs(): 171 """Gets the list of third-party directories. 172 Returns: 173 The list of third-party directories. 174 """ 175 176 # Please don't add here paths that have problems with license files, 177 # as they will end up included in Android WebView snapshot. 178 # Instead, add them into known_issues.py. 179 prune_paths = [ 180 # Placeholder directory, no third-party code. 181 os.path.join('third_party', 'adobe'), 182 # Apache 2.0 license. See 183 # https://code.google.com/p/chromium/issues/detail?id=140478. 184 os.path.join('third_party', 'bidichecker'), 185 # Isn't checked out on clients 186 os.path.join('third_party', 'gles2_conform'), 187 # The llvm-build doesn't exist for non-clang builder 188 os.path.join('third_party', 'llvm-build'), 189 # Binaries doesn't apply to android 190 os.path.join('third_party', 'widevine'), 191 # third_party directories in this tree aren't actually third party, but 192 # provide a way to shadow experimental buildfiles into those directories. 193 os.path.join('tools', 'gn', 'secondary'), 194 ] 195 third_party_dirs = licenses.FindThirdPartyDirs(prune_paths, REPOSITORY_ROOT) 196 return licenses.FilterDirsWithFiles(third_party_dirs, REPOSITORY_ROOT) 197 198 199 def _Scan(): 200 """Checks that license meta-data is present for all third-party code and 201 that all non third-party code doesn't contain external copyrighted code. 202 Returns: 203 ScanResult.Ok if everything is in order; 204 ScanResult.Warnings if there are non-fatal problems (e.g. stale whitelist 205 entries) 206 ScanResult.Errors otherwise. 207 """ 208 209 third_party_dirs = _FindThirdPartyDirs() 210 211 # First, check designated third-party directories using src/tools/licenses.py. 212 all_licenses_valid = True 213 for path in sorted(third_party_dirs): 214 try: 215 licenses.ParseDir(path, REPOSITORY_ROOT) 216 except licenses.LicenseError, e: 217 if not (path in known_issues.KNOWN_ISSUES): 218 print 'Got LicenseError "%s" while scanning %s' % (e, path) 219 all_licenses_valid = False 220 221 # Second, check for non-standard license text. 222 files_data = _ReadFile(os.path.join('android_webview', 'tools', 223 'third_party_files_whitelist.txt')) 224 whitelisted_files = [] 225 for line in files_data.splitlines(): 226 match = re.match(r'([^#\s]+)', line) 227 if match: 228 whitelisted_files.append(match.group(1)) 229 licenses_check = _CheckLicenseHeaders(third_party_dirs, whitelisted_files) 230 231 return licenses_check if all_licenses_valid else ScanResult.Errors 232 233 234 def GenerateNoticeFile(): 235 """Generates the contents of an Android NOTICE file for the third-party code. 236 This is used by the snapshot tool. 237 Returns: 238 The contents of the NOTICE file. 239 """ 240 241 third_party_dirs = _FindThirdPartyDirs() 242 243 # Don't forget Chromium's LICENSE file 244 content = [_ReadFile('LICENSE')] 245 246 # We provide attribution for all third-party directories. 247 # TODO(steveblock): Limit this to only code used by the WebView binary. 248 for directory in sorted(third_party_dirs): 249 metadata = licenses.ParseDir(directory, REPOSITORY_ROOT, 250 require_license_file=False) 251 license_file = metadata['License File'] 252 if license_file and license_file != licenses.NOT_SHIPPED: 253 content.append(_ReadFile(license_file)) 254 255 return '\n'.join(content) 256 257 258 def main(): 259 class FormatterWithNewLines(optparse.IndentedHelpFormatter): 260 def format_description(self, description): 261 paras = description.split('\n') 262 formatted_paras = [textwrap.fill(para, self.width) for para in paras] 263 return '\n'.join(formatted_paras) + '\n' 264 265 parser = optparse.OptionParser(formatter=FormatterWithNewLines(), 266 usage='%prog [options]') 267 parser.description = (__doc__ + 268 '\nCommands:\n' \ 269 ' scan Check licenses.\n' \ 270 ' notice Generate Android NOTICE file on stdout') 271 (options, args) = parser.parse_args() 272 if len(args) != 1: 273 parser.print_help() 274 return ScanResult.Errors 275 276 if args[0] == 'scan': 277 scan_result = _Scan() 278 if scan_result == ScanResult.Ok: 279 print 'OK!' 280 return scan_result 281 elif args[0] == 'notice': 282 print GenerateNoticeFile() 283 return ScanResult.Ok 284 285 parser.print_help() 286 return ScanResult.Errors 287 288 if __name__ == '__main__': 289 sys.exit(main()) 290