summaryrefslogtreecommitdiffstats
path: root/src/debputy/architecture_support.py
blob: e190722ce31e5695a8a0432c5f964aa11fb0d065 (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
import os
import subprocess
from functools import lru_cache
from typing import Dict, Optional, Iterator, Tuple


class DpkgArchitectureBuildProcessValuesTable:
    """Dict-like interface to dpkg-architecture values"""

    def __init__(self, *, mocked_answers: Optional[Dict[str, str]] = None) -> None:
        """Create a new dpkg-architecture table; NO INSTANTIATION

        This object will be created for you; if you need a production instance
        then call dpkg_architecture_table().  If you need a testing instance,
        then call mock_arch_table(...)

        :param mocked_answers: Used for testing purposes.  Do not use directly;
        instead use mock_arch_table(...) to create the table you want.
        """
        self._architecture_cache: Dict[str, str] = {}
        self._has_run_dpkg_architecture = False
        if mocked_answers is None:
            self._architecture_cache = {}
            self._respect_environ: bool = True
            self._has_run_dpkg_architecture = False
        else:
            self._architecture_cache = mocked_answers
            self._respect_environ = False
            self._has_run_dpkg_architecture = True

    def __contains__(self, item: str) -> bool:
        try:
            self[item]
        except KeyError:
            return False
        else:
            return True

    def __getitem__(self, item: str) -> str:
        if item not in self._architecture_cache:
            if self._respect_environ:
                value = os.environ.get(item)
                if value is not None:
                    self._architecture_cache[item] = value
                    return value
            if not self._has_run_dpkg_architecture:
                self._load_dpkg_architecture_values()
            # Fall through and look it up in the cache
        return self._architecture_cache[item]

    def __iter__(self) -> Iterator[str]:
        if not self._has_run_dpkg_architecture:
            self._load_dpkg_architecture_values()
        yield from self._architecture_cache

    @property
    def current_host_arch(self) -> str:
        """The architecture we are building for

        This is the architecture name you need if you are in doubt.
        """
        return self["DEB_HOST_ARCH"]

    @property
    def current_host_multiarch(self) -> str:
        """The multi-arch path basename

        This is the multi-arch basename name you need if you are in doubt.  It
        goes here:

            "/usr/lib/{MA}".format(table.current_host_multiarch)

        """
        return self["DEB_HOST_MULTIARCH"]

    @property
    def is_cross_compiling(self) -> bool:
        """Whether we are cross-compiling

        This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and
        affects whether we can rely on being able to run the binaries
        that are compiled.
        """
        return self["DEB_BUILD_GNU_TYPE"] != self["DEB_HOST_GNU_TYPE"]

    def _load_dpkg_architecture_values(self) -> None:
        env = dict(os.environ)
        # For performance, disable dpkg's translation later
        env["DPKG_NLS"] = "0"
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture"],
                env=env,
            )
        )
        for k, v in kw_pairs:
            self._architecture_cache[k] = os.environ.get(k, v)
        self._has_run_dpkg_architecture = True


def _parse_dpkg_arch_output(output: bytes) -> Iterator[Tuple[str, str]]:
    text = output.decode("utf-8")
    for line in text.splitlines():
        k, v = line.strip().split("=", 1)
        yield k, v


def _rewrite(value: str, from_pattern: str, to_pattern: str) -> str:
    assert value.startswith(from_pattern)
    return to_pattern + value[len(from_pattern) :]


def faked_arch_table(
    host_arch: str,
    *,
    build_arch: Optional[str] = None,
    target_arch: Optional[str] = None,
) -> DpkgArchitectureBuildProcessValuesTable:
    """Creates a mocked instance of DpkgArchitectureBuildProcessValuesTable


    :param host_arch: The dpkg architecture to mock answers for.  This affects
      DEB_HOST_* values and defines the default for DEB_{BUILD,TARGET}_* if
      not overridden.
    :param build_arch: If set and has a different value than host_arch, then
      pretend this is a cross-build.  This value affects the DEB_BUILD_* values.
    :param target_arch: If set and has a different value than host_arch, then
      pretend this is a build _of_ a cross-compiler.  This value affects the
      DEB_TARGET_* values.
    """

    if build_arch is None:
        build_arch = host_arch

    if target_arch is None:
        target_arch = host_arch
    return _faked_arch_tables(host_arch, build_arch, target_arch)


@lru_cache
def _faked_arch_tables(
    host_arch: str, build_arch: str, target_arch: str
) -> DpkgArchitectureBuildProcessValuesTable:
    mock_table = {}

    env = dict(os.environ)
    # Set CC to /bin/true avoid a warning from dpkg-architecture
    env["CC"] = "/bin/true"
    # For performance, disable dpkg's translation later
    env["DPKG_NLS"] = "0"
    # Clear environ variables that might confuse dpkg-architecture
    for k in os.environ:
        if k.startswith("DEB_"):
            del env[k]

    if build_arch == host_arch:
        # easy / common case - we can handle this with a single call
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
                mock_table[k] = v
            # Clone DEB_HOST_* into DEB_BUILD_* as well
            if k.startswith("DEB_HOST_"):
                k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k2] = v
    elif build_arch != host_arch and host_arch != target_arch:
        # This will need two dpkg-architecture calls because we cannot set
        # DEB_BUILD_* directly.  But we can set DEB_HOST_* and then rewrite
        # it
        # First handle the build arch
        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", build_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith("DEB_HOST_"):
                k = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k] = v

        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
                env=env,
            )
        )
        for k, v in kw_pairs:
            if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
                mock_table[k] = v
    else:
        # This is a fun special case.  We know that:
        # * build_arch != host_arch
        # * host_arch == target_arch
        # otherwise we would have hit one of the previous cases.
        #
        # We can do this in a single call to dpkg-architecture by
        # a bit of "cleaver" rewriting.
        #
        # - Use -a to set DEB_HOST_* and then rewrite that as
        #   DEB_BUILD_*
        # - use -A to set DEB_TARGET_* and then use that for both
        #   DEB_HOST_* and DEB_TARGET_*

        kw_pairs = _parse_dpkg_arch_output(
            subprocess.check_output(
                ["dpkg-architecture", "-a", build_arch, "-A", target_arch], env=env
            )
        )
        for k, v in kw_pairs:
            if k.startswith("DEB_HOST_"):
                k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
                mock_table[k2] = v
                continue
            if k.startswith("DEB_TARGET_"):
                mock_table[k] = v
                k2 = _rewrite(k, "DEB_TARGET_", "DEB_HOST_")
                mock_table[k2] = v

    table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table)
    return table


_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable()


def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable:
    return _ARCH_TABLE