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

1"debputy: information from a dpkg-architecture subprocess" 

2 

3import collections.abc 

4import os 

5import subprocess 

6 

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) 

20 

21 

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, "") 

27 

28 

29_cache = dict(_initial_cache()) 

30# __getitem__() sets all empty values at once. 

31 

32_fake = dict[str, dict[str, str]]() 

33# The constructor extends _fake if necessary for new mocked instances. 

34# _fake["amd64"]["ARCH_ABI"] == "base" 

35 

36 

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""" 

43 

44 def __init__(self, *, fake_host="", fake_build="", fake_target="") -> None: 

45 """Create a new dpkg-architecture table. 

46 

47 The keys are the dpkg-architecture variables like 

48 DEB_HOST_ARCH, DEB_BUILD_GNU_TYPE. 

49 

50 The caching mechanism assumes that variables affecting 

51 dpkg-architecture are constant in os.environ. 

52 

53 The optional parameters are intended for testing purposes. 

54 

55 :param fake_host: if set, the instance is a mocked instance 

56 for testing purposes. This affects the DEB_HOST_* variables. 

57 

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. 

61 

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. 

66 

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 

78 

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] 

100 

101 def __len__(self) -> int: 

102 return len(_cache) 

103 

104 def __iter__(self) -> collections.abc.Iterator[str]: 

105 return iter(_cache) 

106 

107 @property 

108 def current_host_arch(self) -> str: 

109 """The architecture we are building for 

110 

111 This is the architecture name you need if you are in doubt. 

112 """ 

113 return self["DEB_HOST_ARCH"] 

114 

115 @property 

116 def current_host_multiarch(self) -> str: 

117 """The multi-arch path basename 

118 

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

120 goes here: 

121 

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

123 

124 """ 

125 return self["DEB_HOST_MULTIARCH"] 

126 

127 @property 

128 def is_cross_compiling(self) -> bool: 

129 """Whether we are cross-compiling 

130 

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"] 

136 

137 

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 

151 

152 

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 

159 

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] = "" 

167 

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)) 

174 

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) 

178 

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) 

183 

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)