Coverage for src/debputy/manifest_conditions.py: 66%

142 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2025-01-27 13:59 +0000

1import dataclasses 

2from enum import Enum 

3from typing import List, Callable, Optional, Sequence, Any, Self, Mapping 

4 

5from debian.debian_support import DpkgArchTable 

6 

7from debputy._deb_options_profiles import DebBuildOptionsAndProfiles 

8from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable 

9from debputy.manifest_parser.tagging_types import DebputyDispatchableType 

10from debputy.packages import BinaryPackage 

11from debputy.substitution import Substitution 

12from debputy.util import active_profiles_match 

13 

14 

15@dataclasses.dataclass(slots=True, frozen=True) 

16class ConditionContext: 

17 binary_package: Optional[BinaryPackage] 

18 deb_options_and_profiles: DebBuildOptionsAndProfiles 

19 substitution: Substitution 

20 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable 

21 dpkg_arch_query_table: DpkgArchTable 

22 

23 def replace(self, /, **changes: Any) -> "Self": 

24 return dataclasses.replace(self, **changes) 

25 

26 

27class ManifestCondition(DebputyDispatchableType): 

28 __slots__ = () 

29 

30 def describe(self) -> str: 

31 raise NotImplementedError 

32 

33 def negated(self) -> "ManifestCondition": 

34 return NegatedManifestCondition(self) 

35 

36 def evaluate(self, context: ConditionContext) -> bool: 

37 raise NotImplementedError 

38 

39 @classmethod 

40 def _manifest_group( 

41 cls, 

42 match_type: "_ConditionGroupMatchType", 

43 conditions: "Sequence[ManifestCondition]", 

44 ) -> "ManifestCondition": 

45 condition = conditions[0] 

46 if ( 46 ↛ 50line 46 didn't jump to line 50

47 isinstance(condition, ManifestConditionGroup) 

48 and condition.match_type == match_type 

49 ): 

50 return condition.extend(conditions[1:]) 

51 return ManifestConditionGroup(match_type, conditions) 

52 

53 @classmethod 

54 def any_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition": 

55 return cls._manifest_group(_ConditionGroupMatchType.ANY_OF, conditions) 

56 

57 @classmethod 

58 def all_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition": 

59 return cls._manifest_group(_ConditionGroupMatchType.ALL_OF, conditions) 

60 

61 @classmethod 

62 def is_cross_building(cls) -> "ManifestCondition": 

63 return _IS_CROSS_BUILDING 

64 

65 @classmethod 

66 def can_execute_compiled_binaries(cls) -> "ManifestCondition": 

67 return _CAN_EXECUTE_COMPILED_BINARIES 

68 

69 @classmethod 

70 def run_build_time_tests(cls) -> "ManifestCondition": 

71 return _RUN_BUILD_TIME_TESTS 

72 

73 

74class NegatedManifestCondition(ManifestCondition): 

75 __slots__ = ("_condition",) 

76 

77 def __init__(self, condition: ManifestCondition) -> None: 

78 super().__init__() 

79 self._condition = condition 

80 

81 def negated(self) -> "ManifestCondition": 

82 return self._condition 

83 

84 def describe(self) -> str: 

85 return f"not ({self._condition.describe()})" 

86 

87 def evaluate(self, context: ConditionContext) -> bool: 

88 return not self._condition.evaluate(context) 

89 

90 

91class _ConditionGroupMatchType(Enum): 

92 ANY_OF = (any, "At least one of: [{conditions}]") 

93 ALL_OF = (all, "All of: [{conditions}]") 

94 

95 def describe(self, conditions: Sequence[ManifestCondition]) -> str: 

96 return self.value[1].format( 

97 conditions=", ".join(x.describe() for x in conditions) 

98 ) 

99 

100 def evaluate( 

101 self, conditions: Sequence[ManifestCondition], context: ConditionContext 

102 ) -> bool: 

103 return self.value[0](c.evaluate(context) for c in conditions) 

104 

105 

106class ManifestConditionGroup(ManifestCondition): 

107 __slots__ = ("match_type", "_conditions") 

108 

109 def __init__( 

110 self, 

111 match_type: _ConditionGroupMatchType, 

112 conditions: Sequence[ManifestCondition], 

113 ) -> None: 

114 super().__init__() 

115 self.match_type = match_type 

116 self._conditions = conditions 

117 

118 def describe(self) -> str: 

119 return self.match_type.describe(self._conditions) 

120 

121 def evaluate(self, context: ConditionContext) -> bool: 

122 return self.match_type.evaluate(self._conditions, context) 

123 

124 def extend( 

125 self, 

126 conditions: Sequence[ManifestCondition], 

127 ) -> "ManifestConditionGroup": 

128 combined = list(self._conditions) 

129 combined.extend(conditions) 

130 return ManifestConditionGroup( 

131 self.match_type, 

132 combined, 

133 ) 

134 

135 

136class ArchMatchManifestConditionBase(ManifestCondition): 

137 __slots__ = ("_arch_spec", "_is_negated") 

138 

139 def __init__(self, arch_spec: List[str], *, is_negated: bool = False) -> None: 

140 super().__init__() 

141 self._arch_spec = arch_spec 

142 self._is_negated = is_negated 

143 

144 def negated(self) -> "ManifestCondition": 

145 return self.__class__(self._arch_spec, is_negated=not self._is_negated) 

146 

147 

148class SourceContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

149 def describe(self) -> str: 

150 if self._is_negated: 

151 return f'architecture (for source package) matches *none* of [{", ".join(self._arch_spec)}]' 

152 return f'architecture (for source package) matches any of [{", ".join(self._arch_spec)}]' 

153 

154 def evaluate(self, context: ConditionContext) -> bool: 

155 arch = context.dpkg_architecture_variables.current_host_arch 

156 match = context.dpkg_arch_query_table.architecture_is_concerned( 

157 arch, self._arch_spec 

158 ) 

159 return not match if self._is_negated else match 

160 

161 

162class BinaryPackageContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

163 def describe(self) -> str: 

164 if self._is_negated: 

165 return f'architecture (for binary package) matches *none* of [{", ".join(self._arch_spec)}]' 

166 return f'architecture (for binary package) matches any of [{", ".join(self._arch_spec)}]' 

167 

168 def evaluate(self, context: ConditionContext) -> bool: 

169 binary_package = context.binary_package 

170 if binary_package is None: 

171 raise RuntimeError( 

172 "Condition only applies in the context of a BinaryPackage, but was evaluated" 

173 " without one" 

174 ) 

175 arch = binary_package.resolved_architecture 

176 match = context.dpkg_arch_query_table.architecture_is_concerned( 

177 arch, self._arch_spec 

178 ) 

179 return not match if self._is_negated else match 

180 

181 

182class BuildProfileMatch(ManifestCondition): 

183 __slots__ = ("_profile_spec", "_is_negated") 

184 

185 def __init__(self, profile_spec: str, *, is_negated: bool = False) -> None: 

186 super().__init__() 

187 self._profile_spec = profile_spec 

188 self._is_negated = is_negated 

189 

190 def negated(self) -> "ManifestCondition": 

191 return self.__class__(self._profile_spec, is_negated=not self._is_negated) 

192 

193 def describe(self) -> str: 

194 if self._is_negated: 

195 return f"DEB_BUILD_PROFILES matches *none* of [{self._profile_spec}]" 

196 return f"DEB_BUILD_PROFILES matches any of [{self._profile_spec}]" 

197 

198 def evaluate(self, context: ConditionContext) -> bool: 

199 match = active_profiles_match( 

200 self._profile_spec, context.deb_options_and_profiles.deb_build_profiles 

201 ) 

202 return not match if self._is_negated else match 

203 

204 

205@dataclasses.dataclass(frozen=True, slots=True) 

206class _SingletonCondition(ManifestCondition): 

207 description: str 

208 implementation: Callable[[ConditionContext], bool] 

209 

210 def describe(self) -> str: 

211 return self.description 

212 

213 def evaluate(self, context: ConditionContext) -> bool: 

214 return self.implementation(context) 

215 

216 

217def _can_run_built_binaries(context: ConditionContext) -> bool: 

218 if not context.dpkg_architecture_variables.is_cross_compiling: 

219 return True 

220 # User / Builder asserted that we could even though we are cross-compiling, so we have to assume it is true 

221 return ( 

222 "crossbuildcanrunhostbinaries" 

223 in context.deb_options_and_profiles.deb_build_options 

224 ) 

225 

226 

227def _run_build_time_tests(deb_build_options: Mapping[str, Optional[str]]) -> bool: 

228 return "nocheck" not in deb_build_options 

229 

230 

231_IS_CROSS_BUILDING = _SingletonCondition( 231 ↛ exitline 231 didn't jump to the function exit

232 "Cross Compiling (i.e., DEB_HOST_GNU_TYPE != DEB_BUILD_GNU_TYPE)", 

233 lambda c: c.dpkg_architecture_variables.is_cross_compiling, 

234) 

235 

236_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition( 

237 "Can run built binaries (natively or via transparent emulation)", 

238 _can_run_built_binaries, 

239) 

240 

241_RUN_BUILD_TIME_TESTS = _SingletonCondition( 241 ↛ exitline 241 didn't jump to the function exit

242 "Run build time tests", 

243 lambda c: _run_build_time_tests(c.deb_options_and_profiles.deb_build_options), 

244) 

245 

246_BUILD_DOCS_BDO = _SingletonCondition( 

247 "Build docs (nodocs not in DEB_BUILD_OPTIONS)", 

248 lambda c: "nodocs" not in c.deb_options_and_profiles.deb_build_options, 

249) 

250 

251 

252del _SingletonCondition 

253del _can_run_built_binaries