Coverage for src/debputy/architecture_support.py: 86%
77 statements
« prev ^ index » next coverage.py v7.8.2, created at 2026-01-26 19:30 +0000
« prev ^ index » next coverage.py v7.8.2, created at 2026-01-26 19:30 +0000
1"debputy: information from a dpkg-architecture subprocess"
3import collections.abc
4import os
5import subprocess
7_VARIABLES = (
8 "ARCH",
9 "ARCH_ABI",
10 "ARCH_LIBC",
11 "ARCH_OS",
12 "ARCH_CPU",
13 "ARCH_BITS",
14 "ARCH_ENDIAN",
15 "GNU_CPU",
16 "GNU_SYSTEM",
17 "GNU_TYPE",
18 "MULTIARCH",
19)
22def _initial_cache() -> collections.abc.Iterator[tuple[str, str]]:
23 for machine in ("BUILD", "HOST", "TARGET"):
24 for variable in _VARIABLES:
25 key = f"DEB_{machine}_{variable}"
26 yield key, os.environ.get(key, "")
29_cache = dict(_initial_cache())
30# __getitem__() sets all empty values at once.
32_fake = dict[str, dict[str, str]]()
33# The constructor extends _fake if necessary for new mocked instances.
34# _fake["amd64"]["ARCH_ABI"] == "base"
37class DpkgArchitectureBuildProcessValuesTable(
38 collections.abc.Mapping[str, str],
39 # An implementation requires __getitem__ __len__ __iter__.
40 # Overriding __str__ seem unneeded, and even harmful when debugging.
41):
42 """Dict-like interface to dpkg-architecture values"""
44 def __init__(self, *, fake_host="", fake_build="", fake_target="") -> None:
45 """Create a new dpkg-architecture table.
47 The keys are the dpkg-architecture variables like
48 DEB_HOST_ARCH, DEB_BUILD_GNU_TYPE.
50 The caching mechanism assumes that variables affecting
51 dpkg-architecture are constant in os.environ.
53 The optional parameters are intended for testing purposes.
55 :param fake_host: if set, the instance is a mocked instance
56 for testing purposes. This affects the DEB_HOST_* variables.
58 :param fake_build: (ignored without fake_host, defaults to the
59 same value) if distinct from fake_host, then pretend this is a
60 cross-build. This affects the DEB_BUILD_* variables.
62 :param fake_target: (ignored without fake_host, defaults to
63 the same value): if distinct from fake_host, then pretend this
64 is a build _of_ a cross-compiler. This affects the
65 DEB_TARGET_* variables.
67 """
68 self._mock: dict[str, str] | None
69 if fake_host:
70 self._mock = {
71 "HOST": fake_host,
72 "BUILD": fake_build or fake_host,
73 "TARGET": fake_target or fake_host,
74 }
75 _ensure_in_fake(self._mock.values())
76 else:
77 self._mock = None
79 def __getitem__(self, item: str) -> str:
80 if self._mock:
81 match item.split(sep="_", maxsplit=2):
82 case "DEB", machine, variable:
83 # Raise KeyError on unexpected machine or variable.
84 return _fake[self._mock[machine]][variable]
85 raise KeyError
86 # This is a normal instance.
87 # Raise KeyError on unexpected keys.
88 value = _cache[item]
89 if value:
90 return value
91 # dpkg-architecture has not been run yet.
92 # The variable was missing or empty in the environment.
93 for k, v in _parse_dpkg_arch_output((), {}):
94 old = _cache[k]
95 if old: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 assert old == v, f"{k}={old} in env, {v} for dpkg-architecture"
97 else:
98 _cache[k] = v
99 return _cache[item]
101 def __len__(self) -> int:
102 return len(_cache)
104 def __iter__(self) -> collections.abc.Iterator[str]:
105 return iter(_cache)
107 @property
108 def current_host_arch(self) -> str:
109 """The architecture we are building for
111 This is the architecture name you need if you are in doubt.
112 """
113 return self["DEB_HOST_ARCH"]
115 @property
116 def current_host_multiarch(self) -> str:
117 """The multi-arch path basename
119 This is the multi-arch basename name you need if you are in doubt. It
120 goes here:
122 "/usr/lib/{MA}".format(table.current_host_multiarch)
124 """
125 return self["DEB_HOST_MULTIARCH"]
127 @property
128 def is_cross_compiling(self) -> bool:
129 """Whether we are cross-compiling
131 This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and
132 affects whether we can rely on being able to run the binaries
133 that are compiled.
134 """
135 return self["DEB_BUILD_GNU_TYPE"] != self["DEB_HOST_GNU_TYPE"]
138def _parse_dpkg_arch_output(
139 args: collections.abc.Iterable[str],
140 env: dict[str, str],
141) -> collections.abc.Iterator[tuple[str, str]]:
142 # For performance, disable dpkg's translation later
143 text = subprocess.check_output(
144 args=("dpkg-architecture", *args),
145 env=collections.ChainMap(env, {"DPKG_NLS": "0"}, os.environ),
146 encoding="utf-8",
147 )
148 for line in text.splitlines():
149 k, v = line.strip().split("=", 1)
150 yield k, v
153def _ensure_in_fake(archs: collections.abc.Iterable[str]) -> None:
154 # len(archs) == 3
155 # Remove duplicates and already cached architectures.
156 todo = {a for a in archs if a not in _fake}
157 if not todo:
158 return
160 env = {}
161 # Set CC to /bin/true avoid a warning from dpkg-architecture
162 env["CC"] = "/bin/true"
163 # Clear environ variables that might confuse dpkg-architecture
164 for k in os.environ:
165 if k.startswith("DEB_"): 165 ↛ 166line 165 didn't jump to line 166 because the condition on line 165 was never true
166 env[k] = ""
168 while todo:
169 # Each iteration consumes at least 1 element.
170 args = ["--host-arch", todo.pop()]
171 if todo: 171 ↛ 172line 171 didn't jump to line 172 because the condition on line 171 was never true
172 args.extend(("--target-arch", todo.pop()))
173 kw = dict(_parse_dpkg_arch_output(args, env))
175 h = kw["DEB_HOST_ARCH"]
176 assert h not in _fake and h not in todo
177 _fake[h] = dict((v, kw[f"DEB_HOST_{v}"]) for v in _VARIABLES)
179 t = kw["DEB_TARGET_ARCH"]
180 assert t not in todo
181 if t not in _fake: 181 ↛ 182line 181 didn't jump to line 182 because the condition on line 181 was never true
182 _fake[t] = dict((v, kw[f"DEB_TARGET_{v}"]) for v in _VARIABLES)
184 b = kw["DEB_BUILD_ARCH"]
185 if b not in _fake: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true
186 _fake[b] = dict((v, (kw[f"DEB_BUILD_{v}"])) for v in _VARIABLES)
187 if b in todo:
188 todo.remove(b)