Coverage for debputy/architecture_support.py: 89%

100 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-22 14:29 +0100

1import os 

2import subprocess 

3from functools import lru_cache 

4from typing import Dict 

5 

6 

7class DpkgArchitectureBuildProcessValuesTable: 

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

9 

10 def __init__(self, *, mocked_answers: Dict[str, str] = 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 = {} 

21 self._has_run_dpkg_architecture = False 

22 if mocked_answers is None: 

23 self._architecture_cache = {} 

24 self._respect_environ = 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): 

40 if item not in self._architecture_cache: 

41 if self._respect_environ: 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true

42 value = os.environ.get(item) 

43 if value is not None: 

44 self._architecture_cache[item] = value 

45 return value 

46 if not self._has_run_dpkg_architecture: 46 ↛ 47line 46 didn't jump to line 47, because the condition on line 46 was never true

47 self._load_dpkg_architecture_values() 

48 # Fall through and look it up in the cache 

49 return self._architecture_cache[item] 

50 

51 @property 

52 def current_host_arch(self): 

53 """The architecture we are building for 

54 

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

56 """ 

57 return self['DEB_HOST_ARCH'] 

58 

59 @property 

60 def current_host_multiarch(self): 

61 """The multi-arch path basename 

62 

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

64 goes here: 

65 

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

67 

68 """ 

69 return self['DEB_HOST_MULTIARCH'] 

70 

71 @property 

72 def is_cross_compiling(self): 

73 """Whether we are cross-compiling 

74 

75 This is defined as DEB_BUILD_GNU_TYPE != DEB_HOST_GNU_TYPE and 

76 affects whether we can rely on being able to run the binaries 

77 that are compiled. 

78 """ 

79 return self['DEB_BUILD_GNU_TYPE'] != self['DEB_HOST_GNU_TYPE'] 

80 

81 def _load_dpkg_architecture_values(self): 

82 kw_pairs = _parse_dpkg_arch_output(subprocess.check_output(['dpkg-architecture'])) 

83 for k, v in kw_pairs: 

84 self._architecture_cache[k] = os.environ.get(k, v) 

85 self._has_run_dpkg_architecture = True 

86 

87 

88def _parse_dpkg_arch_output(output: bytes): 

89 text = output.decode('utf-8') 

90 for line in text.splitlines(): 

91 k, v = line.strip().split('=', 1) 

92 yield k, v 

93 

94 

95def _rewrite(value, from_pattern, to_pattern): 

96 assert value.startswith(from_pattern) 

97 return to_pattern + value[len(from_pattern):] 

98 

99 

100def mock_arch_table(host_arch, *, build_arch=None, target_arch=None): 

101 """Creates a mocked instance of DpkgArchitectureBuildProcessValuesTable 

102 

103 

104 :param host_arch: The dpkg architecture to mock answers for. This affects 

105 DEB_HOST_* values and defines the default for DEB_{BUILD,TARGET}_* if 

106 not overridden. 

107 :param build_arch: If set and has a different value than host_arch, then 

108 pretend this is a cross-build. This value affects the DEB_BUILD_* values. 

109 :param target_arch: If set and has a different value than host_arch, then 

110 pretend this is a build _of_ a cross-compiler. This value affects the 

111 DEB_TARGET_* values. 

112 """ 

113 

114 if build_arch is None: 

115 build_arch = host_arch 

116 

117 if target_arch is None: 

118 target_arch = host_arch 

119 return _mock_arch_tables(host_arch, build_arch, target_arch) 

120 

121 

122@lru_cache 

123def _mock_arch_tables(host_arch, build_arch, target_arch): 

124 mock_table = {} 

125 

126 env = dict(os.environ) 

127 # Set CC to /bin/true avoid a warning from dpkg-architecture 

128 env['CC'] = '/bin/true' 

129 # Clear environ variables that might confuse dpkg-architecture 

130 for k in os.environ: 

131 if k.startswith('DEB_'): 

132 del env[k] 

133 

134 if build_arch == host_arch: 

135 # easy / common case - we can handle this with a single call 

136 kw_pairs = _parse_dpkg_arch_output(subprocess.check_output( 

137 ['dpkg-architecture', '-a', host_arch, '-A', target_arch], 

138 env=env) 

139 ) 

140 for k, v in kw_pairs: 

141 if k.startswith(('DEB_HOST_', 'DEB_TARGET_')): 

142 mock_table[k] = v 

143 # Clone DEB_HOST_* into DEB_BUILD_* as well 

144 if k.startswith('DEB_HOST_'): 

145 k2 = _rewrite(k, 'DEB_HOST_', 'DEB_BUILD_') 

146 mock_table[k2] = v 

147 elif build_arch != host_arch and host_arch != target_arch: 

148 # This will need two dpkg-architecture calls because we cannot set 

149 # DEB_BUILD_* directly. But we can set DEB_HOST_* and then rewrite 

150 # it 

151 # First handle the build arch 

152 kw_pairs = _parse_dpkg_arch_output(subprocess.check_output(['dpkg-architecture', '-a', build_arch], env=env)) 

153 for k, v in kw_pairs: 

154 if k.startswith('DEB_HOST_'): 

155 k = _rewrite(k, 'DEB_HOST_', 'DEB_BUILD_') 

156 mock_table[k] = v 

157 

158 kw_pairs = _parse_dpkg_arch_output(subprocess.check_output( 

159 ['dpkg-architecture', '-a', host_arch, '-A', target_arch], 

160 env=env) 

161 ) 

162 for k, v in kw_pairs: 

163 if k.startswith(('DEB_HOST_', 'DEB_TARGET_')): 

164 mock_table[k] = v 

165 else: 

166 # This is a fun special case. We know that: 

167 # * build_arch != host_arch 

168 # * host_arch == target_arch 

169 # otherwise we would have hit one of the previous cases. 

170 # 

171 # We can do this in a single call to dpkg-architecture by 

172 # a bit of "cleaver" rewriting. 

173 # 

174 # - Use -a to set DEB_HOST_* and then rewrite that as 

175 # DEB_BUILD_* 

176 # - use -A to set DEB_TARGET_* and then use that for both 

177 # DEB_HOST_* and DEB_TARGET_* 

178 

179 kw_pairs = _parse_dpkg_arch_output(subprocess.check_output( 

180 ['dpkg-architecture', '-a', build_arch, '-A', target_arch], 

181 env=env) 

182 ) 

183 for k, v in kw_pairs: 

184 if k.startswith('DEB_HOST_'): 

185 k2 = _rewrite(k, 'DEB_HOST_', 'DEB_BUILD_') 

186 mock_table[k2] = v 

187 continue 

188 if k.startswith('DEB_TARGET_'): 

189 mock_table[k] = v 

190 k2 = _rewrite(k, 'DEB_TARGET_', 'DEB_HOST_') 

191 mock_table[k2] = v 

192 

193 table = DpkgArchitectureBuildProcessValuesTable(mocked_answers=mock_table) 

194 return table 

195 

196 

197_ARCH_TABLE = DpkgArchitectureBuildProcessValuesTable() 

198 

199 

200def dpkg_architecture_table(): 

201 return _ARCH_TABLE