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 """Makes sure that files include headers from allowed directories. 7 8 Checks DEPS files in the source tree for rules, and applies those rules to 9 "#include" commands in source files. Any source file including something not 10 permitted by the DEPS files will fail. 11 12 The format of the deps file: 13 14 First you have the normal module-level deps. These are the ones used by 15 gclient. An example would be: 16 17 deps = { 18 "base":"http://foo.bar/trunk/base" 19 } 20 21 DEPS files not in the top-level of a module won't need this. Then you 22 have any additional include rules. You can add (using "+") or subtract 23 (using "-") from the previously specified rules (including 24 module-level deps). You can also specify a path that is allowed for 25 now but that we intend to remove, using "!"; this is treated the same 26 as "+" when check_deps is run by our bots, but a presubmit step will 27 show a warning if you add a new include of a file that is only allowed 28 by "!". 29 30 Note that for .java files, there is currently no difference between 31 "+" and "!", even in the presubmit step. 32 33 include_rules = { 34 # Code should be able to use base (it's specified in the module-level 35 # deps above), but nothing in "base/evil" because it's evil. 36 "-base/evil", 37 38 # But this one subdirectory of evil is OK. 39 "+base/evil/not", 40 41 # And it can include files from this other directory even though there is 42 # no deps rule for it. 43 "+tools/crime_fighter", 44 45 # This dependency is allowed for now but work is ongoing to remove it, 46 # so you shouldn't add further dependencies on it. 47 "!base/evil/ok_for_now.h", 48 } 49 50 If you have certain include rules that should only be applied for some 51 files within this directory and subdirectories, you can write a 52 section named specific_include_rules that is a hash map of regular 53 expressions to the list of rules that should apply to files matching 54 them. Note that such rules will always be applied before the rules 55 from 'include_rules' have been applied, but the order in which rules 56 associated with different regular expressions is applied is arbitrary. 57 58 specific_include_rules = { 59 ".*_(unit|browser|api)test\.cc": [ 60 "+libraries/testsupport", 61 ], 62 } 63 64 DEPS files may be placed anywhere in the tree. Each one applies to all 65 subdirectories, where there may be more DEPS files that provide additions or 66 subtractions for their own sub-trees. 67 68 There is an implicit rule for the current directory (where the DEPS file lives) 69 and all of its subdirectories. This prevents you from having to explicitly 70 allow the current directory everywhere. This implicit rule is applied first, 71 so you can modify or remove it using the normal include rules. 72 73 The rules are processed in order. This means you can explicitly allow a higher 74 directory and then take away permissions from sub-parts, or the reverse. 75 76 Note that all directory separators must be slashes (Unix-style) and not 77 backslashes. All directories should be relative to the source root and use 78 only lowercase. 79 """ 80 81 import os 82 import optparse 83 import re 84 import subprocess 85 import sys 86 import copy 87 88 import cpp_checker 89 import java_checker 90 import results 91 from rules import Rule, Rules 92 93 94 # Variable name used in the DEPS file to add or subtract include files from 95 # the module-level deps. 96 INCLUDE_RULES_VAR_NAME = 'include_rules' 97 98 # Variable name used in the DEPS file to add or subtract include files 99 # from module-level deps specific to files whose basename (last 100 # component of path) matches a given regular expression. 101 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' 102 103 # Optionally present in the DEPS file to list subdirectories which should not 104 # be checked. This allows us to skip third party code, for example. 105 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' 106 107 108 def NormalizePath(path): 109 """Returns a path normalized to how we write DEPS rules and compare paths. 110 """ 111 return path.lower().replace('\\', '/') 112 113 114 def _IsTestFile(filename): 115 """Does a rudimentary check to try to skip test files; this could be 116 improved but is good enough for now. 117 """ 118 return re.match('(test|mock|dummy)_.*|.*_[a-z]*test\.(cc|mm|java)', filename) 119 120 121 class DepsChecker(object): 122 """Parses include_rules from DEPS files and can verify files in the 123 source tree against them. 124 """ 125 126 def __init__(self, 127 base_directory=None, 128 verbose=False, 129 being_tested=False, 130 ignore_temp_rules=False, 131 skip_tests=False): 132 """Creates a new DepsChecker. 133 134 Args: 135 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src. 136 verbose: Set to true for debug output. 137 being_tested: Set to true to ignore the DEPS file at tools/checkdeps/DEPS. 138 """ 139 self.base_directory = base_directory 140 self.verbose = verbose 141 self._under_test = being_tested 142 self._ignore_temp_rules = ignore_temp_rules 143 self._skip_tests = skip_tests 144 145 if not base_directory: 146 self.base_directory = os.path.abspath( 147 os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..')) 148 149 self.results_formatter = results.NormalResultsFormatter(verbose) 150 151 self.git_source_directories = set() 152 self._AddGitSourceDirectories() 153 154 # Map of normalized directory paths to rules to use for those 155 # directories, or None for directories that should be skipped. 156 self.directory_rules = {} 157 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) 158 159 def Report(self): 160 """Prints a report of results, and returns an exit code for the process.""" 161 if self.results_formatter.GetResults(): 162 self.results_formatter.PrintResults() 163 return 1 164 print '\nSUCCESS\n' 165 return 0 166 167 def _ApplyRules(self, existing_rules, includes, specific_includes, cur_dir): 168 """Applies the given include rules, returning the new rules. 169 170 Args: 171 existing_rules: A set of existing rules that will be combined. 172 include: The list of rules from the "include_rules" section of DEPS. 173 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules 174 from the "specific_include_rules" section of DEPS. 175 cur_dir: The current directory, normalized path. We will create an 176 implicit rule that allows inclusion from this directory. 177 178 Returns: A new set of rules combining the existing_rules with the other 179 arguments. 180 """ 181 rules = copy.deepcopy(existing_rules) 182 183 # First apply the implicit "allow" rule for the current directory. 184 if cur_dir.startswith( 185 NormalizePath(os.path.normpath(self.base_directory))): 186 relative_dir = cur_dir[len(self.base_directory) + 1:] 187 188 source = relative_dir 189 if len(source) == 0: 190 source = 'top level' # Make the help string a little more meaningful. 191 rules.AddRule('+' + relative_dir, 'Default rule for ' + source) 192 else: 193 raise Exception('Internal error: base directory is not at the beginning' + 194 ' for\n %s and base dir\n %s' % 195 (cur_dir, self.base_directory)) 196 197 def ApplyOneRule(rule_str, dependee_regexp=None): 198 """Deduces a sensible description for the rule being added, and 199 adds the rule with its description to |rules|. 200 201 If we are ignoring temporary rules, this function does nothing 202 for rules beginning with the Rule.TEMP_ALLOW character. 203 """ 204 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): 205 return 206 207 rule_block_name = 'include_rules' 208 if dependee_regexp: 209 rule_block_name = 'specific_include_rules' 210 if not relative_dir: 211 rule_description = 'the top level %s' % rule_block_name 212 else: 213 rule_description = relative_dir + "'s %s" % rule_block_name 214 rules.AddRule(rule_str, rule_description, dependee_regexp) 215 216 # Apply the additional explicit rules. 217 for (_, rule_str) in enumerate(includes): 218 ApplyOneRule(rule_str) 219 220 # Finally, apply the specific rules. 221 for regexp, specific_rules in specific_includes.iteritems(): 222 for rule_str in specific_rules: 223 ApplyOneRule(rule_str, regexp) 224 225 return rules 226 227 def _ApplyDirectoryRules(self, existing_rules, dir_name): 228 """Combines rules from the existing rules and the new directory. 229 230 Any directory can contain a DEPS file. Toplevel DEPS files can contain 231 module dependencies which are used by gclient. We use these, along with 232 additional include rules and implicit rules for the given directory, to 233 come up with a combined set of rules to apply for the directory. 234 235 Args: 236 existing_rules: The rules for the parent directory. We'll add-on to these. 237 dir_name: The directory name that the deps file may live in (if 238 it exists). This will also be used to generate the 239 implicit rules. This is a non-normalized path. 240 241 Returns: A tuple containing: (1) the combined set of rules to apply to the 242 sub-tree, and (2) a list of all subdirectories that should NOT be 243 checked, as specified in the DEPS file (if any). 244 """ 245 norm_dir_name = NormalizePath(dir_name) 246 247 # Check for a .svn directory in this directory or check this directory is 248 # contained in git source direcotries. This will tell us if it's a source 249 # directory and should be checked. 250 if not (os.path.exists(os.path.join(dir_name, ".svn")) or 251 (norm_dir_name in self.git_source_directories)): 252 return (None, []) 253 254 # Check the DEPS file in this directory. 255 if self.verbose: 256 print 'Applying rules from', dir_name 257 def FromImpl(_unused, _unused2): 258 pass # NOP function so "From" doesn't fail. 259 260 def FileImpl(_unused): 261 pass # NOP function so "File" doesn't fail. 262 263 class _VarImpl: 264 def __init__(self, local_scope): 265 self._local_scope = local_scope 266 267 def Lookup(self, var_name): 268 """Implements the Var syntax.""" 269 if var_name in self._local_scope.get('vars', {}): 270 return self._local_scope['vars'][var_name] 271 raise Exception('Var is not defined: %s' % var_name) 272 273 local_scope = {} 274 global_scope = { 275 'File': FileImpl, 276 'From': FromImpl, 277 'Var': _VarImpl(local_scope).Lookup, 278 } 279 deps_file = os.path.join(dir_name, 'DEPS') 280 281 # The second conditional here is to disregard the 282 # tools/checkdeps/DEPS file while running tests. This DEPS file 283 # has a skip_child_includes for 'testdata' which is necessary for 284 # running production tests, since there are intentional DEPS 285 # violations under the testdata directory. On the other hand when 286 # running tests, we absolutely need to verify the contents of that 287 # directory to trigger those intended violations and see that they 288 # are handled correctly. 289 if os.path.isfile(deps_file) and ( 290 not self._under_test or not os.path.split(dir_name)[1] == 'checkdeps'): 291 execfile(deps_file, global_scope, local_scope) 292 elif self.verbose: 293 print ' No deps file found in', dir_name 294 295 # Even if a DEPS file does not exist we still invoke ApplyRules 296 # to apply the implicit "allow" rule for the current directory 297 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) 298 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, 299 {}) 300 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) 301 302 return (self._ApplyRules(existing_rules, include_rules, 303 specific_include_rules, norm_dir_name), 304 skip_subdirs) 305 306 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, dir_path): 307 """Given |parent_rules| and a subdirectory |dir_path| from the 308 directory that owns the |parent_rules|, add |dir_path|'s rules to 309 |self.directory_rules|, and add None entries for any of its 310 subdirectories that should be skipped. 311 """ 312 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(parent_rules, 313 dir_path) 314 self.directory_rules[NormalizePath(dir_path)] = directory_rules 315 for subdir in excluded_subdirs: 316 self.directory_rules[NormalizePath( 317 os.path.normpath(os.path.join(dir_path, subdir)))] = None 318 319 def GetDirectoryRules(self, dir_path): 320 """Returns a Rules object to use for the given directory, or None 321 if the given directory should be skipped. This takes care of 322 first building rules for parent directories (up to 323 self.base_directory) if needed. 324 325 Args: 326 dir_path: A real (non-normalized) path to the directory you want 327 rules for. 328 """ 329 norm_dir_path = NormalizePath(dir_path) 330 331 if not norm_dir_path.startswith( 332 NormalizePath(os.path.normpath(self.base_directory))): 333 dir_path = os.path.join(self.base_directory, dir_path) 334 norm_dir_path = NormalizePath(dir_path) 335 336 parent_dir = os.path.dirname(dir_path) 337 parent_rules = None 338 if not norm_dir_path in self.directory_rules: 339 parent_rules = self.GetDirectoryRules(parent_dir) 340 341 # We need to check for an entry for our dir_path again, in case we 342 # are at a path e.g. A/B/C where A/B/DEPS specifies the C 343 # subdirectory to be skipped; in this case, the invocation to 344 # GetDirectoryRules(parent_dir) has already filled in an entry for 345 # A/B/C. 346 if not norm_dir_path in self.directory_rules: 347 if not parent_rules: 348 # If the parent directory should be skipped, then the current 349 # directory should also be skipped. 350 self.directory_rules[norm_dir_path] = None 351 else: 352 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path) 353 return self.directory_rules[norm_dir_path] 354 355 def CheckDirectory(self, start_dir): 356 """Checks all relevant source files in the specified directory and 357 its subdirectories for compliance with DEPS rules throughout the 358 tree (starting at |self.base_directory|). |start_dir| must be a 359 subdirectory of |self.base_directory|. 360 361 On completion, self.results_formatter has the results of 362 processing, and calling Report() will print a report of results. 363 """ 364 java = java_checker.JavaChecker(self.base_directory, self.verbose) 365 cpp = cpp_checker.CppChecker(self.verbose) 366 checkers = dict( 367 (extension, checker) 368 for checker in [java, cpp] for extension in checker.EXTENSIONS) 369 self._CheckDirectoryImpl(checkers, start_dir) 370 371 def _CheckDirectoryImpl(self, checkers, dir_name): 372 rules = self.GetDirectoryRules(dir_name) 373 if rules == None: 374 return 375 376 # Collect a list of all files and directories to check. 377 files_to_check = [] 378 dirs_to_check = [] 379 contents = os.listdir(dir_name) 380 for cur in contents: 381 full_name = os.path.join(dir_name, cur) 382 if os.path.isdir(full_name): 383 dirs_to_check.append(full_name) 384 elif os.path.splitext(full_name)[1] in checkers: 385 if not self._skip_tests or not _IsTestFile(cur): 386 files_to_check.append(full_name) 387 388 # First check all files in this directory. 389 for cur in files_to_check: 390 checker = checkers[os.path.splitext(cur)[1]] 391 file_status = checker.CheckFile(rules, cur) 392 if file_status.HasViolations(): 393 self.results_formatter.AddError(file_status) 394 395 # Next recurse into the subdirectories. 396 for cur in dirs_to_check: 397 self._CheckDirectoryImpl(checkers, cur) 398 399 def CheckAddedCppIncludes(self, added_includes): 400 """This is used from PRESUBMIT.py to check new #include statements added in 401 the change being presubmit checked. 402 403 Args: 404 added_includes: ((file_path, (include_line, include_line, ...), ...) 405 406 Return: 407 A list of tuples, (bad_file_path, rule_type, rule_description) 408 where rule_type is one of Rule.DISALLOW or Rule.TEMP_ALLOW and 409 rule_description is human-readable. Empty if no problems. 410 """ 411 cpp = cpp_checker.CppChecker(self.verbose) 412 problems = [] 413 for file_path, include_lines in added_includes: 414 if not cpp.IsCppFile(file_path): 415 pass 416 rules_for_file = self.GetDirectoryRules(os.path.dirname(file_path)) 417 if rules_for_file: 418 for line in include_lines: 419 is_include, violation = cpp.CheckLine( 420 rules_for_file, line, file_path, True) 421 if violation: 422 rule_type = violation.violated_rule.allow 423 if rule_type != Rule.ALLOW: 424 violation_text = results.NormalResultsFormatter.FormatViolation( 425 violation, self.verbose) 426 problems.append((file_path, rule_type, violation_text)) 427 return problems 428 429 def _AddGitSourceDirectories(self): 430 """Adds any directories containing sources managed by git to 431 self.git_source_directories. 432 """ 433 if not os.path.exists(os.path.join(self.base_directory, '.git')): 434 return 435 436 popen_out = os.popen('cd %s && git ls-files --full-name .' % 437 subprocess.list2cmdline([self.base_directory])) 438 for line in popen_out.readlines(): 439 dir_name = os.path.join(self.base_directory, os.path.dirname(line)) 440 # Add the directory as well as all the parent directories. Use 441 # forward slashes and lower case to normalize paths. 442 while dir_name != self.base_directory: 443 self.git_source_directories.add(NormalizePath(dir_name)) 444 dir_name = os.path.dirname(dir_name) 445 self.git_source_directories.add(NormalizePath(self.base_directory)) 446 447 448 def PrintUsage(): 449 print """Usage: python checkdeps.py [--root <root>] [tocheck] 450 451 --root ROOT Specifies the repository root. This defaults to "../../.." 452 relative to the script file. This will be correct given the 453 normal location of the script in "<root>/tools/checkdeps". 454 455 --(others) There are a few lesser-used options; run with --help to show them. 456 457 tocheck Specifies the directory, relative to root, to check. This defaults 458 to "." so it checks everything. 459 460 Examples: 461 python checkdeps.py 462 python checkdeps.py --root c:\\source chrome""" 463 464 465 def main(): 466 option_parser = optparse.OptionParser() 467 option_parser.add_option( 468 '', '--root', 469 default='', dest='base_directory', 470 help='Specifies the repository root. This defaults ' 471 'to "../../.." relative to the script file, which ' 472 'will normally be the repository root.') 473 option_parser.add_option( 474 '', '--ignore-temp-rules', 475 action='store_true', dest='ignore_temp_rules', default=False, 476 help='Ignore !-prefixed (temporary) rules.') 477 option_parser.add_option( 478 '', '--generate-temp-rules', 479 action='store_true', dest='generate_temp_rules', default=False, 480 help='Print rules to temporarily allow files that fail ' 481 'dependency checking.') 482 option_parser.add_option( 483 '', '--count-violations', 484 action='store_true', dest='count_violations', default=False, 485 help='Count #includes in violation of intended rules.') 486 option_parser.add_option( 487 '', '--skip-tests', 488 action='store_true', dest='skip_tests', default=False, 489 help='Skip checking test files (best effort).') 490 option_parser.add_option( 491 '-v', '--verbose', 492 action='store_true', default=False, 493 help='Print debug logging') 494 options, args = option_parser.parse_args() 495 496 deps_checker = DepsChecker(options.base_directory, 497 verbose=options.verbose, 498 ignore_temp_rules=options.ignore_temp_rules, 499 skip_tests=options.skip_tests) 500 501 # Figure out which directory we have to check. 502 start_dir = deps_checker.base_directory 503 if len(args) == 1: 504 # Directory specified. Start here. It's supposed to be relative to the 505 # base directory. 506 start_dir = os.path.abspath( 507 os.path.join(deps_checker.base_directory, args[0])) 508 elif len(args) >= 2 or (options.generate_temp_rules and 509 options.count_violations): 510 # More than one argument, or incompatible flags, we don't handle this. 511 PrintUsage() 512 return 1 513 514 print 'Using base directory:', deps_checker.base_directory 515 print 'Checking:', start_dir 516 517 if options.generate_temp_rules: 518 deps_checker.results_formatter = results.TemporaryRulesFormatter() 519 elif options.count_violations: 520 deps_checker.results_formatter = results.CountViolationsFormatter() 521 deps_checker.CheckDirectory(start_dir) 522 return deps_checker.Report() 523 524 525 if '__main__' == __name__: 526 sys.exit(main()) 527