blob: 5066a2df497570901331b5e4da510f1cdd6b6678 [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
Paweł Hajdan, Jr7cf96a42017-05-26 18:28:356import collections
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:027
Paweł Hajdan, Jrbeec0062017-05-10 19:51:058from third_party import schema
9
10
11# See https://github.com/keleshev/schema for docs how to configure schema.
12_GCLIENT_HOOKS_SCHEMA = [{
13 # Hook action: list of command-line arguments to invoke.
14 'action': [basestring],
15
16 # Name of the hook. Doesn't affect operation.
17 schema.Optional('name'): basestring,
18
19 # Hook pattern (regex). Originally intended to limit some hooks to run
20 # only when files matching the pattern have changed. In practice, with git,
21 # gclient runs all the hooks regardless of this field.
22 schema.Optional('pattern'): basestring,
23}]
24
25_GCLIENT_SCHEMA = schema.Schema({
26 # List of host names from which dependencies are allowed (whitelist).
27 # NOTE: when not present, all hosts are allowed.
28 # NOTE: scoped to current DEPS file, not recursive.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3729 schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0530
31 # Mapping from paths to repo and revision to check out under that path.
32 # Applying this mapping to the on-disk checkout is the main purpose
33 # of gclient, and also why the config file is called DEPS.
34 #
35 # The following functions are allowed:
36 #
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0537 # Var(): allows variable substitution (either from 'vars' dict below,
38 # or command-line override)
Paweł Hajdan, Jrc7ba0332017-05-29 14:38:4539 schema.Optional('deps'): {
40 schema.Optional(basestring): schema.Or(
41 basestring,
42 {
43 'url': basestring,
44 },
45 ),
46 },
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0547
48 # Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3749 # Also see 'target_os'.
50 schema.Optional('deps_os'): {
51 schema.Optional(basestring): {
52 schema.Optional(basestring): schema.Or(basestring, None)
53 }
54 },
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0555
56 # Hooks executed after gclient sync (unless suppressed), or explicitly
57 # on gclient hooks. See _GCLIENT_HOOKS_SCHEMA for details.
58 # Also see 'pre_deps_hooks'.
59 schema.Optional('hooks'): _GCLIENT_HOOKS_SCHEMA,
60
Scott Grahamc4826742017-05-11 23:59:2361 # Similar to 'hooks', also keyed by OS.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3762 schema.Optional('hooks_os'): {
63 schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
64 },
Scott Grahamc4826742017-05-11 23:59:2365
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0566 # Rules which #includes are allowed in the directory.
67 # Also see 'skip_child_includes' and 'specific_include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3768 schema.Optional('include_rules'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0569
70 # Hooks executed before processing DEPS. See 'hooks' for more details.
71 schema.Optional('pre_deps_hooks'): _GCLIENT_HOOKS_SCHEMA,
72
Paweł Hajdan, Jr6f796792017-06-02 06:40:0673 # Recursion limit for nested DEPS.
74 schema.Optional('recursion'): int,
75
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0576 # Whitelists deps for which recursion should be enabled.
77 schema.Optional('recursedeps'): [
Paweł Hajdan, Jr05fec032017-05-30 21:04:2378 schema.Optional(schema.Or(
79 basestring,
80 (basestring, basestring),
81 [basestring, basestring]
82 )),
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0583 ],
84
85 # Blacklists directories for checking 'include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3786 schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0587
88 # Mapping from paths to include rules specific for that path.
89 # See 'include_rules' for more details.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:3790 schema.Optional('specific_include_rules'): {
91 schema.Optional(basestring): [basestring]
92 },
93
94 # List of additional OS names to consider when selecting dependencies
95 # from deps_os.
96 schema.Optional('target_os'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 19:51:0597
98 # For recursed-upon sub-dependencies, check out their own dependencies
99 # relative to the paren't path, rather than relative to the .gclient file.
100 schema.Optional('use_relative_paths'): bool,
101
102 # Variables that can be referenced using Var() - see 'deps'.
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37103 schema.Optional('vars'): {schema.Optional(basestring): basestring},
Paweł Hajdan, Jrbeec0062017-05-10 19:51:05104})
105
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02106
107def _gclient_eval(node_or_string, global_scope, filename='<unknown>'):
108 """Safely evaluates a single expression. Returns the result."""
109 _allowed_names = {'None': None, 'True': True, 'False': False}
110 if isinstance(node_or_string, basestring):
111 node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
112 if isinstance(node_or_string, ast.Expression):
113 node_or_string = node_or_string.body
114 def _convert(node):
115 if isinstance(node, ast.Str):
116 return node.s
Paweł Hajdan, Jr6f796792017-06-02 06:40:06117 elif isinstance(node, ast.Num):
118 return node.n
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02119 elif isinstance(node, ast.Tuple):
120 return tuple(map(_convert, node.elts))
121 elif isinstance(node, ast.List):
122 return list(map(_convert, node.elts))
123 elif isinstance(node, ast.Dict):
Paweł Hajdan, Jr7cf96a42017-05-26 18:28:35124 return collections.OrderedDict(
125 (_convert(k), _convert(v))
126 for k, v in zip(node.keys, node.values))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02127 elif isinstance(node, ast.Name):
128 if node.id not in _allowed_names:
129 raise ValueError(
130 'invalid name %r (file %r, line %s)' % (
131 node.id, filename, getattr(node, 'lineno', '<unknown>')))
132 return _allowed_names[node.id]
133 elif isinstance(node, ast.Call):
134 if not isinstance(node.func, ast.Name):
135 raise ValueError(
136 'invalid call: func should be a name (file %r, line %s)' % (
137 filename, getattr(node, 'lineno', '<unknown>')))
138 if node.keywords or node.starargs or node.kwargs:
139 raise ValueError(
140 'invalid call: use only regular args (file %r, line %s)' % (
141 filename, getattr(node, 'lineno', '<unknown>')))
142 args = map(_convert, node.args)
143 return global_scope[node.func.id](*args)
144 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
145 return _convert(node.left) + _convert(node.right)
Paweł Hajdan, Jrb7e53332017-05-23 14:57:37146 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
147 return _convert(node.left) % _convert(node.right)
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02148 else:
149 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44150 'unexpected AST node: %s %s (file %r, line %s)' % (
151 node, ast.dump(node), filename,
152 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02153 return _convert(node_or_string)
154
155
Paweł Hajdan, Jrc485d5a2017-06-02 10:08:09156def Exec(content, global_scope, local_scope, filename='<unknown>'):
157 """Safely execs a set of assignments. Mutates |local_scope|."""
158 node_or_string = ast.parse(content, filename=filename, mode='exec')
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02159 if isinstance(node_or_string, ast.Expression):
160 node_or_string = node_or_string.body
161
162 def _visit_in_module(node):
163 if isinstance(node, ast.Assign):
164 if len(node.targets) != 1:
165 raise ValueError(
166 'invalid assignment: use exactly one target (file %r, line %s)' % (
167 filename, getattr(node, 'lineno', '<unknown>')))
168 target = node.targets[0]
169 if not isinstance(target, ast.Name):
170 raise ValueError(
171 'invalid assignment: target should be a name (file %r, line %s)' % (
172 filename, getattr(node, 'lineno', '<unknown>')))
173 value = _gclient_eval(node.value, global_scope, filename=filename)
174
Paweł Hajdan, Jrc485d5a2017-06-02 10:08:09175 if target.id in local_scope:
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02176 raise ValueError(
177 'invalid assignment: overrides var %r (file %r, line %s)' % (
178 target.id, filename, getattr(node, 'lineno', '<unknown>')))
179
Paweł Hajdan, Jrc485d5a2017-06-02 10:08:09180 local_scope[target.id] = value
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02181 else:
182 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44183 'unexpected AST node: %s %s (file %r, line %s)' % (
184 node, ast.dump(node), filename,
185 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02186
187 if isinstance(node_or_string, ast.Module):
188 for stmt in node_or_string.body:
189 _visit_in_module(stmt)
190 else:
191 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44192 'unexpected AST node: %s %s (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02193 node_or_string,
Paweł Hajdan, Jr1ba610b2017-05-24 18:14:44194 ast.dump(node_or_string),
Paweł Hajdan, Jre2f9feec2017-05-09 08:04:02195 filename,
196 getattr(node_or_string, 'lineno', '<unknown>')))
197
Paweł Hajdan, Jrc485d5a2017-06-02 10:08:09198 _GCLIENT_SCHEMA.validate(local_scope)
Paweł Hajdan, Jr76c6ea22017-06-02 19:46:57199
200
201def EvaluateCondition(condition, variables, referenced_variables=None):
202 """Safely evaluates a boolean condition. Returns the result."""
203 if not referenced_variables:
204 referenced_variables = set()
205 _allowed_names = {'None': None, 'True': True, 'False': False}
206 main_node = ast.parse(condition, mode='eval')
207 if isinstance(main_node, ast.Expression):
208 main_node = main_node.body
209 def _convert(node):
210 if isinstance(node, ast.Str):
211 return node.s
212 elif isinstance(node, ast.Name):
213 if node.id in referenced_variables:
214 raise ValueError(
215 'invalid cyclic reference to %r (inside %r)' % (
216 node.id, condition))
217 elif node.id in _allowed_names:
218 return _allowed_names[node.id]
219 elif node.id in variables:
220 return EvaluateCondition(
221 variables[node.id],
222 variables,
223 referenced_variables.union([node.id]))
224 else:
225 raise ValueError(
226 'invalid name %r (inside %r)' % (node.id, condition))
227 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
228 if len(node.values) != 2:
229 raise ValueError(
230 'invalid "or": exactly 2 operands required (inside %r)' % (
231 condition))
232 return _convert(node.values[0]) or _convert(node.values[1])
233 elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
234 if len(node.values) != 2:
235 raise ValueError(
236 'invalid "and": exactly 2 operands required (inside %r)' % (
237 condition))
238 return _convert(node.values[0]) and _convert(node.values[1])
239 elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
240 return not _convert(node.operand)
241 elif isinstance(node, ast.Compare):
242 if len(node.ops) != 1:
243 raise ValueError(
244 'invalid compare: exactly 1 operator required (inside %r)' % (
245 condition))
246 if len(node.comparators) != 1:
247 raise ValueError(
248 'invalid compare: exactly 1 comparator required (inside %r)' % (
249 condition))
250
251 left = _convert(node.left)
252 right = _convert(node.comparators[0])
253
254 if isinstance(node.ops[0], ast.Eq):
255 return left == right
256
257 raise ValueError(
258 'unexpected operator: %s %s (inside %r)' % (
259 node.ops[0], ast.dump(node), condition))
260 else:
261 raise ValueError(
262 'unexpected AST node: %s %s (inside %r)' % (
263 node, ast.dump(node), condition))
264 return _convert(main_node)