Coverage for debputy/packages.py: 0%

151 statements  

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

1import re 

2from typing import Dict, Union, Tuple, Optional, Set, cast, Mapping, FrozenSet 

3 

4from debian.deb822 import Deb822 

5from debian.debian_support import DpkgArchTable 

6 

7from ._deb_options_profiles import DebOptionsAndProfiles 

8from .architecture_support import DpkgArchitectureBuildProcessValuesTable, dpkg_architecture_table 

9from .util import DEFAULT_PACKAGE_TYPE, UDEB_PACKAGE_TYPE, _error, active_profiles_match 

10 

11_MANDATORY_BINARY_PACKAGE_FIELD = [ 

12 'Package', 

13 'Architecture', 

14] 

15 

16 

17def parse_source_debian_control(selected_packages: Set[str], 

18 excluded_packages: Set[str], 

19 select_arch_all: bool, 

20 select_arch_any: bool, 

21 

22 dpkg_architecture_variables: Optional[DpkgArchitectureBuildProcessValuesTable] = None, 

23 dpkg_arch_query_table: Optional[DpkgArchTable] = None, 

24 build_env: Optional[DebOptionsAndProfiles] = None, 

25 debian_control: str = 'debian/control', 

26 ) -> Tuple['SourcePackage', Dict[str, 'BinaryPackage']]: 

27 if dpkg_architecture_variables is None: 

28 dpkg_architecture_variables = dpkg_architecture_table() 

29 if dpkg_arch_query_table is None: 

30 dpkg_arch_query_table = DpkgArchTable.load_arch_table() 

31 if build_env is None: 

32 build_env = DebOptionsAndProfiles.instance() 

33 

34 # If no selection option is set, then all packages are acted on (except the 

35 # excluded ones) 

36 if not selected_packages and not select_arch_all and not select_arch_any: 

37 select_arch_all = True 

38 select_arch_any = True 

39 

40 with open(debian_control) as fd: 

41 dctrl_paragraphs = list(Deb822.iter_paragraphs(fd)) 

42 

43 if len(dctrl_paragraphs) < 2: 

44 _error("debian/control must contain at least two paragraphs (1 Source + 1-N Package paragraphs)") 

45 

46 source_package = SourcePackage(dctrl_paragraphs[0]) 

47 

48 bin_pkgs = [ 

49 _create_binary_package( 

50 p, 

51 selected_packages, 

52 excluded_packages, 

53 select_arch_all, 

54 select_arch_any, 

55 dpkg_architecture_variables, 

56 dpkg_arch_query_table, 

57 build_env, 

58 i, 

59 ) for i, p in enumerate(dctrl_paragraphs[1:], 1) 

60 ] 

61 

62 return source_package, {p.name: p for p in bin_pkgs} 

63 

64 

65def _check_package_sets(provided_packages: Set[str], valid_package_names: Set[str], option_name: str): 

66 # SonarLint proposes to use `provided_packages > valid_package_names`, which is valid for boolean 

67 # logic, but not for set logic. We want to assert that provided_packages is a proper subset 

68 # of valid_package_names. The rewrite would cause no errors for {'foo'} > {'bar'} - in set logic, 

69 # neither is a superset / subset of the other, but we want an error for this case. 

70 # 

71 # Bug filed: 

72 # https://community.sonarsource.com/t/sonarlint-python-s1940-rule-does-not-seem-to-take-set-logic-into-account/79718 

73 if not (provided_packages <= valid_package_names): 

74 non_existing_packages = sorted(provided_packages - valid_package_names) 

75 invalid_package_list = ", ".join(non_existing_packages) 

76 msg = f'Invalid package names passed to {option_name}: {invalid_package_list}: ' \ 

77 f'Valid package names are: {", ".join(valid_package_names)}' 

78 _error(msg) 

79 

80 

81def _create_binary_package(paragraph: Union[Deb822, Dict[str, str]], 

82 selected_packages: Set[str], 

83 excluded_packages: Set[str], 

84 select_arch_all: bool, 

85 select_arch_any: bool, 

86 

87 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, 

88 dpkg_arch_query_table: DpkgArchTable, 

89 build_env: DebOptionsAndProfiles, 

90 paragraph_index: int, 

91 ) -> 'BinaryPackage': 

92 

93 try: 

94 package_name = paragraph['Package'] 

95 except KeyError: 

96 _error(f'Missing mandatory field "Package" in paragraph number {paragraph_index}') 

97 # The raise is there to help PyCharm type-checking (which fails at "NoReturn") 

98 raise 

99 

100 for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD: 

101 if mandatory_field not in paragraph: 

102 _error(f'Missing mandatory field "{mandatory_field}" for binary package {package_name}' 

103 f' (paragraph number {paragraph_index})') 

104 

105 architecture = paragraph['Architecture'] 

106 

107 if paragraph_index < 1: 

108 raise ValueError("paragraph index must be 1-indexed (1, 2, ...)") 

109 is_main_package = paragraph_index == 1 

110 

111 if package_name in excluded_packages: 

112 should_act_on = False 

113 elif package_name in selected_packages: 

114 should_act_on = True 

115 elif architecture == "all": 

116 should_act_on = select_arch_all 

117 else: 

118 should_act_on = select_arch_any 

119 

120 profiles_raw = paragraph.get('Build-Profiles', '').strip() 

121 if should_act_on and profiles_raw: 

122 try: 

123 should_act_on = active_profiles_match(profiles_raw, build_env.deb_build_profiles) 

124 except ValueError as e: 

125 _error(f'Invalid Build-Profiles field for {package_name}: {e.args[0]}') 

126 

127 return BinaryPackage(paragraph, 

128 dpkg_architecture_variables, 

129 dpkg_arch_query_table, 

130 should_be_acted_on=should_act_on, 

131 is_main_package=is_main_package) 

132 

133 

134def _check_binary_arch(arch_table: DpkgArchTable, binary_arch: str, declared_arch: str): 

135 if binary_arch == 'all': 

136 return True 

137 arch_wildcards = declared_arch.split() 

138 for arch_wildcard in arch_wildcards: 

139 if arch_table.matches_architecture(binary_arch, arch_wildcard): 

140 return True 

141 return False 

142 

143 

144class BinaryPackage: 

145 

146 __slots__ = [ 

147 '_package_fields', 

148 '_dbgsym_binary_package', 

149 '_should_be_acted_on', 

150 '_dpkg_architecture_variables', 

151 '_declared_arch_matches_output_arch', 

152 '_is_main_package', 

153 '_substvars', 

154 '_maintscript_snippets', 

155 ] 

156 

157 def __init__(self, 

158 fields: Union[Mapping[str, str], Deb822], 

159 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, 

160 dpkg_arch_query: DpkgArchTable, 

161 *, 

162 is_main_package=False, 

163 should_be_acted_on=True): 

164 super(BinaryPackage, self).__init__() 

165 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough 

166 # like one that we rely on it and just cast it. 

167 self._package_fields = cast('Mapping[str, str]', fields) 

168 self._dbgsym_binary_package = None 

169 self._should_be_acted_on = should_be_acted_on 

170 self._dpkg_architecture_variables = dpkg_architecture_variables 

171 self._is_main_package = is_main_package 

172 self._declared_arch_matches_output_arch = _check_binary_arch( 

173 dpkg_arch_query, 

174 self.resolved_architecture, 

175 self.declared_architecture 

176 ) 

177 

178 @property 

179 def name(self): 

180 return self.fields['Package'] 

181 

182 @property 

183 def archive_section(self) -> str: 

184 value = self.fields.get('Section') 

185 if value is None: 

186 return 'Unknown' 

187 return value 

188 

189 @property 

190 def archive_component(self) -> str: 

191 component = '' 

192 section = self.archive_section 

193 if '/' in section: 

194 component = section.rsplit('/', 1)[0] 

195 # The "main" component is always shortened to "" 

196 if component == 'main': 

197 component = '' 

198 return component 

199 

200 @property 

201 def is_udeb(self) -> bool: 

202 return self.package_type == UDEB_PACKAGE_TYPE 

203 

204 @property 

205 def should_be_acted_on(self) -> bool: 

206 return self._should_be_acted_on and self._declared_arch_matches_output_arch 

207 

208 @property 

209 def fields(self) -> Mapping[str, str]: 

210 return self._package_fields 

211 

212 @property 

213 def resolved_architecture(self): 

214 arch = self.declared_architecture 

215 if arch == 'all': 

216 return arch 

217 if self._x_dh_build_for_type == 'target': 

218 return self._dpkg_architecture_variables['DEB_TARGET_ARCH'] 

219 return self._dpkg_architecture_variables.current_host_arch 

220 

221 @property 

222 def _x_dh_build_for_type(self) -> str: 

223 v = self._package_fields.get('X-DH-Build-For-Type') 

224 if v is None: 

225 return 'host' 

226 return v.lower() 

227 

228 @property 

229 def package_type(self) -> str: 

230 """Short for Package-Type (with proper default if absent)""" 

231 v = self.fields.get('Package-Type') 

232 if v is None: 

233 return DEFAULT_PACKAGE_TYPE 

234 return v 

235 

236 @property 

237 def is_main_package(self) -> bool: 

238 return self._is_main_package 

239 

240 def cross_command(self, command) -> str: 

241 arch_table = self._dpkg_architecture_variables 

242 if self._x_dh_build_for_type == 'target': 

243 target_gnu_type = arch_table['DEB_TARGET_GNU_TYPE'] 

244 if arch_table['DEB_HOST_GNU_TYPE'] != target_gnu_type: 

245 return f"{target_gnu_type}-{command}" 

246 if arch_table.is_cross_compiling: 

247 return f"{arch_table['DEB_HOST_GNU_TYPE']}-{command}" 

248 return command 

249 

250 @property 

251 def declared_architecture(self): 

252 return self.fields['Architecture'] 

253 

254 @property 

255 def is_arch_all(self) -> bool: 

256 return self.declared_architecture == 'all' 

257 

258 

259class SourcePackage: 

260 

261 __slots__ = ('_package_fields',) 

262 

263 def __init__(self, fields: Union[Mapping[str, str], Deb822]): 

264 # Typing-wise, Deb822 is *not* a Mapping[str, str] but it behaves enough 

265 # like one that we rely on it and just cast it. 

266 self._package_fields = cast('Mapping[str, str]', fields) 

267 

268 @property 

269 def package_fields(self) -> Mapping[str, str]: 

270 return self._package_fields 

271 

272 @property 

273 def package_name(self): 

274 return self._package_fields['Source']