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
« 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
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 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)}]'
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
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)}]'
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
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 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}]"
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
230@dataclasses.dataclass(frozen=True, slots=True)
231class _SingletonCondition(ManifestCondition):
232 description: str
233 implementation: Callable[[ConditionContext], bool]
235 def describe(self) -> str:
236 return self.description
238 def evaluate(self, context: ConditionContext) -> bool:
239 return self.implementation(context)
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 )
252def _run_build_time_tests(deb_build_options: Mapping[str, str | None]) -> bool:
253 return "nocheck" not in deb_build_options
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)
261_CAN_EXECUTE_COMPILED_BINARIES = _SingletonCondition(
262 "Can run built binaries (natively or via transparent emulation)",
263 _can_run_built_binaries,
264)
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)
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)
277del _SingletonCondition
278del _can_run_built_binaries