summaryrefslogtreecommitdiffstats
path: root/src/debputy/packages.py
blob: 3204f46a240f2c0ffc9600c44bed537cda4e4648 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
from typing import (
    Dict,
    Union,
    Tuple,
    Optional,
    Set,
    cast,
    Mapping,
    FrozenSet,
    TYPE_CHECKING,
)

from debian.deb822 import Deb822
from debian.debian_support import DpkgArchTable

from ._deb_options_profiles import DebBuildOptionsAndProfiles
from .architecture_support import (
    DpkgArchitectureBuildProcessValuesTable,
    dpkg_architecture_table,
)
from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match

if TYPE_CHECKING:
    from .plugin.api import VirtualPath


_MANDATORY_BINARY_PACKAGE_FIELD = [
    "Package",
    "Architecture",
]


def parse_source_debian_control(
    debian_control: "VirtualPath",
    selected_packages: Union[Set[str], FrozenSet[str]],
    excluded_packages: Union[Set[str], FrozenSet[str]],
    select_arch_all: bool,
    select_arch_any: bool,
    dpkg_architecture_variables: Optional[
        DpkgArchitectureBuildProcessValuesTable
    ] = None,
    dpkg_arch_query_table: Optional[DpkgArchTable] = None,
    build_env: Optional[DebBuildOptionsAndProfiles] = None,
) -> Tuple["SourcePackage", Dict[str, "BinaryPackage"]]:
    if dpkg_architecture_variables is None:
        dpkg_architecture_variables = dpkg_architecture_table()
    if dpkg_arch_query_table is None:
        dpkg_arch_query_table = DpkgArchTable.load_arch_table()
    if build_env is None:
        build_env = DebBuildOptionsAndProfiles.instance()

    # If no selection option is set, then all packages are acted on (except the
    # excluded ones)
    if not selected_packages and not select_arch_all and not select_arch_any:
        select_arch_all = True
        select_arch_any = True

    with debian_control.open() as fd:
        dctrl_paragraphs = list(Deb822.iter_paragraphs(fd))

    if len(dctrl_paragraphs) < 2:
        _error(
            "debian/control must contain at least two stanza (1 Source + 1-N Package stanza)"
        )

    source_package = SourcePackage(dctrl_paragraphs[0])

    bin_pkgs = [
        _create_binary_package(
            p,
            selected_packages,
            excluded_packages,
            select_arch_all,
            select_arch_any,
            dpkg_architecture_variables,
            dpkg_arch_query_table,
            build_env,
            i,
        )
        for i, p in enumerate(dctrl_paragraphs[1:], 1)
    ]
    bin_pkgs_table = {p.name: p for p in bin_pkgs}
    if not selected_packages.issubset(bin_pkgs_table.keys()):
        unknown = selected_packages - bin_pkgs_table.keys()
        _error(
            f"The following *selected* packages (-p) are not listed in debian/control: {sorted(unknown)}"
        )
    if not excluded_packages.issubset(bin_pkgs_table.keys()):
        unknown = selected_packages - bin_pkgs_table.keys()
        _error(
            f"The following *excluded* packages (-N) are not listed in debian/control: {sorted(unknown)}"
        )

    return source_package, bin_pkgs_table


def _check_package_sets(
    provided_packages: Set[str],
    valid_package_names: Set[str],
    option_name: str,
) -> None:
    # SonarLint proposes to use `provided_packages > valid_package_names`, which is valid for boolean
    # logic, but not for set logic.  We want to assert that provided_packages is a proper subset
    # of valid_package_names.  The rewrite would cause no errors for {'foo'} > {'bar'} - in set logic,
    # neither is a superset / subset of the other, but we want an error for this case.
    #
    # Bug filed:
    # https://community.sonarsource.com/t/sonarlint-python-s1940-rule-does-not-seem-to-take-set-logic-into-account/79718
    if not (provided_packages <= valid_package_names):
        non_existing_packages = sorted(provided_packages - valid_package_names)
        invalid_package_list = ", ".join(non_existing_packages)
        msg = (
            f"Invalid package names passed to {option_name}: {invalid_package_list}: "
            f'Valid package names are: {", ".join(valid_package_names)}'
        )
        _error(msg)


def _create_binary_package(
    paragraph: Union[Deb822, Dict[str, str]],
    selected_packages: Union[Set[str], FrozenSet[str]],
    excluded_packages: Union[Set[str], FrozenSet[str]],
    select_arch_all: bool,
    select_arch_any: bool,
    dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
    dpkg_arch_query_table: DpkgArchTable,
    build_env: DebBuildOptionsAndProfiles,
    paragraph_index: int,
) -> "BinaryPackage":
    try:
        package_name = paragraph["Package"]
    except KeyError:
        _error(f'Missing mandatory field "Package" in stanza number {paragraph_index}')
        # The raise is there to help PyCharm type-checking (which fails at "NoReturn")
        raise

    for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD:
        if mandatory_field not in paragraph:
            _error(
                f'Missing mandatory field "{mandatory_field}" for binary package {package_name}'
                f" (stanza number {paragraph_index})"
            )

    architecture = paragraph["Architecture"]

    if paragraph_index < 1:
        raise ValueError("stanza index must be 1-indexed (1, 2, ...)")
    is_main_package = paragraph_index == 1

    if package_name in excluded_packages:
        should_act_on = False
    elif package_name in selected_packages:
        should_act_on = True
    elif architecture == "all":
        should_act_on = select_arch_all
    else:
        should_act_on = select_arch_any

    profiles_raw = paragraph.get("Build-Profiles", "").strip()
    if should_act_on and profiles_raw:
        try:
            should_act_on = active_profiles_match(
                profiles_raw, build_env.deb_build_profiles
            )
        except ValueError as e:
            _error(f"Invalid Build-Profiles field for {package_name}: {e.args[0]}")

    return BinaryPackage(
        paragraph,
        dpkg_architecture_variables,
        dpkg_arch_query_table,
        should_be_acted_on=should_act_on,
        is_main_package=is_main_package,
    )


def _check_binary_arch(
    arch_table: DpkgArchTable,
    binary_arch: str,
    declared_arch: str,
) -> bool:
    if binary_arch == "all":
        return True
    arch_wildcards = declared_arch.split()
    for arch_wildcard in arch_wildcards:
        if arch_table.matches_architecture(binary_arch, arch_wildcard):
            return True
    return False


class BinaryPackage:
    __slots__ = [
        "_package_fields",
        "_dbgsym_binary_package",
        "_should_be_acted_on",
        "_dpkg_architecture_variables",
        "_declared_arch_matches_output_arch",
        "_is_main_package",
        "_substvars",
        "_maintscript_snippets",
    ]

    def __init__(
        self,
        fields: Union[Mapping[str, str], Deb822],
        dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
        dpkg_arch_query: DpkgArchTable,
        *,
        is_main_package: bool = False,
        should_be_acted_on: bool = True,
    ) -> None:
        super(BinaryPackage, self).__init__()
        # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
        # like one that we rely on it and just cast it.
        self._package_fields = cast("Mapping[str, str]", fields)
        self._dbgsym_binary_package = None
        self._should_be_acted_on = should_be_acted_on
        self._dpkg_architecture_variables = dpkg_architecture_variables
        self._is_main_package = is_main_package
        self._declared_arch_matches_output_arch = _check_binary_arch(
            dpkg_arch_query, self.resolved_architecture, self.declared_architecture
        )

    @property
    def name(self) -> str:
        return self.fields["Package"]

    @property
    def archive_section(self) -> str:
        value = self.fields.get("Section")
        if value is None:
            return "Unknown"
        return value

    @property
    def archive_component(self) -> str:
        component = ""
        section = self.archive_section
        if "/" in section:
            component = section.rsplit("/", 1)[0]
            # The "main" component is always shortened to ""
            if component == "main":
                component = ""
        return component

    @property
    def is_essential(self) -> bool:
        return self._package_fields.get("Essential") == "yes"

    @property
    def is_udeb(self) -> bool:
        return self.package_type == UDEB_PACKAGE_TYPE

    @property
    def should_be_acted_on(self) -> bool:
        return self._should_be_acted_on and self._declared_arch_matches_output_arch

    @property
    def fields(self) -> Mapping[str, str]:
        return self._package_fields

    @property
    def resolved_architecture(self) -> str:
        arch = self.declared_architecture
        if arch == "all":
            return arch
        if self._x_dh_build_for_type == "target":
            return self._dpkg_architecture_variables["DEB_TARGET_ARCH"]
        return self._dpkg_architecture_variables.current_host_arch

    def package_deb_architecture_variable(self, variable_suffix: str) -> str:
        if self._x_dh_build_for_type == "target":
            return self._dpkg_architecture_variables[f"DEB_TARGET_{variable_suffix}"]
        return self._dpkg_architecture_variables[f"DEB_HOST_{variable_suffix}"]

    @property
    def deb_multiarch(self) -> str:
        return self.package_deb_architecture_variable("MULTIARCH")

    @property
    def _x_dh_build_for_type(self) -> str:
        v = self._package_fields.get("X-DH-Build-For-Type")
        if v is None:
            return "host"
        return v.lower()

    @property
    def package_type(self) -> str:
        """Short for Package-Type (with proper default if absent)"""
        v = self.fields.get("Package-Type")
        if v is None:
            return DEFAULT_PACKAGE_TYPE
        return v

    @property
    def is_main_package(self) -> bool:
        return self._is_main_package

    def cross_command(self, command: str) -> str:
        arch_table = self._dpkg_architecture_variables
        if self._x_dh_build_for_type == "target":
            target_gnu_type = arch_table["DEB_TARGET_GNU_TYPE"]
            if arch_table["DEB_HOST_GNU_TYPE"] != target_gnu_type:
                return f"{target_gnu_type}-{command}"
        if arch_table.is_cross_compiling:
            return f"{arch_table['DEB_HOST_GNU_TYPE']}-{command}"
        return command

    @property
    def declared_architecture(self) -> str:
        return self.fields["Architecture"]

    @property
    def is_arch_all(self) -> bool:
        return self.declared_architecture == "all"


class SourcePackage:
    __slots__ = ("_package_fields",)

    def __init__(self, fields: Union[Mapping[str, str], Deb822]):
        # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough
        # like one that we rely on it and just cast it.
        self._package_fields = cast("Mapping[str, str]", fields)

    @property
    def fields(self) -> Mapping[str, str]:
        return self._package_fields

    @property
    def name(self) -> str:
        return self._package_fields["Source"]