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