Reland "Add a Str() function to gclient for use in DEPS files."

This relands c7eed83 with a fix to the way variables are
propagated from parent dependencies into child dependencies.

The original CL description from c7eed83 was:
> 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.
>
This reverts commit 84431987dd384c79c84515004d19db67345a1c00.

Bug: 1099242
TBR=ehmaldonado@chromium.org

Change-Id: I047b871df47c367c1f34a3985e5813504e3c5c6f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/tools/depot_tools/+/2274152
Commit-Queue: Dirk Pranke <dpranke@google.com>
Reviewed-by: Ben Pastene <bpastene@chromium.org>
This commit is contained in:
Dirk Pranke
2020-06-30 23:30:47 +00:00
committed by LUCI CQ
parent 1ee86bdf3a
commit fdd2cd6e5f
4 changed files with 179 additions and 25 deletions

View File

@@ -24,6 +24,27 @@ else:
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_DEPS_SCHEMA = _NodeDictSchema({
_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 @@ _GCLIENT_SCHEMA = schema.Schema(
# 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 @@ _GCLIENT_SCHEMA = schema.Schema(
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 @@ def _gclient_eval(node_or_string, filename='<unknown>', vars_dict=None):
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 @@ def _gclient_eval(node_or_string, filename='<unknown>', vars_dict=None):
'%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 RenderDEPSFile(gclient_dict):
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 @@ def GetVar(gclient_dict, var_name):
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):