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

1import os 

2import subprocess 

3from functools import lru_cache 

4from typing import Dict, Optional, Tuple 

5from collections.abc import Iterator 

6 

7 

8class DpkgArchitectureBuildProcessValuesTable: 

9 """Dict-like interface to dpkg-architecture values""" 

10 

11 def __init__(self, *, mocked_answers: dict[str, str] | None = None) -> None: 

12 """Create a new dpkg-architecture table; NO INSTANTIATION 

13 

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

17 

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 

31 

32 def __contains__(self, item: str) -> bool: 

33 try: 

34 self[item] 

35 except KeyError: 

36 return False 

37 else: 

38 return True 

39 

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] 

51 

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 

56 

57 @property 

58 def current_host_arch(self) -> str: 

59 """The architecture we are building for 

60 

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

62 """ 

63 return self["DEB_HOST_ARCH"] 

64 

65 @property 

66 def current_host_multiarch(self) -> str: 

67 """The multi-arch path basename 

68 

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

70 goes here: 

71 

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

73 

74 """ 

75 return self["DEB_HOST_MULTIARCH"] 

76 

77 @property 

78 def is_cross_compiling(self) -> bool: 

79 """Whether we are cross-compiling 

80 

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

86 

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 

100 

101 

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 

107 

108 

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

112 

113 

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 

121 

122 

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

132 

133 if build_arch is None: 

134 build_arch = host_arch 

135 

136 if target_arch is None: 

137 target_arch = host_arch 

138 return _faked_arch_tables(host_arch, build_arch, target_arch) 

139 

140 

141@lru_cache 

142def _faked_arch_tables( 

143 host_arch: str, build_arch: str, target_arch: str 

144) -> DpkgArchitectureBuildProcessValuesTable: 

145 mock_table = {} 

146 

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] 

156 

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 

187 

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_* 

210 

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 

225 

226 table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table) 

227 return table 

228 

229 

230_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable() 

231 

232 

233def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable: 

234 return _ARCH_TABLE