Coverage for debputy/packages.py: 0%
151 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-22 14:29 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-22 14:29 +0100
1import re
2from typing import Dict, Union, Tuple, Optional, Set, cast, Mapping, FrozenSet
4from debian.deb822 import Deb822
5from debian.debian_support import DpkgArchTable
7from ._deb_options_profiles import DebOptionsAndProfiles
8from .architecture_support import DpkgArchitectureBuildProcessValuesTable, dpkg_architecture_table
9from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match
11_MANDATORY_BINARY_PACKAGE_FIELD = [
12 'Package',
13 'Architecture',
14]
17def parse_source_debian_control(selected_packages: Set[str],
18 excluded_packages: Set[str],
19 select_arch_all: bool,
20 select_arch_any: bool,
22 dpkg_architecture_variables: Optional[DpkgArchitectureBuildProcessValuesTable] = None,
23 dpkg_arch_query_table: Optional[DpkgArchTable] = None,
24 build_env: Optional[DebOptionsAndProfiles] = None,
25 debian_control: str = 'debian/control',
26 ) -> Tuple['SourcePackage', Dict[str, 'BinaryPackage']]:
27 if dpkg_architecture_variables is None:
28 dpkg_architecture_variables = dpkg_architecture_table()
29 if dpkg_arch_query_table is None:
30 dpkg_arch_query_table = DpkgArchTable.load_arch_table()
31 if build_env is None:
32 build_env = DebOptionsAndProfiles.instance()
34 # If no selection option is set, then all packages are acted on (except the
35 # excluded ones)
36 if not selected_packages and not select_arch_all and not select_arch_any:
37 select_arch_all = True
38 select_arch_any = True
40 with open(debian_control) as fd:
41 dctrl_paragraphs = list(Deb822.iter_paragraphs(fd))
43 if len(dctrl_paragraphs) < 2:
44 _error("debian/control must contain at least two paragraphs (1 Source + 1-N Package paragraphs)")
46 source_package = SourcePackage(dctrl_paragraphs[0])
48 bin_pkgs = [
49 _create_binary_package(
50 p,
51 selected_packages,
52 excluded_packages,
53 select_arch_all,
54 select_arch_any,
55 dpkg_architecture_variables,
56 dpkg_arch_query_table,
57 build_env,
58 i,
59 ) for i, p in enumerate(dctrl_paragraphs[1:], 1)
60 ]
62 return source_package, {p.name: p for p in bin_pkgs}
65def _check_package_sets(provided_packages: Set[str], valid_package_names: Set[str], option_name: str):
66 # SonarLint proposes to use `provided_packages > valid_package_names`, which is valid for boolean
67 # logic, but not for set logic. We want to assert that provided_packages is a proper subset
68 # of valid_package_names. The rewrite would cause no errors for {'foo'} > {'bar'} - in set logic,
69 # neither is a superset / subset of the other, but we want an error for this case.
70 #
71 # Bug filed:
72 # https://community.sonarsource.com/t/sonarlint-python-s1940-rule-does-not-seem-to-take-set-logic-into-account/79718
73 if not (provided_packages <= valid_package_names):
74 non_existing_packages = sorted(provided_packages - valid_package_names)
75 invalid_package_list = ", ".join(non_existing_packages)
76 msg = f'Invalid package names passed to {option_name}: {invalid_package_list}: ' \
77 f'Valid package names are: {", ".join(valid_package_names)}'
78 _error(msg)
81def _create_binary_package(paragraph: Union[Deb822, Dict[str, str]],
82 selected_packages: Set[str],
83 excluded_packages: Set[str],
84 select_arch_all: bool,
85 select_arch_any: bool,
87 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
88 dpkg_arch_query_table: DpkgArchTable,
89 build_env: DebOptionsAndProfiles,
90 paragraph_index: int,
91 ) -> 'BinaryPackage':
93 try:
94 package_name = paragraph['Package']
95 except KeyError:
96 _error(f'Missing mandatory field "Package" in paragraph number {paragraph_index}')
97 # The raise is there to help PyCharm type-checking (which fails at "NoReturn")
98 raise
100 for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD:
101 if mandatory_field not in paragraph:
102 _error(f'Missing mandatory field "{mandatory_field}" for binary package {package_name}'
103 f' (paragraph number {paragraph_index})')
105 architecture = paragraph['Architecture']
107 if paragraph_index < 1:
108 raise ValueError("paragraph index must be 1-indexed (1, 2, ...)")
109 is_main_package = paragraph_index == 1
111 if package_name in excluded_packages:
112 should_act_on = False
113 elif package_name in selected_packages:
114 should_act_on = True
115 elif architecture == "all":
116 should_act_on = select_arch_all
117 else:
118 should_act_on = select_arch_any
120 profiles_raw = paragraph.get('Build-Profiles', '').strip()
121 if should_act_on and profiles_raw:
122 try:
123 should_act_on = active_profiles_match(profiles_raw, build_env.deb_build_profiles)
124 except ValueError as e:
125 _error(f'Invalid Build-Profiles field for {package_name}: {e.args[0]}')
127 return BinaryPackage(paragraph,
128 dpkg_architecture_variables,
129 dpkg_arch_query_table,
130 should_be_acted_on=should_act_on,
131 is_main_package=is_main_package)
134def _check_binary_arch(arch_table: DpkgArchTable, binary_arch: str, declared_arch: str):
135 if binary_arch == 'all':
136 return True
137 arch_wildcards = declared_arch.split()
138 for arch_wildcard in arch_wildcards:
139 if arch_table.matches_architecture(binary_arch, arch_wildcard):
140 return True
141 return False
144class BinaryPackage:
146 __slots__ = [
147 '_package_fields',
148 '_dbgsym_binary_package',
149 '_should_be_acted_on',
150 '_dpkg_architecture_variables',
151 '_declared_arch_matches_output_arch',
152 '_is_main_package',
153 '_substvars',
154 '_maintscript_snippets',
155 ]
157 def __init__(self,
158 fields: Union[Mapping[str, str], Deb822],
159 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
160 dpkg_arch_query: DpkgArchTable,
161 *,
162 is_main_package=False,
163 should_be_acted_on=True):
164 super(BinaryPackage, self).__init__()
165 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
166 # like one that we rely on it and just cast it.
167 self._package_fields = cast('Mapping[str, str]', fields)
168 self._dbgsym_binary_package = None
169 self._should_be_acted_on = should_be_acted_on
170 self._dpkg_architecture_variables = dpkg_architecture_variables
171 self._is_main_package = is_main_package
172 self._declared_arch_matches_output_arch = _check_binary_arch(
173 dpkg_arch_query,
174 self.resolved_architecture,
175 self.declared_architecture
176 )
178 @property
179 def name(self):
180 return self.fields['Package']
182 @property
183 def archive_section(self) -> str:
184 value = self.fields.get('Section')
185 if value is None:
186 return 'Unknown'
187 return value
189 @property
190 def archive_component(self) -> str:
191 component = ''
192 section = self.archive_section
193 if '/' in section:
194 component = section.rsplit('/', 1)[0]
195 # The "main" component is always shortened to ""
196 if component == 'main':
197 component = ''
198 return component
200 @property
201 def is_udeb(self) -> bool:
202 return self.package_type == UDEB_PACKAGE_TYPE
204 @property
205 def should_be_acted_on(self) -> bool:
206 return self._should_be_acted_on and self._declared_arch_matches_output_arch
208 @property
209 def fields(self) -> Mapping[str, str]:
210 return self._package_fields
212 @property
213 def resolved_architecture(self):
214 arch = self.declared_architecture
215 if arch == 'all':
216 return arch
217 if self._x_dh_build_for_type == 'target':
218 return self._dpkg_architecture_variables['DEB_TARGET_ARCH']
219 return self._dpkg_architecture_variables.current_host_arch
221 @property
222 def _x_dh_build_for_type(self) -> str:
223 v = self._package_fields.get('X-DH-Build-For-Type')
224 if v is None:
225 return 'host'
226 return v.lower()
228 @property
229 def package_type(self) -> str:
230 """Short for Package-Type (with proper default if absent)"""
231 v = self.fields.get('Package-Type')
232 if v is None:
233 return DEFAULT_PACKAGE_TYPE
234 return v
236 @property
237 def is_main_package(self) -> bool:
238 return self._is_main_package
240 def cross_command(self, command) -> str:
241 arch_table = self._dpkg_architecture_variables
242 if self._x_dh_build_for_type == 'target':
243 target_gnu_type = arch_table['DEB_TARGET_GNU_TYPE']
244 if arch_table['DEB_HOST_GNU_TYPE'] != target_gnu_type:
245 return f"{target_gnu_type}-{command}"
246 if arch_table.is_cross_compiling:
247 return f"{arch_table['DEB_HOST_GNU_TYPE']}-{command}"
248 return command
250 @property
251 def declared_architecture(self):
252 return self.fields['Architecture']
254 @property
255 def is_arch_all(self) -> bool:
256 return self.declared_architecture == 'all'
259class SourcePackage:
261 __slots__ = ('_package_fields',)
263 def __init__(self, fields: Union[Mapping[str, str], Deb822]):
264 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
265 # like one that we rely on it and just cast it.
266 self._package_fields = cast('Mapping[str, str]', fields)
268 @property
269 def package_fields(self) -> Mapping[str, str]:
270 return self._package_fields
272 @property
273 def package_name(self):
274 return self._package_fields['Source']