|  | #!/usr/bin/env python | 
|  | # Copyright 2013 The Chromium Authors. All rights reserved. | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """Traverses the source tree, parses all found DEPS files, and constructs | 
|  | a dependency rule table to be used by subclasses. | 
|  |  | 
|  | See README.md for the format of the deps file. | 
|  | """ | 
|  |  | 
|  | import copy | 
|  | import os.path | 
|  | import posixpath | 
|  | import subprocess | 
|  |  | 
|  | from rules import Rule, Rules | 
|  |  | 
|  |  | 
|  | # Variable name used in the DEPS file to add or subtract include files from | 
|  | # the module-level deps. | 
|  | INCLUDE_RULES_VAR_NAME = 'include_rules' | 
|  |  | 
|  | # Variable name used in the DEPS file to add or subtract include files | 
|  | # from module-level deps specific to files whose basename (last | 
|  | # component of path) matches a given regular expression. | 
|  | SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' | 
|  |  | 
|  | # Optionally present in the DEPS file to list subdirectories which should not | 
|  | # be checked. This allows us to skip third party code, for example. | 
|  | SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' | 
|  |  | 
|  | # Optionally discard rules from parent directories, similar to "noparent" in | 
|  | # OWNERS files. For example, if //ash/components has "noparent = True" then | 
|  | # it will not inherit rules from //ash/DEPS, forcing each //ash/component/foo | 
|  | # to declare all its dependencies. | 
|  | NOPARENT_VAR_NAME = 'noparent' | 
|  |  | 
|  |  | 
|  | class DepsBuilderError(Exception): | 
|  | """Base class for exceptions in this module.""" | 
|  | pass | 
|  |  | 
|  |  | 
|  | def NormalizePath(path): | 
|  | """Returns a path normalized to how we write DEPS rules and compare paths.""" | 
|  | return os.path.normcase(path).replace(os.path.sep, posixpath.sep) | 
|  |  | 
|  |  | 
|  | def _GitSourceDirectories(base_directory): | 
|  | """Returns set of normalized paths to subdirectories containing sources | 
|  | managed by git.""" | 
|  | base_dir_norm = NormalizePath(base_directory) | 
|  | git_source_directories = set([base_dir_norm]) | 
|  |  | 
|  | git_cmd = 'git.bat' if os.name == 'nt' else 'git' | 
|  | git_ls_files_cmd = [git_cmd, 'ls-files'] | 
|  | # FIXME: Use a context manager in Python 3.2+ | 
|  | popen = subprocess.Popen(git_ls_files_cmd, | 
|  | stdout=subprocess.PIPE, | 
|  | bufsize=1,  # line buffering, since read by line | 
|  | cwd=base_directory) | 
|  | try: | 
|  | try: | 
|  | for line in popen.stdout: | 
|  | dir_path = os.path.join(base_directory, os.path.dirname(line)) | 
|  | dir_path_norm = NormalizePath(dir_path) | 
|  | # Add the directory as well as all the parent directories, | 
|  | # stopping once we reach an already-listed directory. | 
|  | while dir_path_norm not in git_source_directories: | 
|  | git_source_directories.add(dir_path_norm) | 
|  | dir_path_norm = posixpath.dirname(dir_path_norm) | 
|  | finally: | 
|  | popen.stdout.close() | 
|  | finally: | 
|  | popen.wait() | 
|  |  | 
|  | return git_source_directories | 
|  |  | 
|  |  | 
|  | class DepsBuilder(object): | 
|  | """Parses include_rules from DEPS files.""" | 
|  |  | 
|  | def __init__(self, | 
|  | base_directory=None, | 
|  | extra_repos=[], | 
|  | verbose=False, | 
|  | being_tested=False, | 
|  | ignore_temp_rules=False, | 
|  | ignore_specific_rules=False): | 
|  | """Creates a new DepsBuilder. | 
|  |  | 
|  | Args: | 
|  | base_directory: local path to root of checkout, e.g. C:\chr\src. | 
|  | verbose: Set to True for debug output. | 
|  | being_tested: Set to True to ignore the DEPS file at tools/checkdeps/DEPS. | 
|  | ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!"). | 
|  | """ | 
|  | base_directory = (base_directory or | 
|  | os.path.join(os.path.dirname(__file__), | 
|  | os.path.pardir, os.path.pardir)) | 
|  | self.base_directory = os.path.abspath(base_directory)  # Local absolute path | 
|  | self.extra_repos = extra_repos | 
|  | self.verbose = verbose | 
|  | self._under_test = being_tested | 
|  | self._ignore_temp_rules = ignore_temp_rules | 
|  | self._ignore_specific_rules = ignore_specific_rules | 
|  | self._git_source_directories = None | 
|  |  | 
|  | if os.path.exists(os.path.join(base_directory, '.git')): | 
|  | self.is_git = True | 
|  | elif os.path.exists(os.path.join(base_directory, '.svn')): | 
|  | self.is_git = False | 
|  | else: | 
|  | raise DepsBuilderError("%s is not a repository root" % base_directory) | 
|  |  | 
|  | # Map of normalized directory paths to rules to use for those | 
|  | # directories, or None for directories that should be skipped. | 
|  | # Normalized is: absolute, lowercase, / for separator. | 
|  | self.directory_rules = {} | 
|  | self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) | 
|  |  | 
|  | def _ApplyRules(self, existing_rules, includes, specific_includes, | 
|  | cur_dir_norm): | 
|  | """Applies the given include rules, returning the new rules. | 
|  |  | 
|  | Args: | 
|  | existing_rules: A set of existing rules that will be combined. | 
|  | include: The list of rules from the "include_rules" section of DEPS. | 
|  | specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules | 
|  | from the "specific_include_rules" section of DEPS. | 
|  | cur_dir_norm: The current directory, normalized path. We will create an | 
|  | implicit rule that allows inclusion from this directory. | 
|  |  | 
|  | Returns: A new set of rules combining the existing_rules with the other | 
|  | arguments. | 
|  | """ | 
|  | rules = copy.deepcopy(existing_rules) | 
|  |  | 
|  | # First apply the implicit "allow" rule for the current directory. | 
|  | base_dir_norm = NormalizePath(self.base_directory) | 
|  | if not cur_dir_norm.startswith(base_dir_norm): | 
|  | raise Exception( | 
|  | 'Internal error: base directory is not at the beginning for\n' | 
|  | '  %s and base dir\n' | 
|  | '  %s' % (cur_dir_norm, base_dir_norm)) | 
|  | relative_dir = posixpath.relpath(cur_dir_norm, base_dir_norm) | 
|  |  | 
|  | # Make the help string a little more meaningful. | 
|  | source = relative_dir or 'top level' | 
|  | rules.AddRule('+' + relative_dir, | 
|  | relative_dir, | 
|  | 'Default rule for ' + source) | 
|  |  | 
|  | def ApplyOneRule(rule_str, dependee_regexp=None): | 
|  | """Deduces a sensible description for the rule being added, and | 
|  | adds the rule with its description to |rules|. | 
|  |  | 
|  | If we are ignoring temporary rules, this function does nothing | 
|  | for rules beginning with the Rule.TEMP_ALLOW character. | 
|  | """ | 
|  | if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): | 
|  | return | 
|  |  | 
|  | rule_block_name = 'include_rules' | 
|  | if dependee_regexp: | 
|  | rule_block_name = 'specific_include_rules' | 
|  | if relative_dir: | 
|  | rule_description = relative_dir + "'s %s" % rule_block_name | 
|  | else: | 
|  | rule_description = 'the top level %s' % rule_block_name | 
|  | rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp) | 
|  |  | 
|  | # Apply the additional explicit rules. | 
|  | for rule_str in includes: | 
|  | ApplyOneRule(rule_str) | 
|  |  | 
|  | # Finally, apply the specific rules. | 
|  | if self._ignore_specific_rules: | 
|  | return rules | 
|  |  | 
|  | for regexp, specific_rules in specific_includes.iteritems(): | 
|  | for rule_str in specific_rules: | 
|  | ApplyOneRule(rule_str, regexp) | 
|  |  | 
|  | return rules | 
|  |  | 
|  | def _ApplyDirectoryRules(self, existing_rules, dir_path_local_abs): | 
|  | """Combines rules from the existing rules and the new directory. | 
|  |  | 
|  | Any directory can contain a DEPS file. Top-level DEPS files can contain | 
|  | module dependencies which are used by gclient. We use these, along with | 
|  | additional include rules and implicit rules for the given directory, to | 
|  | come up with a combined set of rules to apply for the directory. | 
|  |  | 
|  | Args: | 
|  | existing_rules: The rules for the parent directory. We'll add-on to these. | 
|  | dir_path_local_abs: The directory path that the DEPS file may live in (if | 
|  | it exists). This will also be used to generate the | 
|  | implicit rules. This is a local path. | 
|  |  | 
|  | Returns: A 2-tuple of: | 
|  | (1) the combined set of rules to apply to the sub-tree, | 
|  | (2) a list of all subdirectories that should NOT be checked, as specified | 
|  | in the DEPS file (if any). | 
|  | Subdirectories are single words, hence no OS dependence. | 
|  | """ | 
|  | dir_path_norm = NormalizePath(dir_path_local_abs) | 
|  |  | 
|  | # Check the DEPS file in this directory. | 
|  | if self.verbose: | 
|  | print 'Applying rules from', dir_path_local_abs | 
|  | def FromImpl(*_): | 
|  | pass  # NOP function so "From" doesn't fail. | 
|  |  | 
|  | def FileImpl(_): | 
|  | pass  # NOP function so "File" doesn't fail. | 
|  |  | 
|  | class _VarImpl: | 
|  | def __init__(self, local_scope): | 
|  | self._local_scope = local_scope | 
|  |  | 
|  | def Lookup(self, var_name): | 
|  | """Implements the Var syntax.""" | 
|  | try: | 
|  | return self._local_scope['vars'][var_name] | 
|  | except KeyError: | 
|  | raise Exception('Var is not defined: %s' % var_name) | 
|  |  | 
|  | local_scope = {} | 
|  | global_scope = { | 
|  | 'File': FileImpl, | 
|  | 'From': FromImpl, | 
|  | 'Var': _VarImpl(local_scope).Lookup, | 
|  | } | 
|  | deps_file_path = os.path.join(dir_path_local_abs, 'DEPS') | 
|  |  | 
|  | # The second conditional here is to disregard the | 
|  | # tools/checkdeps/DEPS file while running tests.  This DEPS file | 
|  | # has a skip_child_includes for 'testdata' which is necessary for | 
|  | # running production tests, since there are intentional DEPS | 
|  | # violations under the testdata directory.  On the other hand when | 
|  | # running tests, we absolutely need to verify the contents of that | 
|  | # directory to trigger those intended violations and see that they | 
|  | # are handled correctly. | 
|  | if os.path.isfile(deps_file_path) and not ( | 
|  | self._under_test and | 
|  | os.path.basename(dir_path_local_abs) == 'checkdeps'): | 
|  | execfile(deps_file_path, global_scope, local_scope) | 
|  | elif self.verbose: | 
|  | print '  No deps file found in', dir_path_local_abs | 
|  |  | 
|  | # Even if a DEPS file does not exist we still invoke ApplyRules | 
|  | # to apply the implicit "allow" rule for the current directory | 
|  | include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) | 
|  | specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, | 
|  | {}) | 
|  | skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) | 
|  | noparent = local_scope.get(NOPARENT_VAR_NAME, False) | 
|  | if noparent: | 
|  | parent_rules = Rules() | 
|  | else: | 
|  | parent_rules = existing_rules | 
|  |  | 
|  | return (self._ApplyRules(parent_rules, include_rules, | 
|  | specific_include_rules, dir_path_norm), | 
|  | skip_subdirs) | 
|  |  | 
|  | def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, | 
|  | dir_path_local_abs): | 
|  | """Given |parent_rules| and a subdirectory |dir_path_local_abs| of the | 
|  | directory that owns the |parent_rules|, add |dir_path_local_abs|'s rules to | 
|  | |self.directory_rules|, and add None entries for any of its | 
|  | subdirectories that should be skipped. | 
|  | """ | 
|  | directory_rules, excluded_subdirs = self._ApplyDirectoryRules( | 
|  | parent_rules, dir_path_local_abs) | 
|  | dir_path_norm = NormalizePath(dir_path_local_abs) | 
|  | self.directory_rules[dir_path_norm] = directory_rules | 
|  | for subdir in excluded_subdirs: | 
|  | subdir_path_norm = posixpath.join(dir_path_norm, subdir) | 
|  | self.directory_rules[subdir_path_norm] = None | 
|  |  | 
|  | def GetAllRulesAndFiles(self, dir_name=None): | 
|  | """Yields (rules, filenames) for each repository directory with DEPS rules. | 
|  |  | 
|  | This walks the directory tree while staying in the repository. Specify | 
|  | |dir_name| to walk just one directory and its children; omit |dir_name| to | 
|  | walk the entire repository. | 
|  |  | 
|  | Yields: | 
|  | Two-element (rules, filenames) tuples. |rules| is a rules.Rules object | 
|  | for a directory, and |filenames| is a list of the absolute local paths | 
|  | of all files in that directory. | 
|  | """ | 
|  | if self.is_git and self._git_source_directories is None: | 
|  | self._git_source_directories = _GitSourceDirectories(self.base_directory) | 
|  | for repo in self.extra_repos: | 
|  | repo_path = os.path.join(self.base_directory, repo) | 
|  | self._git_source_directories.update(_GitSourceDirectories(repo_path)) | 
|  |  | 
|  | # Collect a list of all files and directories to check. | 
|  | files_to_check = [] | 
|  | if dir_name and not os.path.isabs(dir_name): | 
|  | dir_name = os.path.join(self.base_directory, dir_name) | 
|  | dirs_to_check = [dir_name or self.base_directory] | 
|  | while dirs_to_check: | 
|  | current_dir = dirs_to_check.pop() | 
|  |  | 
|  | # Check that this directory is part of the source repository. This | 
|  | # prevents us from descending into third-party code or directories | 
|  | # generated by the build system. | 
|  | if self.is_git: | 
|  | if NormalizePath(current_dir) not in self._git_source_directories: | 
|  | continue | 
|  | elif not os.path.exists(os.path.join(current_dir, '.svn')): | 
|  | continue | 
|  |  | 
|  | current_dir_rules = self.GetDirectoryRules(current_dir) | 
|  |  | 
|  | if not current_dir_rules: | 
|  | continue  # Handle the 'skip_child_includes' case. | 
|  |  | 
|  | current_dir_contents = sorted(os.listdir(current_dir)) | 
|  | file_names = [] | 
|  | sub_dirs = [] | 
|  | for file_name in current_dir_contents: | 
|  | full_name = os.path.join(current_dir, file_name) | 
|  | if os.path.isdir(full_name): | 
|  | sub_dirs.append(full_name) | 
|  | else: | 
|  | file_names.append(full_name) | 
|  | dirs_to_check.extend(reversed(sub_dirs)) | 
|  |  | 
|  | yield (current_dir_rules, file_names) | 
|  |  | 
|  | def GetDirectoryRules(self, dir_path_local): | 
|  | """Returns a Rules object to use for the given directory, or None | 
|  | if the given directory should be skipped. | 
|  |  | 
|  | Also modifies |self.directory_rules| to store the Rules. | 
|  | This takes care of first building rules for parent directories (up to | 
|  | |self.base_directory|) if needed, which may add rules for skipped | 
|  | subdirectories. | 
|  |  | 
|  | Args: | 
|  | dir_path_local: A local path to the directory you want rules for. | 
|  | Can be relative and unnormalized. It is the caller's responsibility | 
|  | to ensure that this is part of the repository rooted at | 
|  | |self.base_directory|. | 
|  | """ | 
|  | if os.path.isabs(dir_path_local): | 
|  | dir_path_local_abs = dir_path_local | 
|  | else: | 
|  | dir_path_local_abs = os.path.join(self.base_directory, dir_path_local) | 
|  | dir_path_norm = NormalizePath(dir_path_local_abs) | 
|  |  | 
|  | if dir_path_norm in self.directory_rules: | 
|  | return self.directory_rules[dir_path_norm] | 
|  |  | 
|  | parent_dir_local_abs = os.path.dirname(dir_path_local_abs) | 
|  | parent_rules = self.GetDirectoryRules(parent_dir_local_abs) | 
|  | # We need to check for an entry for our dir_path again, since | 
|  | # GetDirectoryRules can modify entries for subdirectories, namely setting | 
|  | # to None if they should be skipped, via _ApplyDirectoryRulesAndSkipSubdirs. | 
|  | # For example, if dir_path == 'A/B/C' and A/B/DEPS specifies that the C | 
|  | # subdirectory be skipped, GetDirectoryRules('A/B') will fill in the entry | 
|  | # for 'A/B/C' as None. | 
|  | if dir_path_norm in self.directory_rules: | 
|  | return self.directory_rules[dir_path_norm] | 
|  |  | 
|  | if parent_rules: | 
|  | self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path_local_abs) | 
|  | else: | 
|  | # If the parent directory should be skipped, then the current | 
|  | # directory should also be skipped. | 
|  | self.directory_rules[dir_path_norm] = None | 
|  | return self.directory_rules[dir_path_norm] |