-
Notifications
You must be signed in to change notification settings - Fork 0
/
gclient_eval.py
319 lines (277 loc) · 12 KB
/
gclient_eval.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# Copyright 2017 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import ast
import collections
from third_party import schema
# See https://github.com/keleshev/schema for docs how to configure schema.
_GCLIENT_DEPS_SCHEMA = {
schema.Optional(basestring): schema.Or(
None,
basestring,
{
# Repo and revision to check out under the path
# (same as if no dict was used).
'url': basestring,
# Optional condition string. The dep will only be processed
# if the condition evaluates to True.
schema.Optional('condition'): basestring,
},
),
}
_GCLIENT_HOOKS_SCHEMA = [{
# Hook action: list of command-line arguments to invoke.
'action': [basestring],
# Name of the hook. Doesn't affect operation.
schema.Optional('name'): basestring,
# Hook pattern (regex). Originally intended to limit some hooks to run
# only when files matching the pattern have changed. In practice, with git,
# gclient runs all the hooks regardless of this field.
schema.Optional('pattern'): basestring,
# Working directory where to execute the hook.
schema.Optional('cwd'): basestring,
# Optional condition string. The hook will only be run
# if the condition evaluates to True.
schema.Optional('condition'): basestring,
}]
_GCLIENT_SCHEMA = schema.Schema({
# List of host names from which dependencies are allowed (whitelist).
# NOTE: when not present, all hosts are allowed.
# NOTE: scoped to current DEPS file, not recursive.
schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
# Mapping from paths to repo and revision to check out under that path.
# Applying this mapping to the on-disk checkout is the main purpose
# of gclient, and also why the config file is called DEPS.
#
# The following functions are allowed:
#
# Var(): allows variable substitution (either from 'vars' dict below,
# or command-line override)
schema.Optional('deps'): _GCLIENT_DEPS_SCHEMA,
# Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
# Also see 'target_os'.
schema.Optional('deps_os'): {
schema.Optional(basestring): _GCLIENT_DEPS_SCHEMA,
},
# Path to GN args file to write selected variables.
schema.Optional('gclient_gn_args_file'): basestring,
# Subset of variables to write to the GN args file (see above).
schema.Optional('gclient_gn_args'): [schema.Optional(basestring)],
# Hooks executed after gclient sync (unless suppressed), or explicitly
# on gclient hooks. See _GCLIENT_HOOKS_SCHEMA for details.
# Also see 'pre_deps_hooks'.
schema.Optional('hooks'): _GCLIENT_HOOKS_SCHEMA,
# Similar to 'hooks', also keyed by OS.
schema.Optional('hooks_os'): {
schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
},
# Rules which #includes are allowed in the directory.
# Also see 'skip_child_includes' and 'specific_include_rules'.
schema.Optional('include_rules'): [schema.Optional(basestring)],
# Hooks executed before processing DEPS. See 'hooks' for more details.
schema.Optional('pre_deps_hooks'): _GCLIENT_HOOKS_SCHEMA,
# Recursion limit for nested DEPS.
schema.Optional('recursion'): int,
# Whitelists deps for which recursion should be enabled.
schema.Optional('recursedeps'): [
schema.Optional(schema.Or(
basestring,
(basestring, basestring),
[basestring, basestring]
)),
],
# Blacklists directories for checking 'include_rules'.
schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
# Mapping from paths to include rules specific for that path.
# See 'include_rules' for more details.
schema.Optional('specific_include_rules'): {
schema.Optional(basestring): [basestring]
},
# List of additional OS names to consider when selecting dependencies
# from deps_os.
schema.Optional('target_os'): [schema.Optional(basestring)],
# For recursed-upon sub-dependencies, check out their own dependencies
# relative to the paren't path, rather than relative to the .gclient file.
schema.Optional('use_relative_paths'): bool,
# Variables that can be referenced using Var() - see 'deps'.
schema.Optional('vars'): {
schema.Optional(basestring): schema.Or(basestring, bool),
},
})
def _gclient_eval(node_or_string, global_scope, filename='<unknown>'):
"""Safely evaluates a single expression. Returns the result."""
_allowed_names = {'None': None, 'True': True, 'False': False}
if isinstance(node_or_string, basestring):
node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
if isinstance(node_or_string, ast.Expression):
node_or_string = node_or_string.body
def _convert(node):
if isinstance(node, ast.Str):
return node.s
elif isinstance(node, ast.Num):
return node.n
elif isinstance(node, ast.Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, ast.List):
return list(map(_convert, node.elts))
elif isinstance(node, ast.Dict):
return collections.OrderedDict(
(_convert(k), _convert(v))
for k, v in zip(node.keys, node.values))
elif isinstance(node, ast.Name):
if node.id not in _allowed_names:
raise ValueError(
'invalid name %r (file %r, line %s)' % (
node.id, filename, getattr(node, 'lineno', '<unknown>')))
return _allowed_names[node.id]
elif isinstance(node, ast.Call):
if not isinstance(node.func, ast.Name):
raise ValueError(
'invalid call: func should be a name (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
if node.keywords or node.starargs or node.kwargs:
raise ValueError(
'invalid call: use only regular args (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
args = map(_convert, node.args)
return global_scope[node.func.id](*args)
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
return _convert(node.left) + _convert(node.right)
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
return _convert(node.left) % _convert(node.right)
else:
raise ValueError(
'unexpected AST node: %s %s (file %r, line %s)' % (
node, ast.dump(node), filename,
getattr(node, 'lineno', '<unknown>')))
return _convert(node_or_string)
def Exec(content, global_scope, local_scope, filename='<unknown>'):
"""Safely execs a set of assignments. Mutates |local_scope|."""
node_or_string = ast.parse(content, filename=filename, mode='exec')
if isinstance(node_or_string, ast.Expression):
node_or_string = node_or_string.body
def _visit_in_module(node):
if isinstance(node, ast.Assign):
if len(node.targets) != 1:
raise ValueError(
'invalid assignment: use exactly one target (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
target = node.targets[0]
if not isinstance(target, ast.Name):
raise ValueError(
'invalid assignment: target should be a name (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
value = _gclient_eval(node.value, global_scope, filename=filename)
if target.id in local_scope:
raise ValueError(
'invalid assignment: overrides var %r (file %r, line %s)' % (
target.id, filename, getattr(node, 'lineno', '<unknown>')))
local_scope[target.id] = value
else:
raise ValueError(
'unexpected AST node: %s %s (file %r, line %s)' % (
node, ast.dump(node), filename,
getattr(node, 'lineno', '<unknown>')))
if isinstance(node_or_string, ast.Module):
for stmt in node_or_string.body:
_visit_in_module(stmt)
else:
raise ValueError(
'unexpected AST node: %s %s (file %r, line %s)' % (
node_or_string,
ast.dump(node_or_string),
filename,
getattr(node_or_string, 'lineno', '<unknown>')))
_GCLIENT_SCHEMA.validate(local_scope)
def EvaluateCondition(condition, variables, referenced_variables=None):
"""Safely evaluates a boolean condition. Returns the result."""
if not referenced_variables:
referenced_variables = set()
_allowed_names = {'None': None, 'True': True, 'False': False}
main_node = ast.parse(condition, mode='eval')
if isinstance(main_node, ast.Expression):
main_node = main_node.body
def _convert(node):
if isinstance(node, ast.Str):
return node.s
elif isinstance(node, ast.Name):
if node.id in referenced_variables:
raise ValueError(
'invalid cyclic reference to %r (inside %r)' % (
node.id, condition))
elif node.id in _allowed_names:
return _allowed_names[node.id]
elif node.id in variables:
value = variables[node.id]
# Allow using "native" types, without wrapping everything in strings.
# Note that schema constraints still apply to variables.
if not isinstance(value, basestring):
return value
# Recursively evaluate the variable reference.
return EvaluateCondition(
variables[node.id],
variables,
referenced_variables.union([node.id]))
else:
# Implicitly convert unrecognized names to strings.
# If we want to change this, we'll need to explicitly distinguish
# between arguments for GN to be passed verbatim, and ones to
# be evaluated.
return node.id
elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
if len(node.values) != 2:
raise ValueError(
'invalid "or": exactly 2 operands required (inside %r)' % (
condition))
left = _convert(node.values[0])
right = _convert(node.values[1])
if not isinstance(left, bool):
raise ValueError(
'invalid "or" operand %r (inside %r)' % (left, condition))
if not isinstance(right, bool):
raise ValueError(
'invalid "or" operand %r (inside %r)' % (right, condition))
return left or right
elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
if len(node.values) != 2:
raise ValueError(
'invalid "and": exactly 2 operands required (inside %r)' % (
condition))
left = _convert(node.values[0])
right = _convert(node.values[1])
if not isinstance(left, bool):
raise ValueError(
'invalid "and" operand %r (inside %r)' % (left, condition))
if not isinstance(right, bool):
raise ValueError(
'invalid "and" operand %r (inside %r)' % (right, condition))
return left and right
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
value = _convert(node.operand)
if not isinstance(value, bool):
raise ValueError(
'invalid "not" operand %r (inside %r)' % (value, condition))
return not value
elif isinstance(node, ast.Compare):
if len(node.ops) != 1:
raise ValueError(
'invalid compare: exactly 1 operator required (inside %r)' % (
condition))
if len(node.comparators) != 1:
raise ValueError(
'invalid compare: exactly 1 comparator required (inside %r)' % (
condition))
left = _convert(node.left)
right = _convert(node.comparators[0])
if isinstance(node.ops[0], ast.Eq):
return left == right
if isinstance(node.ops[0], ast.NotEq):
return left != right
raise ValueError(
'unexpected operator: %s %s (inside %r)' % (
node.ops[0], ast.dump(node), condition))
else:
raise ValueError(
'unexpected AST node: %s %s (inside %r)' % (
node, ast.dump(node), condition))
return _convert(main_node)