Coverage for src/debputy/packages.py: 51%

167 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-07 12:14 +0200

1from typing import ( 

2 Dict, 

3 Union, 

4 Tuple, 

5 Optional, 

6 Set, 

7 cast, 

8 Mapping, 

9 FrozenSet, 

10 TYPE_CHECKING, 

11) 

12 

13from debian.deb822 import Deb822 

14from debian.debian_support import DpkgArchTable 

15 

16from ._deb_options_profiles import DebBuildOptionsAndProfiles 

17from .architecture_support import ( 

18 DpkgArchitectureBuildProcessValuesTable, 

19 dpkg_architecture_table, 

20) 

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

22 

23if TYPE_CHECKING: 

24 from .plugin.api import VirtualPath 

25 

26 

27_MANDATORY_BINARY_PACKAGE_FIELD = [ 

28 "Package", 

29 "Architecture", 

30] 

31 

32 

33def parse_source_debian_control( 

34 debian_control: "VirtualPath", 

35 selected_packages: Union[Set[str], FrozenSet[str]], 

36 excluded_packages: Union[Set[str], FrozenSet[str]], 

37 select_arch_all: bool, 

38 select_arch_any: bool, 

39 dpkg_architecture_variables: Optional[ 

40 DpkgArchitectureBuildProcessValuesTable 

41 ] = None, 

42 dpkg_arch_query_table: Optional[DpkgArchTable] = None, 

43 build_env: Optional[DebBuildOptionsAndProfiles] = None, 

44) -> Tuple["SourcePackage", Dict[str, "BinaryPackage"]]: 

45 if dpkg_architecture_variables is None: 

46 dpkg_architecture_variables = dpkg_architecture_table() 

47 if dpkg_arch_query_table is None: 

48 dpkg_arch_query_table = DpkgArchTable.load_arch_table() 

49 if build_env is None: 

50 build_env = DebBuildOptionsAndProfiles.instance() 

51 

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

53 # excluded ones) 

54 if not selected_packages and not select_arch_all and not select_arch_any: 

55 select_arch_all = True 

56 select_arch_any = True 

57 

58 with debian_control.open() as fd: 

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

60 

61 if len(dctrl_paragraphs) < 2: 

62 _error( 

63 "debian/control must contain at least two stanza (1 Source + 1-N Package stanza)" 

64 ) 

65 

66 source_package = SourcePackage(dctrl_paragraphs[0]) 

67 

68 bin_pkgs = [ 

69 _create_binary_package( 

70 p, 

71 selected_packages, 

72 excluded_packages, 

73 select_arch_all, 

74 select_arch_any, 

75 dpkg_architecture_variables, 

76 dpkg_arch_query_table, 

77 build_env, 

78 i, 

79 ) 

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

81 ] 

82 bin_pkgs_table = {p.name: p for p in bin_pkgs} 

83 if not selected_packages.issubset(bin_pkgs_table.keys()): 

84 unknown = selected_packages - bin_pkgs_table.keys() 

85 _error( 

86 f"The following *selected* packages (-p) are not listed in debian/control: {sorted(unknown)}" 

87 ) 

88 if not excluded_packages.issubset(bin_pkgs_table.keys()): 

89 unknown = selected_packages - bin_pkgs_table.keys() 

90 _error( 

91 f"The following *excluded* packages (-N) are not listed in debian/control: {sorted(unknown)}" 

92 ) 

93 

94 return source_package, bin_pkgs_table 

95 

96 

97def _check_package_sets( 

98 provided_packages: Set[str], 

99 valid_package_names: Set[str], 

100 option_name: str, 

101) -> None: 

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

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

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

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

106 # 

107 # Bug filed: 

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

109 if not (provided_packages <= valid_package_names): 

110 non_existing_packages = sorted(provided_packages - valid_package_names) 

111 invalid_package_list = ", ".join(non_existing_packages) 

112 msg = ( 

113 f"Invalid package names passed to {option_name}: {invalid_package_list}: " 

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

115 ) 

116 _error(msg) 

117 

118 

119def _create_binary_package( 

120 paragraph: Union[Deb822, Dict[str, str]], 

121 selected_packages: Union[Set[str], FrozenSet[str]], 

122 excluded_packages: Union[Set[str], FrozenSet[str]], 

123 select_arch_all: bool, 

124 select_arch_any: bool, 

125 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, 

126 dpkg_arch_query_table: DpkgArchTable, 

127 build_env: DebBuildOptionsAndProfiles, 

128 paragraph_index: int, 

129) -> "BinaryPackage": 

130 try: 

131 package_name = paragraph["Package"] 

132 except KeyError: 

133 _error(f'Missing mandatory field "Package" in stanza number {paragraph_index}') 

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

135 raise 

136 

137 for mandatory_field in _MANDATORY_BINARY_PACKAGE_FIELD: 

138 if mandatory_field not in paragraph: 

139 _error( 

140 f'Missing mandatory field "{mandatory_field}" for binary package {package_name}' 

141 f" (stanza number {paragraph_index})" 

142 ) 

143 

144 architecture = paragraph["Architecture"] 

145 

146 if paragraph_index < 1: 

147 raise ValueError("stanza index must be 1-indexed (1, 2, ...)") 

148 is_main_package = paragraph_index == 1 

149 

150 if package_name in excluded_packages: 

151 should_act_on = False 

152 elif package_name in selected_packages: 

153 should_act_on = True 

154 elif architecture == "all": 

155 should_act_on = select_arch_all 

156 else: 

157 should_act_on = select_arch_any 

158 

159 profiles_raw = paragraph.get("Build-Profiles", "").strip() 

160 if should_act_on and profiles_raw: 

161 try: 

162 should_act_on = active_profiles_match( 

163 profiles_raw, build_env.deb_build_profiles 

164 ) 

165 except ValueError as e: 

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

167 

168 return BinaryPackage( 

169 paragraph, 

170 dpkg_architecture_variables, 

171 dpkg_arch_query_table, 

172 should_be_acted_on=should_act_on, 

173 is_main_package=is_main_package, 

174 ) 

175 

176 

177def _check_binary_arch( 

178 arch_table: DpkgArchTable, 

179 binary_arch: str, 

180 declared_arch: str, 

181) -> bool: 

182 if binary_arch == "all": 

183 return True 

184 arch_wildcards = declared_arch.split() 

185 for arch_wildcard in arch_wildcards: 185 ↛ 188line 185 didn't jump to line 188, because the loop on line 185 didn't complete

186 if arch_table.matches_architecture(binary_arch, arch_wildcard): 186 ↛ 185line 186 didn't jump to line 185, because the condition on line 186 was never false

187 return True 

188 return False 

189 

190 

191class BinaryPackage: 

192 __slots__ = [ 

193 "_package_fields", 

194 "_dbgsym_binary_package", 

195 "_should_be_acted_on", 

196 "_dpkg_architecture_variables", 

197 "_declared_arch_matches_output_arch", 

198 "_is_main_package", 

199 "_substvars", 

200 "_maintscript_snippets", 

201 ] 

202 

203 def __init__( 

204 self, 

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

206 dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable, 

207 dpkg_arch_query: DpkgArchTable, 

208 *, 

209 is_main_package: bool = False, 

210 should_be_acted_on: bool = True, 

211 ) -> None: 

212 super(BinaryPackage, self).__init__() 

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

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

215 self._package_fields = cast("Mapping[str, str]", fields) 

216 self._dbgsym_binary_package = None 

217 self._should_be_acted_on = should_be_acted_on 

218 self._dpkg_architecture_variables = dpkg_architecture_variables 

219 self._is_main_package = is_main_package 

220 self._declared_arch_matches_output_arch = _check_binary_arch( 

221 dpkg_arch_query, self.resolved_architecture, self.declared_architecture 

222 ) 

223 

224 @property 

225 def name(self) -> str: 

226 return self.fields["Package"] 

227 

228 @property 

229 def archive_section(self) -> str: 

230 value = self.fields.get("Section") 

231 if value is None: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 return "Unknown" 

233 return value 

234 

235 @property 

236 def archive_component(self) -> str: 

237 component = "" 

238 section = self.archive_section 

239 if "/" in section: 

240 component = section.rsplit("/", 1)[0] 

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

242 if component == "main": 

243 component = "" 

244 return component 

245 

246 @property 

247 def is_essential(self) -> bool: 

248 return self._package_fields.get("Essential") == "yes" 

249 

250 @property 

251 def is_udeb(self) -> bool: 

252 return self.package_type == UDEB_PACKAGE_TYPE 

253 

254 @property 

255 def should_be_acted_on(self) -> bool: 

256 return self._should_be_acted_on and self._declared_arch_matches_output_arch 

257 

258 @property 

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

260 return self._package_fields 

261 

262 @property 

263 def resolved_architecture(self) -> str: 

264 arch = self.declared_architecture 

265 if arch == "all": 

266 return arch 

267 if self._x_dh_build_for_type == "target": 267 ↛ 268line 267 didn't jump to line 268, because the condition on line 267 was never true

268 return self._dpkg_architecture_variables["DEB_TARGET_ARCH"] 

269 return self._dpkg_architecture_variables.current_host_arch 

270 

271 def package_deb_architecture_variable(self, variable_suffix: str) -> str: 

272 if self._x_dh_build_for_type == "target": 272 ↛ 273line 272 didn't jump to line 273, because the condition on line 272 was never true

273 return self._dpkg_architecture_variables[f"DEB_TARGET_{variable_suffix}"] 

274 return self._dpkg_architecture_variables[f"DEB_HOST_{variable_suffix}"] 

275 

276 @property 

277 def deb_multiarch(self) -> str: 

278 return self.package_deb_architecture_variable("MULTIARCH") 

279 

280 @property 

281 def _x_dh_build_for_type(self) -> str: 

282 v = self._package_fields.get("X-DH-Build-For-Type") 

283 if v is None: 283 ↛ 285line 283 didn't jump to line 285, because the condition on line 283 was never false

284 return "host" 

285 return v.lower() 

286 

287 @property 

288 def package_type(self) -> str: 

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

290 v = self.fields.get("Package-Type") 

291 if v is None: 

292 return DEFAULT_PACKAGE_TYPE 

293 return v 

294 

295 @property 

296 def is_main_package(self) -> bool: 

297 return self._is_main_package 

298 

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

300 arch_table = self._dpkg_architecture_variables 

301 if self._x_dh_build_for_type == "target": 

302 target_gnu_type = arch_table["DEB_TARGET_GNU_TYPE"] 

303 if arch_table["DEB_HOST_GNU_TYPE"] != target_gnu_type: 

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

305 if arch_table.is_cross_compiling: 

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

307 return command 

308 

309 @property 

310 def declared_architecture(self) -> str: 

311 return self.fields["Architecture"] 

312 

313 @property 

314 def is_arch_all(self) -> bool: 

315 return self.declared_architecture == "all" 

316 

317 

318class SourcePackage: 

319 __slots__ = ("_package_fields",) 

320 

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

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

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

324 self._package_fields = cast("Mapping[str, str]", fields) 

325 

326 @property 

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

328 return self._package_fields 

329 

330 @property 

331 def name(self) -> str: 

332 return self._package_fields["Source"]