Add an extension override bubble and warning box for proxy extensions.
[chromium-blink-merge.git] / tools / checkdeps / builddeps.py
blob0057f635c2df464b6298b0355b8ff4b50bd4a9aa
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.
6 """Traverses the source tree, parses all found DEPS files, and constructs
7 a dependency rule table to be used by subclasses.
9 The format of the deps file:
11 First you have the normal module-level deps. These are the ones used by
12 gclient. An example would be:
14 deps = {
15 "base":"http://foo.bar/trunk/base"
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 "!".
27 Note that for .java files, there is currently no difference between
28 "+" and "!", even in the presubmit step.
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",
35 # But this one subdirectory of evil is OK.
36 "+base/evil/not",
38 # And it can include files from this other directory even though there is
39 # no deps rule for it.
40 "+tools/crime_fighter",
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",
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.
55 specific_include_rules = {
56 ".*_(unit|browser|api)test\.cc": [
57 "+libraries/testsupport",
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.
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.
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.
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 """
78 import copy
79 import os.path
80 import posixpath
81 import subprocess
83 from rules import Rule, Rules
86 # Variable name used in the DEPS file to add or subtract include files from
87 # the module-level deps.
88 INCLUDE_RULES_VAR_NAME = 'include_rules'
90 # Variable name used in the DEPS file to add or subtract include files
91 # from module-level deps specific to files whose basename (last
92 # component of path) matches a given regular expression.
93 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules'
95 # Optionally present in the DEPS file to list subdirectories which should not
96 # be checked. This allows us to skip third party code, for example.
97 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes'
100 def NormalizePath(path):
101 """Returns a path normalized to how we write DEPS rules and compare paths."""
102 return os.path.normcase(path).replace(os.path.sep, posixpath.sep)
105 def _GitSourceDirectories(base_directory):
106 """Returns set of normalized paths to subdirectories containing sources
107 managed by git."""
108 if not os.path.exists(os.path.join(base_directory, '.git')):
109 return set()
111 base_dir_norm = NormalizePath(base_directory)
112 git_source_directories = set([base_dir_norm])
114 git_ls_files_cmd = ['git', 'ls-files']
115 # FIXME: Use a context manager in Python 3.2+
116 popen = subprocess.Popen(git_ls_files_cmd,
117 stdout=subprocess.PIPE,
118 bufsize=1, # line buffering, since read by line
119 cwd=base_directory)
120 try:
121 try:
122 for line in popen.stdout:
123 dir_path = os.path.join(base_directory, os.path.dirname(line))
124 dir_path_norm = NormalizePath(dir_path)
125 # Add the directory as well as all the parent directories,
126 # stopping once we reach an already-listed directory.
127 while dir_path_norm not in git_source_directories:
128 git_source_directories.add(dir_path_norm)
129 dir_path_norm = posixpath.dirname(dir_path_norm)
130 finally:
131 popen.stdout.close()
132 finally:
133 popen.wait()
135 return git_source_directories
138 class DepsBuilder(object):
139 """Parses include_rules from DEPS files."""
141 def __init__(self,
142 base_directory=None,
143 verbose=False,
144 being_tested=False,
145 ignore_temp_rules=False,
146 ignore_specific_rules=False):
147 """Creates a new DepsBuilder.
149 Args:
150 base_directory: local path to root of checkout, e.g. C:\chr\src.
151 verbose: Set to True for debug output.
152 being_tested: Set to True to ignore the DEPS file at tools/checkdeps/DEPS.
153 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
155 base_directory = (base_directory or
156 os.path.join(os.path.dirname(__file__),
157 os.path.pardir, os.path.pardir))
158 self.base_directory = os.path.abspath(base_directory) # Local absolute path
159 self.verbose = verbose
160 self._under_test = being_tested
161 self._ignore_temp_rules = ignore_temp_rules
162 self._ignore_specific_rules = ignore_specific_rules
164 # Set of normalized paths
165 self.git_source_directories = _GitSourceDirectories(self.base_directory)
167 # Map of normalized directory paths to rules to use for those
168 # directories, or None for directories that should be skipped.
169 # Normalized is: absolute, lowercase, / for separator.
170 self.directory_rules = {}
171 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory)
173 def _ApplyRules(self, existing_rules, includes, specific_includes,
174 cur_dir_norm):
175 """Applies the given include rules, returning the new rules.
177 Args:
178 existing_rules: A set of existing rules that will be combined.
179 include: The list of rules from the "include_rules" section of DEPS.
180 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules
181 from the "specific_include_rules" section of DEPS.
182 cur_dir_norm: The current directory, normalized path. We will create an
183 implicit rule that allows inclusion from this directory.
185 Returns: A new set of rules combining the existing_rules with the other
186 arguments.
188 rules = copy.deepcopy(existing_rules)
190 # First apply the implicit "allow" rule for the current directory.
191 base_dir_norm = NormalizePath(self.base_directory)
192 if not cur_dir_norm.startswith(base_dir_norm):
193 raise Exception(
194 'Internal error: base directory is not at the beginning for\n'
195 ' %s and base dir\n'
196 ' %s' % (cur_dir_norm, base_dir_norm))
197 relative_dir = posixpath.relpath(cur_dir_norm, base_dir_norm)
199 # Make the help string a little more meaningful.
200 source = relative_dir or 'top level'
201 rules.AddRule('+' + relative_dir,
202 relative_dir,
203 'Default rule for ' + source)
205 def ApplyOneRule(rule_str, dependee_regexp=None):
206 """Deduces a sensible description for the rule being added, and
207 adds the rule with its description to |rules|.
209 If we are ignoring temporary rules, this function does nothing
210 for rules beginning with the Rule.TEMP_ALLOW character.
212 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW):
213 return
215 rule_block_name = 'include_rules'
216 if dependee_regexp:
217 rule_block_name = 'specific_include_rules'
218 if relative_dir:
219 rule_description = relative_dir + "'s %s" % rule_block_name
220 else:
221 rule_description = 'the top level %s' % rule_block_name
222 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp)
224 # Apply the additional explicit rules.
225 for rule_str in includes:
226 ApplyOneRule(rule_str)
228 # Finally, apply the specific rules.
229 if self._ignore_specific_rules:
230 return rules
232 for regexp, specific_rules in specific_includes.iteritems():
233 for rule_str in specific_rules:
234 ApplyOneRule(rule_str, regexp)
236 return rules
238 def _ApplyDirectoryRules(self, existing_rules, dir_path_local_abs):
239 """Combines rules from the existing rules and the new directory.
241 Any directory can contain a DEPS file. Top-level DEPS files can contain
242 module dependencies which are used by gclient. We use these, along with
243 additional include rules and implicit rules for the given directory, to
244 come up with a combined set of rules to apply for the directory.
246 Args:
247 existing_rules: The rules for the parent directory. We'll add-on to these.
248 dir_path_local_abs: The directory path that the DEPS file may live in (if
249 it exists). This will also be used to generate the
250 implicit rules. This is a local path.
252 Returns: A 2-tuple of:
253 (1) the combined set of rules to apply to the sub-tree,
254 (2) a list of all subdirectories that should NOT be checked, as specified
255 in the DEPS file (if any).
256 Subdirectories are single words, hence no OS dependence.
258 dir_path_norm = NormalizePath(dir_path_local_abs)
260 # Check for a .svn directory in this directory or that this directory is
261 # contained in git source directories. This will tell us if it's a source
262 # directory and should be checked.
263 if not (os.path.exists(os.path.join(dir_path_local_abs, '.svn')) or
264 dir_path_norm in self.git_source_directories):
265 return None, []
267 # Check the DEPS file in this directory.
268 if self.verbose:
269 print 'Applying rules from', dir_path_local_abs
270 def FromImpl(*_):
271 pass # NOP function so "From" doesn't fail.
273 def FileImpl(_):
274 pass # NOP function so "File" doesn't fail.
276 class _VarImpl:
277 def __init__(self, local_scope):
278 self._local_scope = local_scope
280 def Lookup(self, var_name):
281 """Implements the Var syntax."""
282 try:
283 return self._local_scope['vars'][var_name]
284 except KeyError:
285 raise Exception('Var is not defined: %s' % var_name)
287 local_scope = {}
288 global_scope = {
289 'File': FileImpl,
290 'From': FromImpl,
291 'Var': _VarImpl(local_scope).Lookup,
293 deps_file_path = os.path.join(dir_path_local_abs, 'DEPS')
295 # The second conditional here is to disregard the
296 # tools/checkdeps/DEPS file while running tests. This DEPS file
297 # has a skip_child_includes for 'testdata' which is necessary for
298 # running production tests, since there are intentional DEPS
299 # violations under the testdata directory. On the other hand when
300 # running tests, we absolutely need to verify the contents of that
301 # directory to trigger those intended violations and see that they
302 # are handled correctly.
303 if os.path.isfile(deps_file_path) and not (
304 self._under_test and
305 os.path.basename(dir_path_local_abs) == 'checkdeps'):
306 execfile(deps_file_path, global_scope, local_scope)
307 elif self.verbose:
308 print ' No deps file found in', dir_path_local_abs
310 # Even if a DEPS file does not exist we still invoke ApplyRules
311 # to apply the implicit "allow" rule for the current directory
312 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, [])
313 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME,
315 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, [])
317 return (self._ApplyRules(existing_rules, include_rules,
318 specific_include_rules, dir_path_norm),
319 skip_subdirs)
321 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules,
322 dir_path_local_abs):
323 """Given |parent_rules| and a subdirectory |dir_path_local_abs| of the
324 directory that owns the |parent_rules|, add |dir_path_local_abs|'s rules to
325 |self.directory_rules|, and add None entries for any of its
326 subdirectories that should be skipped.
328 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(
329 parent_rules, dir_path_local_abs)
330 dir_path_norm = NormalizePath(dir_path_local_abs)
331 self.directory_rules[dir_path_norm] = directory_rules
332 for subdir in excluded_subdirs:
333 subdir_path_norm = posixpath.join(dir_path_norm, subdir)
334 self.directory_rules[subdir_path_norm] = None
336 def GetDirectoryRules(self, dir_path_local):
337 """Returns a Rules object to use for the given directory, or None
338 if the given directory should be skipped.
340 Also modifies |self.directory_rules| to store the Rules.
341 This takes care of first building rules for parent directories (up to
342 |self.base_directory|) if needed, which may add rules for skipped
343 subdirectories.
345 Args:
346 dir_path_local: A local path to the directory you want rules for.
347 Can be relative and unnormalized.
349 if os.path.isabs(dir_path_local):
350 dir_path_local_abs = dir_path_local
351 else:
352 dir_path_local_abs = os.path.join(self.base_directory, dir_path_local)
353 dir_path_norm = NormalizePath(dir_path_local_abs)
355 if dir_path_norm in self.directory_rules:
356 return self.directory_rules[dir_path_norm]
358 parent_dir_local_abs = os.path.dirname(dir_path_local_abs)
359 parent_rules = self.GetDirectoryRules(parent_dir_local_abs)
360 # We need to check for an entry for our dir_path again, since
361 # GetDirectoryRules can modify entries for subdirectories, namely setting
362 # to None if they should be skipped, via _ApplyDirectoryRulesAndSkipSubdirs.
363 # For example, if dir_path == 'A/B/C' and A/B/DEPS specifies that the C
364 # subdirectory be skipped, GetDirectoryRules('A/B') will fill in the entry
365 # for 'A/B/C' as None.
366 if dir_path_norm in self.directory_rules:
367 return self.directory_rules[dir_path_norm]
369 if parent_rules:
370 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path_local_abs)
371 else:
372 # If the parent directory should be skipped, then the current
373 # directory should also be skipped.
374 self.directory_rules[dir_path_norm] = None
375 return self.directory_rules[dir_path_norm]