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 7a dependency rule table to be used by subclasses. 8 9See README.md for the format of the deps file. 10""" 11 12 13 14import copy 15import os.path 16import posixpath 17import subprocess 18 19from rules import Rule, Rules 20 21 22# Variable name used in the DEPS file to add or subtract include files from 23# the module-level deps. 24INCLUDE_RULES_VAR_NAME = 'include_rules' 25 26# Variable name used in the DEPS file to add or subtract include files 27# from module-level deps specific to files whose basename (last 28# component of path) matches a given regular expression. 29SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' 30 31# Optionally present in the DEPS file to list subdirectories which should not 32# be checked. This allows us to skip third party code, for example. 33SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' 34 35# Optionally discard rules from parent directories, similar to "noparent" in 36# OWNERS files. For example, if //ash/components has "noparent = True" then 37# it will not inherit rules from //ash/DEPS, forcing each //ash/component/foo 38# to declare all its dependencies. 39NOPARENT_VAR_NAME = 'noparent' 40 41 42class DepsBuilderError(Exception): 43 """Base class for exceptions in this module.""" 44 pass 45 46 47def NormalizePath(path): 48 """Returns a path normalized to how we write DEPS rules and compare paths.""" 49 return os.path.normcase(path).replace(os.path.sep, posixpath.sep) 50 51 52def _GitSourceDirectories(base_directory): 53 """Returns set of normalized paths to subdirectories containing sources 54 managed by git.""" 55 base_dir_norm = NormalizePath(base_directory) 56 git_source_directories = set([base_dir_norm]) 57 58 git_cmd = 'git.bat' if os.name == 'nt' else 'git' 59 git_ls_files_cmd = [git_cmd, 'ls-files'] 60 # FIXME: Use a context manager in Python 3.2+ 61 popen = subprocess.Popen(git_ls_files_cmd, 62 stdout=subprocess.PIPE, 63 cwd=base_directory) 64 try: 65 try: 66 for line in popen.stdout.read().decode('utf-8').splitlines(): 67 dir_path = os.path.join(base_directory, os.path.dirname(line)) 68 dir_path_norm = NormalizePath(dir_path) 69 # Add the directory as well as all the parent directories, 70 # stopping once we reach an already-listed directory. 71 while dir_path_norm not in git_source_directories: 72 git_source_directories.add(dir_path_norm) 73 dir_path_norm = posixpath.dirname(dir_path_norm) 74 finally: 75 popen.stdout.close() 76 finally: 77 popen.wait() 78 79 return git_source_directories 80 81 82class DepsBuilder(object): 83 """Parses include_rules from DEPS files.""" 84 85 def __init__(self, 86 base_directory=None, 87 extra_repos=[], 88 verbose=False, 89 being_tested=False, 90 ignore_temp_rules=False, 91 ignore_specific_rules=False): 92 """Creates a new DepsBuilder. 93 94 Args: 95 base_directory: local path to root of checkout, e.g. C:\chr\src. 96 verbose: Set to True for debug output. 97 being_tested: Set to True to ignore the DEPS file at 98 buildtools/checkdeps/DEPS. 99 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). 100 """ 101 base_directory = (base_directory or 102 os.path.join(os.path.dirname(__file__), 103 os.path.pardir, os.path.pardir)) 104 self.base_directory = os.path.abspath(base_directory) # Local absolute path 105 self.extra_repos = extra_repos 106 self.verbose = verbose 107 self._under_test = being_tested 108 self._ignore_temp_rules = ignore_temp_rules 109 self._ignore_specific_rules = ignore_specific_rules 110 self._git_source_directories = None 111 112 if os.path.exists(os.path.join(base_directory, '.git')): 113 self.is_git = True 114 elif os.path.exists(os.path.join(base_directory, '.svn')): 115 self.is_git = False 116 else: 117 raise DepsBuilderError("%s is not a repository root" % base_directory) 118 119 # Map of normalized directory paths to rules to use for those 120 # directories, or None for directories that should be skipped. 121 # Normalized is: absolute, lowercase, / for separator. 122 self.directory_rules = {} 123 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) 124 125 def _ApplyRules(self, existing_rules, includes, specific_includes, 126 cur_dir_norm): 127 """Applies the given include rules, returning the new rules. 128 129 Args: 130 existing_rules: A set of existing rules that will be combined. 131 include: The list of rules from the "include_rules" section of DEPS. 132 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules 133 from the "specific_include_rules" section of DEPS. 134 cur_dir_norm: The current directory, normalized path. We will create an 135 implicit rule that allows inclusion from this directory. 136 137 Returns: A new set of rules combining the existing_rules with the other 138 arguments. 139 """ 140 rules = copy.deepcopy(existing_rules) 141 142 # First apply the implicit "allow" rule for the current directory. 143 base_dir_norm = NormalizePath(self.base_directory) 144 if not cur_dir_norm.startswith(base_dir_norm): 145 raise Exception( 146 'Internal error: base directory is not at the beginning for\n' 147 ' %s and base dir\n' 148 ' %s' % (cur_dir_norm, base_dir_norm)) 149 relative_dir = posixpath.relpath(cur_dir_norm, base_dir_norm) 150 151 # Make the help string a little more meaningful. 152 source = relative_dir or 'top level' 153 rules.AddRule('+' + relative_dir, 154 relative_dir, 155 'Default rule for ' + source) 156 157 def ApplyOneRule(rule_str, dependee_regexp=None): 158 """Deduces a sensible description for the rule being added, and 159 adds the rule with its description to |rules|. 160 161 If we are ignoring temporary rules, this function does nothing 162 for rules beginning with the Rule.TEMP_ALLOW character. 163 """ 164 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): 165 return 166 167 rule_block_name = 'include_rules' 168 if dependee_regexp: 169 rule_block_name = 'specific_include_rules' 170 if relative_dir: 171 rule_description = relative_dir + "'s %s" % rule_block_name 172 else: 173 rule_description = 'the top level %s' % rule_block_name 174 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp) 175 176 # Apply the additional explicit rules. 177 for rule_str in includes: 178 ApplyOneRule(rule_str) 179 180 # Finally, apply the specific rules. 181 if self._ignore_specific_rules: 182 return rules 183 184 for regexp, specific_rules in specific_includes.items(): 185 for rule_str in specific_rules: 186 ApplyOneRule(rule_str, regexp) 187 188 return rules 189 190 def _ApplyDirectoryRules(self, existing_rules, dir_path_local_abs): 191 """Combines rules from the existing rules and the new directory. 192 193 Any directory can contain a DEPS file. Top-level DEPS files can contain 194 module dependencies which are used by gclient. We use these, along with 195 additional include rules and implicit rules for the given directory, to 196 come up with a combined set of rules to apply for the directory. 197 198 Args: 199 existing_rules: The rules for the parent directory. We'll add-on to these. 200 dir_path_local_abs: The directory path that the DEPS file may live in (if 201 it exists). This will also be used to generate the 202 implicit rules. This is a local path. 203 204 Returns: A 2-tuple of: 205 (1) the combined set of rules to apply to the sub-tree, 206 (2) a list of all subdirectories that should NOT be checked, as specified 207 in the DEPS file (if any). 208 Subdirectories are single words, hence no OS dependence. 209 """ 210 dir_path_norm = NormalizePath(dir_path_local_abs) 211 212 # Check the DEPS file in this directory. 213 if self.verbose: 214 print('Applying rules from', dir_path_local_abs) 215 def FromImpl(*_): 216 pass # NOP function so "From" doesn't fail. 217 218 def FileImpl(_): 219 pass # NOP function so "File" doesn't fail. 220 221 class _VarImpl: 222 def __init__(self, local_scope): 223 self._local_scope = local_scope 224 225 def Lookup(self, var_name): 226 """Implements the Var syntax.""" 227 try: 228 return self._local_scope['vars'][var_name] 229 except KeyError: 230 raise Exception('Var is not defined: %s' % var_name) 231 232 local_scope = {} 233 global_scope = { 234 'File': FileImpl, 235 'From': FromImpl, 236 'Var': _VarImpl(local_scope).Lookup, 237 'Str': str, 238 } 239 deps_file_path = os.path.join(dir_path_local_abs, 'DEPS') 240 241 # The second conditional here is to disregard the 242 # buildtools/checkdeps/DEPS file while running tests. This DEPS file 243 # has a skip_child_includes for 'testdata' which is necessary for 244 # running production tests, since there are intentional DEPS 245 # violations under the testdata directory. On the other hand when 246 # running tests, we absolutely need to verify the contents of that 247 # directory to trigger those intended violations and see that they 248 # are handled correctly. 249 if os.path.isfile(deps_file_path) and not ( 250 self._under_test and 251 os.path.basename(dir_path_local_abs) == 'checkdeps'): 252 try: 253 with open(deps_file_path) as file: 254 exec(file.read(), global_scope, local_scope) 255 except Exception as e: 256 print(' Error reading %s: %s' % (deps_file_path, str(e))) 257 raise 258 elif self.verbose: 259 print(' No deps file found in', dir_path_local_abs) 260 261 # Even if a DEPS file does not exist we still invoke ApplyRules 262 # to apply the implicit "allow" rule for the current directory 263 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) 264 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, 265 {}) 266 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) 267 noparent = local_scope.get(NOPARENT_VAR_NAME, False) 268 if noparent: 269 parent_rules = Rules() 270 else: 271 parent_rules = existing_rules 272 273 return (self._ApplyRules(parent_rules, include_rules, 274 specific_include_rules, dir_path_norm), 275 skip_subdirs) 276 277 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, 278 dir_path_local_abs): 279 """Given |parent_rules| and a subdirectory |dir_path_local_abs| of the 280 directory that owns the |parent_rules|, add |dir_path_local_abs|'s rules to 281 |self.directory_rules|, and add None entries for any of its 282 subdirectories that should be skipped. 283 """ 284 directory_rules, excluded_subdirs = self._ApplyDirectoryRules( 285 parent_rules, dir_path_local_abs) 286 dir_path_norm = NormalizePath(dir_path_local_abs) 287 self.directory_rules[dir_path_norm] = directory_rules 288 for subdir in excluded_subdirs: 289 subdir_path_norm = posixpath.join(dir_path_norm, subdir) 290 self.directory_rules[subdir_path_norm] = None 291 292 def GetAllRulesAndFiles(self, dir_name=None): 293 """Yields (rules, filenames) for each repository directory with DEPS rules. 294 295 This walks the directory tree while staying in the repository. Specify 296 |dir_name| to walk just one directory and its children; omit |dir_name| to 297 walk the entire repository. 298 299 Yields: 300 Two-element (rules, filenames) tuples. |rules| is a rules.Rules object 301 for a directory, and |filenames| is a list of the absolute local paths 302 of all files in that directory. 303 """ 304 if self.is_git and self._git_source_directories is None: 305 self._git_source_directories = _GitSourceDirectories(self.base_directory) 306 for repo in self.extra_repos: 307 repo_path = os.path.join(self.base_directory, repo) 308 self._git_source_directories.update(_GitSourceDirectories(repo_path)) 309 310 # Collect a list of all files and directories to check. 311 if dir_name and not os.path.isabs(dir_name): 312 dir_name = os.path.join(self.base_directory, dir_name) 313 dirs_to_check = [dir_name or self.base_directory] 314 while dirs_to_check: 315 current_dir = dirs_to_check.pop() 316 317 # Check that this directory is part of the source repository. This 318 # prevents us from descending into third-party code or directories 319 # generated by the build system. 320 if self.is_git: 321 if NormalizePath(current_dir) not in self._git_source_directories: 322 continue 323 elif not os.path.exists(os.path.join(current_dir, '.svn')): 324 continue 325 326 current_dir_rules = self.GetDirectoryRules(current_dir) 327 328 if not current_dir_rules: 329 continue # Handle the 'skip_child_includes' case. 330 331 current_dir_contents = sorted(os.listdir(current_dir)) 332 file_names = [] 333 sub_dirs = [] 334 for file_name in current_dir_contents: 335 full_name = os.path.join(current_dir, file_name) 336 if os.path.isdir(full_name): 337 sub_dirs.append(full_name) 338 else: 339 file_names.append(full_name) 340 dirs_to_check.extend(reversed(sub_dirs)) 341 342 yield (current_dir_rules, file_names) 343 344 def GetDirectoryRules(self, dir_path_local): 345 """Returns a Rules object to use for the given directory, or None 346 if the given directory should be skipped. 347 348 Also modifies |self.directory_rules| to store the Rules. 349 This takes care of first building rules for parent directories (up to 350 |self.base_directory|) if needed, which may add rules for skipped 351 subdirectories. 352 353 Args: 354 dir_path_local: A local path to the directory you want rules for. 355 Can be relative and unnormalized. It is the caller's responsibility 356 to ensure that this is part of the repository rooted at 357 |self.base_directory|. 358 """ 359 if os.path.isabs(dir_path_local): 360 dir_path_local_abs = dir_path_local 361 else: 362 dir_path_local_abs = os.path.join(self.base_directory, dir_path_local) 363 dir_path_norm = NormalizePath(dir_path_local_abs) 364 365 if dir_path_norm in self.directory_rules: 366 return self.directory_rules[dir_path_norm] 367 368 parent_dir_local_abs = os.path.dirname(dir_path_local_abs) 369 parent_rules = self.GetDirectoryRules(parent_dir_local_abs) 370 # We need to check for an entry for our dir_path again, since 371 # GetDirectoryRules can modify entries for subdirectories, namely setting 372 # to None if they should be skipped, via _ApplyDirectoryRulesAndSkipSubdirs. 373 # For example, if dir_path == 'A/B/C' and A/B/DEPS specifies that the C 374 # subdirectory be skipped, GetDirectoryRules('A/B') will fill in the entry 375 # for 'A/B/C' as None. 376 if dir_path_norm in self.directory_rules: 377 return self.directory_rules[dir_path_norm] 378 379 if parent_rules: 380 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path_local_abs) 381 else: 382 # If the parent directory should be skipped, then the current 383 # directory should also be skipped. 384 self.directory_rules[dir_path_norm] = None 385 return self.directory_rules[dir_path_norm] 386