blob: 5e2fd337c106d1f1c2e9686cfa6b5723f8815180 [file] [log] [blame]
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:021# Copyright 2017 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import ast
Edward Lesmes6f64a052018-03-20 21:35:496import cStringIO
Paweł Hajdan, Jr7cf96a42017-05-26 18:28:357import collections
Edward Lemur16f4bad2018-05-16 20:53:498import logging
Edward Lesmes6f64a052018-03-20 21:35:499import tokenize
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:0210
Edward Lemur16f4bad2018-05-16 20:53:4911import gclient_utils
12
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0513from third_party import schema
14
15
Edward Lesmes6f64a052018-03-20 21:35:4916class _NodeDict(collections.MutableMapping):
17 """Dict-like type that also stores information on AST nodes and tokens."""
18 def __init__(self, data, tokens=None):
19 self.data = collections.OrderedDict(data)
20 self.tokens = tokens
21
22 def __str__(self):
23 return str({k: v[0] for k, v in self.data.iteritems()})
24
25 def __getitem__(self, key):
26 return self.data[key][0]
27
28 def __setitem__(self, key, value):
29 self.data[key] = (value, None)
30
31 def __delitem__(self, key):
32 del self.data[key]
33
34 def __iter__(self):
35 return iter(self.data)
36
37 def __len__(self):
38 return len(self.data)
39
Edward Lesmes3d993812018-04-02 16:52:4940 def MoveTokens(self, origin, delta):
41 if self.tokens:
42 new_tokens = {}
43 for pos, token in self.tokens.iteritems():
44 if pos[0] >= origin:
45 pos = (pos[0] + delta, pos[1])
46 token = token[:2] + (pos,) + token[3:]
47 new_tokens[pos] = token
48
49 for value, node in self.data.values():
50 if node.lineno >= origin:
51 node.lineno += delta
52 if isinstance(value, _NodeDict):
53 value.MoveTokens(origin, delta)
54
Edward Lesmes6f64a052018-03-20 21:35:4955 def GetNode(self, key):
56 return self.data[key][1]
57
Edward Lesmes6c24d372018-03-28 16:52:2958 def SetNode(self, key, value, node):
Edward Lesmes6f64a052018-03-20 21:35:4959 self.data[key] = (value, node)
60
61
62def _NodeDictSchema(dict_schema):
63 """Validate dict_schema after converting _NodeDict to a regular dict."""
64 def validate(d):
65 schema.Schema(dict_schema).validate(dict(d))
66 return True
67 return validate
68
69
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0570# See https://github.com/keleshev/schema for docs how to configure schema.
Edward Lesmes6f64a052018-03-20 21:35:4971_GCLIENT_DEPS_SCHEMA = _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 16:51:5872 schema.Optional(basestring): schema.Or(
73 None,
74 basestring,
Edward Lesmes6f64a052018-03-20 21:35:4975 _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 16:51:5876 # Repo and revision to check out under the path
77 # (same as if no dict was used).
Michael Moss012013e2018-03-31 00:03:1978 'url': schema.Or(None, basestring),
Paweł Hajdan, Jrad30de62017-06-26 16:51:5879
80 # Optional condition string. The dep will only be processed
81 # if the condition evaluates to True.
82 schema.Optional('condition'): basestring,
John Budorick0f7b2002018-01-19 23:46:1783
84 schema.Optional('dep_type', default='git'): basestring,
Edward Lesmes6f64a052018-03-20 21:35:4985 }),
John Budorick0f7b2002018-01-19 23:46:1786 # CIPD package.
Edward Lesmes6f64a052018-03-20 21:35:4987 _NodeDictSchema({
John Budorick0f7b2002018-01-19 23:46:1788 'packages': [
Edward Lesmes6f64a052018-03-20 21:35:4989 _NodeDictSchema({
John Budorick0f7b2002018-01-19 23:46:1790 'package': basestring,
91
92 'version': basestring,
Edward Lesmes6f64a052018-03-20 21:35:4993 })
John Budorick0f7b2002018-01-19 23:46:1794 ],
95
96 schema.Optional('condition'): basestring,
97
98 schema.Optional('dep_type', default='cipd'): basestring,
Edward Lesmes6f64a052018-03-20 21:35:4999 }),
Paweł Hajdan, Jrad30de62017-06-26 16:51:58100 ),
Edward Lesmes6f64a052018-03-20 21:35:49101})
Paweł Hajdan, Jrad30de62017-06-26 16:51:58102
Edward Lesmes6f64a052018-03-20 21:35:49103_GCLIENT_HOOKS_SCHEMA = [_NodeDictSchema({
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05104 # Hook action: list of command-line arguments to invoke.
105 'action': [basestring],
106
107 # Name of the hook. Doesn't affect operation.
108 schema.Optional('name'): basestring,
109
110 # Hook pattern (regex). Originally intended to limit some hooks to run
111 # only when files matching the pattern have changed. In practice, with git,
112 # gclient runs all the hooks regardless of this field.
113 schema.Optional('pattern'): basestring,
Paweł Hajdan, Jrc9364392017-06-14 15:11:56114
115 # Working directory where to execute the hook.
116 schema.Optional('cwd'): basestring,
Paweł Hajdan, Jr032d5452017-06-22 18:43:53117
118 # Optional condition string. The hook will only be run
119 # if the condition evaluates to True.
120 schema.Optional('condition'): basestring,
Edward Lesmes6f64a052018-03-20 21:35:49121})]
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05122
Edward Lesmes6f64a052018-03-20 21:35:49123_GCLIENT_SCHEMA = schema.Schema(_NodeDictSchema({
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05124 # List of host names from which dependencies are allowed (whitelist).
125 # NOTE: when not present, all hosts are allowed.
126 # NOTE: scoped to current DEPS file, not recursive.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37127 schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05128
129 # Mapping from paths to repo and revision to check out under that path.
130 # Applying this mapping to the on-disk checkout is the main purpose
131 # of gclient, and also why the config file is called DEPS.
132 #
133 # The following functions are allowed:
134 #
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05135 # Var(): allows variable substitution (either from 'vars' dict below,
136 # or command-line override)
Paweł Hajdan, Jrad30de62017-06-26 16:51:58137 schema.Optional('deps'): _GCLIENT_DEPS_SCHEMA,
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05138
139 # Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37140 # Also see 'target_os'.
Edward Lesmes6f64a052018-03-20 21:35:49141 schema.Optional('deps_os'): _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 16:51:58142 schema.Optional(basestring): _GCLIENT_DEPS_SCHEMA,
Edward Lesmes6f64a052018-03-20 21:35:49143 }),
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05144
Michael Moss848c86e2018-05-03 23:05:50145 # Dependency to get gclient_gn_args* settings from. This allows these values
146 # to be set in a recursedeps file, rather than requiring that they exist in
147 # the top-level solution.
148 schema.Optional('gclient_gn_args_from'): basestring,
149
Paweł Hajdan, Jr57253732017-06-06 21:49:11150 # Path to GN args file to write selected variables.
151 schema.Optional('gclient_gn_args_file'): basestring,
152
153 # Subset of variables to write to the GN args file (see above).
154 schema.Optional('gclient_gn_args'): [schema.Optional(basestring)],
155
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05156 # Hooks executed after gclient sync (unless suppressed), or explicitly
157 # on gclient hooks. See _GCLIENT_HOOKS_SCHEMA for details.
158 # Also see 'pre_deps_hooks'.
159 schema.Optional('hooks'): _GCLIENT_HOOKS_SCHEMA,
160
Scott Grahamc4826742017-05-11 23:59:23161 # Similar to 'hooks', also keyed by OS.
Edward Lesmes6f64a052018-03-20 21:35:49162 schema.Optional('hooks_os'): _NodeDictSchema({
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37163 schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
Edward Lesmes6f64a052018-03-20 21:35:49164 }),
Scott Grahamc4826742017-05-11 23:59:23165
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05166 # Rules which #includes are allowed in the directory.
167 # Also see 'skip_child_includes' and 'specific_include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37168 schema.Optional('include_rules'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05169
170 # Hooks executed before processing DEPS. See 'hooks' for more details.
171 schema.Optional('pre_deps_hooks'): _GCLIENT_HOOKS_SCHEMA,
172
Paweł Hajdan, Jr6f796792017-06-02 06:40:06173 # Recursion limit for nested DEPS.
174 schema.Optional('recursion'): int,
175
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05176 # Whitelists deps for which recursion should be enabled.
177 schema.Optional('recursedeps'): [
Paweł Hajdan, Jr05fec032017-05-30 21:04:23178 schema.Optional(schema.Or(
179 basestring,
180 (basestring, basestring),
181 [basestring, basestring]
182 )),
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05183 ],
184
185 # Blacklists directories for checking 'include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37186 schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05187
188 # Mapping from paths to include rules specific for that path.
189 # See 'include_rules' for more details.
Edward Lesmes6f64a052018-03-20 21:35:49190 schema.Optional('specific_include_rules'): _NodeDictSchema({
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37191 schema.Optional(basestring): [basestring]
Edward Lesmes6f64a052018-03-20 21:35:49192 }),
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37193
194 # List of additional OS names to consider when selecting dependencies
195 # from deps_os.
196 schema.Optional('target_os'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05197
198 # For recursed-upon sub-dependencies, check out their own dependencies
199 # relative to the paren't path, rather than relative to the .gclient file.
200 schema.Optional('use_relative_paths'): bool,
201
202 # Variables that can be referenced using Var() - see 'deps'.
Edward Lesmes6f64a052018-03-20 21:35:49203 schema.Optional('vars'): _NodeDictSchema({
Paweł Hajdan, Jre0214742017-09-28 10:21:01204 schema.Optional(basestring): schema.Or(basestring, bool),
Edward Lesmes6f64a052018-03-20 21:35:49205 }),
206}))
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05207
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02208
Edward Lesmes6c24d372018-03-28 16:52:29209def _gclient_eval(node_or_string, vars_dict=None, expand_vars=False,
210 filename='<unknown>'):
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02211 """Safely evaluates a single expression. Returns the result."""
212 _allowed_names = {'None': None, 'True': True, 'False': False}
213 if isinstance(node_or_string, basestring):
214 node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
215 if isinstance(node_or_string, ast.Expression):
216 node_or_string = node_or_string.body
217 def _convert(node):
218 if isinstance(node, ast.Str):
Edward Lesmes6c24d372018-03-28 16:52:29219 if not expand_vars:
220 return node.s
221 try:
222 return node.s.format(**vars_dict)
223 except KeyError as e:
224 raise ValueError(
225 '%s was used as a variable, but was not declared in the vars dict '
226 '(file %r, line %s)' % (
227 e.message, filename, getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jr6f796792017-06-02 06:40:06228 elif isinstance(node, ast.Num):
229 return node.n
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02230 elif isinstance(node, ast.Tuple):
231 return tuple(map(_convert, node.elts))
232 elif isinstance(node, ast.List):
233 return list(map(_convert, node.elts))
234 elif isinstance(node, ast.Dict):
Edward Lesmes6f64a052018-03-20 21:35:49235 return _NodeDict((_convert(k), (_convert(v), v))
Paweł Hajdan, Jr7cf96a42017-05-26 18:28:35236 for k, v in zip(node.keys, node.values))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02237 elif isinstance(node, ast.Name):
238 if node.id not in _allowed_names:
239 raise ValueError(
240 'invalid name %r (file %r, line %s)' % (
241 node.id, filename, getattr(node, 'lineno', '<unknown>')))
242 return _allowed_names[node.id]
243 elif isinstance(node, ast.Call):
Edward Lesmes9f531292018-03-21 01:27:15244 if not isinstance(node.func, ast.Name) or node.func.id != 'Var':
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02245 raise ValueError(
Edward Lesmes9f531292018-03-21 01:27:15246 'Var is the only allowed function (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02247 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes9f531292018-03-21 01:27:15248 if node.keywords or node.starargs or node.kwargs or len(node.args) != 1:
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02249 raise ValueError(
Edward Lesmes9f531292018-03-21 01:27:15250 'Var takes exactly one argument (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02251 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes9f531292018-03-21 01:27:15252 arg = _convert(node.args[0])
253 if not isinstance(arg, basestring):
254 raise ValueError(
255 'Var\'s argument must be a variable name (file %r, line %s)' % (
256 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes6c24d372018-03-28 16:52:29257 if not expand_vars:
258 return '{%s}' % arg
259 if vars_dict is None:
260 raise ValueError(
261 'vars must be declared before Var can be used (file %r, line %s)'
262 % (filename, getattr(node, 'lineno', '<unknown>')))
263 if arg not in vars_dict:
264 raise ValueError(
265 '%s was used as a variable, but was not declared in the vars dict '
266 '(file %r, line %s)' % (
267 arg, filename, getattr(node, 'lineno', '<unknown>')))
268 return vars_dict[arg]
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02269 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
270 return _convert(node.left) + _convert(node.right)
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37271 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
272 return _convert(node.left) % _convert(node.right)
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02273 else:
274 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44275 'unexpected AST node: %s %s (file %r, line %s)' % (
276 node, ast.dump(node), filename,
277 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02278 return _convert(node_or_string)
279
280
Edward Lesmes6c24d372018-03-28 16:52:29281def Exec(content, expand_vars=True, filename='<unknown>', vars_override=None):
282 """Safely execs a set of assignments."""
283 def _validate_statement(node, local_scope):
284 if not isinstance(node, ast.Assign):
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02285 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44286 'unexpected AST node: %s %s (file %r, line %s)' % (
287 node, ast.dump(node), filename,
288 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02289
Edward Lesmes6c24d372018-03-28 16:52:29290 if len(node.targets) != 1:
291 raise ValueError(
292 'invalid assignment: use exactly one target (file %r, line %s)' % (
293 filename, getattr(node, 'lineno', '<unknown>')))
294
295 target = node.targets[0]
296 if not isinstance(target, ast.Name):
297 raise ValueError(
298 'invalid assignment: target should be a name (file %r, line %s)' % (
299 filename, getattr(node, 'lineno', '<unknown>')))
300 if target.id in local_scope:
301 raise ValueError(
302 'invalid assignment: overrides var %r (file %r, line %s)' % (
303 target.id, filename, getattr(node, 'lineno', '<unknown>')))
304
305 node_or_string = ast.parse(content, filename=filename, mode='exec')
306 if isinstance(node_or_string, ast.Expression):
307 node_or_string = node_or_string.body
308
309 if not isinstance(node_or_string, ast.Module):
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02310 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44311 'unexpected AST node: %s %s (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02312 node_or_string,
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44313 ast.dump(node_or_string),
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02314 filename,
315 getattr(node_or_string, 'lineno', '<unknown>')))
316
Edward Lesmes6c24d372018-03-28 16:52:29317 statements = {}
318 for statement in node_or_string.body:
319 _validate_statement(statement, statements)
320 statements[statement.targets[0].id] = statement.value
321
322 tokens = {
323 token[2]: list(token)
324 for token in tokenize.generate_tokens(
325 cStringIO.StringIO(content).readline)
326 }
327 local_scope = _NodeDict({}, tokens)
328
329 # Process vars first, so we can expand variables in the rest of the DEPS file.
330 vars_dict = {}
331 if 'vars' in statements:
332 vars_statement = statements['vars']
333 value = _gclient_eval(vars_statement, None, False, filename)
334 local_scope.SetNode('vars', value, vars_statement)
335 # Update the parsed vars with the overrides, but only if they are already
336 # present (overrides do not introduce new variables).
337 vars_dict.update(value)
338 if vars_override:
339 vars_dict.update({
340 k: v
341 for k, v in vars_override.iteritems()
342 if k in vars_dict})
343
344 for name, node in statements.iteritems():
345 value = _gclient_eval(node, vars_dict, expand_vars, filename)
346 local_scope.SetNode(name, value, node)
347
John Budorick0f7b2002018-01-19 23:46:17348 return _GCLIENT_SCHEMA.validate(local_scope)
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57349
350
Edward Lemur16f4bad2018-05-16 20:53:49351def ExecLegacy(content, expand_vars=True, filename='<unknown>',
352 vars_override=None):
353 """Executes a DEPS file |content| using exec."""
Edward Lesmes6c24d372018-03-28 16:52:29354 local_scope = {}
355 global_scope = {'Var': lambda var_name: '{%s}' % var_name}
356
357 # If we use 'exec' directly, it complains that 'Parse' contains a nested
358 # function with free variables.
359 # This is because on versions of Python < 2.7.9, "exec(a, b, c)" not the same
360 # as "exec a in b, c" (See https://bugs.python.org/issue21591).
361 eval(compile(content, filename, 'exec'), global_scope, local_scope)
362
363 if 'vars' not in local_scope or not expand_vars:
364 return local_scope
365
366 vars_dict = {}
367 vars_dict.update(local_scope['vars'])
368 if vars_override:
369 vars_dict.update({
370 k: v
371 for k, v in vars_override.iteritems()
372 if k in vars_dict
373 })
374
375 def _DeepFormat(node):
376 if isinstance(node, basestring):
377 return node.format(**vars_dict)
378 elif isinstance(node, dict):
379 return {
380 k.format(**vars_dict): _DeepFormat(v)
381 for k, v in node.iteritems()
382 }
383 elif isinstance(node, list):
384 return [_DeepFormat(elem) for elem in node]
385 elif isinstance(node, tuple):
386 return tuple(_DeepFormat(elem) for elem in node)
387 else:
388 return node
389
390 return _DeepFormat(local_scope)
391
392
Edward Lemur16f4bad2018-05-16 20:53:49393def _StandardizeDeps(deps_dict, vars_dict):
394 """"Standardizes the deps_dict.
395
396 For each dependency:
397 - Expands the variable in the dependency name.
398 - Ensures the dependency is a dictionary.
399 - Set's the 'dep_type' to be 'git' by default.
400 """
401 new_deps_dict = {}
402 for dep_name, dep_info in deps_dict.items():
403 dep_name = dep_name.format(**vars_dict)
404 if not isinstance(dep_info, collections.Mapping):
405 dep_info = {'url': dep_info}
406 dep_info.setdefault('dep_type', 'git')
407 new_deps_dict[dep_name] = dep_info
408 return new_deps_dict
409
410
411def _MergeDepsOs(deps_dict, os_deps_dict, os_name):
412 """Merges the deps in os_deps_dict into conditional dependencies in deps_dict.
413
414 The dependencies in os_deps_dict are transformed into conditional dependencies
415 using |'checkout_' + os_name|.
416 If the dependency is already present, the URL and revision must coincide.
417 """
418 for dep_name, dep_info in os_deps_dict.items():
419 # Make this condition very visible, so it's not a silent failure.
420 # It's unclear how to support None override in deps_os.
421 if dep_info['url'] is None:
422 logging.error('Ignoring %r:%r in %r deps_os', dep_name, dep_info, os_name)
423 continue
424
425 os_condition = 'checkout_' + (os_name if os_name != 'unix' else 'linux')
426 UpdateCondition(dep_info, 'and', os_condition)
427
428 if dep_name in deps_dict:
429 if deps_dict[dep_name]['url'] != dep_info['url']:
430 raise gclient_utils.Error(
431 'Value from deps_os (%r; %r: %r) conflicts with existing deps '
432 'entry (%r).' % (
433 os_name, dep_name, dep_info, deps_dict[dep_name]))
434
435 UpdateCondition(dep_info, 'or', deps_dict[dep_name].get('condition'))
436
437 deps_dict[dep_name] = dep_info
438
439
440def UpdateCondition(info_dict, op, new_condition):
441 """Updates info_dict's condition with |new_condition|.
442
443 An absent value is treated as implicitly True.
444 """
445 curr_condition = info_dict.get('condition')
446 # Easy case: Both are present.
447 if curr_condition and new_condition:
448 info_dict['condition'] = '(%s) %s (%s)' % (
449 curr_condition, op, new_condition)
450 # If |op| == 'and', and at least one condition is present, then use it.
451 elif op == 'and' and (curr_condition or new_condition):
452 info_dict['condition'] = curr_condition or new_condition
453 # Otherwise, no condition should be set
454 elif curr_condition:
455 del info_dict['condition']
456
457
458def Parse(content, expand_vars, validate_syntax, filename, vars_override=None):
459 """Parses DEPS strings.
460
461 Executes the Python-like string stored in content, resulting in a Python
462 dictionary specifyied by the schema above. Supports syntax validation and
463 variable expansion.
464
465 Args:
466 content: str. DEPS file stored as a string.
467 expand_vars: bool. Whether variables should be expanded to their values.
468 validate_syntax: bool. Whether syntax should be validated using the schema
469 defined above.
470 filename: str. The name of the DEPS file, or a string describing the source
471 of the content, e.g. '<string>', '<unknown>'.
472 vars_override: dict, optional. A dictionary with overrides for the variables
473 defined by the DEPS file.
474
475 Returns:
476 A Python dict with the parsed contents of the DEPS file, as specified by the
477 schema above.
478 """
479 if validate_syntax:
480 result = Exec(content, expand_vars, filename, vars_override)
481 else:
482 result = ExecLegacy(content, expand_vars, filename, vars_override)
483
484 vars_dict = result.get('vars', {})
485 if 'deps' in result:
486 result['deps'] = _StandardizeDeps(result['deps'], vars_dict)
487
488 if 'deps_os' in result:
489 deps = result.setdefault('deps', {})
490 for os_name, os_deps in result['deps_os'].iteritems():
491 os_deps = _StandardizeDeps(os_deps, vars_dict)
492 _MergeDepsOs(deps, os_deps, os_name)
493 del result['deps_os']
494
495 if 'hooks_os' in result:
496 hooks = result.setdefault('hooks', [])
497 for os_name, os_hooks in result['hooks_os'].iteritems():
498 for hook in os_hooks:
499 UpdateCondition(hook, 'and', 'checkout_' + os_name)
500 hooks.extend(os_hooks)
501 del result['hooks_os']
502
503 return result
504
505
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57506def EvaluateCondition(condition, variables, referenced_variables=None):
507 """Safely evaluates a boolean condition. Returns the result."""
508 if not referenced_variables:
509 referenced_variables = set()
510 _allowed_names = {'None': None, 'True': True, 'False': False}
511 main_node = ast.parse(condition, mode='eval')
512 if isinstance(main_node, ast.Expression):
513 main_node = main_node.body
514 def _convert(node):
515 if isinstance(node, ast.Str):
516 return node.s
517 elif isinstance(node, ast.Name):
518 if node.id in referenced_variables:
519 raise ValueError(
520 'invalid cyclic reference to %r (inside %r)' % (
521 node.id, condition))
522 elif node.id in _allowed_names:
523 return _allowed_names[node.id]
524 elif node.id in variables:
Paweł Hajdan, Jre0214742017-09-28 10:21:01525 value = variables[node.id]
526
527 # Allow using "native" types, without wrapping everything in strings.
528 # Note that schema constraints still apply to variables.
529 if not isinstance(value, basestring):
530 return value
531
532 # Recursively evaluate the variable reference.
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57533 return EvaluateCondition(
534 variables[node.id],
535 variables,
536 referenced_variables.union([node.id]))
537 else:
Paweł Hajdan, Jre0214742017-09-28 10:21:01538 # Implicitly convert unrecognized names to strings.
539 # If we want to change this, we'll need to explicitly distinguish
540 # between arguments for GN to be passed verbatim, and ones to
541 # be evaluated.
542 return node.id
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57543 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
544 if len(node.values) != 2:
545 raise ValueError(
546 'invalid "or": exactly 2 operands required (inside %r)' % (
547 condition))
Paweł Hajdan, Jre0214742017-09-28 10:21:01548 left = _convert(node.values[0])
549 right = _convert(node.values[1])
550 if not isinstance(left, bool):
551 raise ValueError(
552 'invalid "or" operand %r (inside %r)' % (left, condition))
553 if not isinstance(right, bool):
554 raise ValueError(
555 'invalid "or" operand %r (inside %r)' % (right, condition))
556 return left or right
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57557 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
558 if len(node.values) != 2:
559 raise ValueError(
560 'invalid "and": exactly 2 operands required (inside %r)' % (
561 condition))
Paweł Hajdan, Jre0214742017-09-28 10:21:01562 left = _convert(node.values[0])
563 right = _convert(node.values[1])
564 if not isinstance(left, bool):
565 raise ValueError(
566 'invalid "and" operand %r (inside %r)' % (left, condition))
567 if not isinstance(right, bool):
568 raise ValueError(
569 'invalid "and" operand %r (inside %r)' % (right, condition))
570 return left and right
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57571 elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
Paweł Hajdan, Jre0214742017-09-28 10:21:01572 value = _convert(node.operand)
573 if not isinstance(value, bool):
574 raise ValueError(
575 'invalid "not" operand %r (inside %r)' % (value, condition))
576 return not value
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57577 elif isinstance(node, ast.Compare):
578 if len(node.ops) != 1:
579 raise ValueError(
580 'invalid compare: exactly 1 operator required (inside %r)' % (
581 condition))
582 if len(node.comparators) != 1:
583 raise ValueError(
584 'invalid compare: exactly 1 comparator required (inside %r)' % (
585 condition))
586
587 left = _convert(node.left)
588 right = _convert(node.comparators[0])
589
590 if isinstance(node.ops[0], ast.Eq):
591 return left == right
Dirk Pranke77b76872017-10-06 01:29:27592 if isinstance(node.ops[0], ast.NotEq):
593 return left != right
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57594
595 raise ValueError(
596 'unexpected operator: %s %s (inside %r)' % (
597 node.ops[0], ast.dump(node), condition))
598 else:
599 raise ValueError(
600 'unexpected AST node: %s %s (inside %r)' % (
601 node, ast.dump(node), condition))
602 return _convert(main_node)
Edward Lesmes6f64a052018-03-20 21:35:49603
604
605def RenderDEPSFile(gclient_dict):
606 contents = sorted(gclient_dict.tokens.values(), key=lambda token: token[2])
607 return tokenize.untokenize(contents)
608
609
610def _UpdateAstString(tokens, node, value):
611 position = node.lineno, node.col_offset
Edward Lesmes62af4e42018-03-30 22:15:44612 quote_char = tokens[position][1][0]
613 tokens[position][1] = quote_char + value + quote_char
Edward Lesmes6f64a052018-03-20 21:35:49614 node.s = value
615
616
Edward Lesmes3d993812018-04-02 16:52:49617def _ShiftLinesInTokens(tokens, delta, start):
618 new_tokens = {}
619 for token in tokens.values():
620 if token[2][0] >= start:
621 token[2] = token[2][0] + delta, token[2][1]
622 token[3] = token[3][0] + delta, token[3][1]
623 new_tokens[token[2]] = token
624 return new_tokens
625
626
627def AddVar(gclient_dict, var_name, value):
628 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
629 raise ValueError(
630 "Can't use SetVar for the given gclient dict. It contains no "
631 "formatting information.")
632
633 if 'vars' not in gclient_dict:
634 raise KeyError("vars dict is not defined.")
635
636 if var_name in gclient_dict['vars']:
637 raise ValueError(
638 "%s has already been declared in the vars dict. Consider using SetVar "
639 "instead." % var_name)
640
641 if not gclient_dict['vars']:
642 raise ValueError('vars dict is empty. This is not yet supported.')
643
Edward Lesmes8d626572018-04-05 21:53:10644 # We will attempt to add the var right after 'vars = {'.
645 node = gclient_dict.GetNode('vars')
Edward Lesmes3d993812018-04-02 16:52:49646 if node is None:
647 raise ValueError(
648 "The vars dict has no formatting information." % var_name)
Edward Lesmes8d626572018-04-05 21:53:10649 line = node.lineno + 1
650
651 # We will try to match the new var's indentation to the next variable.
652 col = node.keys[0].col_offset
Edward Lesmes3d993812018-04-02 16:52:49653
654 # We use a minimal Python dictionary, so that ast can parse it.
655 var_content = '{\n%s"%s": "%s",\n}' % (' ' * col, var_name, value)
656 var_ast = ast.parse(var_content).body[0].value
657
658 # Set the ast nodes for the key and value.
659 vars_node = gclient_dict.GetNode('vars')
660
661 var_name_node = var_ast.keys[0]
662 var_name_node.lineno += line - 2
663 vars_node.keys.insert(0, var_name_node)
664
665 value_node = var_ast.values[0]
666 value_node.lineno += line - 2
667 vars_node.values.insert(0, value_node)
668
669 # Update the tokens.
670 var_tokens = list(tokenize.generate_tokens(
671 cStringIO.StringIO(var_content).readline))
672 var_tokens = {
673 token[2]: list(token)
674 # Ignore the tokens corresponding to braces and new lines.
675 for token in var_tokens[2:-2]
676 }
677
678 gclient_dict.tokens = _ShiftLinesInTokens(gclient_dict.tokens, 1, line)
679 gclient_dict.tokens.update(_ShiftLinesInTokens(var_tokens, line - 2, 0))
680
681
Edward Lesmes6f64a052018-03-20 21:35:49682def SetVar(gclient_dict, var_name, value):
683 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
684 raise ValueError(
685 "Can't use SetVar for the given gclient dict. It contains no "
686 "formatting information.")
687 tokens = gclient_dict.tokens
688
Edward Lesmes3d993812018-04-02 16:52:49689 if 'vars' not in gclient_dict:
690 raise KeyError("vars dict is not defined.")
691
692 if var_name not in gclient_dict['vars']:
Edward Lesmes6f64a052018-03-20 21:35:49693 raise ValueError(
Edward Lesmes3d993812018-04-02 16:52:49694 "%s has not been declared in the vars dict. Consider using AddVar "
695 "instead." % var_name)
Edward Lesmes6f64a052018-03-20 21:35:49696
697 node = gclient_dict['vars'].GetNode(var_name)
698 if node is None:
699 raise ValueError(
700 "The vars entry for %s has no formatting information." % var_name)
701
702 _UpdateAstString(tokens, node, value)
Edward Lesmes6c24d372018-03-28 16:52:29703 gclient_dict['vars'].SetNode(var_name, value, node)
Edward Lesmes6f64a052018-03-20 21:35:49704
705
706def SetCIPD(gclient_dict, dep_name, package_name, new_version):
707 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
708 raise ValueError(
709 "Can't use SetCIPD for the given gclient dict. It contains no "
710 "formatting information.")
711 tokens = gclient_dict.tokens
712
713 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
Edward Lesmes3d993812018-04-02 16:52:49714 raise KeyError(
Edward Lesmes6f64a052018-03-20 21:35:49715 "Could not find any dependency called %s." % dep_name)
716
717 # Find the package with the given name
718 packages = [
719 package
720 for package in gclient_dict['deps'][dep_name]['packages']
721 if package['package'] == package_name
722 ]
723 if len(packages) != 1:
724 raise ValueError(
725 "There must be exactly one package with the given name (%s), "
726 "%s were found." % (package_name, len(packages)))
727
728 # TODO(ehmaldonado): Support Var in package's version.
729 node = packages[0].GetNode('version')
730 if node is None:
731 raise ValueError(
732 "The deps entry for %s:%s has no formatting information." %
733 (dep_name, package_name))
734
735 new_version = 'version:' + new_version
736 _UpdateAstString(tokens, node, new_version)
Edward Lesmes6c24d372018-03-28 16:52:29737 packages[0].SetNode('version', new_version, node)
Edward Lesmes6f64a052018-03-20 21:35:49738
739
Edward Lesmes9f531292018-03-21 01:27:15740def SetRevision(gclient_dict, dep_name, new_revision):
Edward Lesmes62af4e42018-03-30 22:15:44741 def _GetVarName(node):
742 if isinstance(node, ast.Call):
743 return node.args[0].s
744 elif node.s.endswith('}'):
745 last_brace = node.s.rfind('{')
746 return node.s[last_brace+1:-1]
747 return None
748
749 def _UpdateRevision(dep_dict, dep_key, new_revision):
750 dep_node = dep_dict.GetNode(dep_key)
751 if dep_node is None:
752 raise ValueError(
753 "The deps entry for %s has no formatting information." % dep_name)
754
755 node = dep_node
756 if isinstance(node, ast.BinOp):
757 node = node.right
758
759 if not isinstance(node, ast.Call) and not isinstance(node, ast.Str):
760 raise ValueError(
761 "Unsupported dependency revision format. Please file a bug.")
762
763 var_name = _GetVarName(node)
764 if var_name is not None:
765 SetVar(gclient_dict, var_name, new_revision)
766 else:
767 if '@' in node.s:
Edward Lesmes1118a212018-04-05 22:37:07768 # '@' is part of the last string, which we want to modify. Discard
769 # whatever was after the '@' and put the new revision in its place.
Edward Lesmes62af4e42018-03-30 22:15:44770 new_revision = node.s.split('@')[0] + '@' + new_revision
Edward Lesmes1118a212018-04-05 22:37:07771 elif '@' not in dep_dict[dep_key]:
772 # '@' is not part of the URL at all. This mean the dependency is
773 # unpinned and we should pin it.
774 new_revision = node.s + '@' + new_revision
Edward Lesmes62af4e42018-03-30 22:15:44775 _UpdateAstString(tokens, node, new_revision)
776 dep_dict.SetNode(dep_key, new_revision, node)
777
Edward Lesmes6f64a052018-03-20 21:35:49778 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
779 raise ValueError(
780 "Can't use SetRevision for the given gclient dict. It contains no "
781 "formatting information.")
782 tokens = gclient_dict.tokens
783
784 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
Edward Lesmes3d993812018-04-02 16:52:49785 raise KeyError(
Edward Lesmes6f64a052018-03-20 21:35:49786 "Could not find any dependency called %s." % dep_name)
787
Edward Lesmes6f64a052018-03-20 21:35:49788 if isinstance(gclient_dict['deps'][dep_name], _NodeDict):
Edward Lesmes62af4e42018-03-30 22:15:44789 _UpdateRevision(gclient_dict['deps'][dep_name], 'url', new_revision)
Edward Lesmes6f64a052018-03-20 21:35:49790 else:
Edward Lesmes62af4e42018-03-30 22:15:44791 _UpdateRevision(gclient_dict['deps'], dep_name, new_revision)
Edward Lesmes411041f2018-04-06 00:12:55792
793
794def GetVar(gclient_dict, var_name):
795 if 'vars' not in gclient_dict or var_name not in gclient_dict['vars']:
796 raise KeyError(
797 "Could not find any variable called %s." % var_name)
798
799 return gclient_dict['vars'][var_name]
800
801
802def GetCIPD(gclient_dict, dep_name, package_name):
803 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
804 raise KeyError(
805 "Could not find any dependency called %s." % dep_name)
806
807 # Find the package with the given name
808 packages = [
809 package
810 for package in gclient_dict['deps'][dep_name]['packages']
811 if package['package'] == package_name
812 ]
813 if len(packages) != 1:
814 raise ValueError(
815 "There must be exactly one package with the given name (%s), "
816 "%s were found." % (package_name, len(packages)))
817
818 return packages[0]['version'][len('version:'):]
819
820
821def GetRevision(gclient_dict, dep_name):
822 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
823 raise KeyError(
824 "Could not find any dependency called %s." % dep_name)
825
826 dep = gclient_dict['deps'][dep_name]
827 if dep is None:
828 return None
829 elif isinstance(dep, basestring):
830 _, _, revision = dep.partition('@')
831 return revision or None
832 elif isinstance(dep, collections.Mapping) and 'url' in dep:
833 _, _, revision = dep['url'].partition('@')
834 return revision or None
835 else:
836 raise ValueError(
837 '%s is not a valid git dependency.' % dep_name)