1 #!/usr/bin/env python 2 # Copyright 2013 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 """Traverses the source tree, parses all found DEPS files, and constructs 7 a dependency rule table to be used by subclasses. 8 9 The format of the deps file: 10 11 First you have the normal module-level deps. These are the ones used by 12 gclient. An example would be: 13 14 deps = { 15 "base":"http://foo.bar/trunk/base" 16 } 17 18 DEPS files not in the top-level of a module won't need this. Then you 19 have any additional include rules. You can add (using "+") or subtract 20 (using "-") from the previously specified rules (including 21 module-level deps). You can also specify a path that is allowed for 22 now but that we intend to remove, using "!"; this is treated the same 23 as "+" when check_deps is run by our bots, but a presubmit step will 24 show a warning if you add a new include of a file that is only allowed 25 by "!". 26 27 Note that for .java files, there is currently no difference between 28 "+" and "!", even in the presubmit step. 29 30 include_rules = [ 31 # Code should be able to use base (it's specified in the module-level 32 # deps above), but nothing in "base/evil" because it's evil. 33 "-base/evil", 34 35 # But this one subdirectory of evil is OK. 36 "+base/evil/not", 37 38 # And it can include files from this other directory even though there is 39 # no deps rule for it. 40 "+tools/crime_fighter", 41 42 # This dependency is allowed for now but work is ongoing to remove it, 43 # so you shouldn't add further dependencies on it. 44 "!base/evil/ok_for_now.h", 45 ] 46 47 If you have certain include rules that should only be applied for some 48 files within this directory and subdirectories, you can write a 49 section named specific_include_rules that is a hash map of regular 50 expressions to the list of rules that should apply to files matching 51 them. Note that such rules will always be applied before the rules 52 from 'include_rules' have been applied, but the order in which rules 53 associated with different regular expressions is applied is arbitrary. 54 55 specific_include_rules = { 56 ".*_(unit|browser|api)test\.cc": [ 57 "+libraries/testsupport", 58 ], 59 } 60 61 DEPS files may be placed anywhere in the tree. Each one applies to all 62 subdirectories, where there may be more DEPS files that provide additions or 63 subtractions for their own sub-trees. 64 65 There is an implicit rule for the current directory (where the DEPS file lives) 66 and all of its subdirectories. This prevents you from having to explicitly 67 allow the current directory everywhere. This implicit rule is applied first, 68 so you can modify or remove it using the normal include rules. 69 70 The rules are processed in order. This means you can explicitly allow a higher 71 directory and then take away permissions from sub-parts, or the reverse. 72 73 Note that all directory separators must be slashes (Unix-style) and not 74 backslashes. All directories should be relative to the source root and use 75 only lowercase. 76 """ 77 78 import os 79 import subprocess 80 import copy 81 82 from rules import Rule, Rules 83 84 85 # Variable name used in the DEPS file to add or subtract include files from 86 # the module-level deps. 87 INCLUDE_RULES_VAR_NAME = 'include_rules' 88 89 # Variable name used in the DEPS file to add or subtract include files 90 # from module-level deps specific to files whose basename (last 91 # component of path) matches a given regular expression. 92 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' 93 94 # Optionally present in the DEPS file to list subdirectories which should not 95 # be checked. This allows us to skip third party code, for example. 96 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' 97 98 99 def NormalizePath(path): 100 """Returns a path normalized to how we write DEPS rules and compare paths. 101 """ 102 return path.lower().replace('\\', '/') 103 104 105 class DepsBuilder(object): 106 """Parses include_rules from DEPS files. 107 """ 108 109 def __init__(self, 110 base_directory=None, 111 verbose=False, 112 being_tested=False, 113 ignore_temp_rules=False, 114 ignore_specific_rules=False): 115 """Creates a new DepsBuilder. 116 117 Args: 118 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src. 119 verbose: Set to true for debug output. 120 being_tested: Set to true to ignore the DEPS file at tools/checkdeps/DEPS. 121 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). 122 """ 123 self.base_directory = base_directory 124 self.verbose = verbose 125 self._under_test = being_tested 126 self._ignore_temp_rules = ignore_temp_rules 127 self._ignore_specific_rules = ignore_specific_rules 128 129 if not base_directory: 130 self.base_directory = os.path.abspath( 131 os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', '..')) 132 133 self.git_source_directories = set() 134 self._AddGitSourceDirectories() 135 136 # Map of normalized directory paths to rules to use for those 137 # directories, or None for directories that should be skipped. 138 self.directory_rules = {} 139 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) 140 141 def _ApplyRules(self, existing_rules, includes, specific_includes, cur_dir): 142 """Applies the given include rules, returning the new rules. 143 144 Args: 145 existing_rules: A set of existing rules that will be combined. 146 include: The list of rules from the "include_rules" section of DEPS. 147 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules 148 from the "specific_include_rules" section of DEPS. 149 cur_dir: The current directory, normalized path. We will create an 150 implicit rule that allows inclusion from this directory. 151 152 Returns: A new set of rules combining the existing_rules with the other 153 arguments. 154 """ 155 rules = copy.deepcopy(existing_rules) 156 157 # First apply the implicit "allow" rule for the current directory. 158 if cur_dir.startswith( 159 NormalizePath(os.path.normpath(self.base_directory))): 160 relative_dir = cur_dir[len(self.base_directory) + 1:] 161 162 source = relative_dir 163 if len(source) == 0: 164 source = 'top level' # Make the help string a little more meaningful. 165 rules.AddRule('+' + relative_dir, 166 relative_dir, 167 'Default rule for ' + source) 168 else: 169 raise Exception('Internal error: base directory is not at the beginning' + 170 ' for\n %s and base dir\n %s' % 171 (cur_dir, self.base_directory)) 172 173 def ApplyOneRule(rule_str, cur_dir, dependee_regexp=None): 174 """Deduces a sensible description for the rule being added, and 175 adds the rule with its description to |rules|. 176 177 If we are ignoring temporary rules, this function does nothing 178 for rules beginning with the Rule.TEMP_ALLOW character. 179 """ 180 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): 181 return 182 183 rule_block_name = 'include_rules' 184 if dependee_regexp: 185 rule_block_name = 'specific_include_rules' 186 if not relative_dir: 187 rule_description = 'the top level %s' % rule_block_name 188 else: 189 rule_description = relative_dir + "'s %s" % rule_block_name 190 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp) 191 192 # Apply the additional explicit rules. 193 for (_, rule_str) in enumerate(includes): 194 ApplyOneRule(rule_str, cur_dir) 195 196 # Finally, apply the specific rules. 197 if not self._ignore_specific_rules: 198 for regexp, specific_rules in specific_includes.iteritems(): 199 for rule_str in specific_rules: 200 ApplyOneRule(rule_str, cur_dir, regexp) 201 202 return rules 203 204 def _ApplyDirectoryRules(self, existing_rules, dir_name): 205 """Combines rules from the existing rules and the new directory. 206 207 Any directory can contain a DEPS file. Toplevel DEPS files can contain 208 module dependencies which are used by gclient. We use these, along with 209 additional include rules and implicit rules for the given directory, to 210 come up with a combined set of rules to apply for the directory. 211 212 Args: 213 existing_rules: The rules for the parent directory. We'll add-on to these. 214 dir_name: The directory name that the deps file may live in (if 215 it exists). This will also be used to generate the 216 implicit rules. This is a non-normalized path. 217 218 Returns: A tuple containing: (1) the combined set of rules to apply to the 219 sub-tree, and (2) a list of all subdirectories that should NOT be 220 checked, as specified in the DEPS file (if any). 221 """ 222 norm_dir_name = NormalizePath(dir_name) 223 224 # Check for a .svn directory in this directory or check this directory is 225 # contained in git source direcotries. This will tell us if it's a source 226 # directory and should be checked. 227 if not (os.path.exists(os.path.join(dir_name, ".svn")) or 228 (norm_dir_name in self.git_source_directories)): 229 return (None, []) 230 231 # Check the DEPS file in this directory. 232 if self.verbose: 233 print 'Applying rules from', dir_name 234 def FromImpl(_unused, _unused2): 235 pass # NOP function so "From" doesn't fail. 236 237 def FileImpl(_unused): 238 pass # NOP function so "File" doesn't fail. 239 240 class _VarImpl: 241 def __init__(self, local_scope): 242 self._local_scope = local_scope 243 244 def Lookup(self, var_name): 245 """Implements the Var syntax.""" 246 if var_name in self._local_scope.get('vars', {}): 247 return self._local_scope['vars'][var_name] 248 raise Exception('Var is not defined: %s' % var_name) 249 250 local_scope = {} 251 global_scope = { 252 'File': FileImpl, 253 'From': FromImpl, 254 'Var': _VarImpl(local_scope).Lookup, 255 } 256 deps_file = os.path.join(dir_name, 'DEPS') 257 258 # The second conditional here is to disregard the 259 # tools/checkdeps/DEPS file while running tests. This DEPS file 260 # has a skip_child_includes for 'testdata' which is necessary for 261 # running production tests, since there are intentional DEPS 262 # violations under the testdata directory. On the other hand when 263 # running tests, we absolutely need to verify the contents of that 264 # directory to trigger those intended violations and see that they 265 # are handled correctly. 266 if os.path.isfile(deps_file) and ( 267 not self._under_test or not os.path.split(dir_name)[1] == 'checkdeps'): 268 execfile(deps_file, global_scope, local_scope) 269 elif self.verbose: 270 print ' No deps file found in', dir_name 271 272 # Even if a DEPS file does not exist we still invoke ApplyRules 273 # to apply the implicit "allow" rule for the current directory 274 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) 275 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, 276 {}) 277 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) 278 279 return (self._ApplyRules(existing_rules, include_rules, 280 specific_include_rules, norm_dir_name), 281 skip_subdirs) 282 283 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, dir_path): 284 """Given |parent_rules| and a subdirectory |dir_path| from the 285 directory that owns the |parent_rules|, add |dir_path|'s rules to 286 |self.directory_rules|, and add None entries for any of its 287 subdirectories that should be skipped. 288 """ 289 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(parent_rules, 290 dir_path) 291 self.directory_rules[NormalizePath(dir_path)] = directory_rules 292 for subdir in excluded_subdirs: 293 self.directory_rules[NormalizePath( 294 os.path.normpath(os.path.join(dir_path, subdir)))] = None 295 296 def GetDirectoryRules(self, dir_path): 297 """Returns a Rules object to use for the given directory, or None 298 if the given directory should be skipped. This takes care of 299 first building rules for parent directories (up to 300 self.base_directory) if needed. 301 302 Args: 303 dir_path: A real (non-normalized) path to the directory you want 304 rules for. 305 """ 306 norm_dir_path = NormalizePath(dir_path) 307 308 if not norm_dir_path.startswith( 309 NormalizePath(os.path.normpath(self.base_directory))): 310 dir_path = os.path.join(self.base_directory, dir_path) 311 norm_dir_path = NormalizePath(dir_path) 312 313 parent_dir = os.path.dirname(dir_path) 314 parent_rules = None 315 if not norm_dir_path in self.directory_rules: 316 parent_rules = self.GetDirectoryRules(parent_dir) 317 318 # We need to check for an entry for our dir_path again, in case we 319 # are at a path e.g. A/B/C where A/B/DEPS specifies the C 320 # subdirectory to be skipped; in this case, the invocation to 321 # GetDirectoryRules(parent_dir) has already filled in an entry for 322 # A/B/C. 323 if not norm_dir_path in self.directory_rules: 324 if not parent_rules: 325 # If the parent directory should be skipped, then the current 326 # directory should also be skipped. 327 self.directory_rules[norm_dir_path] = None 328 else: 329 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path) 330 return self.directory_rules[norm_dir_path] 331 332 def _AddGitSourceDirectories(self): 333 """Adds any directories containing sources managed by git to 334 self.git_source_directories. 335 """ 336 if not os.path.exists(os.path.join(self.base_directory, '.git')): 337 return 338 339 popen_out = os.popen('cd %s && git ls-files --full-name .' % 340 subprocess.list2cmdline([self.base_directory])) 341 for line in popen_out.readlines(): 342 dir_name = os.path.join(self.base_directory, os.path.dirname(line)) 343 # Add the directory as well as all the parent directories. Use 344 # forward slashes and lower case to normalize paths. 345 while dir_name != self.base_directory: 346 self.git_source_directories.add(NormalizePath(dir_name)) 347 dir_name = os.path.dirname(dir_name) 348 self.git_source_directories.add(NormalizePath(self.base_directory)) 349