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
« 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
6from debian.debian_support import DpkgArchTable
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
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
24 def replace(self, /, **changes: Any) -> "Self":
25 return dataclasses.replace(self, **changes)
28class ManifestCondition(DebputyDispatchableType):
29 __slots__ = ()
31 def describe(self) -> str:
32 raise NotImplementedError
34 def negated(self) -> "ManifestCondition":
35 return NegatedManifestCondition(self)
37 def evaluate(self, context: ConditionContext) -> bool:
38 raise NotImplementedError
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)
54 @classmethod
55 def any_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
56 return cls._manifest_group(_ConditionGroupMatchType.ANY_OF, conditions)
58 @classmethod
59 def all_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
60 return cls._manifest_group(_ConditionGroupMatchType.ALL_OF, conditions)
62 @classmethod
63 def is_cross_building(cls) -> "ManifestCondition":
64 return _IS_CROSS_BUILDING
66 @classmethod
67 def can_execute_compiled_binaries(cls) -> "ManifestCondition":
68 return _CAN_EXECUTE_COMPILED_BINARIES
70 @classmethod
71 def run_build_time_tests(cls) -> "ManifestCondition":
72 return _RUN_BUILD_TIME_TESTS
74 @classmethod
75 def literal_bool(cls, value: bool) -> "ManifestCondition":
76 return MANIFEST_CONDITION_TRUE if value else MANIFEST_CONDITION_FALSE
79@dataclasses.dataclass(slots=True, frozen=True)
80class LiteralManifestCondition(ManifestCondition):
81 value: bool
83 def negated(self) -> "ManifestCondition":
84 return ManifestCondition.literal_bool(not self.value)
86 def describe(self) -> str:
87 return "true" if self.value else "false"
89 def evaluate(self, context: ConditionContext) -> bool:
90 return self.value
93MANIFEST_CONDITION_TRUE = LiteralManifestCondition(True)
94MANIFEST_CONDITION_FALSE = LiteralManifestCondition(False)
96del LiteralManifestCondition
99class NegatedManifestCondition(ManifestCondition):
100 __slots__ = ("_condition",)
102 def __init__(self, condition: ManifestCondition) -> None:
103 super().__init__()
104 self._condition = condition
106 def negated(self) -> "ManifestCondition":
107 return self._condition
109 def describe(self) -> str:
110 return f"not ({self._condition.describe()})"
112 def evaluate(self, context: ConditionContext) -> bool:
113 return not self._condition.evaluate(context)
116class _ConditionGroupMatchType(Enum):
117 ANY_OF = (any, "At least one of: [{conditions}]")
118 ALL_OF = (all, "All of: [{conditions}]")
120 def describe(self, conditions: Sequence[ManifestCondition]) -> str:
121 return self.value[1].format(
122 conditions=", ".join(x.describe() for x in conditions)
123 )
125 def evaluate(
126 self, conditions: Sequence[ManifestCondition], context: ConditionContext
127 ) -> bool:
128 return self.value[0](c.evaluate(context) for c in conditions)
131class ManifestConditionGroup(ManifestCondition):
132 __slots__ = ("match_type", "_conditions")
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
143 def describe(self) -> str:
144 return self.match_type.describe(self._conditions)
146 def evaluate(self, context: ConditionContext) -> bool:
147 return self.match_type.evaluate(self._conditions, context)
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 )
161class ArchMatchManifestConditionBase(ManifestCondition):
162 __slots__ = ("_arch_spec", "_is_negated")
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
169 def negated(self) -> "ManifestCondition":
170 return self.__class__(self._arch_spec, is_negated=not self._is_negated)
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}]"
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
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}]"
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
207class BuildProfileMatch(ManifestCondition):
208 __slots__ = ("_profile_spec", "_is_negated")
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
215 def negated(self) -> "ManifestCondition":
216 return self.__class__(self._profile_spec, is_negated=not self._is_negated)
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}]"
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
229@dataclasses.dataclass(frozen=True, slots=True)
230class _SingletonCondition(ManifestCondition):
231 description: str
232 implementation: Callable[[ConditionContext], bool]
234 def describe(self) -> str:
235 return self.description
237 def evaluate(self, context: ConditionContext) -> bool:
238 return self.implementation(context)
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 )
251def _run_build_time_tests(deb_build_options: Mapping[str, str | None]) -> bool:
252 return "nocheck" not in deb_build_options
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)
260_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition(
261 "Can run built binaries (natively or via transparent emulation)",
262 _can_run_built_binaries,
263)
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)
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)
276del _SingletonCondition
277del _can_run_built_binaries