Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 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 # This tool checks third-party licenses for the purposes of the Android WebView | |
| 7 # build. See the output of '--help' for details. | |
| 8 | |
| 9 | |
| 10 import optparse | |
| 11 import os | |
| 12 import re | |
| 13 import subprocess | |
| 14 import sys | |
| 15 import textwrap | |
| 16 | |
| 17 | |
| 18 REPOSITORY_ROOT = os.path.abspath(os.path.join( | |
| 19 os.path.dirname(__file__), '..', '..')) | |
| 20 | |
| 21 sys.path.append(os.path.join(REPOSITORY_ROOT, 'tools')) | |
| 22 import licenses | |
| 23 | |
| 24 | |
| 25 def _CheckDirectories(directory_list): | |
| 26 """Checks that all top-level directories under directories named 'third_party' | |
| 27 are listed. | |
| 28 Args: | |
| 29 directory_list: The list of directories. | |
| 30 Returns: | |
| 31 True if all directories are listed and the list contains no stale entries, | |
| 32 otherwise false. | |
| 33 """ | |
| 34 | |
| 35 cwd = os.getcwd() | |
| 36 os.chdir(REPOSITORY_ROOT) | |
| 37 unlisted_directories = [] | |
| 38 parent_listed_directory = None | |
| 39 for root, _, _ in os.walk('.'): | |
| 40 root = os.path.normpath(root) | |
| 41 if root in directory_list: | |
| 42 parent_listed_directory = root | |
| 43 is_listed = (parent_listed_directory and | |
| 44 root.startswith(parent_listed_directory)) | |
| 45 if (not is_listed | |
| 46 and not root.startswith('out/') | |
| 47 and os.path.dirname(root).endswith('third_party')): | |
| 48 unlisted_directories += [root] | |
| 49 stale = [x for x in directory_list if not os.path.exists(x)] | |
| 50 os.chdir(cwd) | |
| 51 | |
| 52 if unlisted_directories: | |
| 53 print 'Some third-party directories are not listed. You must add the ' \ | |
| 54 'following directories to the list.\n%s' % \ | |
| 55 '\n'.join(unlisted_directories) | |
| 56 return False | |
| 57 | |
| 58 if stale: | |
| 59 print 'Some third-party directories are listed but not present. You must ' \ | |
| 60 'remove the following directories from the list.\n%s' % \ | |
| 61 '\n'.join(stale) | |
| 62 return False | |
| 63 | |
| 64 return True | |
| 65 | |
| 66 | |
| 67 def _GetCmdOutput(args): | |
| 68 p = subprocess.Popen(args=args, cwd=REPOSITORY_ROOT, stdout=subprocess.PIPE) | |
| 69 ret = p.communicate()[0] | |
| 70 return ret | |
| 71 | |
| 72 | |
| 73 def _CheckLicenseHeaders(directory_list, file_list): | |
| 74 """Checks that all files which are not in a listed third-party directory, | |
| 75 and which do not use the standard Chromium license, are listed. | |
| 76 Args: | |
| 77 directory_list: The list of directories. | |
| 78 file_list: The list of files. | |
| 79 Returns: | |
| 80 True if all files with non-standard license headers are listed and the | |
| 81 file list contains no stale entries, otherwise false. | |
| 82 """ | |
| 83 # Matches one of ... | |
| 84 # - '[Cc]opyright' but not when followed by | |
| 85 # ' 20[0-9][0-9] [Tt]he Chromium Authors' or | |
| 86 # ' 20[0-9][0-9]-20[0-9][0-9] [Tt]he [Cc]hromium [Aa]uthors', with an | |
| 87 # optional '([Cc])' | |
| 88 # - '([Cc]) 20[0-9][0-9] but not when preceeded by '[Cc]opyright' or | |
| 89 # 'opyright ' | |
| 90 regex = '[Cc]opyright(?!( \([Cc]\))? 20[0-9][0-9](-20[0-9][0-9])? ' \ | |
| 91 '[Tt]he [Cc]hromium [Aa]uthors)' \ | |
| 92 '|' \ | |
| 93 '(?<!([Cc]opyright|opyright ))\([Cc]\) (19|20)[0-9][0-9]' | |
|
Evan Martin
2012/07/24 19:27:55
Why not fix the case on the code rather than this
| |
| 94 | |
| 95 args = ['grep', | |
| 96 '-rPlI', | |
| 97 '--exclude-dir', 'third_party', | |
| 98 '--exclude-dir', 'out', | |
| 99 '--exclude-dir', '.git', | |
| 100 regex, | |
| 101 '.'] | |
| 102 files = _GetCmdOutput(args).splitlines() | |
| 103 | |
| 104 # Exclude files under listed directories and some known offendors. | |
| 105 offending_files = [] | |
| 106 for x in files: | |
| 107 x = os.path.normpath(x) | |
| 108 is_in_listed_directory = False | |
| 109 for y in directory_list: | |
| 110 if x.startswith(y): | |
| 111 is_in_listed_directory = True | |
| 112 break | |
| 113 if (not is_in_listed_directory | |
| 114 # Exists in Android tree. | |
| 115 and not x == 'ThirdPartyProject.prop' | |
| 116 # Ignore these tools. | |
| 117 and not x.startswith('android_webview/tools/') | |
| 118 # This is a build intermediate directory. | |
| 119 and not x.startswith('chrome/app/theme/google_chrome/') | |
| 120 # This is a test output directory. | |
| 121 and not x.startswith('data/page_cycler/') | |
| 122 # 'Copyright' appears in strings. | |
| 123 and not x.startswith('chrome/app/resources/')): | |
| 124 offending_files += [x] | |
| 125 | |
| 126 unknown = set(offending_files) - set(file_list) | |
| 127 if unknown: | |
| 128 print 'The following files contain a third-party license but are not in ' \ | |
| 129 'a listed third-party directory and are not themselves listed. You ' \ | |
| 130 'must add the following files to the list.\n%s' % '\n'.join(unknown) | |
| 131 return False | |
| 132 | |
| 133 stale = set(file_list) - set(offending_files) | |
| 134 if stale: | |
| 135 print 'The following third-party files are listed unnecessarily. You ' \ | |
| 136 'must remove the following files from the list.\n%s' % \ | |
| 137 '\n'.join(stale) | |
| 138 return False | |
| 139 | |
| 140 return True | |
| 141 | |
| 142 | |
| 143 def _GetEntriesWithAnnotation(entries, annotation): | |
| 144 """Gets a list of all entries with the specified annotation. | |
| 145 Args: | |
| 146 entries: The list of entries. | |
| 147 annotation: The annotation. | |
| 148 Returns: | |
| 149 A list of entries. | |
| 150 """ | |
| 151 | |
| 152 result = [] | |
| 153 for line in entries.splitlines(): | |
| 154 match = re.match(r'([^#\s]*)\s+' + annotation + r'\s+', line) | |
| 155 if match: | |
| 156 result += [match.group(1)] | |
| 157 return result | |
| 158 | |
| 159 | |
| 160 def _GetLicenseFile(directory): | |
| 161 """Gets the path to the license file for the specified directory. Uses the | |
| 162 licenses tool from scripts/'. | |
| 163 Args: | |
| 164 directory: The directory to consider, relative to the root of the | |
| 165 repository. | |
| 166 Returns: | |
| 167 The absolute path to the license file. | |
| 168 """ | |
| 169 | |
| 170 return licenses.ParseDir(directory, False)['License File'] | |
| 171 | |
| 172 | |
| 173 def _CheckLicenseFiles(directories): | |
| 174 """Checks that all directories annotated with REQUIRES_ATTRIBUTION have a | |
| 175 license file. | |
| 176 Args: | |
| 177 directories: The list of directories. | |
| 178 Returns: | |
| 179 Whether the check succeeded. | |
| 180 """ | |
| 181 | |
| 182 offending_directories = [] | |
| 183 for directory in directories: | |
| 184 if not os.path.exists(_GetLicenseFile(directory)): | |
| 185 offending_directories += [license_file] | |
| 186 | |
| 187 if offending_directories: | |
| 188 print 'Some license files are missing. You must provide license files in ' \ | |
| 189 'the following directories.\n%s' % '\n'.join(offending_directories) | |
| 190 return False | |
| 191 | |
| 192 return True | |
| 193 | |
| 194 | |
| 195 def _ReadFile(path): | |
| 196 """Reads a file from disk. | |
| 197 Args: | |
| 198 path: The path of the file to read, relative to the root of the repository. | |
| 199 Returns: | |
| 200 The contents of the file as a string. | |
| 201 """ | |
| 202 | |
| 203 with file(os.path.join(REPOSITORY_ROOT, path), 'r') as f: | |
| 204 lines = f.read() | |
| 205 return lines | |
|
Evan Martin
2012/07/24 19:41:28
"lines" is a confusing name, as it is a single str
| |
| 206 | |
| 207 | |
| 208 def _GetEntriesWithoutAnnotation(entries, annotation): | |
| 209 """Gets a list of all entries without the specified annotation. | |
| 210 Args: | |
| 211 entries: The list of entries. | |
| 212 annotation: The annotation. | |
| 213 Returns: | |
| 214 A list of entries. | |
| 215 """ | |
| 216 | |
| 217 result = [] | |
| 218 for line in entries.splitlines(): | |
| 219 match = re.match(r'([^#\s]*)((?!' + annotation + r').)*$', line) | |
| 220 if match and not len(match.group(1)) == 0: | |
| 221 result += [match.group(1)] | |
| 222 return result | |
| 223 | |
| 224 | |
| 225 def _Check(directories_data, files_data): | |
| 226 """Checks that all third-party code in projects used by the WebView either | |
| 227 uses a license compatible with Android or is exlcuded from the snapshot. Also | |
| 228 checks that license text is present for third-party code requiring | |
| 229 attribution. | |
| 230 Args: | |
| 231 directories_data: The contents of the directories data file. | |
| 232 files_data: The contents of the files data file. | |
| 233 Returns: | |
| 234 Whether the check succeeded. | |
| 235 """ | |
| 236 | |
| 237 | |
| 238 # We use two signals to find third-party code. First, directories named | |
| 239 # 'third-party' and second, non-standard license text. | |
| 240 directories = _GetEntriesWithoutAnnotation(directories_data, | |
| 241 'INCOMPATIBLE_AND_UNUSED') | |
| 242 files = _GetEntriesWithoutAnnotation(files_data, 'INCOMPATIBLE_AND_UNUSED') | |
| 243 result = _CheckDirectories(directories) | |
| 244 result = _CheckLicenseHeaders(directories, files) and result | |
| 245 | |
| 246 # Also check that all directories annotated with REQUIRES_ATTRIBUTION have a | |
| 247 # license file. | |
| 248 directories = _GetEntriesWithAnnotation(directories_data, | |
| 249 'REQUIRES_ATTRIBUTION') | |
| 250 return _CheckLicenseFiles(directories) and result | |
| 251 | |
| 252 | |
| 253 def _GenerateNoticeFile(directories_data, print_warnings): | |
| 254 """Generates the contents of an Android NOTICE file for the third-party code. | |
| 255 Args: | |
| 256 directories_data: The contents of the directories data file. | |
| 257 print_warnings: Whether to print warnings. | |
| 258 Returns: | |
| 259 The contents of the NOTICE file. | |
| 260 """ | |
| 261 | |
| 262 # Don't forget Chromium's LICENSE file | |
| 263 content = [_ReadFile('LICENSE')] | |
| 264 | |
| 265 for directory in _GetEntriesWithAnnotation(directories_data, | |
| 266 'REQUIRES_ATTRIBUTION'): | |
| 267 content += [_ReadFile(_GetLicenseFile(directory))] | |
| 268 | |
| 269 return '\n'.join(content) | |
| 270 | |
| 271 | |
| 272 def main(): | |
| 273 class IndentedHelpFormatterWithNL(optparse.IndentedHelpFormatter): | |
| 274 def format_description(self, description): | |
| 275 if not description: return "" | |
| 276 desc_width = self.width - self.current_indent | |
| 277 indent = " "*self.current_indent | |
| 278 bits = description.split('\n') | |
| 279 formatted_bits = [ | |
| 280 textwrap.fill(bit, | |
| 281 desc_width, | |
| 282 initial_indent=indent, | |
| 283 subsequent_indent=indent) | |
| 284 for bit in bits] | |
| 285 result = '\n'.join(formatted_bits) + '\n' | |
| 286 return result | |
| 287 | |
| 288 parser = optparse.OptionParser(formatter=IndentedHelpFormatterWithNL(), | |
| 289 usage='%prog [options]') | |
| 290 parser.description = 'Checks third-party licenses for the purposes of the ' \ | |
| 291 'Android WebView build.\n\n' \ | |
| 292 'The Android tree includes a snapshot of Chromium in ' \ | |
| 293 'order to power the system WebView. The snapshot '\ | |
| 294 'includes only the third-party DEPS projects required ' \ | |
| 295 'for the WebView. This tool is intended to be run in ' \ | |
| 296 'the snapshot and checks that all code uses ' \ | |
| 297 'open-source licenses compatible with Android, and ' \ | |
| 298 'that we meet the requirements of those licenses. It ' \ | |
| 299 'can also be used to generate an Android NOTICE file ' \ | |
| 300 'for the third-party code.\n\n' \ | |
| 301 'It makes use of two data files, ' \ | |
| 302 'third_party_files.txt and ' \ | |
| 303 'third_party_directories.txt. These record the ' \ | |
| 304 'license status of all third-party code in the main ' \ | |
| 305 'Chromium repository and in the third-party DEPS ' \ | |
| 306 'projects used in the snapshot. This status includes ' \ | |
| 307 'why the code\'s license is compatible with Android, ' \ | |
| 308 'or why the code must be excluded from the ' \ | |
| 309 'snapshot.\n\n' \ | |
|
Evan Martin
2012/07/24 19:27:55
I think this long string should be the docstring o
| |
| 310 'Commands:\n' \ | |
| 311 ' check Check licenses.\n' \ | |
| 312 ' notice Generate Android NOTICE file on stdout' | |
| 313 (options, args) = parser.parse_args() | |
| 314 if len(args) != 1: | |
| 315 parser.print_help() | |
| 316 return 1 | |
| 317 | |
| 318 tools_directory = os.path.join('android_webview', 'tools') | |
| 319 directories_data = _ReadFile(os.path.join(tools_directory, | |
| 320 'third_party_directories.txt')) | |
| 321 files_data = _ReadFile(os.path.join(tools_directory, 'third_party_files.txt')) | |
| 322 | |
| 323 if args[0] == 'check': | |
| 324 if _Check(directories_data, files_data): | |
| 325 print 'OK!' | |
| 326 return 0 | |
| 327 else: | |
| 328 return 1 | |
| 329 elif args[0] == 'notice': | |
| 330 print _GenerateNoticeFile(directories_data, False) | |
| 331 return 0 | |
| 332 | |
| 333 parser.print_help() | |
| 334 return 1 | |
| 335 | |
| 336 if __name__ == '__main__': | |
| 337 sys.exit(main()) | |
| OLD | NEW |