• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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