"""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 functools
import os.path
import posixpath
import subprocess
import rules
INCLUDE_RULES_VAR_NAME = 'include_rules'
SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules'
SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes'
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']
popen = subprocess.Popen(git_ls_files_cmd,
stdout=subprocess.PIPE,
cwd=base_directory)
try:
try:
for line in popen.stdout.read().decode('utf-8').splitlines():
dir_path = os.path.join(base_directory, os.path.dirname(line))
dir_path_norm = NormalizePath(dir_path)
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
@functools.lru_cache(maxsize=None)
def _ParseDepsMemoize(dir_path, under_test, verbose):
def FromImpl(*_):
pass
def FileImpl(_):
pass
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,
'Str': str,
}
deps_file_path = os.path.join(dir_path, 'DEPS')
if os.path.isfile(deps_file_path) and not (
under_test and
os.path.basename(dir_path) == 'checkdeps'):
try:
with open(deps_file_path) as file:
exec(file.read(), global_scope, local_scope)
except Exception as e:
print(' Error reading %s: %s' % (deps_file_path, str(e)))
raise
elif verbose:
print(' No deps file found in', dir_path)
return local_scope
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):
r"""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
buildtools/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.normcase(os.path.abspath(base_directory))
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
self._instance_index = len(rules.deps_builders)
rules.deps_builders.append(self)
if os.getcwd().startswith('/google/cog/cloud'):
self.is_git = False
elif 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)
self.directory_rules = {}
self._ApplyDirectoryRulesAndSkipSubdirs(
rules.Rules(self._instance_index), self.base_directory)
def _ApplyRules(self, existing_rules, includes, specific_includes,
cur_dir_norm):
r"""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.
"""
new_rules = copy.deepcopy(existing_rules)
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)
source = relative_dir or 'top level'
new_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 |new_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(rules.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
new_rules.AddRule(rule_str, relative_dir, rule_description,
dependee_regexp)
for rule_str in includes:
ApplyOneRule(rule_str)
if self._ignore_specific_rules:
return new_rules
for regexp, specific_rules in specific_includes.items():
for rule_str in specific_rules:
ApplyOneRule(rule_str, regexp)
return new_rules
def _ParseDeps(self, dir_path):
return _ParseDepsMemoize(dir_path, self._under_test, self.verbose)
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)
if self.verbose:
print('Applying rules from', dir_path_local_abs)
deps_dict = self._ParseDeps(dir_path_local_abs)
include_rules = deps_dict.get(INCLUDE_RULES_VAR_NAME, [])
specific_include_rules = deps_dict.get(SPECIFIC_INCLUDE_RULES_VAR_NAME,
{})
skip_subdirs = deps_dict.get(SKIP_SUBDIRS_VAR_NAME, [])
noparent = deps_dict.get(NOPARENT_VAR_NAME, False)
if noparent:
parent_rules = rules.Rules(self._instance_index)
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))
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()
if self.is_git:
if NormalizePath(current_dir) not in self._git_source_directories:
continue
elif os.getcwd().startswith('/google/cog/cloud'):
pass
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
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 _IterSelfAndParentDirectories(self, dir_name):
dir_name = os.path.normcase(os.path.abspath(dir_name))
current_dir_name = dir_name
yield current_dir_name
while current_dir_name != self.base_directory:
parent = os.path.dirname(current_dir_name)
assert parent != current_dir_name, (
f'{dir_name}, {self.base_directory}, {current_dir_name}')
current_dir_name = parent
yield current_dir_name
def _DirectoryRequiresReview(self, dir_name):
deps_dict = self._ParseDeps(dir_name)
return deps_dict.get('new_usages_require_review')
def FindFirstAncestorThatRequiresReview(self, include_path):
if not os.path.isabs(include_path):
include_path = os.path.join(self.base_directory, include_path)
include_path = os.path.normpath(include_path)
parent_dir = os.path.dirname(include_path)
if not include_path.startswith(self.base_directory):
return None
for current_dir in self._IterSelfAndParentDirectories(parent_dir):
requires_review = self._DirectoryRequiresReview(current_dir)
if requires_review is not None:
if requires_review:
return NormalizePath(
os.path.relpath(current_dir, self.base_directory))
return None
return None
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)
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:
self.directory_rules[dir_path_norm] = None
return self.directory_rules[dir_path_norm]