OLD | NEW |
(Empty) | |
| 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 copy |
| 79 import os.path |
| 80 import posixpath |
| 81 import subprocess |
| 82 |
| 83 from rules import Rule, Rules |
| 84 |
| 85 |
| 86 # Variable name used in the DEPS file to add or subtract include files from |
| 87 # the module-level deps. |
| 88 INCLUDE_RULES_VAR_NAME = 'include_rules' |
| 89 |
| 90 # Variable name used in the DEPS file to add or subtract include files |
| 91 # from module-level deps specific to files whose basename (last |
| 92 # component of path) matches a given regular expression. |
| 93 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' |
| 94 |
| 95 # Optionally present in the DEPS file to list subdirectories which should not |
| 96 # be checked. This allows us to skip third party code, for example. |
| 97 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' |
| 98 |
| 99 |
| 100 def NormalizePath(path): |
| 101 """Returns a path normalized to how we write DEPS rules and compare paths.""" |
| 102 return os.path.normcase(path).replace(os.path.sep, posixpath.sep) |
| 103 |
| 104 |
| 105 def _GitSourceDirectories(base_directory): |
| 106 """Returns set of normalized paths to subdirectories containing sources |
| 107 managed by git.""" |
| 108 if not os.path.exists(os.path.join(base_directory, '.git')): |
| 109 return set() |
| 110 |
| 111 base_dir_norm = NormalizePath(base_directory) |
| 112 git_source_directories = set([base_dir_norm]) |
| 113 |
| 114 git_ls_files_cmd = ['git', 'ls-files'] |
| 115 # FIXME: Use a context manager in Python 3.2+ |
| 116 popen = subprocess.Popen(git_ls_files_cmd, |
| 117 stdout=subprocess.PIPE, |
| 118 bufsize=1, # line buffering, since read by line |
| 119 cwd=base_directory) |
| 120 try: |
| 121 try: |
| 122 for line in popen.stdout: |
| 123 dir_path = os.path.join(base_directory, os.path.dirname(line)) |
| 124 dir_path_norm = NormalizePath(dir_path) |
| 125 # Add the directory as well as all the parent directories, |
| 126 # stopping once we reach an already-listed directory. |
| 127 while dir_path_norm not in git_source_directories: |
| 128 git_source_directories.add(dir_path_norm) |
| 129 dir_path_norm = posixpath.dirname(dir_path_norm) |
| 130 finally: |
| 131 popen.stdout.close() |
| 132 finally: |
| 133 popen.wait() |
| 134 |
| 135 return git_source_directories |
| 136 |
| 137 |
| 138 class DepsBuilder(object): |
| 139 """Parses include_rules from DEPS files.""" |
| 140 |
| 141 def __init__(self, |
| 142 base_directory=None, |
| 143 verbose=False, |
| 144 being_tested=False, |
| 145 ignore_temp_rules=False, |
| 146 ignore_specific_rules=False): |
| 147 """Creates a new DepsBuilder. |
| 148 |
| 149 Args: |
| 150 base_directory: local path to root of checkout, e.g. C:\chr\src. |
| 151 verbose: Set to True for debug output. |
| 152 being_tested: Set to True to ignore the DEPS file at tools/checkdeps/DEPS. |
| 153 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). |
| 154 """ |
| 155 base_directory = (base_directory or |
| 156 os.path.join(os.path.dirname(__file__), |
| 157 os.path.pardir, os.path.pardir)) |
| 158 self.base_directory = os.path.abspath(base_directory) # Local absolute path |
| 159 self.verbose = verbose |
| 160 self._under_test = being_tested |
| 161 self._ignore_temp_rules = ignore_temp_rules |
| 162 self._ignore_specific_rules = ignore_specific_rules |
| 163 |
| 164 # Set of normalized paths |
| 165 self.git_source_directories = _GitSourceDirectories(self.base_directory) |
| 166 |
| 167 # Map of normalized directory paths to rules to use for those |
| 168 # directories, or None for directories that should be skipped. |
| 169 # Normalized is: absolute, lowercase, / for separator. |
| 170 self.directory_rules = {} |
| 171 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) |
| 172 |
| 173 def _ApplyRules(self, existing_rules, includes, specific_includes, |
| 174 cur_dir_norm): |
| 175 """Applies the given include rules, returning the new rules. |
| 176 |
| 177 Args: |
| 178 existing_rules: A set of existing rules that will be combined. |
| 179 include: The list of rules from the "include_rules" section of DEPS. |
| 180 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules |
| 181 from the "specific_include_rules" section of DEPS. |
| 182 cur_dir_norm: The current directory, normalized path. We will create an |
| 183 implicit rule that allows inclusion from this directory. |
| 184 |
| 185 Returns: A new set of rules combining the existing_rules with the other |
| 186 arguments. |
| 187 """ |
| 188 rules = copy.deepcopy(existing_rules) |
| 189 |
| 190 # First apply the implicit "allow" rule for the current directory. |
| 191 base_dir_norm = NormalizePath(self.base_directory) |
| 192 if not cur_dir_norm.startswith(base_dir_norm): |
| 193 raise Exception( |
| 194 'Internal error: base directory is not at the beginning for\n' |
| 195 ' %s and base dir\n' |
| 196 ' %s' % (cur_dir_norm, base_dir_norm)) |
| 197 relative_dir = posixpath.relpath(cur_dir_norm, base_dir_norm) |
| 198 |
| 199 # Make the help string a little more meaningful. |
| 200 source = relative_dir or 'top level' |
| 201 rules.AddRule('+' + relative_dir, |
| 202 relative_dir, |
| 203 'Default rule for ' + source) |
| 204 |
| 205 def ApplyOneRule(rule_str, dependee_regexp=None): |
| 206 """Deduces a sensible description for the rule being added, and |
| 207 adds the rule with its description to |rules|. |
| 208 |
| 209 If we are ignoring temporary rules, this function does nothing |
| 210 for rules beginning with the Rule.TEMP_ALLOW character. |
| 211 """ |
| 212 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): |
| 213 return |
| 214 |
| 215 rule_block_name = 'include_rules' |
| 216 if dependee_regexp: |
| 217 rule_block_name = 'specific_include_rules' |
| 218 if relative_dir: |
| 219 rule_description = relative_dir + "'s %s" % rule_block_name |
| 220 else: |
| 221 rule_description = 'the top level %s' % rule_block_name |
| 222 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp) |
| 223 |
| 224 # Apply the additional explicit rules. |
| 225 for rule_str in includes: |
| 226 ApplyOneRule(rule_str) |
| 227 |
| 228 # Finally, apply the specific rules. |
| 229 if self._ignore_specific_rules: |
| 230 return rules |
| 231 |
| 232 for regexp, specific_rules in specific_includes.iteritems(): |
| 233 for rule_str in specific_rules: |
| 234 ApplyOneRule(rule_str, regexp) |
| 235 |
| 236 return rules |
| 237 |
| 238 def _ApplyDirectoryRules(self, existing_rules, dir_path_local_abs): |
| 239 """Combines rules from the existing rules and the new directory. |
| 240 |
| 241 Any directory can contain a DEPS file. Top-level DEPS files can contain |
| 242 module dependencies which are used by gclient. We use these, along with |
| 243 additional include rules and implicit rules for the given directory, to |
| 244 come up with a combined set of rules to apply for the directory. |
| 245 |
| 246 Args: |
| 247 existing_rules: The rules for the parent directory. We'll add-on to these. |
| 248 dir_path_local_abs: The directory path that the DEPS file may live in (if |
| 249 it exists). This will also be used to generate the |
| 250 implicit rules. This is a local path. |
| 251 |
| 252 Returns: A 2-tuple of: |
| 253 (1) the combined set of rules to apply to the sub-tree, |
| 254 (2) a list of all subdirectories that should NOT be checked, as specified |
| 255 in the DEPS file (if any). |
| 256 Subdirectories are single words, hence no OS dependence. |
| 257 """ |
| 258 dir_path_norm = NormalizePath(dir_path_local_abs) |
| 259 |
| 260 # Check for a .svn directory in this directory or that this directory is |
| 261 # contained in git source directories. This will tell us if it's a source |
| 262 # directory and should be checked. |
| 263 if not (os.path.exists(os.path.join(dir_path_local_abs, '.svn')) or |
| 264 dir_path_norm in self.git_source_directories): |
| 265 return None, [] |
| 266 |
| 267 # Check the DEPS file in this directory. |
| 268 if self.verbose: |
| 269 print 'Applying rules from', dir_path_local_abs |
| 270 def FromImpl(*_): |
| 271 pass # NOP function so "From" doesn't fail. |
| 272 |
| 273 def FileImpl(_): |
| 274 pass # NOP function so "File" doesn't fail. |
| 275 |
| 276 class _VarImpl: |
| 277 def __init__(self, local_scope): |
| 278 self._local_scope = local_scope |
| 279 |
| 280 def Lookup(self, var_name): |
| 281 """Implements the Var syntax.""" |
| 282 try: |
| 283 return self._local_scope['vars'][var_name] |
| 284 except KeyError: |
| 285 raise Exception('Var is not defined: %s' % var_name) |
| 286 |
| 287 local_scope = {} |
| 288 global_scope = { |
| 289 'File': FileImpl, |
| 290 'From': FromImpl, |
| 291 'Var': _VarImpl(local_scope).Lookup, |
| 292 } |
| 293 deps_file_path = os.path.join(dir_path_local_abs, 'DEPS') |
| 294 |
| 295 # The second conditional here is to disregard the |
| 296 # tools/checkdeps/DEPS file while running tests. This DEPS file |
| 297 # has a skip_child_includes for 'testdata' which is necessary for |
| 298 # running production tests, since there are intentional DEPS |
| 299 # violations under the testdata directory. On the other hand when |
| 300 # running tests, we absolutely need to verify the contents of that |
| 301 # directory to trigger those intended violations and see that they |
| 302 # are handled correctly. |
| 303 if os.path.isfile(deps_file_path) and not ( |
| 304 self._under_test and |
| 305 os.path.basename(dir_path_local_abs) == 'checkdeps'): |
| 306 execfile(deps_file_path, global_scope, local_scope) |
| 307 elif self.verbose: |
| 308 print ' No deps file found in', dir_path_local_abs |
| 309 |
| 310 # Even if a DEPS file does not exist we still invoke ApplyRules |
| 311 # to apply the implicit "allow" rule for the current directory |
| 312 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) |
| 313 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, |
| 314 {}) |
| 315 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) |
| 316 |
| 317 return (self._ApplyRules(existing_rules, include_rules, |
| 318 specific_include_rules, dir_path_norm), |
| 319 skip_subdirs) |
| 320 |
| 321 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, |
| 322 dir_path_local_abs): |
| 323 """Given |parent_rules| and a subdirectory |dir_path_local_abs| of the |
| 324 directory that owns the |parent_rules|, add |dir_path_local_abs|'s rules to |
| 325 |self.directory_rules|, and add None entries for any of its |
| 326 subdirectories that should be skipped. |
| 327 """ |
| 328 directory_rules, excluded_subdirs = self._ApplyDirectoryRules( |
| 329 parent_rules, dir_path_local_abs) |
| 330 dir_path_norm = NormalizePath(dir_path_local_abs) |
| 331 self.directory_rules[dir_path_norm] = directory_rules |
| 332 for subdir in excluded_subdirs: |
| 333 subdir_path_norm = posixpath.join(dir_path_norm, subdir) |
| 334 self.directory_rules[subdir_path_norm] = None |
| 335 |
| 336 def GetDirectoryRules(self, dir_path_local): |
| 337 """Returns a Rules object to use for the given directory, or None |
| 338 if the given directory should be skipped. |
| 339 |
| 340 Also modifies |self.directory_rules| to store the Rules. |
| 341 This takes care of first building rules for parent directories (up to |
| 342 |self.base_directory|) if needed, which may add rules for skipped |
| 343 subdirectories. |
| 344 |
| 345 Args: |
| 346 dir_path_local: A local path to the directory you want rules for. |
| 347 Can be relative and unnormalized. |
| 348 """ |
| 349 if os.path.isabs(dir_path_local): |
| 350 dir_path_local_abs = dir_path_local |
| 351 else: |
| 352 dir_path_local_abs = os.path.join(self.base_directory, dir_path_local) |
| 353 dir_path_norm = NormalizePath(dir_path_local_abs) |
| 354 |
| 355 if dir_path_norm in self.directory_rules: |
| 356 return self.directory_rules[dir_path_norm] |
| 357 |
| 358 parent_dir_local_abs = os.path.dirname(dir_path_local_abs) |
| 359 parent_rules = self.GetDirectoryRules(parent_dir_local_abs) |
| 360 # We need to check for an entry for our dir_path again, since |
| 361 # GetDirectoryRules can modify entries for subdirectories, namely setting |
| 362 # to None if they should be skipped, via _ApplyDirectoryRulesAndSkipSubdirs. |
| 363 # For example, if dir_path == 'A/B/C' and A/B/DEPS specifies that the C |
| 364 # subdirectory be skipped, GetDirectoryRules('A/B') will fill in the entry |
| 365 # for 'A/B/C' as None. |
| 366 if dir_path_norm in self.directory_rules: |
| 367 return self.directory_rules[dir_path_norm] |
| 368 |
| 369 if parent_rules: |
| 370 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path_local_abs) |
| 371 else: |
| 372 # If the parent directory should be skipped, then the current |
| 373 # directory should also be skipped. |
| 374 self.directory_rules[dir_path_norm] = None |
| 375 return self.directory_rules[dir_path_norm] |
OLD | NEW |