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

157 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-09-07 09:27 +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 because the condition on line 46 was never true

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 @classmethod 

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

75 return MANIFEST_CONDITION_TRUE if value else MANIFEST_CONDITION_FALSE 

76 

77 

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

79class LiteralManifestCondition(ManifestCondition): 

80 value: bool 

81 

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

83 return ManifestCondition.literal_bool(not self.value) 

84 

85 def describe(self) -> str: 

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

87 

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

89 return self.value 

90 

91 

92MANIFEST_CONDITION_TRUE = LiteralManifestCondition(True) 

93MANIFEST_CONDITION_FALSE = LiteralManifestCondition(False) 

94 

95del LiteralManifestCondition 

96 

97 

98class NegatedManifestCondition(ManifestCondition): 

99 __slots__ = ("_condition",) 

100 

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

102 super().__init__() 

103 self._condition = condition 

104 

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

106 return self._condition 

107 

108 def describe(self) -> str: 

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

110 

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

112 return not self._condition.evaluate(context) 

113 

114 

115class _ConditionGroupMatchType(Enum): 

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

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

118 

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

120 return self.value[1].format( 

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

122 ) 

123 

124 def evaluate( 

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

126 ) -> bool: 

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

128 

129 

130class ManifestConditionGroup(ManifestCondition): 

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

132 

133 def __init__( 

134 self, 

135 match_type: _ConditionGroupMatchType, 

136 conditions: Sequence[ManifestCondition], 

137 ) -> None: 

138 super().__init__() 

139 self.match_type = match_type 

140 self._conditions = conditions 

141 

142 def describe(self) -> str: 

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

144 

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

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

147 

148 def extend( 

149 self, 

150 conditions: Sequence[ManifestCondition], 

151 ) -> "ManifestConditionGroup": 

152 combined = list(self._conditions) 

153 combined.extend(conditions) 

154 return ManifestConditionGroup( 

155 self.match_type, 

156 combined, 

157 ) 

158 

159 

160class ArchMatchManifestConditionBase(ManifestCondition): 

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

162 

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

164 super().__init__() 

165 self._arch_spec = arch_spec 

166 self._is_negated = is_negated 

167 

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

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

170 

171 

172class SourceContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

173 def describe(self) -> str: 

174 if self._is_negated: 

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

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

177 

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

179 arch = context.dpkg_architecture_variables.current_host_arch 

180 match = context.dpkg_arch_query_table.architecture_is_concerned( 

181 arch, self._arch_spec 

182 ) 

183 return not match if self._is_negated else match 

184 

185 

186class BinaryPackageContextArchMatchManifestCondition(ArchMatchManifestConditionBase): 

187 def describe(self) -> str: 

188 if self._is_negated: 

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

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

191 

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

193 binary_package = context.binary_package 

194 if binary_package is None: 

195 raise RuntimeError( 

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

197 " without one" 

198 ) 

199 arch = binary_package.resolved_architecture 

200 match = context.dpkg_arch_query_table.architecture_is_concerned( 

201 arch, self._arch_spec 

202 ) 

203 return not match if self._is_negated else match 

204 

205 

206class BuildProfileMatch(ManifestCondition): 

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

208 

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

210 super().__init__() 

211 self._profile_spec = profile_spec 

212 self._is_negated = is_negated 

213 

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

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

216 

217 def describe(self) -> str: 

218 if self._is_negated: 

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

220 return f"DEB_BUILD_PROFILES matches any 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 not match if self._is_negated else match 

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, Optional[str]]) -> 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