Coverage for rust2rpm/cfg.py: 100%

72 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-27 15:21 +0100

1import ast 

2import functools 

3 

4import pyparsing as pp 

5from pyparsing import ParseException 

6 

7from rust2rpm import log, TARGET_ARCHES 

8 

9__all__ = ["ParseException", "parse_and_evaluate", "IDENT_CHARS"] 

10 

11pp.ParserElement.enablePackrat() 

12 

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)* ,? 

28 

29# cfg(target_os = "macos") 

30# cfg(any(foo, bar)) 

31# cfg(all(unix, target_pointer_width = "32")) 

32# cfg(not(foo)) 

33 

34IDENT_CHARS = pp.alphas + "_", pp.alphanums + "_" 

35 

36 

37def _call(word, arg): 

38 return pp.Group(pp.Literal(word) + pp.Suppress("(") + arg + pp.Suppress(")")) 

39 

40 

41@functools.cache 

42def cfg_grammar(): 

43 pred = pp.Forward() 

44 

45 ident = pp.Word(IDENT_CHARS[0], IDENT_CHARS[1]) 

46 option = pp.Group(ident + pp.Optional(pp.Suppress("=") + pp.quotedString)) 

47 

48 not_ = _call("not", pred) 

49 

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)) 

54 

55 any_ = _call("any", pred + pp.ZeroOrMore(pp.Suppress(",") + pred)) 

56 all_ = _call("all", pred + pp.ZeroOrMore(pp.Suppress(",") + pred)) 

57 

58 pred <<= not_ | any_ | all_ | option 

59 

60 grammar = _call("cfg", pred) 

61 return grammar 

62 

63 

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 

67 

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 

74 

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 

79 

80 case "target_os": 

81 return value == "linux" 

82 

83 case "target_family": 

84 return value == "unix" 

85 

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"] 

92 

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 

97 

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 

102 

103 case "target_vendor": 

104 # On linux systems, "target_vendor" is always "unknown". 

105 return value == "unknown" 

106 

107 case _: # pragma nocover 

108 log.warn(f'Ignoring invalid predicate \'"{name}" = "{value}"\' in cfg-expression.') 

109 return False 

110 

111 

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 

128 

129 

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 

149 

150 

151def parse_and_evaluate(expr: str) -> bool: 

152 parsed = cfg_grammar().parseString(expr)[0] 

153 

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)