blob: b8ed31c7079ff378636f1855c774c0a9f3f32a9a [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
73 # Whitelists deps for which recursion should be enabled.
74 schema.Optional('recursedeps'): [
Paweł Hajdan, Jr05fec032017-05-30 23:04:23 +020075 schema.Optional(schema.Or(
76 basestring,
77 (basestring, basestring),
78 [basestring, basestring]
79 )),
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020080 ],
81
82 # Blacklists directories for checking 'include_rules'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020083 schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020084
85 # Mapping from paths to include rules specific for that path.
86 # See 'include_rules' for more details.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +020087 schema.Optional('specific_include_rules'): {
88 schema.Optional(basestring): [basestring]
89 },
90
91 # List of additional OS names to consider when selecting dependencies
92 # from deps_os.
93 schema.Optional('target_os'): [schema.Optional(basestring)],
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +020094
95 # For recursed-upon sub-dependencies, check out their own dependencies
96 # relative to the paren't path, rather than relative to the .gclient file.
97 schema.Optional('use_relative_paths'): bool,
98
99 # Variables that can be referenced using Var() - see 'deps'.
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200100 schema.Optional('vars'): {schema.Optional(basestring): basestring},
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200101})
102
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200103
104def _gclient_eval(node_or_string, global_scope, filename='<unknown>'):
105 """Safely evaluates a single expression. Returns the result."""
106 _allowed_names = {'None': None, 'True': True, 'False': False}
107 if isinstance(node_or_string, basestring):
108 node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
109 if isinstance(node_or_string, ast.Expression):
110 node_or_string = node_or_string.body
111 def _convert(node):
112 if isinstance(node, ast.Str):
113 return node.s
114 elif isinstance(node, ast.Tuple):
115 return tuple(map(_convert, node.elts))
116 elif isinstance(node, ast.List):
117 return list(map(_convert, node.elts))
118 elif isinstance(node, ast.Dict):
Paweł Hajdan, Jr7cf96a42017-05-26 20:28:35 +0200119 return collections.OrderedDict(
120 (_convert(k), _convert(v))
121 for k, v in zip(node.keys, node.values))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200122 elif isinstance(node, ast.Name):
123 if node.id not in _allowed_names:
124 raise ValueError(
125 'invalid name %r (file %r, line %s)' % (
126 node.id, filename, getattr(node, 'lineno', '<unknown>')))
127 return _allowed_names[node.id]
128 elif isinstance(node, ast.Call):
129 if not isinstance(node.func, ast.Name):
130 raise ValueError(
131 'invalid call: func should be a name (file %r, line %s)' % (
132 filename, getattr(node, 'lineno', '<unknown>')))
133 if node.keywords or node.starargs or node.kwargs:
134 raise ValueError(
135 'invalid call: use only regular args (file %r, line %s)' % (
136 filename, getattr(node, 'lineno', '<unknown>')))
137 args = map(_convert, node.args)
138 return global_scope[node.func.id](*args)
139 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
140 return _convert(node.left) + _convert(node.right)
Paweł Hajdan, Jrb7e53332017-05-23 16:57:37 +0200141 elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
142 return _convert(node.left) % _convert(node.right)
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200143 else:
144 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200145 'unexpected AST node: %s %s (file %r, line %s)' % (
146 node, ast.dump(node), filename,
147 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200148 return _convert(node_or_string)
149
150
151def _gclient_exec(node_or_string, global_scope, filename='<unknown>'):
152 """Safely execs a set of assignments. Returns resulting scope."""
153 result_scope = {}
154
155 if isinstance(node_or_string, basestring):
156 node_or_string = ast.parse(node_or_string, filename=filename, mode='exec')
157 if isinstance(node_or_string, ast.Expression):
158 node_or_string = node_or_string.body
159
160 def _visit_in_module(node):
161 if isinstance(node, ast.Assign):
162 if len(node.targets) != 1:
163 raise ValueError(
164 'invalid assignment: use exactly one target (file %r, line %s)' % (
165 filename, getattr(node, 'lineno', '<unknown>')))
166 target = node.targets[0]
167 if not isinstance(target, ast.Name):
168 raise ValueError(
169 'invalid assignment: target should be a name (file %r, line %s)' % (
170 filename, getattr(node, 'lineno', '<unknown>')))
171 value = _gclient_eval(node.value, global_scope, filename=filename)
172
173 if target.id in result_scope:
174 raise ValueError(
175 'invalid assignment: overrides var %r (file %r, line %s)' % (
176 target.id, filename, getattr(node, 'lineno', '<unknown>')))
177
178 result_scope[target.id] = value
179 else:
180 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200181 'unexpected AST node: %s %s (file %r, line %s)' % (
182 node, ast.dump(node), filename,
183 getattr(node, 'lineno', '<unknown>')))
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200184
185 if isinstance(node_or_string, ast.Module):
186 for stmt in node_or_string.body:
187 _visit_in_module(stmt)
188 else:
189 raise ValueError(
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200190 'unexpected AST node: %s %s (file %r, line %s)' % (
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200191 node_or_string,
Paweł Hajdan, Jr1ba610b2017-05-24 20:14:44 +0200192 ast.dump(node_or_string),
Paweł Hajdan, Jre2f9feec2017-05-09 10:04:02 +0200193 filename,
194 getattr(node_or_string, 'lineno', '<unknown>')))
195
196 return result_scope
197
198
199class CheckFailure(Exception):
200 """Contains details of a check failure."""
201 def __init__(self, msg, path, exp, act):
202 super(CheckFailure, self).__init__(msg)
203 self.path = path
204 self.exp = exp
205 self.act = act
206
207
208def Check(content, path, global_scope, expected_scope):
209 """Cross-checks the old and new gclient eval logic.
210
211 Safely execs |content| (backed by file |path|) using |global_scope|,
212 and compares with |expected_scope|.
213
214 Throws CheckFailure if any difference between |expected_scope| and scope
215 returned by new gclient eval code is detected.
216 """
217 def fail(prefix, exp, act):
218 raise CheckFailure(
219 'gclient check for %s: %s exp %s, got %s' % (
220 path, prefix, repr(exp), repr(act)), prefix, exp, act)
221
222 def compare(expected, actual, var_path, actual_scope):
223 if isinstance(expected, dict):
224 exp = set(expected.keys())
225 act = set(actual.keys())
226 if exp != act:
227 fail(var_path, exp, act)
228 for k in expected:
229 compare(expected[k], actual[k], var_path + '["%s"]' % k, actual_scope)
230 return
231 elif isinstance(expected, list):
232 exp = len(expected)
233 act = len(actual)
234 if exp != act:
235 fail('len(%s)' % var_path, expected_scope, actual_scope)
236 for i in range(exp):
237 compare(expected[i], actual[i], var_path + '[%d]' % i, actual_scope)
238 else:
239 if expected != actual:
240 fail(var_path, expected_scope, actual_scope)
241
242 result_scope = _gclient_exec(content, global_scope, filename=path)
243
244 compare(expected_scope, result_scope, '', result_scope)
Paweł Hajdan, Jrbeec0062017-05-10 21:51:05 +0200245
246 _GCLIENT_SCHEMA.validate(result_scope)