Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(10)

Side by Side Diff: tools/checkdeps/builddeps.py

Issue 244313005: Fix a few typos and style in checkdeps (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Consistent local/norm directories Created 6 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tools/checkdeps/checkdeps.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright 2013 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Traverses the source tree, parses all found DEPS files, and constructs 6 """Traverses the source tree, parses all found DEPS files, and constructs
7 a dependency rule table to be used by subclasses. 7 a dependency rule table to be used by subclasses.
8 8
9 The format of the deps file: 9 The format of the deps file:
10 10
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after
68 so you can modify or remove it using the normal include rules. 68 so you can modify or remove it using the normal include rules.
69 69
70 The rules are processed in order. This means you can explicitly allow a higher 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. 71 directory and then take away permissions from sub-parts, or the reverse.
72 72
73 Note that all directory separators must be slashes (Unix-style) and not 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 74 backslashes. All directories should be relative to the source root and use
75 only lowercase. 75 only lowercase.
76 """ 76 """
77 77
78 import copy
78 import os 79 import os
80 import posixpath
79 import subprocess 81 import subprocess
80 import copy
81 82
82 from rules import Rule, Rules 83 from rules import Rule, Rules
83 84
84 85
85 # Variable name used in the DEPS file to add or subtract include files from 86 # Variable name used in the DEPS file to add or subtract include files from
86 # the module-level deps. 87 # the module-level deps.
87 INCLUDE_RULES_VAR_NAME = 'include_rules' 88 INCLUDE_RULES_VAR_NAME = 'include_rules'
88 89
89 # Variable name used in the DEPS file to add or subtract include files 90 # 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 # from module-level deps specific to files whose basename (last
91 # component of path) matches a given regular expression. 92 # component of path) matches a given regular expression.
92 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules' 93 SPECIFIC_INCLUDE_RULES_VAR_NAME = 'specific_include_rules'
93 94
94 # Optionally present in the DEPS file to list subdirectories which should not 95 # 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 # be checked. This allows us to skip third party code, for example.
96 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes' 97 SKIP_SUBDIRS_VAR_NAME = 'skip_child_includes'
97 98
98 99
99 def NormalizePath(path): 100 def NormalizePath(path):
100 """Returns a path normalized to how we write DEPS rules and compare paths. 101 """Returns a path normalized to how we write DEPS rules and compare paths."""
101 """ 102 return os.path.normcase(path).replace(os.path.sep, posixpath.sep)
102 return path.lower().replace('\\', '/')
103 103
104 104
105 class DepsBuilder(object): 105 class DepsBuilder(object):
106 """Parses include_rules from DEPS files. 106 """Parses include_rules from DEPS files."""
107 """
108 107
109 def __init__(self, 108 def __init__(self,
110 base_directory=None, 109 base_directory=None,
111 verbose=False, 110 verbose=False,
112 being_tested=False, 111 being_tested=False,
113 ignore_temp_rules=False, 112 ignore_temp_rules=False,
114 ignore_specific_rules=False): 113 ignore_specific_rules=False):
115 """Creates a new DepsBuilder. 114 """Creates a new DepsBuilder.
116 115
117 Args: 116 Args:
118 base_directory: OS-compatible path to root of checkout, e.g. C:\chr\src. 117 base_directory: local path to root of checkout, e.g. C:\chr\src.
119 verbose: Set to true for debug output. 118 verbose: Set to True for debug output.
120 being_tested: Set to true to ignore the DEPS file at tools/checkdeps/DEPS. 119 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 ("!"). 120 ignore_temp_rules: Ignore rules that start with Rule.TEMP_ALLOW ("!").
122 """ 121 """
123 base_directory = (base_directory or 122 base_directory = (base_directory or
124 os.path.join(os.path.dirname(__file__), '..', '..')) 123 os.path.join(os.path.dirname(__file__),
125 self.base_directory = os.path.abspath(base_directory) 124 os.pardir, os.pardir))
125 self.base_directory = os.path.abspath(base_directory) # Local absolute path
126 self.verbose = verbose 126 self.verbose = verbose
127 self._under_test = being_tested 127 self._under_test = being_tested
128 self._ignore_temp_rules = ignore_temp_rules 128 self._ignore_temp_rules = ignore_temp_rules
129 self._ignore_specific_rules = ignore_specific_rules 129 self._ignore_specific_rules = ignore_specific_rules
130 130
131 self.git_source_directories = set() 131 self.git_source_directories = set() # Normalized paths
132 self._AddGitSourceDirectories() 132 self._AddGitSourceDirectories()
133 133
134 # Map of normalized directory paths to rules to use for those 134 # Map of normalized directory paths to rules to use for those
135 # directories, or None for directories that should be skipped. 135 # directories, or None for directories that should be skipped.
136 # Normalized is: absolute, lowercase, / for separator.
136 self.directory_rules = {} 137 self.directory_rules = {}
137 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory) 138 self._ApplyDirectoryRulesAndSkipSubdirs(Rules(), self.base_directory)
138 139
139 def _ApplyRules(self, existing_rules, includes, specific_includes, cur_dir): 140 def _ApplyRules(self, existing_rules, includes, specific_includes,
141 cur_dir_norm):
140 """Applies the given include rules, returning the new rules. 142 """Applies the given include rules, returning the new rules.
141 143
142 Args: 144 Args:
143 existing_rules: A set of existing rules that will be combined. 145 existing_rules: A set of existing rules that will be combined.
144 include: The list of rules from the "include_rules" section of DEPS. 146 include: The list of rules from the "include_rules" section of DEPS.
145 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules 147 specific_includes: E.g. {'.*_unittest\.cc': ['+foo', '-blat']} rules
146 from the "specific_include_rules" section of DEPS. 148 from the "specific_include_rules" section of DEPS.
147 cur_dir: The current directory, normalized path. We will create an 149 cur_dir_norm: The current directory, normalized path. We will create an
148 implicit rule that allows inclusion from this directory. 150 implicit rule that allows inclusion from this directory.
149 151
150 Returns: A new set of rules combining the existing_rules with the other 152 Returns: A new set of rules combining the existing_rules with the other
151 arguments. 153 arguments.
152 """ 154 """
153 rules = copy.deepcopy(existing_rules) 155 rules = copy.deepcopy(existing_rules)
154 156
155 # First apply the implicit "allow" rule for the current directory. 157 # First apply the implicit "allow" rule for the current directory.
156 if cur_dir.startswith( 158 base_dir_norm = NormalizePath(self.base_directory)
157 NormalizePath(os.path.normpath(self.base_directory))): 159 if not cur_dir_norm.startswith(base_dir_norm):
158 relative_dir = cur_dir[len(self.base_directory) + 1:] 160 raise Exception(
161 'Internal error: base directory is not at the beginning for\n'
162 ' %s and base dir\n'
163 ' %s' % (cur_dir_norm, base_dir_norm))
164 relative_dir = posixpath.relpath(cur_dir_norm, base_dir_norm)
159 165
160 source = relative_dir 166 # Make the help string a little more meaningful.
161 if len(source) == 0: 167 source = relative_dir or 'top level'
162 source = 'top level' # Make the help string a little more meaningful. 168 rules.AddRule('+' + relative_dir,
163 rules.AddRule('+' + relative_dir, 169 relative_dir,
164 relative_dir, 170 'Default rule for ' + source)
165 'Default rule for ' + source)
166 else:
167 raise Exception('Internal error: base directory is not at the beginning' +
168 ' for\n %s and base dir\n %s' %
169 (cur_dir, self.base_directory))
170 171
171 def ApplyOneRule(rule_str, cur_dir, dependee_regexp=None): 172 def ApplyOneRule(rule_str, dependee_regexp=None):
172 """Deduces a sensible description for the rule being added, and 173 """Deduces a sensible description for the rule being added, and
173 adds the rule with its description to |rules|. 174 adds the rule with its description to |rules|.
174 175
175 If we are ignoring temporary rules, this function does nothing 176 If we are ignoring temporary rules, this function does nothing
176 for rules beginning with the Rule.TEMP_ALLOW character. 177 for rules beginning with the Rule.TEMP_ALLOW character.
177 """ 178 """
178 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW): 179 if self._ignore_temp_rules and rule_str.startswith(Rule.TEMP_ALLOW):
179 return 180 return
180 181
181 rule_block_name = 'include_rules' 182 rule_block_name = 'include_rules'
182 if dependee_regexp: 183 if dependee_regexp:
183 rule_block_name = 'specific_include_rules' 184 rule_block_name = 'specific_include_rules'
184 if not relative_dir: 185 if relative_dir:
186 rule_description = relative_dir + "'s %s" % rule_block_name
187 else:
185 rule_description = 'the top level %s' % rule_block_name 188 rule_description = 'the top level %s' % rule_block_name
186 else:
187 rule_description = relative_dir + "'s %s" % rule_block_name
188 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp) 189 rules.AddRule(rule_str, relative_dir, rule_description, dependee_regexp)
189 190
190 # Apply the additional explicit rules. 191 # Apply the additional explicit rules.
191 for (_, rule_str) in enumerate(includes): 192 for rule_str in includes:
192 ApplyOneRule(rule_str, cur_dir) 193 ApplyOneRule(rule_str)
193 194
194 # Finally, apply the specific rules. 195 # Finally, apply the specific rules.
195 if not self._ignore_specific_rules: 196 if self._ignore_specific_rules:
196 for regexp, specific_rules in specific_includes.iteritems(): 197 return rules
197 for rule_str in specific_rules: 198
198 ApplyOneRule(rule_str, cur_dir, regexp) 199 for regexp, specific_rules in specific_includes.iteritems():
200 for rule_str in specific_rules:
201 ApplyOneRule(rule_str, regexp)
199 202
200 return rules 203 return rules
201 204
202 def _ApplyDirectoryRules(self, existing_rules, dir_name): 205 def _ApplyDirectoryRules(self, existing_rules, dir_path_local_abs):
203 """Combines rules from the existing rules and the new directory. 206 """Combines rules from the existing rules and the new directory.
204 207
205 Any directory can contain a DEPS file. Toplevel DEPS files can contain 208 Any directory can contain a DEPS file. Toplevel DEPS files can contain
206 module dependencies which are used by gclient. We use these, along with 209 module dependencies which are used by gclient. We use these, along with
207 additional include rules and implicit rules for the given directory, to 210 additional include rules and implicit rules for the given directory, to
208 come up with a combined set of rules to apply for the directory. 211 come up with a combined set of rules to apply for the directory.
209 212
210 Args: 213 Args:
211 existing_rules: The rules for the parent directory. We'll add-on to these. 214 existing_rules: The rules for the parent directory. We'll add-on to these.
212 dir_name: The directory name that the deps file may live in (if 215 dir_path_local_abs: The directory path that the DEPS file may live in (if
213 it exists). This will also be used to generate the 216 it exists). This will also be used to generate the
214 implicit rules. This is a non-normalized path. 217 implicit rules. This is a local, non-normalized path.
215 218
216 Returns: A tuple containing: (1) the combined set of rules to apply to the 219 Returns: A tuple containing: (1) the combined set of rules to apply to the
217 sub-tree, and (2) a list of all subdirectories that should NOT be 220 sub-tree, and (2) a list of all subdirectories that should NOT be
218 checked, as specified in the DEPS file (if any). 221 checked, as specified in the DEPS file (if any). Subdirectories
222 are single words, hence no OS-dependence.
219 """ 223 """
220 norm_dir_name = NormalizePath(dir_name) 224 dir_path_norm = NormalizePath(dir_path_local_abs)
221 225
222 # Check for a .svn directory in this directory or check this directory is 226 # Check for a .svn directory in this directory or check this directory is
223 # contained in git source direcotries. This will tell us if it's a source 227 # contained in git source direcotries. This will tell us if it's a source
224 # directory and should be checked. 228 # directory and should be checked.
225 if not (os.path.exists(os.path.join(dir_name, ".svn")) or 229 if not (os.path.exists(os.path.join(dir_path_local_abs, '.svn')) or
226 (norm_dir_name in self.git_source_directories)): 230 dir_path_norm in self.git_source_directories):
227 return (None, []) 231 return None, []
228 232
229 # Check the DEPS file in this directory. 233 # Check the DEPS file in this directory.
230 if self.verbose: 234 if self.verbose:
231 print 'Applying rules from', dir_name 235 print 'Applying rules from', dir_path_local_abs
232 def FromImpl(_unused, _unused2): 236 def FromImpl(*_):
233 pass # NOP function so "From" doesn't fail. 237 pass # NOP function so "From" doesn't fail.
234 238
235 def FileImpl(_unused): 239 def FileImpl(_):
236 pass # NOP function so "File" doesn't fail. 240 pass # NOP function so "File" doesn't fail.
237 241
238 class _VarImpl: 242 class _VarImpl:
239 def __init__(self, local_scope): 243 def __init__(self, local_scope):
240 self._local_scope = local_scope 244 self._local_scope = local_scope
241 245
242 def Lookup(self, var_name): 246 def Lookup(self, var_name):
243 """Implements the Var syntax.""" 247 """Implements the Var syntax."""
244 if var_name in self._local_scope.get('vars', {}): 248 try:
245 return self._local_scope['vars'][var_name] 249 return self._local_scope['vars'][var_name]
246 raise Exception('Var is not defined: %s' % var_name) 250 except KeyError:
251 raise Exception('Var is not defined: %s' % var_name)
247 252
248 local_scope = {} 253 local_scope = {}
249 global_scope = { 254 global_scope = {
250 'File': FileImpl, 255 'File': FileImpl,
251 'From': FromImpl, 256 'From': FromImpl,
252 'Var': _VarImpl(local_scope).Lookup, 257 'Var': _VarImpl(local_scope).Lookup,
253 } 258 }
254 deps_file = os.path.join(dir_name, 'DEPS') 259 deps_file_path = os.path.join(dir_path_local_abs, 'DEPS')
255 260
256 # The second conditional here is to disregard the 261 # The second conditional here is to disregard the
257 # tools/checkdeps/DEPS file while running tests. This DEPS file 262 # tools/checkdeps/DEPS file while running tests. This DEPS file
258 # has a skip_child_includes for 'testdata' which is necessary for 263 # has a skip_child_includes for 'testdata' which is necessary for
259 # running production tests, since there are intentional DEPS 264 # running production tests, since there are intentional DEPS
260 # violations under the testdata directory. On the other hand when 265 # violations under the testdata directory. On the other hand when
261 # running tests, we absolutely need to verify the contents of that 266 # running tests, we absolutely need to verify the contents of that
262 # directory to trigger those intended violations and see that they 267 # directory to trigger those intended violations and see that they
263 # are handled correctly. 268 # are handled correctly.
264 if os.path.isfile(deps_file) and ( 269 if os.path.isfile(deps_file_path) and not (
265 not self._under_test or not os.path.split(dir_name)[1] == 'checkdeps'): 270 self._under_test and
266 execfile(deps_file, global_scope, local_scope) 271 os.path.basename(dir_path_local_abs) == 'checkdeps'):
272 execfile(deps_file_path, global_scope, local_scope)
267 elif self.verbose: 273 elif self.verbose:
268 print ' No deps file found in', dir_name 274 print ' No deps file found in', dir_path_local_abs
269 275
270 # Even if a DEPS file does not exist we still invoke ApplyRules 276 # Even if a DEPS file does not exist we still invoke ApplyRules
271 # to apply the implicit "allow" rule for the current directory 277 # to apply the implicit "allow" rule for the current directory
272 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, []) 278 include_rules = local_scope.get(INCLUDE_RULES_VAR_NAME, [])
273 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME, 279 specific_include_rules = local_scope.get(SPECIFIC_INCLUDE_RULES_VAR_NAME,
274 {}) 280 {})
275 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, []) 281 skip_subdirs = local_scope.get(SKIP_SUBDIRS_VAR_NAME, [])
276 282
277 return (self._ApplyRules(existing_rules, include_rules, 283 return (self._ApplyRules(existing_rules, include_rules,
278 specific_include_rules, norm_dir_name), 284 specific_include_rules, dir_path_norm),
279 skip_subdirs) 285 skip_subdirs)
280 286
281 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules, dir_path): 287 def _ApplyDirectoryRulesAndSkipSubdirs(self, parent_rules,
282 """Given |parent_rules| and a subdirectory |dir_path| from the 288 dir_path_local_abs):
283 directory that owns the |parent_rules|, add |dir_path|'s rules to 289 """Given |parent_rules| and a subdirectory |dir_path_local_abs| of the
290 directory that owns the |parent_rules|, add |dir_path_local_abs|'s rules to
284 |self.directory_rules|, and add None entries for any of its 291 |self.directory_rules|, and add None entries for any of its
285 subdirectories that should be skipped. 292 subdirectories that should be skipped.
286 """ 293 """
287 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(parent_rules, 294 directory_rules, excluded_subdirs = self._ApplyDirectoryRules(
288 dir_path) 295 parent_rules, dir_path_local_abs)
289 self.directory_rules[NormalizePath(dir_path)] = directory_rules 296 dir_path_norm = NormalizePath(dir_path_local_abs)
297 self.directory_rules[dir_path_norm] = directory_rules
290 for subdir in excluded_subdirs: 298 for subdir in excluded_subdirs:
291 self.directory_rules[NormalizePath( 299 subdir_path_norm = posixpath.join(dir_path_norm, subdir)
292 os.path.normpath(os.path.join(dir_path, subdir)))] = None 300 self.directory_rules[subdir_path_norm] = None
293 301
294 def GetDirectoryRules(self, dir_path): 302 def GetDirectoryRules(self, dir_path_local):
295 """Returns a Rules object to use for the given directory, or None 303 """Returns a Rules object to use for the given directory, or None
296 if the given directory should be skipped. This takes care of 304 if the given directory should be skipped.
297 first building rules for parent directories (up to 305
298 self.base_directory) if needed. 306 Also modifies |self.directory_rules| to store the Rules.
307 This takes care of first building rules for parent directories (up to
308 |self.base_directory|) if needed, which may add rules for skipped
309 subdirectories.
299 310
300 Args: 311 Args:
301 dir_path: A real (non-normalized) path to the directory you want 312 dir_path_local: A local path to the directory you want rules for.
302 rules for. 313 Can be relative and unnormalized.
303 """ 314 """
304 norm_dir_path = NormalizePath(dir_path) 315 if os.path.isabs(dir_path_local):
316 dir_path_local_abs = dir_path_local
317 else:
318 dir_path_local_abs = os.path.join(self.base_directory, dir_path_local)
319 dir_path_norm = NormalizePath(dir_path_local_abs)
305 320
306 if not norm_dir_path.startswith( 321 if dir_path_norm in self.directory_rules:
307 NormalizePath(os.path.normpath(self.base_directory))): 322 return self.directory_rules[dir_path_norm]
308 dir_path = os.path.join(self.base_directory, dir_path)
309 norm_dir_path = NormalizePath(dir_path)
310 323
311 parent_dir = os.path.dirname(dir_path) 324 parent_dir_local_abs = os.path.dirname(dir_path_local_abs)
312 parent_rules = None 325 parent_rules = self.GetDirectoryRules(parent_dir_local_abs)
313 if not norm_dir_path in self.directory_rules: 326 # We need to check for an entry for our dir_path again, since
314 parent_rules = self.GetDirectoryRules(parent_dir) 327 # GetDirectoryRules can modify entries for subdirectories, namely setting
328 # to None if they should be skipped, via _ApplyDirectoryRulesAndSkipSubdirs.
329 # For example, if dir_path == 'A/B/C' and A/B/DEPS specifies that the C
330 # subdirectory be skipped, GetDirectoryRules('A/B') will fill in the entry
331 # for 'A/B/C' as None.
332 if dir_path_norm in self.directory_rules:
333 return self.directory_rules[dir_path_norm]
315 334
316 # We need to check for an entry for our dir_path again, in case we 335 if parent_rules:
317 # are at a path e.g. A/B/C where A/B/DEPS specifies the C 336 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path_local_abs)
318 # subdirectory to be skipped; in this case, the invocation to 337 else:
319 # GetDirectoryRules(parent_dir) has already filled in an entry for 338 # If the parent directory should be skipped, then the current
320 # A/B/C. 339 # directory should also be skipped.
321 if not norm_dir_path in self.directory_rules: 340 self.directory_rules[dir_path_norm] = None
322 if not parent_rules: 341 return self.directory_rules[dir_path_norm]
323 # If the parent directory should be skipped, then the current
324 # directory should also be skipped.
325 self.directory_rules[norm_dir_path] = None
326 else:
327 self._ApplyDirectoryRulesAndSkipSubdirs(parent_rules, dir_path)
328 return self.directory_rules[norm_dir_path]
329 342
330 def _AddGitSourceDirectories(self): 343 def _AddGitSourceDirectories(self):
331 """Adds any directories containing sources managed by git to 344 """Adds any directories containing sources managed by git to
332 self.git_source_directories. 345 self.git_source_directories.
333 """ 346 """
334 if not os.path.exists(os.path.join(self.base_directory, '.git')): 347 if not os.path.exists(os.path.join(self.base_directory, '.git')):
335 return 348 return
336 349
337 popen_out = os.popen('cd %s && git ls-files --full-name .' % 350 popen_out = os.popen('cd %s && git ls-files --full-name .' %
338 subprocess.list2cmdline([self.base_directory])) 351 subprocess.list2cmdline([self.base_directory]))
339 for line in popen_out.readlines(): 352 for line in popen_out.readlines():
340 dir_name = os.path.join(self.base_directory, os.path.dirname(line)) 353 dir_path = os.path.join(self.base_directory, os.path.dirname(line))
341 # Add the directory as well as all the parent directories. Use 354 # Add the directory as well as all the parent directories. Use
342 # forward slashes and lower case to normalize paths. 355 # forward slashes and lower case to normalize paths.
343 while dir_name != self.base_directory: 356 while dir_path != self.base_directory:
344 self.git_source_directories.add(NormalizePath(dir_name)) 357 self.git_source_directories.add(NormalizePath(dir_path))
345 dir_name = os.path.dirname(dir_name) 358 dir_path = os.path.dirname(dir_path)
346 self.git_source_directories.add(NormalizePath(self.base_directory)) 359 self.git_source_directories.add(NormalizePath(self.base_directory))
OLDNEW
« no previous file with comments | « no previous file | tools/checkdeps/checkdeps.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698