Add parser and evaluator for rust cfg expressions
This commit is contained in:
parent
3fdc1b24cb
commit
61e7b5fc7a
2 changed files with 173 additions and 0 deletions
113
rust2rpm/cfg.py
Normal file
113
rust2rpm/cfg.py
Normal file
|
@ -0,0 +1,113 @@
|
|||
import ast
|
||||
import ctypes
|
||||
import functools
|
||||
import platform
|
||||
import sys
|
||||
|
||||
import pyparsing as pp
|
||||
|
||||
pp.ParserElement.enable_packrat()
|
||||
|
||||
# ConfigurationPredicate :
|
||||
# ConfigurationOption
|
||||
# | ConfigurationAll
|
||||
# | ConfigurationAny
|
||||
# | ConfigurationNot
|
||||
# ConfigurationOption :
|
||||
# IDENTIFIER (= (STRING_LITERAL | RAW_STRING_LITERAL))?
|
||||
# ConfigurationAll
|
||||
# all ( ConfigurationPredicateList? )
|
||||
# ConfigurationAny
|
||||
# any ( ConfigurationPredicateList? )
|
||||
# ConfigurationNot
|
||||
# not ( ConfigurationPredicate )
|
||||
# ConfigurationPredicateList
|
||||
# ConfigurationPredicate (, ConfigurationPredicate)* ,?
|
||||
|
||||
# cfg(target_os = "macos")
|
||||
# cfg(any(foo, bar))
|
||||
# cfg(all(unix, target_pointer_width = "32"))
|
||||
# cfg(not(foo))
|
||||
|
||||
def _call(word, arg):
|
||||
return pp.Group(pp.Literal(word) + pp.Suppress('(') + arg + pp.Suppress(')'), aslist=True)
|
||||
|
||||
@functools.cache
|
||||
def cfg_grammar():
|
||||
pred = pp.Forward()
|
||||
|
||||
ident = pp.Word(pp.alphas + '_', pp.alphanums + '_')
|
||||
option = pp.Group(ident + pp.Optional(pp.Suppress('=') + pp.quotedString), aslist=True)
|
||||
|
||||
not_ = _call('not', pred)
|
||||
|
||||
# pp.pyparsing_common.comma_separated_list?
|
||||
# any_ = _call('any', pp.pyparsing_common.comma_separated_list(pred))
|
||||
# all_ = _call('all', pp.pyparsing_common.comma_separated_list(pred))
|
||||
# all_ = _call('all', pp.delimited_list(pred))
|
||||
|
||||
any_ = _call('any', pred + pp.ZeroOrMore(pp.Suppress(',') + pred))
|
||||
all_ = _call('all', pred + pp.ZeroOrMore(pp.Suppress(',') + pred))
|
||||
|
||||
pred <<= not_ | any_ | all_ | option
|
||||
|
||||
grammar = _call('cfg', pred)
|
||||
return grammar
|
||||
|
||||
@functools.cache
|
||||
def evaluate_variable(name):
|
||||
# print(f'evaluate_variable: {expr}')
|
||||
match name:
|
||||
case 'target_arch':
|
||||
return platform.machine()
|
||||
|
||||
case 'target_os':
|
||||
return 'linux'
|
||||
|
||||
case 'target_family':
|
||||
return 'unix'
|
||||
|
||||
case 'unix':
|
||||
return evaluate_variable('target_family') == 'unix'
|
||||
|
||||
case 'windows':
|
||||
return evaluate_variable('target_family') == 'windows'
|
||||
|
||||
case 'target_env':
|
||||
# Key-value option set with further disambiguating information about the
|
||||
# target platform with information about the ABI or libc used
|
||||
return ...
|
||||
|
||||
case 'target_endian':
|
||||
return sys.byteorder
|
||||
|
||||
case 'target_pointer_width':
|
||||
return str(ctypes.sizeof(ctypes.c_voidp) * 8)
|
||||
|
||||
case 'target_vendor':
|
||||
return 'unknown'
|
||||
|
||||
case _:
|
||||
print(f'Unknown variable {name}, assuming False')
|
||||
return False
|
||||
|
||||
def evaluate(expr, nested=False):
|
||||
# print(f'evaluate: {expr}')
|
||||
match expr:
|
||||
case ['cfg', expr] if not nested:
|
||||
return evaluate(expr, True)
|
||||
case ['not', expr] if nested:
|
||||
return not evaluate(expr, True)
|
||||
case ['all', *args] if nested:
|
||||
return all(evaluate(arg, True) for arg in args)
|
||||
case ['any', *args] if nested:
|
||||
return any(evaluate(arg, True) for arg in args)
|
||||
case [variable, value] if nested:
|
||||
v = ast.literal_eval(value)
|
||||
x = evaluate_variable(variable)
|
||||
return x == v
|
||||
case [variable] if nested:
|
||||
x = evaluate_variable(variable)
|
||||
return x
|
||||
case _:
|
||||
raise ValueError
|
60
rust2rpm/tests/test_cfg.py
Normal file
60
rust2rpm/tests/test_cfg.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
import pytest
|
||||
|
||||
from rust2rpm import cfg
|
||||
|
||||
def test_pyparsing_run_tests():
|
||||
g = cfg.cfg_grammar()
|
||||
|
||||
g.run_tests("""\
|
||||
cfg(target_os = "macos")
|
||||
cfg(any(foo, bar))
|
||||
cfg(all(unix, target_pointer_width = "32"))
|
||||
cfg(not(foo))
|
||||
""")
|
||||
|
||||
@pytest.mark.parametrize('expr, expected', [
|
||||
('cfg(target_os = "macos")',
|
||||
False),
|
||||
('cfg(any(foo, bar))',
|
||||
False),
|
||||
('cfg(all(unix, target_pointer_width = "16"))',
|
||||
False),
|
||||
('cfg(not(foo))',
|
||||
True),
|
||||
|
||||
('cfg(unix)',
|
||||
True),
|
||||
('cfg(not(unix))',
|
||||
False),
|
||||
('cfg(windows)',
|
||||
False),
|
||||
('cfg(linux)', # not defined
|
||||
False),
|
||||
('cfg(not(windows))',
|
||||
True),
|
||||
('cfg(any(unix, windows))',
|
||||
True),
|
||||
('cfg(any(windows, unix))',
|
||||
True),
|
||||
('cfg(any(windows, windows, windows))',
|
||||
False),
|
||||
|
||||
('cfg(target_os = "linux")',
|
||||
True),
|
||||
('cfg(any(target_os = "linux"))',
|
||||
True),
|
||||
('cfg(all(target_os = "linux"))',
|
||||
True),
|
||||
('cfg(any(target_os = "linux", target_os = "macos"))',
|
||||
True),
|
||||
|
||||
('cfg(any(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))',
|
||||
True),
|
||||
('cfg(all(target_pointer_width = "16", target_pointer_width = "32", target_pointer_width = "64"))',
|
||||
False),
|
||||
|
||||
])
|
||||
def test_expressions(expr, expected):
|
||||
parsed = cfg.cfg_grammar().parse_string(expr)
|
||||
value = cfg.evaluate(parsed[0])
|
||||
assert value == expected
|
Loading…
Reference in a new issue