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
« 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
5from debian.debian_support import DpkgArchTable
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
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
23 def replace(self, /, **changes: Any) -> "Self":
24 return dataclasses.replace(self, **changes)
27class ManifestCondition(DebputyDispatchableType):
28 __slots__ = ()
30 def describe(self) -> str:
31 raise NotImplementedError
33 def negated(self) -> "ManifestCondition":
34 return NegatedManifestCondition(self)
36 def evaluate(self, context: ConditionContext) -> bool:
37 raise NotImplementedError
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)
53 @classmethod
54 def any_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
55 return cls._manifest_group(_ConditionGroupMatchType.ANY_OF, conditions)
57 @classmethod
58 def all_of(cls, conditions: "Sequence[ManifestCondition]") -> "ManifestCondition":
59 return cls._manifest_group(_ConditionGroupMatchType.ALL_OF, conditions)
61 @classmethod
62 def is_cross_building(cls) -> "ManifestCondition":
63 return _IS_CROSS_BUILDING
65 @classmethod
66 def can_execute_compiled_binaries(cls) -> "ManifestCondition":
67 return _CAN_EXECUTE_COMPILED_BINARIES
69 @classmethod
70 def run_build_time_tests(cls) -> "ManifestCondition":
71 return _RUN_BUILD_TIME_TESTS
73 @classmethod
74 def literal_bool(cls, value: bool) -> "ManifestCondition":
75 return MANIFEST_CONDITION_TRUE if value else MANIFEST_CONDITION_FALSE
78@dataclasses.dataclass(slots=True, frozen=True)
79class LiteralManifestCondition(ManifestCondition):
80 value: bool
82 def negated(self) -> "ManifestCondition":
83 return ManifestCondition.literal_bool(not self.value)
85 def describe(self) -> str:
86 return "true" if self.value else "false"
88 def evaluate(self, context: ConditionContext) -> bool:
89 return self.value
92MANIFEST_CONDITION_TRUE = LiteralManifestCondition(True)
93MANIFEST_CONDITION_FALSE = LiteralManifestCondition(False)
95del LiteralManifestCondition
98class NegatedManifestCondition(ManifestCondition):
99 __slots__ = ("_condition",)
101 def __init__(self, condition: ManifestCondition) -> None:
102 super().__init__()
103 self._condition = condition
105 def negated(self) -> "ManifestCondition":
106 return self._condition
108 def describe(self) -> str:
109 return f"not ({self._condition.describe()})"
111 def evaluate(self, context: ConditionContext) -> bool:
112 return not self._condition.evaluate(context)
115class _ConditionGroupMatchType(Enum):
116 ANY_OF = (any, "At least one of: [{conditions}]")
117 ALL_OF = (all, "All of: [{conditions}]")
119 def describe(self, conditions: Sequence[ManifestCondition]) -> str:
120 return self.value[1].format(
121 conditions=", ".join(x.describe() for x in conditions)
122 )
124 def evaluate(
125 self, conditions: Sequence[ManifestCondition], context: ConditionContext
126 ) -> bool:
127 return self.value[0](c.evaluate(context) for c in conditions)
130class ManifestConditionGroup(ManifestCondition):
131 __slots__ = ("match_type", "_conditions")
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
142 def describe(self) -> str:
143 return self.match_type.describe(self._conditions)
145 def evaluate(self, context: ConditionContext) -> bool:
146 return self.match_type.evaluate(self._conditions, context)
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 )
160class ArchMatchManifestConditionBase(ManifestCondition):
161 __slots__ = ("_arch_spec", "_is_negated")
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
168 def negated(self) -> "ManifestCondition":
169 return self.__class__(self._arch_spec, is_negated=not self._is_negated)
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)}]'
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
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)}]'
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
206class BuildProfileMatch(ManifestCondition):
207 __slots__ = ("_profile_spec", "_is_negated")
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
214 def negated(self) -> "ManifestCondition":
215 return self.__class__(self._profile_spec, is_negated=not self._is_negated)
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}]"
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
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, Optional[str]]) -> 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