Coverage for rust2rpm/cfg.py: 100%
72 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-27 15:21 +0100
1import ast
2import functools
4import pyparsing as pp
5from pyparsing import ParseException
7from rust2rpm import log, TARGET_ARCHES
9__all__ = ["ParseException", "parse_and_evaluate", "IDENT_CHARS"]
11pp.ParserElement.enablePackrat()
13# ConfigurationPredicate :
14# ConfigurationOption
15# | ConfigurationAll
16# | ConfigurationAny
17# | ConfigurationNot
18# ConfigurationOption :
19# IDENTIFIER (= (STRING_LITERAL | RAW_STRING_LITERAL))?
20# ConfigurationAll
21# all ( ConfigurationPredicateList? )
22# ConfigurationAny
23# any ( ConfigurationPredicateList? )
24# ConfigurationNot
25# not ( ConfigurationPredicate )
26# ConfigurationPredicateList
27# ConfigurationPredicate (, ConfigurationPredicate)* ,?
29# cfg(target_os = "macos")
30# cfg(any(foo, bar))
31# cfg(all(unix, target_pointer_width = "32"))
32# cfg(not(foo))
34IDENT_CHARS = pp.alphas + "_", pp.alphanums + "_"
37def _call(word, arg):
38 return pp.Group(pp.Literal(word) + pp.Suppress("(") + arg + pp.Suppress(")"))
41@functools.cache
42def cfg_grammar():
43 pred = pp.Forward()
45 ident = pp.Word(IDENT_CHARS[0], IDENT_CHARS[1])
46 option = pp.Group(ident + pp.Optional(pp.Suppress("=") + pp.quotedString))
48 not_ = _call("not", pred)
50 # pp.pyparsing_common.comma_separated_list?
51 # any_ = _call('any', pp.pyparsing_common.comma_separated_list(pred))
52 # all_ = _call('all', pp.pyparsing_common.comma_separated_list(pred))
53 # all_ = _call('all', pp.delimited_list(pred))
55 any_ = _call("any", pred + pp.ZeroOrMore(pp.Suppress(",") + pred))
56 all_ = _call("all", pred + pp.ZeroOrMore(pp.Suppress(",") + pred))
58 pred <<= not_ | any_ | all_ | option
60 grammar = _call("cfg", pred)
61 return grammar
64@functools.cache
65def evaluate_predicate(name: str, value: str, target_arch: str) -> bool:
66 # based on: https://doc.rust-lang.org/reference/conditional-compilation.html
68 match name:
69 case "target_arch":
70 # Needs to be ignored, as we cannot generate patches that are
71 # different depending on the host architecture - except if the
72 # target architecture is "wasm32", which we don't support.
73 return value == target_arch
75 case "target_feature":
76 # The "target_feature" predicate can be ignored as well, since the
77 # valid values for this predicate are architecture-dependent.
78 return True
80 case "target_os":
81 return value == "linux"
83 case "target_family":
84 return value == "unix"
86 case "target_env":
87 # The "target_env" predicate is used to disambiguate target
88 # platforms based on its C library / C ABI (i.e. we can ignore
89 # "msvc" and "musl"), and if there's no need to disambiguate, the
90 # value can be the empty string.
91 return value in ["", "gnu"]
93 case "target_endian":
94 # Needs to be ignored, as we cannot generate patches that are
95 # different depending on the host architecture.
96 return True
98 case "target_pointer_width":
99 # Needs to be ignored, as we cannot generate patches that are
100 # different depending on the host architecture.
101 return True
103 case "target_vendor":
104 # On linux systems, "target_vendor" is always "unknown".
105 return value == "unknown"
107 case _: # pragma nocover
108 log.warn(f'Ignoring invalid predicate \'"{name}" = "{value}"\' in cfg-expression.')
109 return False
112@functools.cache
113def evaluate_atom(name: str) -> bool:
114 match name:
115 case "unix":
116 return True
117 case "windows":
118 return False
119 case "miri":
120 return False
121 case _:
122 log.warn(
123 f"Ignoring unknown identifier {name!r} in cfg-expression. "
124 + 'only "unix", "windows", and "miri" are standard identifiers; '
125 + 'any non-standard "--cfg" flags are ignored.'
126 )
127 return False
130def evaluate(expr, target_arch: str, nested: bool = False) -> bool:
131 if hasattr(expr, "asList"):
132 expr = expr.asList() # compat with pyparsing 2.7.x
133 match expr:
134 case ["cfg", subexpr] if not nested:
135 return evaluate(subexpr, target_arch, True)
136 case ["not", subexpr] if nested:
137 return not evaluate(subexpr, target_arch, True)
138 case ["all", *args] if nested:
139 return all(evaluate(arg, target_arch, True) for arg in args)
140 case ["any", *args] if nested:
141 return any(evaluate(arg, target_arch, True) for arg in args)
142 case [variable, value] if nested:
143 v = ast.literal_eval(value)
144 return evaluate_predicate(variable, v, target_arch)
145 case [variable] if nested:
146 return evaluate_atom(variable)
147 case _: # pragma nocover
148 raise ValueError
151def parse_and_evaluate(expr: str) -> bool:
152 parsed = cfg_grammar().parseString(expr)[0]
154 # evaluate cfg-expression for all supported target_arch values
155 # returns True if it evaluates to True for any supported architecture
156 return any(evaluate(parsed, target_arch) for target_arch in TARGET_ARCHES)