blob: aac3535f06c5afab6b62c83e20ac7372ebe15f11 [file] [log] [blame]
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02001# 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 17:35:49 -04006import cStringIO
Paweł Hajdan, Jr7cf96a42017-05-26 20:28:35 +02007import collections
Edward Lesmes6f64a052018-03-20 17:35:49 -04008import tokenize
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02009
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020010from third_party import schema
11
12
Edward Lesmes6f64a052018-03-20 17:35:49 -040013class _NodeDict(collections.MutableMapping):
14 """Dict-like type that also stores information on AST nodes and tokens."""
15 def __init__(self, data, tokens=None):
16 self.data = collections.OrderedDict(data)
17 self.tokens = tokens
18
19 def __str__(self):
20 return str({k: v[0] for k, v in self.data.iteritems()})
21
22 def __getitem__(self, key):
23 return self.data[key][0]
24
25 def __setitem__(self, key, value):
26 self.data[key] = (value, None)
27
28 def __delitem__(self, key):
29 del self.data[key]
30
31 def __iter__(self):
32 return iter(self.data)
33
34 def __len__(self):
35 return len(self.data)
36
Edward Lesmes3d993812018-04-02 12:52:49 -040037 def MoveTokens(self, origin, delta):
38 if self.tokens:
39 new_tokens = {}
40 for pos, token in self.tokens.iteritems():
41 if pos[0] >= origin:
42 pos = (pos[0] + delta, pos[1])
43 token = token[:2] + (pos,) + token[3:]
44 new_tokens[pos] = token
45
46 for value, node in self.data.values():
47 if node.lineno >= origin:
48 node.lineno += delta
49 if isinstance(value, _NodeDict):
50 value.MoveTokens(origin, delta)
51
Edward Lesmes6f64a052018-03-20 17:35:49 -040052 def GetNode(self, key):
53 return self.data[key][1]
54
Edward Lesmes6c24d372018-03-28 12:52:29 -040055 def SetNode(self, key, value, node):
Edward Lesmes6f64a052018-03-20 17:35:49 -040056 self.data[key] = (value, node)
57
58
59def _NodeDictSchema(dict_schema):
60 """Validate dict_schema after converting _NodeDict to a regular dict."""
61 def validate(d):
62 schema.Schema(dict_schema).validate(dict(d))
63 return True
64 return validate
65
66
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020067# See https://github.com/keleshev/schema for docs how to configure schema.
Edward Lesmes6f64a052018-03-20 17:35:49 -040068_GCLIENT_DEPS_SCHEMA = _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +020069 schema.Optional(basestring): schema.Or(
70 None,
71 basestring,
Edward Lesmes6f64a052018-03-20 17:35:49 -040072 _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +020073 # Repo and revision to check out under the path
74 # (same as if no dict was used).
Michael Moss012013e2018-03-30 17:03:19 -070075 'url': schema.Or(None, basestring),
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +020076
77 # Optional condition string. The dep will only be processed
78 # if the condition evaluates to True.
79 schema.Optional('condition'): basestring,
John Budorick0f7b2002018-01-19 15:46:17 -080080
81 schema.Optional('dep_type', default='git'): basestring,
Edward Lesmes6f64a052018-03-20 17:35:49 -040082 }),
John Budorick0f7b2002018-01-19 15:46:17 -080083 # CIPD package.
Edward Lesmes6f64a052018-03-20 17:35:49 -040084 _NodeDictSchema({
John Budorick0f7b2002018-01-19 15:46:17 -080085 'packages': [
Edward Lesmes6f64a052018-03-20 17:35:49 -040086 _NodeDictSchema({
John Budorick0f7b2002018-01-19 15:46:17 -080087 'package': basestring,
88
89 'version': basestring,
Edward Lesmes6f64a052018-03-20 17:35:49 -040090 })
John Budorick0f7b2002018-01-19 15:46:17 -080091 ],
92
93 schema.Optional('condition'): basestring,
94
95 schema.Optional('dep_type', default='cipd'): basestring,
Edward Lesmes6f64a052018-03-20 17:35:49 -040096 }),
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +020097 ),
Edward Lesmes6f64a052018-03-20 17:35:49 -040098})
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +020099
Edward Lesmes6f64a052018-03-20 17:35:49 -0400100_GCLIENT_HOOKS_SCHEMA = [_NodeDictSchema({
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200101 # Hook action: list of command-line arguments to invoke.
102 'action': [basestring],
103
104 # Name of the hook. Doesn't affect operation.
105 schema.Optional('name'): basestring,
106
107 # Hook pattern (regex). Originally intended to limit some hooks to run
108 # only when files matching the pattern have changed. In practice, with git,
109 # gclient runs all the hooks regardless of this field.
110 schema.Optional('pattern'): basestring,
Paweł Hajdan, Jrc9364392017-06-14 17:11:56 +0200111
112 # Working directory where to execute the hook.
113 schema.Optional('cwd'): basestring,
Paweł Hajdan, Jr032d5452017-06-22 20:43:53 +0200114
115 # Optional condition string. The hook will only be run
116 # if the condition evaluates to True.
117 schema.Optional('condition'): basestring,
Edward Lesmes6f64a052018-03-20 17:35:49 -0400118})]
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200119
Edward Lesmes6f64a052018-03-20 17:35:49 -0400120_GCLIENT_SCHEMA = schema.Schema(_NodeDictSchema({
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200121 # List of host names from which dependencies are allowed (whitelist).
122 # NOTE: when not present, all hosts are allowed.
123 # NOTE: scoped to current DEPS file, not recursive.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200124 schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200125
126 # Mapping from paths to repo and revision to check out under that path.
127 # Applying this mapping to the on-disk checkout is the main purpose
128 # of gclient, and also why the config file is called DEPS.
129 #
130 # The following functions are allowed:
131 #
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200132 # Var(): allows variable substitution (either from 'vars' dict below,
133 # or command-line override)
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +0200134 schema.Optional('deps'): _GCLIENT_DEPS_SCHEMA,
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200135
136 # Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200137 # Also see 'target_os'.
Edward Lesmes6f64a052018-03-20 17:35:49 -0400138 schema.Optional('deps_os'): _NodeDictSchema({
Paweł Hajdan, Jrad30de62017-06-26 18:51:58 +0200139 schema.Optional(basestring): _GCLIENT_DEPS_SCHEMA,
Edward Lesmes6f64a052018-03-20 17:35:49 -0400140 }),
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200141
Paweł Hajdan, Jr57253732017-06-06 23:49:11 +0200142 # Path to GN args file to write selected variables.
143 schema.Optional('gclient_gn_args_file'): basestring,
144
145 # Subset of variables to write to the GN args file (see above).
146 schema.Optional('gclient_gn_args'): [schema.Optional(basestring)],
147
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200148 # Hooks executed after gclient sync (unless suppressed), or explicitly
149 # on gclient hooks. See _GCLIENT_HOOKS_SCHEMA for details.
150 # Also see 'pre_deps_hooks'.
151 schema.Optional('hooks'): _GCLIENT_HOOKS_SCHEMA,
152
Scott Grahamc4826742017-05-11 16:59:23 -0700153 # Similar to 'hooks', also keyed by OS.
Edward Lesmes6f64a052018-03-20 17:35:49 -0400154 schema.Optional('hooks_os'): _NodeDictSchema({
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200155 schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
Edward Lesmes6f64a052018-03-20 17:35:49 -0400156 }),
Scott Grahamc4826742017-05-11 16:59:23 -0700157
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200158 # Rules which #includes are allowed in the directory.
159 # Also see 'skip_child_includes' and 'specific_include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200160 schema.Optional('include_rules'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200161
162 # Hooks executed before processing DEPS. See 'hooks' for more details.
163 schema.Optional('pre_deps_hooks'): _GCLIENT_HOOKS_SCHEMA,
164
Paweł Hajdan, Jr6f796792017-06-02 08:40:06 +0200165 # Recursion limit for nested DEPS.
166 schema.Optional('recursion'): int,
167
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200168 # Whitelists deps for which recursion should be enabled.
169 schema.Optional('recursedeps'): [
Paweł Hajdan, Jr05fec032017-05-30 23:04:23 +0200170 schema.Optional(schema.Or(
171 basestring,
172 (basestring, basestring),
173 [basestring, basestring]
174 )),
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200175 ],
176
177 # Blacklists directories for checking 'include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200178 schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200179
180 # Mapping from paths to include rules specific for that path.
181 # See 'include_rules' for more details.
Edward Lesmes6f64a052018-03-20 17:35:49 -0400182 schema.Optional('specific_include_rules'): _NodeDictSchema({
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200183 schema.Optional(basestring): [basestring]
Edward Lesmes6f64a052018-03-20 17:35:49 -0400184 }),
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200185
186 # List of additional OS names to consider when selecting dependencies
187 # from deps_os.
188 schema.Optional('target_os'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200189
190 # For recursed-upon sub-dependencies, check out their own dependencies
191 # relative to the paren't path, rather than relative to the .gclient file.
192 schema.Optional('use_relative_paths'): bool,
193
194 # Variables that can be referenced using Var() - see 'deps'.
Edward Lesmes6f64a052018-03-20 17:35:49 -0400195 schema.Optional('vars'): _NodeDictSchema({
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200196 schema.Optional(basestring): schema.Or(basestring, bool),
Edward Lesmes6f64a052018-03-20 17:35:49 -0400197 }),
198}))
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200199
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200200
Edward Lesmes6c24d372018-03-28 12:52:29 -0400201def _gclient_eval(node_or_string, vars_dict=None, expand_vars=False,
202 filename='<unknown>'):
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200203 """Safely evaluates a single expression. Returns the result."""
204 _allowed_names = {'None': None, 'True': True, 'False': False}
205 if isinstance(node_or_string, basestring):
206 node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
207 if isinstance(node_or_string, ast.Expression):
208 node_or_string = node_or_string.body
209 def _convert(node):
210 if isinstance(node, ast.Str):
Edward Lesmes6c24d372018-03-28 12:52:29 -0400211 if not expand_vars:
212 return node.s
213 try:
214 return node.s.format(**vars_dict)
215 except KeyError as e:
216 raise ValueError(
217 '%s was used as a variable, but was not declared in the vars dict '
218 '(file %r, line %s)' % (
219 e.message, filename, getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jr6f796792017-06-02 08:40:06 +0200220 elif isinstance(node, ast.Num):
221 return node.n
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200222 elif isinstance(node, ast.Tuple):
223 return tuple(map(_convert, node.elts))
224 elif isinstance(node, ast.List):
225 return list(map(_convert, node.elts))
226 elif isinstance(node, ast.Dict):
Edward Lesmes6f64a052018-03-20 17:35:49 -0400227 return _NodeDict((_convert(k), (_convert(v), v))
Paweł Hajdan, Jr7cf96a42017-05-26 20:28:35 +0200228 for k, v in zip(node.keys, node.values))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200229 elif isinstance(node, ast.Name):
230 if node.id not in _allowed_names:
231 raise ValueError(
232 'invalid name %r (file %r, line %s)' % (
233 node.id, filename, getattr(node, 'lineno', '<unknown>')))
234 return _allowed_names[node.id]
235 elif isinstance(node, ast.Call):
Edward Lesmes9f531292018-03-20 21:27:15 -0400236 if not isinstance(node.func, ast.Name) or node.func.id != 'Var':
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200237 raise ValueError(
Edward Lesmes9f531292018-03-20 21:27:15 -0400238 'Var is the only allowed function (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200239 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes9f531292018-03-20 21:27:15 -0400240 if node.keywords or node.starargs or node.kwargs or len(node.args) != 1:
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200241 raise ValueError(
Edward Lesmes9f531292018-03-20 21:27:15 -0400242 'Var takes exactly one argument (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200243 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes9f531292018-03-20 21:27:15 -0400244 arg = _convert(node.args[0])
245 if not isinstance(arg, basestring):
246 raise ValueError(
247 'Var\'s argument must be a variable name (file %r, line %s)' % (
248 filename, getattr(node, 'lineno', '<unknown>')))
Edward Lesmes6c24d372018-03-28 12:52:29 -0400249 if not expand_vars:
250 return '{%s}' % arg
251 if vars_dict is None:
252 raise ValueError(
253 'vars must be declared before Var can be used (file %r, line %s)'
254 % (filename, getattr(node, 'lineno', '<unknown>')))
255 if arg not in vars_dict:
256 raise ValueError(
257 '%s was used as a variable, but was not declared in the vars dict '
258 '(file %r, line %s)' % (
259 arg, filename, getattr(node, 'lineno', '<unknown>')))
260 return vars_dict[arg]
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200261 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
262 return _convert(node.left) + _convert(node.right)
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200263 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
264 return _convert(node.left) % _convert(node.right)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200265 else:
266 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200267 'unexpected AST node: %s %s (file %r, line %s)' % (
268 node, ast.dump(node), filename,
269 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200270 return _convert(node_or_string)
271
272
Edward Lesmes6c24d372018-03-28 12:52:29 -0400273def Exec(content, expand_vars=True, filename='<unknown>', vars_override=None):
274 """Safely execs a set of assignments."""
275 def _validate_statement(node, local_scope):
276 if not isinstance(node, ast.Assign):
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200277 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200278 'unexpected AST node: %s %s (file %r, line %s)' % (
279 node, ast.dump(node), filename,
280 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200281
Edward Lesmes6c24d372018-03-28 12:52:29 -0400282 if len(node.targets) != 1:
283 raise ValueError(
284 'invalid assignment: use exactly one target (file %r, line %s)' % (
285 filename, getattr(node, 'lineno', '<unknown>')))
286
287 target = node.targets[0]
288 if not isinstance(target, ast.Name):
289 raise ValueError(
290 'invalid assignment: target should be a name (file %r, line %s)' % (
291 filename, getattr(node, 'lineno', '<unknown>')))
292 if target.id in local_scope:
293 raise ValueError(
294 'invalid assignment: overrides var %r (file %r, line %s)' % (
295 target.id, filename, getattr(node, 'lineno', '<unknown>')))
296
297 node_or_string = ast.parse(content, filename=filename, mode='exec')
298 if isinstance(node_or_string, ast.Expression):
299 node_or_string = node_or_string.body
300
301 if not isinstance(node_or_string, ast.Module):
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200302 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200303 'unexpected AST node: %s %s (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200304 node_or_string,
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200305 ast.dump(node_or_string),
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200306 filename,
307 getattr(node_or_string, 'lineno', '<unknown>')))
308
Edward Lesmes6c24d372018-03-28 12:52:29 -0400309 statements = {}
310 for statement in node_or_string.body:
311 _validate_statement(statement, statements)
312 statements[statement.targets[0].id] = statement.value
313
314 tokens = {
315 token[2]: list(token)
316 for token in tokenize.generate_tokens(
317 cStringIO.StringIO(content).readline)
318 }
319 local_scope = _NodeDict({}, tokens)
320
321 # Process vars first, so we can expand variables in the rest of the DEPS file.
322 vars_dict = {}
323 if 'vars' in statements:
324 vars_statement = statements['vars']
325 value = _gclient_eval(vars_statement, None, False, filename)
326 local_scope.SetNode('vars', value, vars_statement)
327 # Update the parsed vars with the overrides, but only if they are already
328 # present (overrides do not introduce new variables).
329 vars_dict.update(value)
330 if vars_override:
331 vars_dict.update({
332 k: v
333 for k, v in vars_override.iteritems()
334 if k in vars_dict})
335
336 for name, node in statements.iteritems():
337 value = _gclient_eval(node, vars_dict, expand_vars, filename)
338 local_scope.SetNode(name, value, node)
339
John Budorick0f7b2002018-01-19 15:46:17 -0800340 return _GCLIENT_SCHEMA.validate(local_scope)
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200341
342
Edward Lesmes6c24d372018-03-28 12:52:29 -0400343def Parse(content, expand_vars, validate_syntax, filename, vars_override=None):
344 """Parses DEPS strings.
345
346 Executes the Python-like string stored in content, resulting in a Python
347 dictionary specifyied by the schema above. Supports syntax validation and
348 variable expansion.
349
350 Args:
351 content: str. DEPS file stored as a string.
352 expand_vars: bool. Whether variables should be expanded to their values.
353 validate_syntax: bool. Whether syntax should be validated using the schema
354 defined above.
355 filename: str. The name of the DEPS file, or a string describing the source
356 of the content, e.g. '<string>', '<unknown>'.
357 vars_override: dict, optional. A dictionary with overrides for the variables
358 defined by the DEPS file.
359
360 Returns:
361 A Python dict with the parsed contents of the DEPS file, as specified by the
362 schema above.
363 """
364 # TODO(ehmaldonado): Make validate_syntax = True the only case
365 if validate_syntax:
366 return Exec(content, expand_vars, filename, vars_override)
367
368 local_scope = {}
369 global_scope = {'Var': lambda var_name: '{%s}' % var_name}
370
371 # If we use 'exec' directly, it complains that 'Parse' contains a nested
372 # function with free variables.
373 # This is because on versions of Python < 2.7.9, "exec(a, b, c)" not the same
374 # as "exec a in b, c" (See https://bugs.python.org/issue21591).
375 eval(compile(content, filename, 'exec'), global_scope, local_scope)
376
377 if 'vars' not in local_scope or not expand_vars:
378 return local_scope
379
380 vars_dict = {}
381 vars_dict.update(local_scope['vars'])
382 if vars_override:
383 vars_dict.update({
384 k: v
385 for k, v in vars_override.iteritems()
386 if k in vars_dict
387 })
388
389 def _DeepFormat(node):
390 if isinstance(node, basestring):
391 return node.format(**vars_dict)
392 elif isinstance(node, dict):
393 return {
394 k.format(**vars_dict): _DeepFormat(v)
395 for k, v in node.iteritems()
396 }
397 elif isinstance(node, list):
398 return [_DeepFormat(elem) for elem in node]
399 elif isinstance(node, tuple):
400 return tuple(_DeepFormat(elem) for elem in node)
401 else:
402 return node
403
404 return _DeepFormat(local_scope)
405
406
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200407def EvaluateCondition(condition, variables, referenced_variables=None):
408 """Safely evaluates a boolean condition. Returns the result."""
409 if not referenced_variables:
410 referenced_variables = set()
411 _allowed_names = {'None': None, 'True': True, 'False': False}
412 main_node = ast.parse(condition, mode='eval')
413 if isinstance(main_node, ast.Expression):
414 main_node = main_node.body
415 def _convert(node):
416 if isinstance(node, ast.Str):
417 return node.s
418 elif isinstance(node, ast.Name):
419 if node.id in referenced_variables:
420 raise ValueError(
421 'invalid cyclic reference to %r (inside %r)' % (
422 node.id, condition))
423 elif node.id in _allowed_names:
424 return _allowed_names[node.id]
425 elif node.id in variables:
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200426 value = variables[node.id]
427
428 # Allow using "native" types, without wrapping everything in strings.
429 # Note that schema constraints still apply to variables.
430 if not isinstance(value, basestring):
431 return value
432
433 # Recursively evaluate the variable reference.
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200434 return EvaluateCondition(
435 variables[node.id],
436 variables,
437 referenced_variables.union([node.id]))
438 else:
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200439 # Implicitly convert unrecognized names to strings.
440 # If we want to change this, we'll need to explicitly distinguish
441 # between arguments for GN to be passed verbatim, and ones to
442 # be evaluated.
443 return node.id
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200444 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
445 if len(node.values) != 2:
446 raise ValueError(
447 'invalid "or": exactly 2 operands required (inside %r)' % (
448 condition))
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200449 left = _convert(node.values[0])
450 right = _convert(node.values[1])
451 if not isinstance(left, bool):
452 raise ValueError(
453 'invalid "or" operand %r (inside %r)' % (left, condition))
454 if not isinstance(right, bool):
455 raise ValueError(
456 'invalid "or" operand %r (inside %r)' % (right, condition))
457 return left or right
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200458 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
459 if len(node.values) != 2:
460 raise ValueError(
461 'invalid "and": exactly 2 operands required (inside %r)' % (
462 condition))
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200463 left = _convert(node.values[0])
464 right = _convert(node.values[1])
465 if not isinstance(left, bool):
466 raise ValueError(
467 'invalid "and" operand %r (inside %r)' % (left, condition))
468 if not isinstance(right, bool):
469 raise ValueError(
470 'invalid "and" operand %r (inside %r)' % (right, condition))
471 return left and right
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200472 elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
Paweł Hajdan, Jre0214742017-09-28 12:21:01 +0200473 value = _convert(node.operand)
474 if not isinstance(value, bool):
475 raise ValueError(
476 'invalid "not" operand %r (inside %r)' % (value, condition))
477 return not value
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200478 elif isinstance(node, ast.Compare):
479 if len(node.ops) != 1:
480 raise ValueError(
481 'invalid compare: exactly 1 operator required (inside %r)' % (
482 condition))
483 if len(node.comparators) != 1:
484 raise ValueError(
485 'invalid compare: exactly 1 comparator required (inside %r)' % (
486 condition))
487
488 left = _convert(node.left)
489 right = _convert(node.comparators[0])
490
491 if isinstance(node.ops[0], ast.Eq):
492 return left == right
Dirk Pranke77b76872017-10-05 18:29:27 -0700493 if isinstance(node.ops[0], ast.NotEq):
494 return left != right
Paweł Hajdan, Jr76c6ea22017-06-02 21:46:57 +0200495
496 raise ValueError(
497 'unexpected operator: %s %s (inside %r)' % (
498 node.ops[0], ast.dump(node), condition))
499 else:
500 raise ValueError(
501 'unexpected AST node: %s %s (inside %r)' % (
502 node, ast.dump(node), condition))
503 return _convert(main_node)
Edward Lesmes6f64a052018-03-20 17:35:49 -0400504
505
506def RenderDEPSFile(gclient_dict):
507 contents = sorted(gclient_dict.tokens.values(), key=lambda token: token[2])
508 return tokenize.untokenize(contents)
509
510
511def _UpdateAstString(tokens, node, value):
512 position = node.lineno, node.col_offset
Edward Lesmes62af4e42018-03-30 18:15:44 -0400513 quote_char = tokens[position][1][0]
514 tokens[position][1] = quote_char + value + quote_char
Edward Lesmes6f64a052018-03-20 17:35:49 -0400515 node.s = value
516
517
Edward Lesmes3d993812018-04-02 12:52:49 -0400518def _ShiftLinesInTokens(tokens, delta, start):
519 new_tokens = {}
520 for token in tokens.values():
521 if token[2][0] >= start:
522 token[2] = token[2][0] + delta, token[2][1]
523 token[3] = token[3][0] + delta, token[3][1]
524 new_tokens[token[2]] = token
525 return new_tokens
526
527
528def AddVar(gclient_dict, var_name, value):
529 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
530 raise ValueError(
531 "Can't use SetVar for the given gclient dict. It contains no "
532 "formatting information.")
533
534 if 'vars' not in gclient_dict:
535 raise KeyError("vars dict is not defined.")
536
537 if var_name in gclient_dict['vars']:
538 raise ValueError(
539 "%s has already been declared in the vars dict. Consider using SetVar "
540 "instead." % var_name)
541
542 if not gclient_dict['vars']:
543 raise ValueError('vars dict is empty. This is not yet supported.')
544
Edward Lesmes8d626572018-04-05 17:53:10 -0400545 # We will attempt to add the var right after 'vars = {'.
546 node = gclient_dict.GetNode('vars')
Edward Lesmes3d993812018-04-02 12:52:49 -0400547 if node is None:
548 raise ValueError(
549 "The vars dict has no formatting information." % var_name)
Edward Lesmes8d626572018-04-05 17:53:10 -0400550 line = node.lineno + 1
551
552 # We will try to match the new var's indentation to the next variable.
553 col = node.keys[0].col_offset
Edward Lesmes3d993812018-04-02 12:52:49 -0400554
555 # We use a minimal Python dictionary, so that ast can parse it.
556 var_content = '{\n%s"%s": "%s",\n}' % (' ' * col, var_name, value)
557 var_ast = ast.parse(var_content).body[0].value
558
559 # Set the ast nodes for the key and value.
560 vars_node = gclient_dict.GetNode('vars')
561
562 var_name_node = var_ast.keys[0]
563 var_name_node.lineno += line - 2
564 vars_node.keys.insert(0, var_name_node)
565
566 value_node = var_ast.values[0]
567 value_node.lineno += line - 2
568 vars_node.values.insert(0, value_node)
569
570 # Update the tokens.
571 var_tokens = list(tokenize.generate_tokens(
572 cStringIO.StringIO(var_content).readline))
573 var_tokens = {
574 token[2]: list(token)
575 # Ignore the tokens corresponding to braces and new lines.
576 for token in var_tokens[2:-2]
577 }
578
579 gclient_dict.tokens = _ShiftLinesInTokens(gclient_dict.tokens, 1, line)
580 gclient_dict.tokens.update(_ShiftLinesInTokens(var_tokens, line - 2, 0))
581
582
Edward Lesmes6f64a052018-03-20 17:35:49 -0400583def SetVar(gclient_dict, var_name, value):
584 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
585 raise ValueError(
586 "Can't use SetVar for the given gclient dict. It contains no "
587 "formatting information.")
588 tokens = gclient_dict.tokens
589
Edward Lesmes3d993812018-04-02 12:52:49 -0400590 if 'vars' not in gclient_dict:
591 raise KeyError("vars dict is not defined.")
592
593 if var_name not in gclient_dict['vars']:
Edward Lesmes6f64a052018-03-20 17:35:49 -0400594 raise ValueError(
Edward Lesmes3d993812018-04-02 12:52:49 -0400595 "%s has not been declared in the vars dict. Consider using AddVar "
596 "instead." % var_name)
Edward Lesmes6f64a052018-03-20 17:35:49 -0400597
598 node = gclient_dict['vars'].GetNode(var_name)
599 if node is None:
600 raise ValueError(
601 "The vars entry for %s has no formatting information." % var_name)
602
603 _UpdateAstString(tokens, node, value)
Edward Lesmes6c24d372018-03-28 12:52:29 -0400604 gclient_dict['vars'].SetNode(var_name, value, node)
Edward Lesmes6f64a052018-03-20 17:35:49 -0400605
606
607def SetCIPD(gclient_dict, dep_name, package_name, new_version):
608 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
609 raise ValueError(
610 "Can't use SetCIPD for the given gclient dict. It contains no "
611 "formatting information.")
612 tokens = gclient_dict.tokens
613
614 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
Edward Lesmes3d993812018-04-02 12:52:49 -0400615 raise KeyError(
Edward Lesmes6f64a052018-03-20 17:35:49 -0400616 "Could not find any dependency called %s." % dep_name)
617
618 # Find the package with the given name
619 packages = [
620 package
621 for package in gclient_dict['deps'][dep_name]['packages']
622 if package['package'] == package_name
623 ]
624 if len(packages) != 1:
625 raise ValueError(
626 "There must be exactly one package with the given name (%s), "
627 "%s were found." % (package_name, len(packages)))
628
629 # TODO(ehmaldonado): Support Var in package's version.
630 node = packages[0].GetNode('version')
631 if node is None:
632 raise ValueError(
633 "The deps entry for %s:%s has no formatting information." %
634 (dep_name, package_name))
635
636 new_version = 'version:' + new_version
637 _UpdateAstString(tokens, node, new_version)
Edward Lesmes6c24d372018-03-28 12:52:29 -0400638 packages[0].SetNode('version', new_version, node)
Edward Lesmes6f64a052018-03-20 17:35:49 -0400639
640
Edward Lesmes9f531292018-03-20 21:27:15 -0400641def SetRevision(gclient_dict, dep_name, new_revision):
Edward Lesmes62af4e42018-03-30 18:15:44 -0400642 def _GetVarName(node):
643 if isinstance(node, ast.Call):
644 return node.args[0].s
645 elif node.s.endswith('}'):
646 last_brace = node.s.rfind('{')
647 return node.s[last_brace+1:-1]
648 return None
649
650 def _UpdateRevision(dep_dict, dep_key, new_revision):
651 dep_node = dep_dict.GetNode(dep_key)
652 if dep_node is None:
653 raise ValueError(
654 "The deps entry for %s has no formatting information." % dep_name)
655
656 node = dep_node
657 if isinstance(node, ast.BinOp):
658 node = node.right
659
660 if not isinstance(node, ast.Call) and not isinstance(node, ast.Str):
661 raise ValueError(
662 "Unsupported dependency revision format. Please file a bug.")
663
664 var_name = _GetVarName(node)
665 if var_name is not None:
666 SetVar(gclient_dict, var_name, new_revision)
667 else:
668 if '@' in node.s:
Edward Lesmes1118a212018-04-05 18:37:07 -0400669 # '@' is part of the last string, which we want to modify. Discard
670 # whatever was after the '@' and put the new revision in its place.
Edward Lesmes62af4e42018-03-30 18:15:44 -0400671 new_revision = node.s.split('@')[0] + '@' + new_revision
Edward Lesmes1118a212018-04-05 18:37:07 -0400672 elif '@' not in dep_dict[dep_key]:
673 # '@' is not part of the URL at all. This mean the dependency is
674 # unpinned and we should pin it.
675 new_revision = node.s + '@' + new_revision
Edward Lesmes62af4e42018-03-30 18:15:44 -0400676 _UpdateAstString(tokens, node, new_revision)
677 dep_dict.SetNode(dep_key, new_revision, node)
678
Edward Lesmes6f64a052018-03-20 17:35:49 -0400679 if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
680 raise ValueError(
681 "Can't use SetRevision for the given gclient dict. It contains no "
682 "formatting information.")
683 tokens = gclient_dict.tokens
684
685 if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
Edward Lesmes3d993812018-04-02 12:52:49 -0400686 raise KeyError(
Edward Lesmes6f64a052018-03-20 17:35:49 -0400687 "Could not find any dependency called %s." % dep_name)
688
Edward Lesmes6f64a052018-03-20 17:35:49 -0400689 if isinstance(gclient_dict['deps'][dep_name], _NodeDict):
Edward Lesmes62af4e42018-03-30 18:15:44 -0400690 _UpdateRevision(gclient_dict['deps'][dep_name], 'url', new_revision)
Edward Lesmes6f64a052018-03-20 17:35:49 -0400691 else:
Edward Lesmes62af4e42018-03-30 18:15:44 -0400692 _UpdateRevision(gclient_dict['deps'], dep_name, new_revision)