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

158 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-10-12 15:06 +0000

1import dataclasses 

2from enum import Enum 

3from typing import List, Optional, Any, Self 

4from collections.abc import Callable, Sequence, Mapping 

5 

6from debian.debian_support import DpkgArchTable 

7 

8from debputy._deb_options_profiles import DebBuildOptionsAndProfiles 

9from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable 

10from debputy.manifest_parser.tagging_types import DebputyDispatchableType 

11from debputy.packages import BinaryPackage 

12from debputy.substitution import Substitution 

13from debputy.util import active_profiles_match 

14 

15 

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

17class ConditionContext: 

18 binary_package: BinaryPackage | None 

19 deb_options_and_profiles: DebBuildOptionsAndProfiles 

20 substitution: Substitution 

21 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable 

22 dpkg_arch_query_table: DpkgArchTable 

23 

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

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

26 

27 

28class ManifestCondition(DebputyDispatchableType): 

29 __slots__ = () 

30 

31 def describe(self) -> str: 

32 raise NotImplementedError 

33 

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

35 return NegatedManifestCondition(self) 

36 

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

38 raise NotImplementedError 

39 

40 @classmethod 

41 def _manifest_group( 

42 cls, 

43 match_type: "_ConditionGroupMatchType", 

44 conditions: "Sequence[ManifestCondition]", 

45 ) -> "ManifestCondition": 

46 condition = conditions[0] 

47 if ( 47 ↛ 51line 47 didn't jump to line 51 because the condition on line 47 was never true

48 isinstance(condition, ManifestConditionGroup) 

49 and condition.match_type == match_type 

50 ): 

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

52 return ManifestConditionGroup(match_type, conditions) 

53 

54 @classmethod 

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

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

57 

58 @classmethod 

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

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

61 

62 @classmethod 

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

64 return _IS_CROSS_BUILDING 

65 

66 @classmethod 

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

68 return _CAN_EXECUTE_COMPILED_BINARIES 

69 

70 @classmethod 

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

72 return _RUN_BUILD_TIME_TESTS 

73 

74 @classmethod 

75 def literal_bool(cls, value: bool) -> "ManifestCondition": 

76 return MANIFEST_CONDITION_TRUE if value else MANIFEST_CONDITION_FALSE 

77 

78 

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

80class LiteralManifestCondition(ManifestCondition): 

81 value: bool 

82 

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

84 return ManifestCondition.literal_bool(not self.value) 

85 

86 def describe(self) -> str: 

87 return "true" if self.value else "false" 

88 

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

90 return self.value 

91 

92 

93MANIFEST_CONDITION_TRUE = LiteralManifestCondition(True) 

94MANIFEST_CONDITION_FALSE = LiteralManifestCondition(False) 

95 

96del LiteralManifestCondition 

97 

98 

99class NegatedManifestCondition(ManifestCondition): 

100 __slots__ = ("_condition",) 

101 

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

103 super().__init__() 

104 self._condition = condition 

105 

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

107 return self._condition 

108 

109 def describe(self) -> str: 

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

111 

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

113 return not self._condition.evaluate(context) 

114 

115 

116class _ConditionGroupMatchType(Enum): 

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

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

119 

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

121 return self.value[1].format( 

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

123 ) 

124 

125 def evaluate( 

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

127 ) -> bool: 

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

129 

130 

131class ManifestConditionGroup(ManifestCondition): 

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

133 

134 def __init__( 

135 self, 

136 match_type: _ConditionGroupMatchType, 

137 conditions: Sequence[ManifestCondition], 

138 ) -> None: 

139 super().__init__() 

140 self.match_type = match_type 

141 self._conditions = conditions 

142 

143 def describe(self) -> str: 

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

145 

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

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

148 

149 def extend( 

150 self, 

151 conditions: Sequence[ManifestCondition], 

152 ) -> "ManifestConditionGroup": 

153 combined = list(self._conditions) 

154 combined.extend(conditions) 

155 return ManifestConditionGroup( 

156 self.match_type, 

157 combined, 

158 ) 

159 

160 

161class ArchMatchManifestConditionBase(ManifestCondition): 

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

163 

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

165 super().__init__() 

166 self._arch_spec = arch_spec 

167 self._is_negated = is_negated 

168 

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

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

171 

172 

173class SourceContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

174 def describe(self) -> str: 

175 if self._is_negated: 

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

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

178 

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

180 arch = context.dpkg_architecture_variables.current_host_arch 

181 match = context.dpkg_arch_query_table.architecture_is_concerned( 

182 arch, self._arch_spec 

183 ) 

184 return not match if self._is_negated else match 

185 

186 

187class BinaryPackageContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

188 def describe(self) -> str: 

189 if self._is_negated: 

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

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

192 

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

194 binary_package = context.binary_package 

195 if binary_package is None: 

196 raise RuntimeError( 

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

198 " without one" 

199 ) 

200 arch = binary_package.resolved_architecture 

201 match = context.dpkg_arch_query_table.architecture_is_concerned( 

202 arch, self._arch_spec 

203 ) 

204 return not match if self._is_negated else match 

205 

206 

207class BuildProfileMatch(ManifestCondition): 

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

209 

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

211 super().__init__() 

212 self._profile_spec = profile_spec 

213 self._is_negated = is_negated 

214 

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

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

217 

218 def describe(self) -> str: 

219 if self._is_negated: 

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

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

222 

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

224 match = active_profiles_match( 

225 self._profile_spec, context.deb_options_and_profiles.deb_build_profiles 

226 ) 

227 return not match if self._is_negated else match 

228 

229 

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

231class _SingletonCondition(ManifestCondition): 

232 description: str 

233 implementation: Callable[[ConditionContext], bool] 

234 

235 def describe(self) -> str: 

236 return self.description 

237 

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

239 return self.implementation(context) 

240 

241 

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

243 if not context.dpkg_architecture_variables.is_cross_compiling: 

244 return True 

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

246 return ( 

247 "crossbuildcanrunhostbinaries" 

248 in context.deb_options_and_profiles.deb_build_options 

249 ) 

250 

251 

252def _run_build_time_tests(deb_build_options: Mapping[str, str | None]) -> bool: 

253 return "nocheck" not in deb_build_options 

254 

255 

256_IS_CROSS_BUILDING = _SingletonCondition( 

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

258 lambda c: c.dpkg_architecture_variables.is_cross_compiling, 

259) 

260 

261_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition( 

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

263 _can_run_built_binaries, 

264) 

265 

266_RUN_BUILD_TIME_TESTS = _SingletonCondition( 

267 "Run build time tests", 

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

269) 

270 

271_BUILD_DOCS_BDO = _SingletonCondition( 

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

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

274) 

275 

276 

277del _SingletonCondition 

278del _can_run_built_binaries