gclient_eval.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. # 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. import ast
  5. import collections
  6. import logging
  7. import sys
  8. import tokenize
  9. import gclient_utils
  10. from third_party import schema
  11. from third_party import six
  12. if six.PY2:
  13. # We use cStringIO.StringIO because it is equivalent to Py3's io.StringIO.
  14. from cStringIO import StringIO
  15. else:
  16. from io import StringIO
  17. # pylint: disable=redefined-builtin
  18. basestring = str
  19. class _NodeDict(collections.MutableMapping):
  20. """Dict-like type that also stores information on AST nodes and tokens."""
  21. def __init__(self, data, tokens=None):
  22. self.data = collections.OrderedDict(data)
  23. self.tokens = tokens
  24. def __str__(self):
  25. return str({k: v[0] for k, v in self.data.items()})
  26. def __repr__(self):
  27. return self.__str__()
  28. def __getitem__(self, key):
  29. return self.data[key][0]
  30. def __setitem__(self, key, value):
  31. self.data[key] = (value, None)
  32. def __delitem__(self, key):
  33. del self.data[key]
  34. def __iter__(self):
  35. return iter(self.data)
  36. def __len__(self):
  37. return len(self.data)
  38. def MoveTokens(self, origin, delta):
  39. if self.tokens:
  40. new_tokens = {}
  41. for pos, token in self.tokens.items():
  42. if pos[0] >= origin:
  43. pos = (pos[0] + delta, pos[1])
  44. token = token[:2] + (pos,) + token[3:]
  45. new_tokens[pos] = token
  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. def GetNode(self, key):
  52. return self.data[key][1]
  53. def SetNode(self, key, value, node):
  54. self.data[key] = (value, node)
  55. def _NodeDictSchema(dict_schema):
  56. """Validate dict_schema after converting _NodeDict to a regular dict."""
  57. def validate(d):
  58. schema.Schema(dict_schema).validate(dict(d))
  59. return True
  60. return validate
  61. # See https://github.com/keleshev/schema for docs how to configure schema.
  62. _GCLIENT_DEPS_SCHEMA = _NodeDictSchema({
  63. schema.Optional(basestring):
  64. schema.Or(
  65. None,
  66. basestring,
  67. _NodeDictSchema({
  68. # Repo and revision to check out under the path
  69. # (same as if no dict was used).
  70. 'url': schema.Or(None, basestring),
  71. # Optional condition string. The dep will only be processed
  72. # if the condition evaluates to True.
  73. schema.Optional('condition'): basestring,
  74. schema.Optional('dep_type', default='git'): basestring,
  75. }),
  76. # CIPD package.
  77. _NodeDictSchema({
  78. 'packages': [
  79. _NodeDictSchema({
  80. 'package': basestring,
  81. 'version': basestring,
  82. })
  83. ],
  84. schema.Optional('condition'): basestring,
  85. schema.Optional('dep_type', default='cipd'): basestring,
  86. }),
  87. ),
  88. })
  89. _GCLIENT_HOOKS_SCHEMA = [
  90. _NodeDictSchema({
  91. # Hook action: list of command-line arguments to invoke.
  92. 'action': [basestring],
  93. # Name of the hook. Doesn't affect operation.
  94. schema.Optional('name'): basestring,
  95. # Hook pattern (regex). Originally intended to limit some hooks to run
  96. # only when files matching the pattern have changed. In practice, with
  97. # git, gclient runs all the hooks regardless of this field.
  98. schema.Optional('pattern'): basestring,
  99. # Working directory where to execute the hook.
  100. schema.Optional('cwd'): basestring,
  101. # Optional condition string. The hook will only be run
  102. # if the condition evaluates to True.
  103. schema.Optional('condition'): basestring,
  104. })
  105. ]
  106. _GCLIENT_SCHEMA = schema.Schema(
  107. _NodeDictSchema({
  108. # List of host names from which dependencies are allowed (whitelist).
  109. # NOTE: when not present, all hosts are allowed.
  110. # NOTE: scoped to current DEPS file, not recursive.
  111. schema.Optional('allowed_hosts'): [schema.Optional(basestring)],
  112. # Mapping from paths to repo and revision to check out under that path.
  113. # Applying this mapping to the on-disk checkout is the main purpose
  114. # of gclient, and also why the config file is called DEPS.
  115. #
  116. # The following functions are allowed:
  117. #
  118. # Var(): allows variable substitution (either from 'vars' dict below,
  119. # or command-line override)
  120. schema.Optional('deps'): _GCLIENT_DEPS_SCHEMA,
  121. # Similar to 'deps' (see above) - also keyed by OS (e.g. 'linux').
  122. # Also see 'target_os'.
  123. schema.Optional('deps_os'): _NodeDictSchema({
  124. schema.Optional(basestring): _GCLIENT_DEPS_SCHEMA,
  125. }),
  126. # Dependency to get gclient_gn_args* settings from. This allows these
  127. # values to be set in a recursedeps file, rather than requiring that
  128. # they exist in the top-level solution.
  129. schema.Optional('gclient_gn_args_from'): basestring,
  130. # Path to GN args file to write selected variables.
  131. schema.Optional('gclient_gn_args_file'): basestring,
  132. # Subset of variables to write to the GN args file (see above).
  133. schema.Optional('gclient_gn_args'): [schema.Optional(basestring)],
  134. # Hooks executed after gclient sync (unless suppressed), or explicitly
  135. # on gclient hooks. See _GCLIENT_HOOKS_SCHEMA for details.
  136. # Also see 'pre_deps_hooks'.
  137. schema.Optional('hooks'): _GCLIENT_HOOKS_SCHEMA,
  138. # Similar to 'hooks', also keyed by OS.
  139. schema.Optional('hooks_os'): _NodeDictSchema({
  140. schema.Optional(basestring): _GCLIENT_HOOKS_SCHEMA
  141. }),
  142. # Rules which #includes are allowed in the directory.
  143. # Also see 'skip_child_includes' and 'specific_include_rules'.
  144. schema.Optional('include_rules'): [schema.Optional(basestring)],
  145. # Hooks executed before processing DEPS. See 'hooks' for more details.
  146. schema.Optional('pre_deps_hooks'): _GCLIENT_HOOKS_SCHEMA,
  147. # Recursion limit for nested DEPS.
  148. schema.Optional('recursion'): int,
  149. # Whitelists deps for which recursion should be enabled.
  150. schema.Optional('recursedeps'): [
  151. schema.Optional(schema.Or(
  152. basestring,
  153. (basestring, basestring),
  154. [basestring, basestring]
  155. )),
  156. ],
  157. # Blacklists directories for checking 'include_rules'.
  158. schema.Optional('skip_child_includes'): [schema.Optional(basestring)],
  159. # Mapping from paths to include rules specific for that path.
  160. # See 'include_rules' for more details.
  161. schema.Optional('specific_include_rules'): _NodeDictSchema({
  162. schema.Optional(basestring): [basestring]
  163. }),
  164. # List of additional OS names to consider when selecting dependencies
  165. # from deps_os.
  166. schema.Optional('target_os'): [schema.Optional(basestring)],
  167. # For recursed-upon sub-dependencies, check out their own dependencies
  168. # relative to the parent's path, rather than relative to the .gclient
  169. # file.
  170. schema.Optional('use_relative_paths'): bool,
  171. # For recursed-upon sub-dependencies, run their hooks relative to the
  172. # parent's path instead of relative to the .gclient file.
  173. schema.Optional('use_relative_hooks'): bool,
  174. # Variables that can be referenced using Var() - see 'deps'.
  175. schema.Optional('vars'): _NodeDictSchema({
  176. schema.Optional(basestring): schema.Or(basestring, bool),
  177. }),
  178. }))
  179. def _gclient_eval(node_or_string, filename='<unknown>', vars_dict=None):
  180. """Safely evaluates a single expression. Returns the result."""
  181. _allowed_names = {'None': None, 'True': True, 'False': False}
  182. if isinstance(node_or_string, basestring):
  183. node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
  184. if isinstance(node_or_string, ast.Expression):
  185. node_or_string = node_or_string.body
  186. def _convert(node):
  187. if isinstance(node, ast.Str):
  188. if vars_dict is None:
  189. return node.s
  190. try:
  191. return node.s.format(**vars_dict)
  192. except KeyError as e:
  193. raise KeyError(
  194. '%s was used as a variable, but was not declared in the vars dict '
  195. '(file %r, line %s)' % (
  196. e.args[0], filename, getattr(node, 'lineno', '<unknown>')))
  197. elif isinstance(node, ast.Num):
  198. return node.n
  199. elif isinstance(node, ast.Tuple):
  200. return tuple(map(_convert, node.elts))
  201. elif isinstance(node, ast.List):
  202. return list(map(_convert, node.elts))
  203. elif isinstance(node, ast.Dict):
  204. return _NodeDict((_convert(k), (_convert(v), v))
  205. for k, v in zip(node.keys, node.values))
  206. elif isinstance(node, ast.Name):
  207. if node.id not in _allowed_names:
  208. raise ValueError(
  209. 'invalid name %r (file %r, line %s)' % (
  210. node.id, filename, getattr(node, 'lineno', '<unknown>')))
  211. return _allowed_names[node.id]
  212. elif not sys.version_info[:2] < (3, 4) and isinstance(
  213. node, ast.NameConstant): # Since Python 3.4
  214. return node.value
  215. elif isinstance(node, ast.Call):
  216. if not isinstance(node.func, ast.Name) or node.func.id != 'Var':
  217. raise ValueError(
  218. 'Var is the only allowed function (file %r, line %s)' % (
  219. filename, getattr(node, 'lineno', '<unknown>')))
  220. if node.keywords or getattr(node, 'starargs', None) or getattr(
  221. node, 'kwargs', None) or len(node.args) != 1:
  222. raise ValueError(
  223. 'Var takes exactly one argument (file %r, line %s)' % (
  224. filename, getattr(node, 'lineno', '<unknown>')))
  225. arg = _convert(node.args[0])
  226. if not isinstance(arg, basestring):
  227. raise ValueError(
  228. 'Var\'s argument must be a variable name (file %r, line %s)' % (
  229. filename, getattr(node, 'lineno', '<unknown>')))
  230. if vars_dict is None:
  231. return '{' + arg + '}'
  232. if arg not in vars_dict:
  233. raise KeyError(
  234. '%s was used as a variable, but was not declared in the vars dict '
  235. '(file %r, line %s)' % (
  236. arg, filename, getattr(node, 'lineno', '<unknown>')))
  237. return vars_dict[arg]
  238. elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
  239. return _convert(node.left) + _convert(node.right)
  240. elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
  241. return _convert(node.left) % _convert(node.right)
  242. else:
  243. raise ValueError(
  244. 'unexpected AST node: %s %s (file %r, line %s)' % (
  245. node, ast.dump(node), filename,
  246. getattr(node, 'lineno', '<unknown>')))
  247. return _convert(node_or_string)
  248. def Exec(content, filename='<unknown>', vars_override=None, builtin_vars=None):
  249. """Safely execs a set of assignments."""
  250. def _validate_statement(node, local_scope):
  251. if not isinstance(node, ast.Assign):
  252. raise ValueError(
  253. 'unexpected AST node: %s %s (file %r, line %s)' % (
  254. node, ast.dump(node), filename,
  255. getattr(node, 'lineno', '<unknown>')))
  256. if len(node.targets) != 1:
  257. raise ValueError(
  258. 'invalid assignment: use exactly one target (file %r, line %s)' % (
  259. filename, getattr(node, 'lineno', '<unknown>')))
  260. target = node.targets[0]
  261. if not isinstance(target, ast.Name):
  262. raise ValueError(
  263. 'invalid assignment: target should be a name (file %r, line %s)' % (
  264. filename, getattr(node, 'lineno', '<unknown>')))
  265. if target.id in local_scope:
  266. raise ValueError(
  267. 'invalid assignment: overrides var %r (file %r, line %s)' % (
  268. target.id, filename, getattr(node, 'lineno', '<unknown>')))
  269. node_or_string = ast.parse(content, filename=filename, mode='exec')
  270. if isinstance(node_or_string, ast.Expression):
  271. node_or_string = node_or_string.body
  272. if not isinstance(node_or_string, ast.Module):
  273. raise ValueError(
  274. 'unexpected AST node: %s %s (file %r, line %s)' % (
  275. node_or_string,
  276. ast.dump(node_or_string),
  277. filename,
  278. getattr(node_or_string, 'lineno', '<unknown>')))
  279. statements = {}
  280. for statement in node_or_string.body:
  281. _validate_statement(statement, statements)
  282. statements[statement.targets[0].id] = statement.value
  283. # The tokenized representation needs to end with a newline token, otherwise
  284. # untokenization will trigger an assert later on.
  285. # In Python 2.7 on Windows we need to ensure the input ends with a newline
  286. # for a newline token to be generated.
  287. # In other cases a newline token is always generated during tokenization so
  288. # this has no effect.
  289. # TODO: Remove this workaround after migrating to Python 3.
  290. content += '\n'
  291. tokens = {
  292. token[2]: list(token) for token in tokenize.generate_tokens(
  293. StringIO(content).readline)
  294. }
  295. local_scope = _NodeDict({}, tokens)
  296. # Process vars first, so we can expand variables in the rest of the DEPS file.
  297. vars_dict = {}
  298. if 'vars' in statements:
  299. vars_statement = statements['vars']
  300. value = _gclient_eval(vars_statement, filename)
  301. local_scope.SetNode('vars', value, vars_statement)
  302. # Update the parsed vars with the overrides, but only if they are already
  303. # present (overrides do not introduce new variables).
  304. vars_dict.update(value)
  305. if builtin_vars:
  306. vars_dict.update(builtin_vars)
  307. if vars_override:
  308. vars_dict.update({k: v for k, v in vars_override.items() if k in vars_dict})
  309. for name, node in statements.items():
  310. value = _gclient_eval(node, filename, vars_dict)
  311. local_scope.SetNode(name, value, node)
  312. return _GCLIENT_SCHEMA.validate(local_scope)
  313. def ExecLegacy(content, filename='<unknown>', vars_override=None,
  314. builtin_vars=None):
  315. """Executes a DEPS file |content| using exec."""
  316. local_scope = {}
  317. global_scope = {'Var': lambda var_name: '{%s}' % var_name}
  318. # If we use 'exec' directly, it complains that 'Parse' contains a nested
  319. # function with free variables.
  320. # This is because on versions of Python < 2.7.9, "exec(a, b, c)" not the same
  321. # as "exec a in b, c" (See https://bugs.python.org/issue21591).
  322. eval(compile(content, filename, 'exec'), global_scope, local_scope)
  323. vars_dict = {}
  324. vars_dict.update(local_scope.get('vars', {}))
  325. if builtin_vars:
  326. vars_dict.update(builtin_vars)
  327. if vars_override:
  328. vars_dict.update({k: v for k, v in vars_override.items() if k in vars_dict})
  329. if not vars_dict:
  330. return local_scope
  331. def _DeepFormat(node):
  332. if isinstance(node, basestring):
  333. return node.format(**vars_dict)
  334. elif isinstance(node, dict):
  335. return {k.format(**vars_dict): _DeepFormat(v) for k, v in node.items()}
  336. elif isinstance(node, list):
  337. return [_DeepFormat(elem) for elem in node]
  338. elif isinstance(node, tuple):
  339. return tuple(_DeepFormat(elem) for elem in node)
  340. else:
  341. return node
  342. return _DeepFormat(local_scope)
  343. def _StandardizeDeps(deps_dict, vars_dict):
  344. """"Standardizes the deps_dict.
  345. For each dependency:
  346. - Expands the variable in the dependency name.
  347. - Ensures the dependency is a dictionary.
  348. - Set's the 'dep_type' to be 'git' by default.
  349. """
  350. new_deps_dict = {}
  351. for dep_name, dep_info in deps_dict.items():
  352. dep_name = dep_name.format(**vars_dict)
  353. if not isinstance(dep_info, collections.Mapping):
  354. dep_info = {'url': dep_info}
  355. dep_info.setdefault('dep_type', 'git')
  356. new_deps_dict[dep_name] = dep_info
  357. return new_deps_dict
  358. def _MergeDepsOs(deps_dict, os_deps_dict, os_name):
  359. """Merges the deps in os_deps_dict into conditional dependencies in deps_dict.
  360. The dependencies in os_deps_dict are transformed into conditional dependencies
  361. using |'checkout_' + os_name|.
  362. If the dependency is already present, the URL and revision must coincide.
  363. """
  364. for dep_name, dep_info in os_deps_dict.items():
  365. # Make this condition very visible, so it's not a silent failure.
  366. # It's unclear how to support None override in deps_os.
  367. if dep_info['url'] is None:
  368. logging.error('Ignoring %r:%r in %r deps_os', dep_name, dep_info, os_name)
  369. continue
  370. os_condition = 'checkout_' + (os_name if os_name != 'unix' else 'linux')
  371. UpdateCondition(dep_info, 'and', os_condition)
  372. if dep_name in deps_dict:
  373. if deps_dict[dep_name]['url'] != dep_info['url']:
  374. raise gclient_utils.Error(
  375. 'Value from deps_os (%r; %r: %r) conflicts with existing deps '
  376. 'entry (%r).' % (
  377. os_name, dep_name, dep_info, deps_dict[dep_name]))
  378. UpdateCondition(dep_info, 'or', deps_dict[dep_name].get('condition'))
  379. deps_dict[dep_name] = dep_info
  380. def UpdateCondition(info_dict, op, new_condition):
  381. """Updates info_dict's condition with |new_condition|.
  382. An absent value is treated as implicitly True.
  383. """
  384. curr_condition = info_dict.get('condition')
  385. # Easy case: Both are present.
  386. if curr_condition and new_condition:
  387. info_dict['condition'] = '(%s) %s (%s)' % (
  388. curr_condition, op, new_condition)
  389. # If |op| == 'and', and at least one condition is present, then use it.
  390. elif op == 'and' and (curr_condition or new_condition):
  391. info_dict['condition'] = curr_condition or new_condition
  392. # Otherwise, no condition should be set
  393. elif curr_condition:
  394. del info_dict['condition']
  395. def Parse(content, validate_syntax, filename, vars_override=None,
  396. builtin_vars=None):
  397. """Parses DEPS strings.
  398. Executes the Python-like string stored in content, resulting in a Python
  399. dictionary specifyied by the schema above. Supports syntax validation and
  400. variable expansion.
  401. Args:
  402. content: str. DEPS file stored as a string.
  403. validate_syntax: bool. Whether syntax should be validated using the schema
  404. defined above.
  405. filename: str. The name of the DEPS file, or a string describing the source
  406. of the content, e.g. '<string>', '<unknown>'.
  407. vars_override: dict, optional. A dictionary with overrides for the variables
  408. defined by the DEPS file.
  409. builtin_vars: dict, optional. A dictionary with variables that are provided
  410. by default.
  411. Returns:
  412. A Python dict with the parsed contents of the DEPS file, as specified by the
  413. schema above.
  414. """
  415. if validate_syntax:
  416. result = Exec(content, filename, vars_override, builtin_vars)
  417. else:
  418. result = ExecLegacy(content, filename, vars_override, builtin_vars)
  419. vars_dict = result.get('vars', {})
  420. if 'deps' in result:
  421. result['deps'] = _StandardizeDeps(result['deps'], vars_dict)
  422. if 'deps_os' in result:
  423. deps = result.setdefault('deps', {})
  424. for os_name, os_deps in result['deps_os'].items():
  425. os_deps = _StandardizeDeps(os_deps, vars_dict)
  426. _MergeDepsOs(deps, os_deps, os_name)
  427. del result['deps_os']
  428. if 'hooks_os' in result:
  429. hooks = result.setdefault('hooks', [])
  430. for os_name, os_hooks in result['hooks_os'].items():
  431. for hook in os_hooks:
  432. UpdateCondition(hook, 'and', 'checkout_' + os_name)
  433. hooks.extend(os_hooks)
  434. del result['hooks_os']
  435. return result
  436. def EvaluateCondition(condition, variables, referenced_variables=None):
  437. """Safely evaluates a boolean condition. Returns the result."""
  438. if not referenced_variables:
  439. referenced_variables = set()
  440. _allowed_names = {'None': None, 'True': True, 'False': False}
  441. main_node = ast.parse(condition, mode='eval')
  442. if isinstance(main_node, ast.Expression):
  443. main_node = main_node.body
  444. def _convert(node, allow_tuple=False):
  445. if isinstance(node, ast.Str):
  446. return node.s
  447. elif isinstance(node, ast.Tuple) and allow_tuple:
  448. return tuple(map(_convert, node.elts))
  449. elif isinstance(node, ast.Name):
  450. if node.id in referenced_variables:
  451. raise ValueError(
  452. 'invalid cyclic reference to %r (inside %r)' % (
  453. node.id, condition))
  454. elif node.id in _allowed_names:
  455. return _allowed_names[node.id]
  456. elif node.id in variables:
  457. value = variables[node.id]
  458. # Allow using "native" types, without wrapping everything in strings.
  459. # Note that schema constraints still apply to variables.
  460. if not isinstance(value, basestring):
  461. return value
  462. # Recursively evaluate the variable reference.
  463. return EvaluateCondition(
  464. variables[node.id],
  465. variables,
  466. referenced_variables.union([node.id]))
  467. else:
  468. # Implicitly convert unrecognized names to strings.
  469. # If we want to change this, we'll need to explicitly distinguish
  470. # between arguments for GN to be passed verbatim, and ones to
  471. # be evaluated.
  472. return node.id
  473. elif not sys.version_info[:2] < (3, 4) and isinstance(
  474. node, ast.NameConstant): # Since Python 3.4
  475. return node.value
  476. elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.Or):
  477. bool_values = []
  478. for value in node.values:
  479. bool_values.append(_convert(value))
  480. if not isinstance(bool_values[-1], bool):
  481. raise ValueError(
  482. 'invalid "or" operand %r (inside %r)' % (
  483. bool_values[-1], condition))
  484. return any(bool_values)
  485. elif isinstance(node, ast.BoolOp) and isinstance(node.op, ast.And):
  486. bool_values = []
  487. for value in node.values:
  488. bool_values.append(_convert(value))
  489. if not isinstance(bool_values[-1], bool):
  490. raise ValueError(
  491. 'invalid "and" operand %r (inside %r)' % (
  492. bool_values[-1], condition))
  493. return all(bool_values)
  494. elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
  495. value = _convert(node.operand)
  496. if not isinstance(value, bool):
  497. raise ValueError(
  498. 'invalid "not" operand %r (inside %r)' % (value, condition))
  499. return not value
  500. elif isinstance(node, ast.Compare):
  501. if len(node.ops) != 1:
  502. raise ValueError(
  503. 'invalid compare: exactly 1 operator required (inside %r)' % (
  504. condition))
  505. if len(node.comparators) != 1:
  506. raise ValueError(
  507. 'invalid compare: exactly 1 comparator required (inside %r)' % (
  508. condition))
  509. left = _convert(node.left)
  510. right = _convert(
  511. node.comparators[0], allow_tuple=isinstance(node.ops[0], ast.In))
  512. if isinstance(node.ops[0], ast.Eq):
  513. return left == right
  514. if isinstance(node.ops[0], ast.NotEq):
  515. return left != right
  516. if isinstance(node.ops[0], ast.In):
  517. return left in right
  518. raise ValueError(
  519. 'unexpected operator: %s %s (inside %r)' % (
  520. node.ops[0], ast.dump(node), condition))
  521. else:
  522. raise ValueError(
  523. 'unexpected AST node: %s %s (inside %r)' % (
  524. node, ast.dump(node), condition))
  525. return _convert(main_node)
  526. def RenderDEPSFile(gclient_dict):
  527. contents = sorted(gclient_dict.tokens.values(), key=lambda token: token[2])
  528. # The last token is a newline, which we ensure in Exec() for compatibility.
  529. # However tests pass in inputs not ending with a newline and expect the same
  530. # back, so for backwards compatibility need to remove that newline character.
  531. # TODO: Fix tests to expect the newline
  532. return tokenize.untokenize(contents)[:-1]
  533. def _UpdateAstString(tokens, node, value):
  534. position = node.lineno, node.col_offset
  535. quote_char = ''
  536. if isinstance(node, ast.Str):
  537. quote_char = tokens[position][1][0]
  538. tokens[position][1] = quote_char + value + quote_char
  539. node.s = value
  540. def _ShiftLinesInTokens(tokens, delta, start):
  541. new_tokens = {}
  542. for token in tokens.values():
  543. if token[2][0] >= start:
  544. token[2] = token[2][0] + delta, token[2][1]
  545. token[3] = token[3][0] + delta, token[3][1]
  546. new_tokens[token[2]] = token
  547. return new_tokens
  548. def AddVar(gclient_dict, var_name, value):
  549. if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
  550. raise ValueError(
  551. "Can't use SetVar for the given gclient dict. It contains no "
  552. "formatting information.")
  553. if 'vars' not in gclient_dict:
  554. raise KeyError("vars dict is not defined.")
  555. if var_name in gclient_dict['vars']:
  556. raise ValueError(
  557. "%s has already been declared in the vars dict. Consider using SetVar "
  558. "instead." % var_name)
  559. if not gclient_dict['vars']:
  560. raise ValueError('vars dict is empty. This is not yet supported.')
  561. # We will attempt to add the var right after 'vars = {'.
  562. node = gclient_dict.GetNode('vars')
  563. if node is None:
  564. raise ValueError(
  565. "The vars dict has no formatting information." % var_name)
  566. line = node.lineno + 1
  567. # We will try to match the new var's indentation to the next variable.
  568. col = node.keys[0].col_offset
  569. # We use a minimal Python dictionary, so that ast can parse it.
  570. var_content = '{\n%s"%s": "%s",\n}\n' % (' ' * col, var_name, value)
  571. var_ast = ast.parse(var_content).body[0].value
  572. # Set the ast nodes for the key and value.
  573. vars_node = gclient_dict.GetNode('vars')
  574. var_name_node = var_ast.keys[0]
  575. var_name_node.lineno += line - 2
  576. vars_node.keys.insert(0, var_name_node)
  577. value_node = var_ast.values[0]
  578. value_node.lineno += line - 2
  579. vars_node.values.insert(0, value_node)
  580. # Update the tokens.
  581. var_tokens = list(tokenize.generate_tokens(StringIO(var_content).readline))
  582. var_tokens = {
  583. token[2]: list(token)
  584. # Ignore the tokens corresponding to braces and new lines.
  585. for token in var_tokens[2:-3]
  586. }
  587. gclient_dict.tokens = _ShiftLinesInTokens(gclient_dict.tokens, 1, line)
  588. gclient_dict.tokens.update(_ShiftLinesInTokens(var_tokens, line - 2, 0))
  589. def SetVar(gclient_dict, var_name, value):
  590. if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
  591. raise ValueError(
  592. "Can't use SetVar for the given gclient dict. It contains no "
  593. "formatting information.")
  594. tokens = gclient_dict.tokens
  595. if 'vars' not in gclient_dict:
  596. raise KeyError("vars dict is not defined.")
  597. if var_name not in gclient_dict['vars']:
  598. raise ValueError(
  599. "%s has not been declared in the vars dict. Consider using AddVar "
  600. "instead." % var_name)
  601. node = gclient_dict['vars'].GetNode(var_name)
  602. if node is None:
  603. raise ValueError(
  604. "The vars entry for %s has no formatting information." % var_name)
  605. _UpdateAstString(tokens, node, value)
  606. gclient_dict['vars'].SetNode(var_name, value, node)
  607. def _GetVarName(node):
  608. if isinstance(node, ast.Call):
  609. return node.args[0].s
  610. elif node.s.endswith('}'):
  611. last_brace = node.s.rfind('{')
  612. return node.s[last_brace+1:-1]
  613. return None
  614. def SetCIPD(gclient_dict, dep_name, package_name, new_version):
  615. if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
  616. raise ValueError(
  617. "Can't use SetCIPD for the given gclient dict. It contains no "
  618. "formatting information.")
  619. tokens = gclient_dict.tokens
  620. if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
  621. raise KeyError(
  622. "Could not find any dependency called %s." % dep_name)
  623. # Find the package with the given name
  624. packages = [
  625. package
  626. for package in gclient_dict['deps'][dep_name]['packages']
  627. if package['package'] == package_name
  628. ]
  629. if len(packages) != 1:
  630. raise ValueError(
  631. "There must be exactly one package with the given name (%s), "
  632. "%s were found." % (package_name, len(packages)))
  633. # TODO(ehmaldonado): Support Var in package's version.
  634. node = packages[0].GetNode('version')
  635. if node is None:
  636. raise ValueError(
  637. "The deps entry for %s:%s has no formatting information." %
  638. (dep_name, package_name))
  639. if not isinstance(node, ast.Call) and not isinstance(node, ast.Str):
  640. raise ValueError(
  641. "Unsupported dependency revision format. Please file a bug to the "
  642. "Infra>SDK component in crbug.com")
  643. var_name = _GetVarName(node)
  644. if var_name is not None:
  645. SetVar(gclient_dict, var_name, new_version)
  646. else:
  647. _UpdateAstString(tokens, node, new_version)
  648. packages[0].SetNode('version', new_version, node)
  649. def SetRevision(gclient_dict, dep_name, new_revision):
  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. node = dep_node
  656. if isinstance(node, ast.BinOp):
  657. node = node.right
  658. if not isinstance(node, ast.Call) and not isinstance(node, ast.Str):
  659. raise ValueError(
  660. "Unsupported dependency revision format. Please file a bug to the "
  661. "Infra>SDK component in crbug.com")
  662. var_name = _GetVarName(node)
  663. if var_name is not None:
  664. SetVar(gclient_dict, var_name, new_revision)
  665. else:
  666. if '@' in node.s:
  667. # '@' is part of the last string, which we want to modify. Discard
  668. # whatever was after the '@' and put the new revision in its place.
  669. new_revision = node.s.split('@')[0] + '@' + new_revision
  670. elif '@' not in dep_dict[dep_key]:
  671. # '@' is not part of the URL at all. This mean the dependency is
  672. # unpinned and we should pin it.
  673. new_revision = node.s + '@' + new_revision
  674. _UpdateAstString(tokens, node, new_revision)
  675. dep_dict.SetNode(dep_key, new_revision, node)
  676. if not isinstance(gclient_dict, _NodeDict) or gclient_dict.tokens is None:
  677. raise ValueError(
  678. "Can't use SetRevision for the given gclient dict. It contains no "
  679. "formatting information.")
  680. tokens = gclient_dict.tokens
  681. if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
  682. raise KeyError(
  683. "Could not find any dependency called %s." % dep_name)
  684. if isinstance(gclient_dict['deps'][dep_name], _NodeDict):
  685. _UpdateRevision(gclient_dict['deps'][dep_name], 'url', new_revision)
  686. else:
  687. _UpdateRevision(gclient_dict['deps'], dep_name, new_revision)
  688. def GetVar(gclient_dict, var_name):
  689. if 'vars' not in gclient_dict or var_name not in gclient_dict['vars']:
  690. raise KeyError(
  691. "Could not find any variable called %s." % var_name)
  692. return gclient_dict['vars'][var_name]
  693. def GetCIPD(gclient_dict, dep_name, package_name):
  694. if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
  695. raise KeyError(
  696. "Could not find any dependency called %s." % dep_name)
  697. # Find the package with the given name
  698. packages = [
  699. package
  700. for package in gclient_dict['deps'][dep_name]['packages']
  701. if package['package'] == package_name
  702. ]
  703. if len(packages) != 1:
  704. raise ValueError(
  705. "There must be exactly one package with the given name (%s), "
  706. "%s were found." % (package_name, len(packages)))
  707. return packages[0]['version']
  708. def GetRevision(gclient_dict, dep_name):
  709. if 'deps' not in gclient_dict or dep_name not in gclient_dict['deps']:
  710. raise KeyError(
  711. "Could not find any dependency called %s." % dep_name)
  712. dep = gclient_dict['deps'][dep_name]
  713. if dep is None:
  714. return None
  715. elif isinstance(dep, basestring):
  716. _, _, revision = dep.partition('@')
  717. return revision or None
  718. elif isinstance(dep, collections.Mapping) and 'url' in dep:
  719. _, _, revision = dep['url'].partition('@')
  720. return revision or None
  721. else:
  722. raise ValueError(
  723. '%s is not a valid git dependency.' % dep_name)