Add a Str() function to gclient for use in DEPS files.
gclient's existing functionality for handling variables is
ambiguous: the value of a variable can either be a string literal
or an expression fragment. The implementation is required to
parse a value as an expression, and, if it is legal, treat it
as an expression instead of a literal. This means that
gclient_gn_args_file = 'src/build/args.gni'
gclient_gn_args = ['xcode_version']
vars = {
'xcode_version': 'xcode-12'
}
would cause a problem because gclient would try to parse the
variable as an expression, and 'xcode' would not be defined.
This patch adds a workaround for this, where you can instead
use the Str() function to explicitly tell gclient to treat the
value as a string and not a potential expression.
The above example would be changed to:
gclient_gn_args_file = 'src/build/args.gni'
gclient_gn_args = ['xcode_version']
vars = {
'xcode_version': Str('xcode-12')
}
The variable may still be used in every context where it was legal
to be used before.
Bug: 1099242
Change-Id: Ic2a17eea5f7098113bdba0557fe29e1a931a74b8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2268406
Reviewed-by: Ben Pastene <bpastene@chromium.org>
Reviewed-by: Edward Lesmes <ehmaldonado@chromium.org>
Commit-Queue: Dirk Pranke <dpranke@google.com>
diff --git a/gclient_eval.py b/gclient_eval.py
index a6dd03b..c0098e4 100644
--- a/gclient_eval.py
+++ b/gclient_eval.py
@@ -24,6 +24,27 @@
basestring = str
+class ConstantString(object):
+ def __init__(self, value):
+ self.value = value
+
+ def __format__(self, format_spec):
+ del format_spec
+ return self.value
+
+ def __repr__(self):
+ return "Str('" + self.value + "')"
+
+ def __eq__(self, other):
+ if isinstance(other, ConstantString):
+ return self.value == other.value
+ else:
+ return self.value == other
+
+ def __hash__(self):
+ return self.value.__hash__()
+
+
class _NodeDict(collections_abc.MutableMapping):
"""Dict-like type that also stores information on AST nodes and tokens."""
def __init__(self, data=None, tokens=None):
@@ -114,7 +135,7 @@
_GCLIENT_HOOKS_SCHEMA = [
_NodeDictSchema({
# Hook action: list of command-line arguments to invoke.
- 'action': [basestring],
+ 'action': [schema.Or(basestring)],
# Name of the hook. Doesn't affect operation.
schema.Optional('name'): basestring,
@@ -220,7 +241,9 @@
# Variables that can be referenced using Var() - see 'deps'.
schema.Optional('vars'): _NodeDictSchema({
- schema.Optional(basestring): schema.Or(basestring, bool),
+ schema.Optional(basestring): schema.Or(ConstantString,
+ basestring,
+ bool),
}),
}))
@@ -228,6 +251,8 @@
def _gclient_eval(node_or_string, filename='<unknown>', vars_dict=None):
"""Safely evaluates a single expression. Returns the result."""
_allowed_names = {'None': None, 'True': True, 'False': False}
+ if isinstance(node_or_string, ConstantString):
+ return node_or_string.value
if isinstance(node_or_string, basestring):
node_or_string = ast.parse(node_or_string, filename=filename, mode='eval')
if isinstance(node_or_string, ast.Expression):
@@ -269,16 +294,23 @@
node, ast.NameConstant): # Since Python 3.4
return node.value
elif isinstance(node, ast.Call):
- if not isinstance(node.func, ast.Name) or node.func.id != 'Var':
+ if (not isinstance(node.func, ast.Name) or
+ (node.func.id not in ('Str', 'Var'))):
raise ValueError(
- 'Var is the only allowed function (file %r, line %s)' % (
+ 'Str and Var are the only allowed functions (file %r, line %s)' % (
filename, getattr(node, 'lineno', '<unknown>')))
if node.keywords or getattr(node, 'starargs', None) or getattr(
node, 'kwargs', None) or len(node.args) != 1:
raise ValueError(
- 'Var takes exactly one argument (file %r, line %s)' % (
- filename, getattr(node, 'lineno', '<unknown>')))
- arg = _convert(node.args[0])
+ '%s takes exactly one argument (file %r, line %s)' % (
+ node.func.id, filename, getattr(node, 'lineno', '<unknown>')))
+ if node.func.id == 'Str':
+ if isinstance(node.args[0], ast.Str):
+ return ConstantString(node.args[0].s)
+ raise ValueError('Passed a non-string to Str() (file %r, line%s)' % (
+ filename, getattr(node, 'lineno', '<unknown>')))
+ else:
+ arg = _convert(node.args[0])
if not isinstance(arg, basestring):
raise ValueError(
'Var\'s argument must be a variable name (file %r, line %s)' % (
@@ -290,7 +322,10 @@
'%s was used as a variable, but was not declared in the vars dict '
'(file %r, line %s)' % (
arg, filename, getattr(node, 'lineno', '<unknown>')))
- return vars_dict[arg]
+ val = vars_dict[arg]
+ if isinstance(val, ConstantString):
+ val = val.value
+ return val
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
return _convert(node.left) + _convert(node.right)
elif isinstance(node, ast.BinOp) and isinstance(node.op, ast.Mod):
@@ -601,6 +636,8 @@
def _UpdateAstString(tokens, node, value):
+ if isinstance(node, ast.Call):
+ node = node.args[0]
position = node.lineno, node.col_offset
quote_char = ''
if isinstance(node, ast.Str):
@@ -810,7 +847,10 @@
raise KeyError(
"Could not find any variable called %s." % var_name)
- return gclient_dict['vars'][var_name]
+ val = gclient_dict['vars'][var_name]
+ if isinstance(val, ConstantString):
+ return val.value
+ return val
def GetCIPD(gclient_dict, dep_name, package_name):