blob: 8df68f6cc8492b729b442d7acc9b07dca0e4947a [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
Paweł Hajdan, Jr7cf96a42017-05-26 20:28:35 +02006import collections
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +02007
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +02008from 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 16:57:37 +020029 schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020030
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 21:51:05 +020037 # Var(): allows variable substitution (either from 'vars' dict below,
38 # or command-line override)
Paweł Hajdan, Jrc7ba0332017-05-29 16:38:45 +020039 schema.Optional('deps'): {
40 schema.Optional(basestring): schema.Or(
41 basestring,
42 {
43 'url': basestring,
44 },
45 ),
46 },
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020047
48 # Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020049 # 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 21:51:05 +020055
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 16:59:23 -070061 # Similar to 'hooks', also keyed by OS.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020062 schema.Optional('hooks_os'): {
63 schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
64 },
Scott Grahamc4826742017-05-11 16:59:23 -070065
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020066 # Rules which #includes are allowed in the directory.
67 # Also see 'skip_child_includes' and 'specific_include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020068 schema.Optional('include_rules'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020069
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 08:40:06 +020073 # Recursion limit for nested DEPS.
74 schema.Optional('recursion'): int,
75
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020076 # Whitelists deps for which recursion should be enabled.
77 schema.Optional('recursedeps'): [
Paweł Hajdan, Jr05fec032017-05-30 23:04:23 +020078 schema.Optional(schema.Or(
79 basestring,
80 (basestring, basestring),
81 [basestring, basestring]
82 )),
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020083 ],
84
85 # Blacklists directories for checking 'include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020086 schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020087
88 # Mapping from paths to include rules specific for that path.
89 # See 'include_rules' for more details.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020090 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 21:51:05 +020097
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 16:57:37 +0200103 schema.Optional('vars'): {schema.Optional(basestring): basestring},
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200104})
105
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200106
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 08:40:06 +0200117 elif isinstance(node, ast.Num):
118 return node.n
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200119 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 20:28:35 +0200124 return collections.OrderedDict(
125 (_convert(k), _convert(v))
126 for k, v in zip(node.keys, node.values))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200127 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 16:57:37 +0200146 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
147 return _convert(node.left) % _convert(node.right)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200148 else:
149 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200150 '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 10:04:02 +0200153 return _convert(node_or_string)
154
155
156def _gclient_exec(node_or_string, global_scope, filename='<unknown>'):
157 """Safely execs a set of assignments. Returns resulting scope."""
158 result_scope = {}
159
160 if isinstance(node_or_string, basestring):
161 node_or_string = ast.parse(node_or_string, filename=filename, mode='exec')
162 if isinstance(node_or_string, ast.Expression):
163 node_or_string = node_or_string.body
164
165 def _visit_in_module(node):
166 if isinstance(node, ast.Assign):
167 if len(node.targets) != 1:
168 raise ValueError(
169 'invalid assignment: use exactly one target (file %r, line %s)' % (
170 filename, getattr(node, 'lineno', '<unknown>')))
171 target = node.targets[0]
172 if not isinstance(target, ast.Name):
173 raise ValueError(
174 'invalid assignment: target should be a name (file %r, line %s)' % (
175 filename, getattr(node, 'lineno', '<unknown>')))
176 value = _gclient_eval(node.value, global_scope, filename=filename)
177
178 if target.id in result_scope:
179 raise ValueError(
180 'invalid assignment: overrides var %r (file %r, line %s)' % (
181 target.id, filename, getattr(node, 'lineno', '<unknown>')))
182
183 result_scope[target.id] = value
184 else:
185 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200186 'unexpected AST node: %s %s (file %r, line %s)' % (
187 node, ast.dump(node), filename,
188 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200189
190 if isinstance(node_or_string, ast.Module):
191 for stmt in node_or_string.body:
192 _visit_in_module(stmt)
193 else:
194 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200195 'unexpected AST node: %s %s (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200196 node_or_string,
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200197 ast.dump(node_or_string),
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200198 filename,
199 getattr(node_or_string, 'lineno', '<unknown>')))
200
201 return result_scope
202
203
204class CheckFailure(Exception):
205 """Contains details of a check failure."""
206 def __init__(self, msg, path, exp, act):
207 super(CheckFailure, self).__init__(msg)
208 self.path = path
209 self.exp = exp
210 self.act = act
211
212
213def Check(content, path, global_scope, expected_scope):
214 """Cross-checks the old and new gclient eval logic.
215
216 Safely execs |content| (backed by file |path|) using |global_scope|,
217 and compares with |expected_scope|.
218
219 Throws CheckFailure if any difference between |expected_scope| and scope
220 returned by new gclient eval code is detected.
221 """
222 def fail(prefix, exp, act):
223 raise CheckFailure(
224 'gclient check for %s: %s exp %s, got %s' % (
225 path, prefix, repr(exp), repr(act)), prefix, exp, act)
226
227 def compare(expected, actual, var_path, actual_scope):
228 if isinstance(expected, dict):
229 exp = set(expected.keys())
230 act = set(actual.keys())
231 if exp != act:
232 fail(var_path, exp, act)
233 for k in expected:
234 compare(expected[k], actual[k], var_path + '["%s"]' % k, actual_scope)
235 return
236 elif isinstance(expected, list):
237 exp = len(expected)
238 act = len(actual)
239 if exp != act:
240 fail('len(%s)' % var_path, expected_scope, actual_scope)
241 for i in range(exp):
242 compare(expected[i], actual[i], var_path + '[%d]' % i, actual_scope)
243 else:
244 if expected != actual:
245 fail(var_path, expected_scope, actual_scope)
246
247 result_scope = _gclient_exec(content, global_scope, filename=path)
248
249 compare(expected_scope, result_scope, '', result_scope)
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200250
251 _GCLIENT_SCHEMA.validate(result_scope)