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

157 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2026-01-26 19:30 +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 qual = "*none*" if self._is_negated else "any" 

176 archs = ", ".join(self._arch_spec) 

177 return f"architecture (for source package) matches {qual} of [{archs}]" 

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 match != self._is_negated # xor 

185 

186 

187class BinaryPackageContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

188 def describe(self) -> str: 

189 qual = "*none*" if self._is_negated else "any" 

190 archs = ", ".join(self._arch_spec) 

191 return f"architecture (for binary package) matches {qual} of [{archs}]" 

192 

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

194 binary_package = context.binary_package 

195 if binary_package is None: 195 ↛ 196line 195 didn't jump to line 196 because the condition on line 195 was never true

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 qual = "*none*" if self._is_negated else "any" 

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

221 

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

223 match = active_profiles_match( 

224 self._profile_spec, context.deb_options_and_profiles.deb_build_profiles 

225 ) 

226 return match != self._is_negated # xor 

227 

228 

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

230class _SingletonCondition(ManifestCondition): 

231 description: str 

232 implementation: Callable[[ConditionContext], bool] 

233 

234 def describe(self) -> str: 

235 return self.description 

236 

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

238 return self.implementation(context) 

239 

240 

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

242 if not context.dpkg_architecture_variables.is_cross_compiling: 

243 return True 

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

245 return ( 

246 "crossbuildcanrunhostbinaries" 

247 in context.deb_options_and_profiles.deb_build_options 

248 ) 

249 

250 

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

252 return "nocheck" not in deb_build_options 

253 

254 

255_IS_CROSS_BUILDING = _SingletonCondition( 

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

257 lambda c: c.dpkg_architecture_variables.is_cross_compiling, 

258) 

259 

260_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition( 

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

262 _can_run_built_binaries, 

263) 

264 

265_RUN_BUILD_TIME_TESTS = _SingletonCondition( 

266 "Run build time tests", 

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

268) 

269 

270_BUILD_DOCS_BDO = _SingletonCondition( 

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

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

273) 

274 

275 

276del _SingletonCondition 

277del _can_run_built_binaries