gclient_eval.py 36 KB

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