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