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

1import os 

2import subprocess 

3from functools import lru_cache 

4from typing import Dict, Optional, Iterator, Tuple 

5 

6 

7class DpkgArchitectureBuildProcessValuesTable: 

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

9 

10 def __init__(self, *, mocked_answers: Optional[Dict[str, str]] = None) -> None: 

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

12 

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

16 

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 

30 

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

32 try: 

33 self[item] 

34 except KeyError: 

35 return False 

36 else: 

37 return True 

38 

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] 

50 

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 

55 

56 @property 

57 def current_host_arch(self) -> str: 

58 """The architecture we are building for 

59 

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

61 """ 

62 return self["DEB_HOST_ARCH"] 

63 

64 @property 

65 def current_host_multiarch(self) -> str: 

66 """The multi-arch path basename 

67 

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

69 goes here: 

70 

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

72 

73 """ 

74 return self["DEB_HOST_MULTIARCH"] 

75 

76 @property 

77 def is_cross_compiling(self) -> bool: 

78 """Whether we are cross-compiling 

79 

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

85 

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 

99 

100 

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 

106 

107 

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

111 

112 

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 

120 

121 

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

131 

132 if build_arch is None: 

133 build_arch = host_arch 

134 

135 if target_arch is None: 

136 target_arch = host_arch 

137 return _faked_arch_tables(host_arch, build_arch, target_arch) 

138 

139 

140@lru_cache 

141def _faked_arch_tables( 

142 host_arch: str, build_arch: str, target_arch: str 

143) -> DpkgArchitectureBuildProcessValuesTable: 

144 mock_table = {} 

145 

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] 

155 

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 

186 

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

209 

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 

224 

225 table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table) 

226 return table 

227 

228 

229_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable() 

230 

231 

232def dpkg_architecture_table() -> DpkgArchitectureBuildProcessValuesTable: 

233 return _ARCH_TABLE