Coverage for src/debputy/architecture_support.py: 94%
108 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 os
2import subprocess
3from functools import lru_cache
4from typing import Dict, Optional, Tuple
5from collections.abc import Iterator
8class DpkgArchitectureBuildProcessValuesTable:
9 """Dict-like interface to dpkg-architecture values"""
11 def __init__(self, *, mocked_answers: dict[str, str] | None = None) -> None:
12 """Create a new dpkg-architecture table; NO INSTANTIATION
14 This object will be created for you; if you need a production instance
15 then call dpkg_architecture_table(). If you need a testing instance,
16 then call mock_arch_table(...)
18 :param mocked_answers: Used for testing purposes. Do not use directly;
19 instead use mock_arch_table(...) to create the table you want.
20 """
21 self._architecture_cache: dict[str, str] = {}
22 self._has_run_dpkg_architecture = False
23 if mocked_answers is None:
24 self._architecture_cache = {}
25 self._respect_environ: bool = True
26 self._has_run_dpkg_architecture = False
27 else:
28 self._architecture_cache = mocked_answers
29 self._respect_environ = False
30 self._has_run_dpkg_architecture = True
32 def __contains__(self, item: str) -> bool:
33 try:
34 self[item]
35 except KeyError:
36 return False
37 else:
38 return True
40 def __getitem__(self, item: str) -> str:
41 if item not in self._architecture_cache:
42 if self._respect_environ:
43 value = os.environ.get(item)
44 if value is not None: 44 ↛ 45line 44 didn't jump to line 45 because the condition on line 44 was never true
45 self._architecture_cache[item] = value
46 return value
47 if not self._has_run_dpkg_architecture:
48 self._load_dpkg_architecture_values()
49 # Fall through and look it up in the cache
50 return self._architecture_cache[item]
52 def __iter__(self) -> Iterator[str]:
53 if not self._has_run_dpkg_architecture:
54 self._load_dpkg_architecture_values()
55 yield from self._architecture_cache
57 @property
58 def current_host_arch(self) -> str:
59 """The architecture we are building for
61 This is the architecture name you need if you are in doubt.
62 """
63 return self["DEB_HOST_ARCH"]
65 @property
66 def current_host_multiarch(self) -> str:
67 """The multi-arch path basename
69 This is the multi-arch basename name you need if you are in doubt. It
70 goes here:
72 "/usr/lib/{MA}".format(table.current_host_multiarch)
74 """
75 return self["DEB_HOST_MULTIARCH"]
77 @property
78 def is_cross_compiling(self) -> bool:
79 """Whether we are cross-compiling
81 This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and
82 affects whether we can rely on being able to run the binaries
83 that are compiled.
84 """
85 return self["DEB_BUILD_GNU_TYPE"] != self["DEB_HOST_GNU_TYPE"]
87 def _load_dpkg_architecture_values(self) -> None:
88 env = dict(os.environ)
89 # For performance, disable dpkg's translation later
90 env["DPKG_NLS"] = "0"
91 kw_pairs = _parse_dpkg_arch_output(
92 subprocess.check_output(
93 ["dpkg-architecture"],
94 env=env,
95 )
96 )
97 for k, v in kw_pairs:
98 self._architecture_cache[k] = os.environ.get(k, v)
99 self._has_run_dpkg_architecture = True
102def _parse_dpkg_arch_output(output: bytes) -> Iterator[tuple[str, str]]:
103 text = output.decode("utf-8")
104 for line in text.splitlines():
105 k, v = line.strip().split("=", 1)
106 yield k, v
109def _rewrite(value: str, from_pattern: str, to_pattern: str) -> str:
110 assert value.startswith(from_pattern)
111 return to_pattern + value[len(from_pattern) :]
114def faked_arch_table(
115 host_arch: str,
116 *,
117 build_arch: str | None = None,
118 target_arch: str | None = None,
119) -> DpkgArchitectureBuildProcessValuesTable:
120 """Creates a mocked instance of DpkgArchitectureBuildProcessValuesTable
123 :param host_arch: The dpkg architecture to mock answers for. This affects
124 DEB_HOST_* values and defines the default for DEB_{BUILD,TARGET}_* if
125 not overridden.
126 :param build_arch: If set and has a different value than host_arch, then
127 pretend this is a cross-build. This value affects the DEB_BUILD_* values.
128 :param target_arch: If set and has a different value than host_arch, then
129 pretend this is a build _of_ a cross-compiler. This value affects the
130 DEB_TARGET_* values.
131 """
133 if build_arch is None:
134 build_arch = host_arch
136 if target_arch is None:
137 target_arch = host_arch
138 return _faked_arch_tables(host_arch, build_arch, target_arch)
141@lru_cache
142def _faked_arch_tables(
143 host_arch: str, build_arch: str, target_arch: str
144) -> DpkgArchitectureBuildProcessValuesTable:
145 mock_table = {}
147 env = dict(os.environ)
148 # Set CC to /bin/true avoid a warning from dpkg-architecture
149 env["CC"] = "/bin/true"
150 # For performance, disable dpkg's translation later
151 env["DPKG_NLS"] = "0"
152 # Clear environ variables that might confuse dpkg-architecture
153 for k in os.environ:
154 if k.startswith("DEB_"): 154 ↛ 155line 154 didn't jump to line 155 because the condition on line 154 was never true
155 del env[k]
157 if build_arch == host_arch:
158 # easy / common case - we can handle this with a single call
159 kw_pairs = _parse_dpkg_arch_output(
160 subprocess.check_output(
161 ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
162 env=env,
163 )
164 )
165 for k, v in kw_pairs:
166 if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
167 mock_table[k] = v
168 # Clone DEB_HOST_* into DEB_BUILD_* as well
169 if k.startswith("DEB_HOST_"):
170 k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
171 mock_table[k2] = v
172 elif build_arch != host_arch and host_arch != target_arch:
173 # This will need two dpkg-architecture calls because we cannot set
174 # DEB_BUILD_* directly. But we can set DEB_HOST_* and then rewrite
175 # it
176 # First handle the build arch
177 kw_pairs = _parse_dpkg_arch_output(
178 subprocess.check_output(
179 ["dpkg-architecture", "-a", build_arch],
180 env=env,
181 )
182 )
183 for k, v in kw_pairs:
184 if k.startswith("DEB_HOST_"):
185 k = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
186 mock_table[k] = v
188 kw_pairs = _parse_dpkg_arch_output(
189 subprocess.check_output(
190 ["dpkg-architecture", "-a", host_arch, "-A", target_arch],
191 env=env,
192 )
193 )
194 for k, v in kw_pairs:
195 if k.startswith(("DEB_HOST_", "DEB_TARGET_")):
196 mock_table[k] = v
197 else:
198 # This is a fun special case. We know that:
199 # * build_arch != host_arch
200 # * host_arch == target_arch
201 # otherwise we would have hit one of the previous cases.
202 #
203 # We can do this in a single call to dpkg-architecture by
204 # a bit of "cleaver" rewriting.
205 #
206 # - Use -a to set DEB_HOST_* and then rewrite that as
207 # DEB_BUILD_*
208 # - use -A to set DEB_TARGET_* and then use that for both
209 # DEB_HOST_* and DEB_TARGET_*
211 kw_pairs = _parse_dpkg_arch_output(
212 subprocess.check_output(
213 ["dpkg-architecture", "-a", build_arch, "-A", target_arch], env=env
214 )
215 )
216 for k, v in kw_pairs:
217 if k.startswith("DEB_HOST_"):
218 k2 = _rewrite(k, "DEB_HOST_", "DEB_BUILD_")
219 mock_table[k2] = v
220 continue
221 if k.startswith("DEB_TARGET_"):
222 mock_table[k] = v
223 k2 = _rewrite(k, "DEB_TARGET_", "DEB_HOST_")
224 mock_table[k2] = v
226 table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table)
227 return table
230_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable()
233def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable:
234 return _ARCH_TABLE