Coverage for src/debputy/packages.py: 51%
167 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-07 12:14 +0200
1from typing import (
2 Dict,
3 Union,
4 Tuple,
5 Optional,
6 Set,
7 cast,
8 Mapping,
9 FrozenSet,
10 TYPE_CHECKING,
11)
13from debian.deb822 import Deb822
14from debian.debian_support import DpkgArchTable
16from ._deb_options_profiles import DebBuildOptionsAndProfiles
17from .architecture_support import (
18 DpkgArchitectureBuildProcessValuesTable,
19 dpkg_architecture_table,
20)
21from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match
23if TYPE_CHECKING:
24 from .plugin.api import VirtualPath
27_MANDATORY_BINARY_PACKAGE_FIELD = [
28 "Package",
29 "Architecture",
30]
33def parse_source_debian_control(
34 debian_control: "VirtualPath",
35 selected_packages: Union[Set[str], FrozenSet[str]],
36 excluded_packages: Union[Set[str], FrozenSet[str]],
37 select_arch_all: bool,
38 select_arch_any: bool,
39 dpkg_architecture_variables: Optional[
40 DpkgArchitectureBuildProcessValuesTable
41 ] = None,
42 dpkg_arch_query_table: Optional[DpkgArchTable] = None,
43 build_env: Optional[DebBuildOptionsAndProfiles] = None,
44) -> Tuple["SourcePackage", Dict[str, "BinaryPackage"]]:
45 if dpkg_architecture_variables is None:
46 dpkg_architecture_variables = dpkg_architecture_table()
47 if dpkg_arch_query_table is None:
48 dpkg_arch_query_table = DpkgArchTable.load_arch_table()
49 if build_env is None:
50 build_env = DebBuildOptionsAndProfiles.instance()
52 # If no selection option is set, then all packages are acted on (except the
53 # excluded ones)
54 if not selected_packages and not select_arch_all and not select_arch_any:
55 select_arch_all = True
56 select_arch_any = True
58 with debian_control.open() as fd:
59 dctrl_paragraphs = list(Deb822.iter_paragraphs(fd))
61 if len(dctrl_paragraphs) < 2:
62 _error(
63 "debian/control must contain at least two stanza (1 Source + 1-N Package stanza)"
64 )
66 source_package = SourcePackage(dctrl_paragraphs[0])
68 bin_pkgs = [
69 _create_binary_package(
70 p,
71 selected_packages,
72 excluded_packages,
73 select_arch_all,
74 select_arch_any,
75 dpkg_architecture_variables,
76 dpkg_arch_query_table,
77 build_env,
78 i,
79 )
80 for i, p in enumerate(dctrl_paragraphs[1:], 1)
81 ]
82 bin_pkgs_table = {p.name: p for p in bin_pkgs}
83 if not selected_packages.issubset(bin_pkgs_table.keys()):
84 unknown = selected_packages - bin_pkgs_table.keys()
85 _error(
86 f"The following *selected* packages (-p) are not listed in debian/control: {sorted(unknown)}"
87 )
88 if not excluded_packages.issubset(bin_pkgs_table.keys()):
89 unknown = selected_packages - bin_pkgs_table.keys()
90 _error(
91 f"The following *excluded* packages (-N) are not listed in debian/control: {sorted(unknown)}"
92 )
94 return source_package, bin_pkgs_table
97def _check_package_sets(
98 provided_packages: Set[str],
99 valid_package_names: Set[str],
100 option_name: str,
101) -> None:
102 # SonarLint proposes to use `provided_packages > valid_package_names`, which is valid for boolean
103 # logic, but not for set logic. We want to assert that provided_packages is a proper subset
104 # of valid_package_names. The rewrite would cause no errors for {'foo'} > {'bar'} - in set logic,
105 # neither is a superset / subset of the other, but we want an error for this case.
106 #
107 # Bug filed:
108 # https://community.sonarsource.com/t/sonarlint-python-s1940-rule-does-not-seem-to-take-set-logic-into-account/79718
109 if not (provided_packages <= valid_package_names):
110 non_existing_packages = sorted(provided_packages - valid_package_names)
111 invalid_package_list = ", ".join(non_existing_packages)
112 msg = (
113 f"Invalid package names passed to {option_name}: {invalid_package_list}: "
114 f'Valid package names are: {", ".join(valid_package_names)}'
115 )
116 _error(msg)
119def _create_binary_package(
120 paragraph: Union[Deb822, Dict[str, str]],
121 selected_packages: Union[Set[str], FrozenSet[str]],
122 excluded_packages: Union[Set[str], FrozenSet[str]],
123 select_arch_all: bool,
124 select_arch_any: bool,
125 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
126 dpkg_arch_query_table: DpkgArchTable,
127 build_env: DebBuildOptionsAndProfiles,
128 paragraph_index: int,
129) -> "BinaryPackage":
130 try:
131 package_name = paragraph["Package"]
132 except KeyError:
133 _error(f'Missing mandatory field "Package" in stanza number {paragraph_index}')
134 # The raise is there to help PyCharm type-checking (which fails at "NoReturn")
135 raise
137 for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD:
138 if mandatory_field not in paragraph:
139 _error(
140 f'Missing mandatory field "{mandatory_field}" for binary package {package_name}'
141 f" (stanza number {paragraph_index})"
142 )
144 architecture = paragraph["Architecture"]
146 if paragraph_index < 1:
147 raise ValueError("stanza index must be 1-indexed (1, 2, ...)")
148 is_main_package = paragraph_index == 1
150 if package_name in excluded_packages:
151 should_act_on = False
152 elif package_name in selected_packages:
153 should_act_on = True
154 elif architecture == "all":
155 should_act_on = select_arch_all
156 else:
157 should_act_on = select_arch_any
159 profiles_raw = paragraph.get("Build-Profiles", "").strip()
160 if should_act_on and profiles_raw:
161 try:
162 should_act_on = active_profiles_match(
163 profiles_raw, build_env.deb_build_profiles
164 )
165 except ValueError as e:
166 _error(f"Invalid Build-Profiles field for {package_name}: {e.args[0]}")
168 return BinaryPackage(
169 paragraph,
170 dpkg_architecture_variables,
171 dpkg_arch_query_table,
172 should_be_acted_on=should_act_on,
173 is_main_package=is_main_package,
174 )
177def _check_binary_arch(
178 arch_table: DpkgArchTable,
179 binary_arch: str,
180 declared_arch: str,
181) -> bool:
182 if binary_arch == "all":
183 return True
184 arch_wildcards = declared_arch.split()
185 for arch_wildcard in arch_wildcards: 185 ↛ 188line 185 didn't jump to line 188, because the loop on line 185 didn't complete
186 if arch_table.matches_architecture(binary_arch, arch_wildcard): 186 ↛ 185line 186 didn't jump to line 185, because the condition on line 186 was never false
187 return True
188 return False
191class BinaryPackage:
192 __slots__ = [
193 "_package_fields",
194 "_dbgsym_binary_package",
195 "_should_be_acted_on",
196 "_dpkg_architecture_variables",
197 "_declared_arch_matches_output_arch",
198 "_is_main_package",
199 "_substvars",
200 "_maintscript_snippets",
201 ]
203 def __init__(
204 self,
205 fields: Union[Mapping[str, str], Deb822],
206 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
207 dpkg_arch_query: DpkgArchTable,
208 *,
209 is_main_package: bool = False,
210 should_be_acted_on: bool = True,
211 ) -> None:
212 super(BinaryPackage, self).__init__()
213 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
214 # like one that we rely on it and just cast it.
215 self._package_fields = cast("Mapping[str, str]", fields)
216 self._dbgsym_binary_package = None
217 self._should_be_acted_on = should_be_acted_on
218 self._dpkg_architecture_variables = dpkg_architecture_variables
219 self._is_main_package = is_main_package
220 self._declared_arch_matches_output_arch = _check_binary_arch(
221 dpkg_arch_query, self.resolved_architecture, self.declared_architecture
222 )
224 @property
225 def name(self) -> str:
226 return self.fields["Package"]
228 @property
229 def archive_section(self) -> str:
230 value = self.fields.get("Section")
231 if value is None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true
232 return "Unknown"
233 return value
235 @property
236 def archive_component(self) -> str:
237 component = ""
238 section = self.archive_section
239 if "/" in section:
240 component = section.rsplit("/", 1)[0]
241 # The "main" component is always shortened to ""
242 if component == "main":
243 component = ""
244 return component
246 @property
247 def is_essential(self) -> bool:
248 return self._package_fields.get("Essential") == "yes"
250 @property
251 def is_udeb(self) -> bool:
252 return self.package_type == UDEB_PACKAGE_TYPE
254 @property
255 def should_be_acted_on(self) -> bool:
256 return self._should_be_acted_on and self._declared_arch_matches_output_arch
258 @property
259 def fields(self) -> Mapping[str, str]:
260 return self._package_fields
262 @property
263 def resolved_architecture(self) -> str:
264 arch = self.declared_architecture
265 if arch == "all":
266 return arch
267 if self._x_dh_build_for_type == "target": 267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true
268 return self._dpkg_architecture_variables["DEB_TARGET_ARCH"]
269 return self._dpkg_architecture_variables.current_host_arch
271 def package_deb_architecture_variable(self, variable_suffix: str) -> str:
272 if self._x_dh_build_for_type == "target": 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true
273 return self._dpkg_architecture_variables[f"DEB_TARGET_{variable_suffix}"]
274 return self._dpkg_architecture_variables[f"DEB_HOST_{variable_suffix}"]
276 @property
277 def deb_multiarch(self) -> str:
278 return self.package_deb_architecture_variable("MULTIARCH")
280 @property
281 def _x_dh_build_for_type(self) -> str:
282 v = self._package_fields.get("X-DH-Build-For-Type")
283 if v is None: 283 ↛ 285line 283 didn't jump to line 285, because the condition on line 283 was never false
284 return "host"
285 return v.lower()
287 @property
288 def package_type(self) -> str:
289 """Short for Package-Type (with proper default if absent)"""
290 v = self.fields.get("Package-Type")
291 if v is None:
292 return DEFAULT_PACKAGE_TYPE
293 return v
295 @property
296 def is_main_package(self) -> bool:
297 return self._is_main_package
299 def cross_command(self, command: str) -> str:
300 arch_table = self._dpkg_architecture_variables
301 if self._x_dh_build_for_type == "target":
302 target_gnu_type = arch_table["DEB_TARGET_GNU_TYPE"]
303 if arch_table["DEB_HOST_GNU_TYPE"] != target_gnu_type:
304 return f"{target_gnu_type}-{command}"
305 if arch_table.is_cross_compiling:
306 return f"{arch_table['DEB_HOST_GNU_TYPE']}-{command}"
307 return command
309 @property
310 def declared_architecture(self) -> str:
311 return self.fields["Architecture"]
313 @property
314 def is_arch_all(self) -> bool:
315 return self.declared_architecture == "all"
318class SourcePackage:
319 __slots__ = ("_package_fields",)
321 def __init__(self, fields: Union[Mapping[str, str], Deb822]):
322 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
323 # like one that we rely on it and just cast it.
324 self._package_fields = cast("Mapping[str, str]", fields)
326 @property
327 def fields(self) -> Mapping[str, str]:
328 return self._package_fields
330 @property
331 def name(self) -> str:
332 return self._package_fields["Source"]